Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.37% |
220 / 304 |
|
46.88% |
15 / 32 |
CRAP | |
0.00% |
0 / 1 |
PagedTiffHandler | |
72.37% |
220 / 304 |
|
46.88% |
15 / 32 |
423.79 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
isEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
mustRender | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isMultiPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
verifyUpload | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
3.07 | |||
verifyMetaData | |
57.14% |
12 / 21 |
|
0.00% |
0 / 1 |
10.86 | |||
getParamMap | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
validateParam | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
15.66 | |||
makeParamString | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
4.59 | |||
parseParamString | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getScriptParams | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
normaliseParams | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
getMetadataErrors | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
isMetadataError | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
joinMessages | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
12.58 | |||
getScalerType | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
transformIM | |
65.00% |
26 / 40 |
|
0.00% |
0 / 1 |
12.47 | |||
getThumbType | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
pageCount | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
firstPage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
lastPage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
adjustPage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
doThumbError | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getSizeAndMetadata | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
isFileMetadataValid | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
formatMetadata | |
60.98% |
25 / 41 |
|
0.00% |
0 / 1 |
11.80 | |||
getTiffImage | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getCachedTiffImage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getPageDimensions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
isExpensiveToThumbnail | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getThumbnailSource | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getIntermediaryStep | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
7 |
1 | <?php |
2 | /** |
3 | * Copyright © Wikimedia Deutschland, 2009 |
4 | * Authors Hallo Welt! Medienwerkstatt GmbH |
5 | * Authors Sebastian Ulbricht, Daniel Lynge, Marc Reymann, Markus Glaser |
6 | * |
7 | * This program is free software; you can redistribute it and/or modify |
8 | * it under the terms of the GNU General Public License as published by |
9 | * the Free Software Foundation; either version 2 of the License, or |
10 | * (at your option) any later version. |
11 | * |
12 | * This program is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | * GNU General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU General Public License along |
18 | * with this program; if not, write to the Free Software Foundation, Inc., |
19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
20 | * http://www.gnu.org/copyleft/gpl.html |
21 | */ |
22 | |
23 | namespace MediaWiki\Extension\PagedTiffHandler; |
24 | |
25 | use File; |
26 | use FormatMetadata; |
27 | use LogicException; |
28 | use MapCacheLRU; |
29 | use MediaHandlerState; |
30 | use MediaTransformError; |
31 | use MediaTransformOutput; |
32 | use MediaWiki\Context\IContextSource; |
33 | use MediaWiki\Context\RequestContext; |
34 | use MediaWiki\HookContainer\HookContainer; |
35 | use MediaWiki\MediaWikiServices; |
36 | use MediaWiki\Message\Message; |
37 | use MediaWiki\Shell\CommandFactory; |
38 | use MediaWiki\Status\Status; |
39 | use MediaWiki\User\Options\UserOptionsLookup; |
40 | use RuntimeException; |
41 | use TransformationalImageHandler; |
42 | use Wikimedia\Stats\StatsFactory; |
43 | |
44 | class PagedTiffHandler extends TransformationalImageHandler { |
45 | // TIFF files over 10M are considered expensive to thumbnail |
46 | private const EXPENSIVE_SIZE_LIMIT = 10485760; |
47 | |
48 | /** |
49 | * 1.0: Initial |
50 | * 1.1: Fixed bugs in imageinfo parser |
51 | * 1.2: Photoshop quirks (reverted) |
52 | * 1.3: Handing extra IFDs reported by tiffinfo |
53 | * 1.4: Allowed page numbering to start from numbers other than 1 |
54 | */ |
55 | public const TIFF_METADATA_VERSION = '1.4'; |
56 | |
57 | /** |
58 | * Known images cache |
59 | * |
60 | * @var MapCacheLRU |
61 | */ |
62 | private $knownImages; |
63 | |
64 | /** @var CommandFactory */ |
65 | private $commandFactory; |
66 | |
67 | /** @var HookContainer */ |
68 | private $hookContainer; |
69 | |
70 | /** @var UserOptionsLookup */ |
71 | private $userOptionsLookup; |
72 | |
73 | /** @var StatsFactory */ |
74 | private $statsFactory; |
75 | |
76 | /** |
77 | * Number of images to keep in $knownImages |
78 | */ |
79 | private const CACHE_SIZE = 5; |
80 | |
81 | public function __construct() { |
82 | $this->knownImages = new MapCacheLRU( self::CACHE_SIZE ); |
83 | |
84 | $services = MediaWikiServices::getInstance(); |
85 | $this->commandFactory = $services->getShellCommandFactory(); |
86 | $this->hookContainer = $services->getHookContainer(); |
87 | $this->userOptionsLookup = $services->getUserOptionsLookup(); |
88 | $this->statsFactory = $services->getStatsFactory(); |
89 | } |
90 | |
91 | /** |
92 | * @return bool |
93 | */ |
94 | public function isEnabled() { |
95 | return true; |
96 | } |
97 | |
98 | /** |
99 | * @param File $img |
100 | * @return bool |
101 | */ |
102 | public function mustRender( $img ) { |
103 | return true; |
104 | } |
105 | |
106 | /** |
107 | * Does the file format support multi-page documents? |
108 | * @param File $img |
109 | * @return bool |
110 | */ |
111 | public function isMultiPage( $img ) { |
112 | return true; |
113 | } |
114 | |
115 | /** |
116 | * Various checks against the uploaded file |
117 | * - maximum upload size |
118 | * - maximum number of embedded files |
119 | * - maximum size of metadata |
120 | * - identify-errors |
121 | * - identify-warnings |
122 | * - check for running-identify-service |
123 | * @param string $fileName |
124 | * @return Status |
125 | */ |
126 | public function verifyUpload( $fileName ) { |
127 | $status = Status::newGood(); |
128 | $meta = $this->getTiffImage( false, $fileName )->retrieveMetaData(); |
129 | if ( !$meta ) { |
130 | wfDebug( __METHOD__ . ": unable to retrieve metadata" ); |
131 | $status->fatal( 'tiff_out_of_service' ); |
132 | } else { |
133 | $ok = $this->verifyMetaData( $meta, $error ); |
134 | |
135 | if ( !$ok ) { |
136 | $this->getCachedTiffImage( $fileName )->resetMetaData(); |
137 | // @phan-suppress-next-line PhanParamTooFewUnpack |
138 | $status->fatal( ...$error ); |
139 | } |
140 | } |
141 | |
142 | return $status; |
143 | } |
144 | |
145 | /** |
146 | * @param array $meta |
147 | * @param array &$error |
148 | * @return bool |
149 | */ |
150 | private function verifyMetaData( $meta, &$error ) { |
151 | global $wgTiffMaxEmbedFiles, $wgTiffMaxMetaSize; |
152 | |
153 | $errors = $this->getMetadataErrors( $meta ); |
154 | if ( $errors ) { |
155 | $error = [ 'tiff_bad_file', $this->joinMessages( $errors ) ]; |
156 | |
157 | wfDebug( __METHOD__ . ": {$error[0]} " . |
158 | $this->joinMessages( $errors, false ) ); |
159 | return false; |
160 | } |
161 | |
162 | if ( $meta['page_count'] <= 0 || empty( $meta['page_data'] ) ) { |
163 | $error = [ 'tiff_page_error', $meta['page_count'] ]; |
164 | wfDebug( __METHOD__ . ": {$error[0]}" ); |
165 | return false; |
166 | } |
167 | if ( $wgTiffMaxEmbedFiles && $meta['page_count'] > $wgTiffMaxEmbedFiles ) { |
168 | $error = [ 'tiff_too_much_embed_files', $meta['page_count'], $wgTiffMaxEmbedFiles ]; |
169 | wfDebug( __METHOD__ . ": {$error[0]}" ); |
170 | return false; |
171 | } |
172 | $len = strlen( serialize( $meta ) ); |
173 | if ( ( $len + 1 ) > $wgTiffMaxMetaSize ) { |
174 | $error = [ 'tiff_too_much_meta', $len, $wgTiffMaxMetaSize ]; |
175 | wfDebug( __METHOD__ . ": {$error[0]}" ); |
176 | return false; |
177 | } |
178 | |
179 | wfDebug( __METHOD__ . ": metadata is ok" ); |
180 | return true; |
181 | } |
182 | |
183 | /** |
184 | * Maps MagicWord-IDs to parameters. |
185 | * In this case, width, page, and lossy. |
186 | * @return array |
187 | */ |
188 | public function getParamMap() { |
189 | return [ |
190 | 'img_width' => 'width', |
191 | 'img_page' => 'page', |
192 | 'img_lossy' => 'lossy', |
193 | ]; |
194 | } |
195 | |
196 | /** |
197 | * Checks whether parameters are valid and have valid values. |
198 | * Check for lossy was added. |
199 | * @param string $name |
200 | * @param string $value |
201 | * @return bool |
202 | */ |
203 | public function validateParam( $name, $value ) { |
204 | if ( in_array( $name, [ 'width', 'height', 'page', 'lossy' ] ) ) { |
205 | if ( $name === 'page' && trim( $value ) !== (string)intval( $value ) ) { |
206 | // Extra junk on the end of page, probably actually a caption |
207 | // e.g. [[File:Foo.tiff|thumb|Page 3 of the document shows foo]] |
208 | return false; |
209 | } |
210 | |
211 | if ( $name == 'lossy' ) { |
212 | # NOTE: make sure to use === for comparison. in PHP, '' == 0 and 'foo' == 1. |
213 | |
214 | if ( $value === 1 || $value === 0 || $value === '1' || $value === '0' ) { |
215 | return true; |
216 | } |
217 | |
218 | if ( $value === 'true' || $value === 'false' |
219 | || $value === 'lossy' || $value === 'lossless' |
220 | ) { |
221 | return true; |
222 | } |
223 | |
224 | return false; |
225 | } elseif ( $value <= 0 || $value > 65535 ) { |
226 | // ImageMagick overflows for values > 65536 |
227 | return false; |
228 | } else { |
229 | return true; |
230 | } |
231 | } else { |
232 | return false; |
233 | } |
234 | } |
235 | |
236 | /** |
237 | * Creates parameter string for file name. |
238 | * Page number was added. |
239 | * @param array $params |
240 | * @return string|false |
241 | */ |
242 | public function makeParamString( $params ) { |
243 | if ( |
244 | !isset( $params['width'] ) || !isset( $params['lossy'] ) || !isset( $params['page'] ) |
245 | ) { |
246 | return false; |
247 | } |
248 | |
249 | return "{$params['lossy']}-page{$params['page']}-{$params['width']}px"; |
250 | } |
251 | |
252 | /** |
253 | * Parses parameter string into an array. |
254 | * @param string $str |
255 | * @return array|false |
256 | */ |
257 | public function parseParamString( $str ) { |
258 | if ( preg_match( '/^(\w+)-page(\d+)-(\d+)px$/', $str, $matches ) ) { |
259 | return [ 'width' => $matches[3], 'page' => $matches[2], 'lossy' => $matches[1] ]; |
260 | } |
261 | |
262 | return false; |
263 | } |
264 | |
265 | /** |
266 | * The function is used to specify which parameters to File::transform() should be |
267 | * passed through to thumb.php, in the case where the configuration specifies |
268 | * thumb.php is to be used (e.g. $wgThumbnailScriptPath !== false). You should |
269 | * pass through the same parameters as in makeParamString(). |
270 | * @param array $params |
271 | * @return array |
272 | */ |
273 | protected function getScriptParams( $params ) { |
274 | return [ |
275 | 'width' => $params['width'], |
276 | 'page' => $params['page'], |
277 | 'lossy' => $params['lossy'], |
278 | ]; |
279 | } |
280 | |
281 | /** |
282 | * Prepares param array and sets standard values. |
283 | * Adds normalisation for parameter "lossy". |
284 | * @param File $image |
285 | * @param array &$params |
286 | * @return bool |
287 | */ |
288 | public function normaliseParams( $image, &$params ) { |
289 | if ( isset( $params['page'] ) ) { |
290 | $params['page'] = $this->adjustPage( $image, $params['page'] ); |
291 | } else { |
292 | $params['page'] = $this->firstPage( $image ); |
293 | } |
294 | |
295 | if ( isset( $params['lossy'] ) ) { |
296 | if ( in_array( $params['lossy'], [ 1, '1', 'true', 'lossy' ] ) ) { |
297 | $params['lossy'] = 'lossy'; |
298 | } else { |
299 | $params['lossy'] = 'lossless'; |
300 | } |
301 | } else { |
302 | $page = $params['page']; |
303 | $data = $image->getMetadataArray(); |
304 | |
305 | if ( !$this->isMetadataError( $data ) |
306 | && strtolower( $data['page_data'][$page]['alpha'] ?? '' ) == 'true' |
307 | ) { |
308 | // If there is an alpha channel, use png. |
309 | $params['lossy'] = 'lossless'; |
310 | } else { |
311 | $params['lossy'] = 'lossy'; |
312 | } |
313 | } |
314 | |
315 | return parent::normaliseParams( $image, $params ); |
316 | } |
317 | |
318 | /** |
319 | * @param array|false $metadata |
320 | * @return bool|string[] a list of errors or an error flag (true = error) |
321 | */ |
322 | private function getMetadataErrors( $metadata ) { |
323 | if ( !$metadata ) { |
324 | return true; |
325 | } elseif ( !isset( $metadata['errors'] ) ) { |
326 | return false; |
327 | } |
328 | |
329 | return $metadata['errors']; |
330 | } |
331 | |
332 | /** |
333 | * Is metadata an error condition? |
334 | * @param array|false $metadata Metadata to test |
335 | * @return bool True if metadata is an error, false if it has normal info |
336 | */ |
337 | private function isMetadataError( $metadata ) { |
338 | $errors = $this->getMetadataErrors( $metadata ); |
339 | if ( is_array( $errors ) ) { |
340 | return count( $errors ) > 0; |
341 | } else { |
342 | return $errors; |
343 | } |
344 | } |
345 | |
346 | /** |
347 | * @param array|string $errors_raw |
348 | * @param bool $to_html |
349 | * @return bool|string |
350 | */ |
351 | private function joinMessages( $errors_raw, $to_html = true ) { |
352 | if ( is_array( $errors_raw ) ) { |
353 | if ( !$errors_raw ) { |
354 | return false; |
355 | } |
356 | |
357 | $errors = []; |
358 | foreach ( $errors_raw as $error ) { |
359 | if ( $error === false || $error === null || $error === 0 || $error === '' ) { |
360 | continue; |
361 | } |
362 | |
363 | $error = trim( $error ); |
364 | |
365 | if ( $error === '' ) { |
366 | continue; |
367 | } |
368 | |
369 | if ( $to_html ) { |
370 | $error = htmlspecialchars( $error ); |
371 | } |
372 | |
373 | $errors[] = $error; |
374 | } |
375 | |
376 | if ( $to_html ) { |
377 | return trim( implode( '<br />', $errors ) ); |
378 | } else { |
379 | return trim( implode( ";\n", $errors ) ); |
380 | } |
381 | } |
382 | |
383 | return $errors_raw; |
384 | } |
385 | |
386 | /** |
387 | * What method to use to scale this file |
388 | * |
389 | * @see TransformationalImageHandler::getScalerType |
390 | * @param string $dstPath Path to store thumbnail |
391 | * @param bool $checkDstPath Whether to verify destination path exists |
392 | * @return callable Transform function to call. |
393 | */ |
394 | protected function getScalerType( $dstPath, $checkDstPath = true ) { |
395 | if ( !$dstPath && $checkDstPath ) { |
396 | // We don't have the option of doing client side scaling for this filetype. |
397 | throw new RuntimeException( "Cannot create thumbnail, no destination path" ); |
398 | } |
399 | |
400 | return [ $this, 'transformIM' ]; |
401 | } |
402 | |
403 | /** |
404 | * Actually scale the file (using ImageMagick). |
405 | * |
406 | * @param File $file File object |
407 | * @param array $scalerParams Scaling options (see TransformationalImageHandler::doTransform) |
408 | * @return bool|MediaTransformError False on success, an instance of MediaTransformError |
409 | * otherwise. |
410 | * @note Success is noted by $scalerParams['dstPath'] no longer being a 0 byte file. |
411 | */ |
412 | protected function transformIM( $file, $scalerParams ) { |
413 | global $wgImageMagickConvertCommand, $wgMaxImageArea; |
414 | |
415 | $meta = $file->getMetadataArray(); |
416 | |
417 | $errors = $this->getMetadataErrors( $meta ); |
418 | |
419 | if ( $errors ) { |
420 | $errors = $this->joinMessages( $errors ); |
421 | if ( is_string( $errors ) ) { |
422 | // TODO: original error as param // TESTME |
423 | return $this->doThumbError( $scalerParams, 'tiff_bad_file' ); |
424 | } else { |
425 | return $this->doThumbError( $scalerParams, 'tiff_no_metadata' ); |
426 | } |
427 | } |
428 | |
429 | if ( !$this->verifyMetaData( $meta, $error ) ) { |
430 | return $this->doThumbError( $scalerParams, $error ); |
431 | } |
432 | if ( !wfMkdirParents( dirname( $scalerParams['dstPath'] ), null, __METHOD__ ) ) { |
433 | return $this->doThumbError( $scalerParams, 'thumbnail_dest_directory' ); |
434 | } |
435 | |
436 | // Get params and force width, height and page to be integers |
437 | $width = intval( $scalerParams['physicalWidth'] ); |
438 | $height = intval( $scalerParams['physicalHeight'] ); |
439 | $page = intval( $scalerParams['page'] ); |
440 | $srcPath = $this->escapeMagickInput( $scalerParams['srcPath'], (string)( $page - 1 ) ); |
441 | $dstPath = $this->escapeMagickOutput( $scalerParams['dstPath'] ); |
442 | |
443 | if ( $wgMaxImageArea |
444 | && isset( $meta['page_data'][$page]['pixels'] ) |
445 | && $meta['page_data'][$page]['pixels'] > $wgMaxImageArea |
446 | ) { |
447 | return $this->doThumbError( $scalerParams, 'tiff_sourcefile_too_large' ); |
448 | } |
449 | |
450 | $command = $this->commandFactory->create() |
451 | ->params( |
452 | $wgImageMagickConvertCommand, |
453 | $srcPath, |
454 | '-depth', '8', |
455 | '-resize', $width, |
456 | $dstPath |
457 | ) |
458 | ->includeStderr(); |
459 | |
460 | $result = $command->execute(); |
461 | $exitCode = $result->getExitCode(); |
462 | if ( $exitCode !== 0 ) { |
463 | $err = $result->getStdout(); |
464 | $cmd = $command->getCommandString(); |
465 | wfDebugLog( |
466 | 'thumbnail', |
467 | "thumbnail failed on " . wfHostname() . "; error $exitCode \"$err\" from \"$cmd\"" |
468 | ); |
469 | return $this->getMediaTransformError( $scalerParams, $err ); |
470 | } else { |
471 | // no error |
472 | return false; |
473 | } |
474 | } |
475 | |
476 | /** |
477 | * Get the thumbnail extension and MIME type for a given source MIME type |
478 | * @param string $ext |
479 | * @param string $mime |
480 | * @param array|null $params |
481 | * @return array thumbnail extension and MIME type |
482 | */ |
483 | public function getThumbType( $ext, $mime, $params = null ) { |
484 | // Make sure the file is actually a tiff image |
485 | $tiffImageThumbType = parent::getThumbType( $ext, $mime, $params ); |
486 | if ( $tiffImageThumbType[1] !== 'image/tiff' ) { |
487 | // We have some other file pretending to be a tiff image. |
488 | return $tiffImageThumbType; |
489 | } |
490 | |
491 | if ( isset( $params['lossy'] ) && $params['lossy'] == 'lossy' ) { |
492 | return [ 'jpg', 'image/jpeg' ]; |
493 | } else { |
494 | return [ 'png', 'image/png' ]; |
495 | } |
496 | } |
497 | |
498 | /** |
499 | * Returns the number of available pages/embedded files |
500 | * @param File $image |
501 | * @return int |
502 | */ |
503 | public function pageCount( File $image ) { |
504 | $data = $image->getMetadataArray(); |
505 | if ( $this->isMetadataError( $data ) ) { |
506 | return 1; |
507 | } |
508 | |
509 | return intval( $data['page_count'] ); |
510 | } |
511 | |
512 | /** |
513 | * Returns the number of the first page in the file |
514 | * @param File $image |
515 | * @return int |
516 | */ |
517 | private function firstPage( $image ) { |
518 | $data = $image->getMetadataArray(); |
519 | if ( $this->isMetadataError( $data ) ) { |
520 | return 1; |
521 | } |
522 | return intval( $data['first_page'] ); |
523 | } |
524 | |
525 | /** |
526 | * Returns the number of the last page in the file |
527 | * @param File $image |
528 | * @return int |
529 | */ |
530 | private function lastPage( $image ) { |
531 | $data = $image->getMetadataArray(); |
532 | if ( $this->isMetadataError( $data ) ) { |
533 | return 1; |
534 | } |
535 | return intval( $data['last_page'] ); |
536 | } |
537 | |
538 | /** |
539 | * Returns a page number within range. |
540 | * @param File $image |
541 | * @param int|string $page |
542 | * @return int |
543 | */ |
544 | private function adjustPage( $image, $page ) { |
545 | $page = intval( $page ); |
546 | |
547 | if ( !$page || $page < $this->firstPage( $image ) ) { |
548 | $page = $this->firstPage( $image ); |
549 | } |
550 | |
551 | if ( $page > $this->lastPage( $image ) ) { |
552 | $page = $this->lastPage( $image ); |
553 | } |
554 | |
555 | return $page; |
556 | } |
557 | |
558 | /** |
559 | * Returns a new error message. |
560 | * @param array $params |
561 | * @param string|array $msg |
562 | * @return MediaTransformError |
563 | */ |
564 | protected function doThumbError( $params, $msg ) { |
565 | global $wgThumbLimits; |
566 | |
567 | $errorParams = []; |
568 | if ( empty( $params['width'] ) ) { |
569 | $user = RequestContext::getMain()->getUser(); |
570 | // no usable width/height in the parameter array |
571 | // only happens if we don't have image meta-data, and no |
572 | // size was specified by the user. |
573 | // we need to pick *some* size, and the preferred |
574 | // thumbnail size seems sane. |
575 | $sz = $this->userOptionsLookup->getOption( $user, 'thumbsize' ); |
576 | $errorParams['clientWidth'] = $wgThumbLimits[ $sz ]; |
577 | // we don't have a height or aspect ratio. make it square. |
578 | $errorParams['clientHeight'] = $wgThumbLimits[ $sz ]; |
579 | } else { |
580 | $errorParams['clientWidth'] = intval( $params['width'] ); |
581 | |
582 | if ( !empty( $params['height'] ) ) { |
583 | $errorParams['clientHeight'] = intval( $params['height'] ); |
584 | } else { |
585 | // we don't have a height or aspect ratio. make it square. |
586 | $errorParams['clientHeight'] = $errorParams['clientWidth']; |
587 | } |
588 | } |
589 | |
590 | return $this->getMediaTransformError( $errorParams, Message::newFromSpecifier( $msg )->text() ); |
591 | } |
592 | |
593 | /** |
594 | * @param MediaHandlerState $state |
595 | * @param string $path |
596 | * @return array |
597 | */ |
598 | public function getSizeAndMetadata( $state, $path ) { |
599 | $metadata = $state->getHandlerState( 'TiffMetaArray' ); |
600 | if ( !$metadata ) { |
601 | $metadata = $this->getTiffImage( $state, $path )->retrieveMetaData(); |
602 | $state->setHandlerState( 'TiffMetaArray', $metadata ); |
603 | } |
604 | |
605 | $gis = getimagesize( $path ); |
606 | if ( $gis ) { |
607 | [ $width, $height ] = $gis; |
608 | } else { |
609 | $width = 0; |
610 | $height = 0; |
611 | } |
612 | |
613 | return [ |
614 | 'width' => $width, |
615 | 'height' => $height, |
616 | 'metadata' => $metadata |
617 | ]; |
618 | } |
619 | |
620 | /** |
621 | * Check if the metadata string is valid for this handler. |
622 | * If it returns false, Image will reload the metadata from the file and update the database |
623 | * @param File $image |
624 | * @return bool |
625 | */ |
626 | public function isFileMetadataValid( $image ) { |
627 | $metadata = $image->getMetadataArray(); |
628 | if ( isset( $metadata['errors'] ) ) { |
629 | // In the case of a broken file, we do not want to reload the |
630 | // metadata on every request. |
631 | return self::METADATA_GOOD; |
632 | } |
633 | |
634 | if ( !isset( $metadata['TIFF_METADATA_VERSION'] ) |
635 | || $metadata['TIFF_METADATA_VERSION'] != self::TIFF_METADATA_VERSION |
636 | ) { |
637 | return self::METADATA_BAD; |
638 | } |
639 | |
640 | return self::METADATA_GOOD; |
641 | } |
642 | |
643 | /** |
644 | * Get an array structure that looks like this: |
645 | * |
646 | * [ |
647 | * 'visible' => [ |
648 | * 'Human-readable name' => 'Human readable value', |
649 | * ... |
650 | * ], |
651 | * 'collapsed' => [ |
652 | * 'Human-readable name' => 'Human readable value', |
653 | * ... |
654 | * ] |
655 | * ] |
656 | * The UI will format this into a table where the visible fields are always |
657 | * visible, and the collapsed fields are optionally visible. |
658 | * |
659 | * The function should return false if there is no metadata to display. |
660 | * |
661 | * @param File $image |
662 | * @param bool|IContextSource $context Context to use (optional) |
663 | * @return array|bool |
664 | */ |
665 | public function formatMetadata( $image, $context = false ) { |
666 | $result = [ |
667 | 'visible' => [], |
668 | 'collapsed' => [] |
669 | ]; |
670 | $metadata = $image->getMetadata(); |
671 | if ( !$metadata ) { |
672 | return false; |
673 | } |
674 | $exif = unserialize( $metadata ); |
675 | if ( !isset( $exif['exif'] ) || !$exif['exif'] ) { |
676 | return false; |
677 | } |
678 | $exif = $exif['exif']; |
679 | unset( $exif['MEDIAWIKI_EXIF_VERSION'] ); |
680 | $formatted = FormatMetadata::getFormattedData( $exif, $context ); |
681 | |
682 | // Sort fields into visible and collapsed |
683 | $visibleFields = $this->visibleMetadataFields(); |
684 | foreach ( $formatted as $name => $value ) { |
685 | $tag = strtolower( $name ); |
686 | $this->addMeta( $result, |
687 | in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed', |
688 | 'exif', |
689 | $tag, |
690 | $value |
691 | ); |
692 | } |
693 | $meta = unserialize( $metadata ); |
694 | $errors_raw = $this->getMetadataErrors( $meta ); |
695 | if ( $errors_raw ) { |
696 | $errors = $this->joinMessages( $errors_raw ); |
697 | $this->addMeta( $result, |
698 | 'collapsed', |
699 | 'metadata', |
700 | 'error', |
701 | $errors |
702 | ); |
703 | // XXX: need translation for <metadata-error> |
704 | } |
705 | if ( !empty( $meta['warnings'] ) ) { |
706 | $warnings = $this->joinMessages( $meta['warnings'] ); |
707 | $this->addMeta( $result, |
708 | 'collapsed', |
709 | 'metadata', |
710 | 'warning', |
711 | $warnings |
712 | ); |
713 | // XXX: need translation for <metadata-warning> |
714 | } |
715 | return $result; |
716 | } |
717 | |
718 | /** |
719 | * Returns a PagedTiffImage or creates a new one if it doesn't exist. |
720 | * @param MediaHandlerState|false $state The image object, or false if there isn't one |
721 | * @param string $path path to the image? |
722 | * @return PagedTiffImage |
723 | */ |
724 | private function getTiffImage( $state, $path ) { |
725 | if ( !$state ) { |
726 | return $this->getCachedTiffImage( $path ); |
727 | } |
728 | |
729 | // If there is an Image object, we check whether there's already a TiffImage |
730 | // instance in there; if not, a new instance is created and stored in the Image object |
731 | $tiffImage = $state->getHandlerState( 'TiffImage' ); |
732 | if ( !$tiffImage ) { |
733 | $tiffImage = $this->getCachedTiffImage( $path ); |
734 | $state->setHandlerState( 'TiffImage', $tiffImage ); |
735 | } |
736 | |
737 | return $tiffImage; |
738 | } |
739 | |
740 | /** |
741 | * Gets a PagedTiffImage from the cache, or creates one |
742 | * @param string $path path to the image |
743 | * @return PagedTiffImage |
744 | */ |
745 | private function getCachedTiffImage( $path ) { |
746 | $image = $this->knownImages->get( $path ); |
747 | if ( $image === null ) { |
748 | $image = new PagedTiffImage( $this->commandFactory, $this->statsFactory, $path ); |
749 | $this->knownImages->set( $path, $image ); |
750 | } |
751 | return $image; |
752 | } |
753 | |
754 | /** |
755 | * Get an associative array of page dimensions |
756 | * Currently "width" and "height" are understood, but this might be |
757 | * expanded in the future. |
758 | * @param File $image |
759 | * @param int $page |
760 | * @return int|false Returns false if unknown or if the document is not multi-page. |
761 | */ |
762 | public function getPageDimensions( File $image, $page ) { |
763 | // makeImageLink (Linker.php) sets $page to false if no page parameter |
764 | // is set in wiki code |
765 | $page = $this->adjustPage( $image, $page ); |
766 | $data = $image->getMetadataArray(); |
767 | return PagedTiffImage::getPageSize( $data, $page ); |
768 | } |
769 | |
770 | /** |
771 | * @param File $file |
772 | * @return bool |
773 | */ |
774 | public function isExpensiveToThumbnail( $file ) { |
775 | return $file->getSize() > self::EXPENSIVE_SIZE_LIMIT; |
776 | } |
777 | |
778 | /** |
779 | * What source thumbnail to use. |
780 | * |
781 | * This does not use MW's builtin bucket system, as it tries to take |
782 | * advantage of the fact that VIPS can scale integer shrink factors |
783 | * much more efficiently than non-integral scaling factors. |
784 | * |
785 | * @param File $file |
786 | * @param array $params Parameters to transform file with. |
787 | * @return array Array with keys path, width and height |
788 | */ |
789 | protected function getThumbnailSource( $file, $params ) { |
790 | /** @var MediaTransformOutput */ |
791 | $mto = $this->getIntermediaryStep( $file, $params ); |
792 | if ( $mto && !$mto->isError() ) { |
793 | return [ |
794 | 'path' => $mto->getLocalCopyPath(), |
795 | 'width' => $mto->getWidth(), |
796 | 'height' => $mto->getHeight(), |
797 | // The path to the temporary file may be deleted when last |
798 | // instance of the MediaTransformOutput is garbage collected, |
799 | // so keep a reference around. |
800 | 'mto' => $mto |
801 | ]; |
802 | } else { |
803 | return parent::getThumbnailSource( $file, $params ); |
804 | } |
805 | } |
806 | |
807 | /** |
808 | * Get an intermediary sized thumb to do further rendering on |
809 | * |
810 | * Tiff files can be huge. This method gets a large thumbnail |
811 | * to further scale things down. Size is chosen to be |
812 | * efficient to scale in vips for those who use VipsScaler |
813 | * |
814 | * @param File $file |
815 | * @param array $params Scaling parameters for original thumbnail |
816 | * @return MediaTransformOutput|MediaTransformError|bool false if no in between step needed, |
817 | * MediaTransformError on error. False if the doTransform method returns false |
818 | * MediaTransformOutput on success. |
819 | */ |
820 | private function getIntermediaryStep( $file, $params ) { |
821 | global $wgTiffIntermediaryScaleStep, $wgThumbnailMinimumBucketDistance; |
822 | |
823 | $page = intval( $params['page'] ); |
824 | $page = $this->adjustPage( $file, $page ); |
825 | $srcWidth = $file->getWidth( $page ); |
826 | $srcHeight = $file->getHeight( $page ); |
827 | |
828 | if ( $srcWidth <= $wgTiffIntermediaryScaleStep ) { |
829 | // Image is already smaller than intermediary step or at that step |
830 | return false; |
831 | } |
832 | |
833 | $widthOfFinalThumb = $params['physicalWidth']; |
834 | |
835 | // Try and get a width that's easy for VipsScaler to work with |
836 | // i.e. Is an integer shrink factor. |
837 | $rx = floor( $srcWidth / ( $wgTiffIntermediaryScaleStep + 0.125 ) ); |
838 | $intermediaryWidth = intval( floor( $srcWidth / $rx ) ); |
839 | $intermediaryHeight = intval( floor( $srcHeight / $rx ) ); |
840 | |
841 | // We need both the vertical and horizontal shrink factors to be |
842 | // integers, and at the same time make sure that both vips and mediawiki |
843 | // have the same height for a given width (MediaWiki makes the assumption |
844 | // that the height of an image functionally depends on its width) |
845 | for ( ; $rx >= 2; $rx-- ) { |
846 | $intermediaryWidth = intval( floor( $srcWidth / $rx ) ); |
847 | $intermediaryHeight = intval( floor( $srcHeight / $rx ) ); |
848 | if ( $intermediaryHeight == |
849 | File::scaleHeight( $srcWidth, $srcHeight, $intermediaryWidth ) |
850 | ) { |
851 | break; |
852 | } |
853 | } |
854 | |
855 | if ( |
856 | $intermediaryWidth <= $widthOfFinalThumb + $wgThumbnailMinimumBucketDistance || $rx < 2 |
857 | ) { |
858 | // Need to scale the original full sized thumb |
859 | return false; |
860 | } |
861 | |
862 | static $isInThisFunction; |
863 | |
864 | if ( $isInThisFunction ) { |
865 | // Sanity check, should never be reached |
866 | throw new LogicException( "Loop detected in " . __METHOD__ ); |
867 | } |
868 | $isInThisFunction = true; |
869 | |
870 | $newParams = [ |
871 | 'width' => $intermediaryWidth, |
872 | 'page' => $page, |
873 | // Render a png, to avoid loss of quality when doing multi-step |
874 | 'lossy' => 'lossless' |
875 | ]; |
876 | |
877 | // RENDER_NOW causes rendering in this process if |
878 | // thumb doesn't exist, but unlike RENDER_FORCE, will return |
879 | // a cached thumb if available. |
880 | $mto = $file->transform( $newParams, File::RENDER_NOW ); |
881 | |
882 | $isInThisFunction = false; |
883 | return $mto; |
884 | } |
885 | } |