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