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