1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2020 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use League\Flysystem\Adapter\Local; 23use League\Flysystem\FileNotFoundException; 24use League\Flysystem\Filesystem; 25use League\Flysystem\FilesystemInterface; 26use League\Glide\Signatures\SignatureFactory; 27 28use function extension_loaded; 29use function getimagesize; 30use function intdiv; 31use function pathinfo; 32use function strtolower; 33 34use const PATHINFO_EXTENSION; 35 36/** 37 * A GEDCOM media file. A media object can contain many media files, 38 * such as scans of both sides of a document, the transcript of an audio 39 * recording, etc. 40 */ 41class MediaFile 42{ 43 private const SUPPORTED_IMAGE_MIME_TYPES = [ 44 'image/gif', 45 'image/jpeg', 46 'image/png', 47 ]; 48 49 /** @var string The filename */ 50 private $multimedia_file_refn = ''; 51 52 /** @var string The file extension; jpeg, txt, mp4, etc. */ 53 private $multimedia_format = ''; 54 55 /** @var string The type of document; newspaper, microfiche, etc. */ 56 private $source_media_type = ''; 57 /** @var string The filename */ 58 59 /** @var string The name of the document */ 60 private $descriptive_title = ''; 61 62 /** @var Media $media The media object to which this file belongs */ 63 private $media; 64 65 /** @var string */ 66 private $fact_id; 67 68 /** 69 * Create a MediaFile from raw GEDCOM data. 70 * 71 * @param string $gedcom 72 * @param Media $media 73 */ 74 public function __construct($gedcom, Media $media) 75 { 76 $this->media = $media; 77 $this->fact_id = md5($gedcom); 78 79 if (preg_match('/^\d FILE (.+)/m', $gedcom, $match)) { 80 $this->multimedia_file_refn = $match[1]; 81 $this->multimedia_format = pathinfo($match[1], PATHINFO_EXTENSION); 82 } 83 84 if (preg_match('/^\d FORM (.+)/m', $gedcom, $match)) { 85 $this->multimedia_format = $match[1]; 86 } 87 88 if (preg_match('/^\d TYPE (.+)/m', $gedcom, $match)) { 89 $this->source_media_type = $match[1]; 90 } 91 92 if (preg_match('/^\d TITL (.+)/m', $gedcom, $match)) { 93 $this->descriptive_title = $match[1]; 94 } 95 } 96 97 /** 98 * Get the format. 99 * 100 * @return string 101 */ 102 public function format(): string 103 { 104 return $this->multimedia_format; 105 } 106 107 /** 108 * Get the type. 109 * 110 * @return string 111 */ 112 public function type(): string 113 { 114 return $this->source_media_type; 115 } 116 117 /** 118 * Get the title. 119 * 120 * @return string 121 */ 122 public function title(): string 123 { 124 return $this->descriptive_title; 125 } 126 127 /** 128 * Get the fact ID. 129 * 130 * @return string 131 */ 132 public function factId(): string 133 { 134 return $this->fact_id; 135 } 136 137 /** 138 * @return bool 139 */ 140 public function isPendingAddition(): bool 141 { 142 foreach ($this->media->facts() as $fact) { 143 if ($fact->id() === $this->fact_id) { 144 return $fact->isPendingAddition(); 145 } 146 } 147 148 return false; 149 } 150 151 /** 152 * @return bool 153 */ 154 public function isPendingDeletion(): bool 155 { 156 foreach ($this->media->facts() as $fact) { 157 if ($fact->id() === $this->fact_id) { 158 return $fact->isPendingDeletion(); 159 } 160 } 161 162 return false; 163 } 164 165 /** 166 * Display an image-thumbnail or a media-icon, and add markup for image viewers such as colorbox. 167 * 168 * @param int $width Pixels 169 * @param int $height Pixels 170 * @param string $fit "crop" or "contain" 171 * @param string[] $image_attributes Additional HTML attributes 172 * 173 * @return string 174 */ 175 public function displayImage($width, $height, $fit, $image_attributes = []): string 176 { 177 if ($this->isExternal()) { 178 $src = $this->multimedia_file_refn; 179 $srcset = []; 180 } else { 181 // Generate multiple images for displays with higher pixel densities. 182 $src = $this->imageUrl($width, $height, $fit); 183 $srcset = []; 184 foreach ([2, 3, 4] as $x) { 185 $srcset[] = $this->imageUrl($width * $x, $height * $x, $fit) . ' ' . $x . 'x'; 186 } 187 } 188 189 if ($this->isImage()) { 190 $image = '<img ' . Html::attributes($image_attributes + [ 191 'dir' => 'auto', 192 'src' => $src, 193 'srcset' => implode(',', $srcset), 194 'alt' => strip_tags($this->media->fullName()), 195 ]) . '>'; 196 197 $link_attributes = Html::attributes([ 198 'class' => 'gallery', 199 'type' => $this->mimeType(), 200 'href' => $this->imageUrl(0, 0, 'contain'), 201 'data-title' => strip_tags($this->media->fullName()), 202 ]); 203 } else { 204 $image = view('icons/mime', ['type' => $this->mimeType()]); 205 206 $link_attributes = Html::attributes([ 207 'type' => $this->mimeType(), 208 'href' => $this->downloadUrl('inline'), 209 ]); 210 } 211 212 return '<a ' . $link_attributes . '>' . $image . '</a>'; 213 } 214 215 /** 216 * Is the media file actually a URL? 217 */ 218 public function isExternal(): bool 219 { 220 return strpos($this->multimedia_file_refn, '://') !== false; 221 } 222 223 /** 224 * Generate a URL for an image. 225 * 226 * @param int $width Maximum width in pixels 227 * @param int $height Maximum height in pixels 228 * @param string $fit "crop" or "contain" 229 * 230 * @return string 231 */ 232 public function imageUrl($width, $height, $fit): string 233 { 234 // Sign the URL, to protect against mass-resize attacks. 235 $glide_key = Site::getPreference('glide-key'); 236 if ($glide_key === '') { 237 $glide_key = bin2hex(random_bytes(128)); 238 Site::setPreference('glide-key', $glide_key); 239 } 240 241 if (Auth::accessLevel($this->media->tree()) > $this->media->tree()->getPreference('SHOW_NO_WATERMARK')) { 242 $mark = 'watermark.png'; 243 } else { 244 $mark = ''; 245 } 246 247 // Automatic rotation only works when the php-exif library is loaded. 248 $orientation = extension_loaded('exif') ? 'or' : 0; 249 250 $params = [ 251 'xref' => $this->media->xref(), 252 'tree' => $this->media->tree()->name(), 253 'fact_id' => $this->fact_id, 254 'w' => $width, 255 'h' => $height, 256 'fit' => $fit, 257 'mark' => $mark, 258 'markh' => '100h', 259 'markw' => '100w', 260 'markpos' => 'center', 261 'markalpha' => 25, 262 'or' => $orientation, 263 'q' => 45, 264 ]; 265 266 $signature = SignatureFactory::create($glide_key)->generateSignature('', $params); 267 268 $params = ['route' => '/media-thumbnail', 's' => $signature] + $params; 269 270 return route('media-thumbnail', $params); 271 } 272 273 /** 274 * Is the media file an image? 275 */ 276 public function isImage(): bool 277 { 278 return in_array($this->mimeType(), self::SUPPORTED_IMAGE_MIME_TYPES, true); 279 } 280 281 /** 282 * What is the mime-type of this object? 283 * For simplicity and efficiency, use the extension, rather than the contents. 284 * 285 * @return string 286 */ 287 public function mimeType(): string 288 { 289 $extension = strtolower(pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION)); 290 291 return Mime::TYPES[$extension] ?? Mime::DEFAULT_TYPE; 292 } 293 294 /** 295 * Generate a URL to download a non-image media file. 296 * 297 * @param string $disposition How should the image be returned - "attachment" or "inline" 298 * 299 * @return string 300 */ 301 public function downloadUrl(string $disposition): string 302 { 303 return route('media-download', [ 304 'xref' => $this->media->xref(), 305 'tree' => $this->media->tree()->name(), 306 'fact_id' => $this->fact_id, 307 'disposition' => $disposition, 308 ]); 309 } 310 311 /** 312 * A list of image attributes 313 * 314 * @param FilesystemInterface $data_filesystem 315 * 316 * @return string[] 317 */ 318 public function attributes(FilesystemInterface $data_filesystem): array 319 { 320 $attributes = []; 321 322 if (!$this->isExternal() || $this->fileExists($data_filesystem)) { 323 try { 324 $bytes = $this->media()->tree()->mediaFilesystem($data_filesystem)->getSize($this->filename()); 325 $kb = intdiv($bytes + 1023, 1024); 326 $attributes['__FILE_SIZE__'] = I18N::translate('%s KB', I18N::number($kb)); 327 } catch (FileNotFoundException $ex) { 328 // External/missing files have no size. 329 } 330 331 // Note: getAdapter() is defined on Filesystem, but not on FilesystemInterface. 332 $filesystem = $this->media()->tree()->mediaFilesystem($data_filesystem); 333 if ($filesystem instanceof Filesystem) { 334 $adapter = $filesystem->getAdapter(); 335 // Only works for local filesystems. 336 if ($adapter instanceof Local) { 337 $file = $adapter->applyPathPrefix($this->filename()); 338 [$width, $height] = getimagesize($file); 339 $attributes['__IMAGE_SIZE__'] = I18N::translate('%1$s × %2$s pixels', I18N::number($width), I18N::number($height)); 340 } 341 } 342 } 343 344 return $attributes; 345 } 346 347 /** 348 * Read the contents of a media file. 349 * 350 * @param FilesystemInterface $data_filesystem 351 * 352 * @return string 353 */ 354 public function fileContents(FilesystemInterface $data_filesystem): string 355 { 356 return $this->media->tree()->mediaFilesystem($data_filesystem)->read($this->multimedia_file_refn); 357 } 358 359 /** 360 * Check if the file exists on this server 361 * 362 * @param FilesystemInterface $data_filesystem 363 * 364 * @return bool 365 */ 366 public function fileExists(FilesystemInterface $data_filesystem): bool 367 { 368 return $this->media->tree()->mediaFilesystem($data_filesystem)->has($this->multimedia_file_refn); 369 } 370 371 /** 372 * @return Media 373 */ 374 public function media(): Media 375 { 376 return $this->media; 377 } 378 379 /** 380 * Get the filename. 381 * 382 * @return string 383 */ 384 public function filename(): string 385 { 386 return $this->multimedia_file_refn; 387 } 388 389 /** 390 * What file extension is used by this file? 391 * 392 * @return string 393 * 394 * @deprecated since 2.0.4. Will be removed in 2.1.0 395 */ 396 public function extension(): string 397 { 398 return pathinfo($this->multimedia_file_refn, PATHINFO_EXTENSION); 399 } 400} 401