Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
46.88% covered (danger)
46.88%
15 / 32
CRAP
73.84% covered (warning)
73.84%
206 / 279
PagedTiffHandler
0.00% covered (danger)
0.00%
0 / 1
46.88% covered (danger)
46.88%
15 / 32
377.94
73.84% covered (warning)
73.84%
206 / 279
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
7 / 7
 isEnabled
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 mustRender
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 isMultiPage
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 verifyUpload
0.00% covered (danger)
0.00%
0 / 1
3.07
80.00% covered (warning)
80.00%
8 / 10
 verifyMetaData
0.00% covered (danger)
0.00%
0 / 1
10.86
57.14% covered (warning)
57.14%
12 / 21
 getParamMap
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 validateParam
0.00% covered (danger)
0.00%
0 / 1
15.66
85.71% covered (warning)
85.71%
12 / 14
 makeParamString
0.00% covered (danger)
0.00%
0 / 1
4.59
66.67% covered (warning)
66.67%
2 / 3
 parseParamString
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 getScriptParams
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 normaliseParams
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
14 / 14
 getMetadataErrors
0.00% covered (danger)
0.00%
0 / 1
3.07
80.00% covered (warning)
80.00%
4 / 5
 isMetadataError
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 joinMessages
0.00% covered (danger)
0.00%
0 / 1
12.58
76.47% covered (warning)
76.47%
13 / 17
 getScalerType
0.00% covered (danger)
0.00%
0 / 1
3.33
66.67% covered (warning)
66.67%
2 / 3
 transformIM
0.00% covered (danger)
0.00%
0 / 1
12.81
63.89% covered (warning)
63.89%
23 / 36
 getThumbType
0.00% covered (danger)
0.00%
0 / 1
4.07
83.33% covered (warning)
83.33%
5 / 6
 pageCount
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 firstPage
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 lastPage
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 adjustPage
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
6 / 6
 doThumbError
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 11
 getSizeAndMetadata
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
12 / 12
 isFileMetadataValid
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 7
 formatMetadata
0.00% covered (danger)
0.00%
0 / 1
11.71
61.29% covered (warning)
61.29%
19 / 31
 getTiffImage
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 getCachedTiffImage
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 getPageDimensions
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 isExpensiveToThumbnail
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getThumbnailSource
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 getIntermediaryStep
0.00% covered (danger)
0.00%
0 / 1
7
96.30% covered (success)
96.30%
26 / 27
<?php
/**
 * Copyright © Wikimedia Deutschland, 2009
 * Authors Hallo Welt! Medienwerkstatt GmbH
 * Authors Sebastian Ulbricht, Daniel Lynge, Marc Reymann, Markus Glaser
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 */
namespace MediaWiki\Extension\PagedTiffHandler;
use Exception;
use File;
use FormatMetadata;
use IContextSource;
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
use MapCacheLRU;
use MediaHandlerState;
use MediaTransformError;
use MediaTransformOutput;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\MediaWikiServices;
use MediaWiki\Shell\CommandFactory;
use MediaWiki\User\UserOptionsLookup;
use Message;
use RequestContext;
use Status;
use TransformationalImageHandler;
class PagedTiffHandler extends TransformationalImageHandler {
    // TIFF files over 10M are considered expensive to thumbnail
    private const EXPENSIVE_SIZE_LIMIT = 10485760;
    /**
     * 1.0: Initial
     * 1.1: Fixed bugs in imageinfo parser
     * 1.2: Photoshop quirks (reverted)
     * 1.3: Handing extra IFDs reported by tiffinfo
     * 1.4: Allowed page numbering to start from numbers other than 1
     */
    public const TIFF_METADATA_VERSION = '1.4';
    /**
     * Known images cache
     *
     * @var MapCacheLRU
     */
    private $knownImages;
    /** @var CommandFactory */
    private $commandFactory;
    /** @var HookContainer */
    private $hookContainer;
    /** @var UserOptionsLookup */
    private $userOptionsLookup;
    /** @var StatsdDataFactoryInterface */
    private $statsdFactory;
    /**
     * Number of images to keep in $knownImages
     */
    private const CACHE_SIZE = 5;
    public function __construct() {
        $this->knownImages = new MapCacheLRU( self::CACHE_SIZE );
        $services = MediaWikiServices::getInstance();
        $this->commandFactory = $services->getShellCommandFactory();
        $this->hookContainer = $services->getHookContainer();
        $this->userOptionsLookup = $services->getUserOptionsLookup();
        $this->statsdFactory = $services->getStatsdDataFactory();
    }
    /**
     * @return bool
     */
    public function isEnabled() {
        return true;
    }
    /**
     * @param File $img
     * @return bool
     */
    public function mustRender( $img ) {
        return true;
    }
    /**
     * Does the file format support multi-page documents?
     * @param File $img
     * @return bool
     */
    public function isMultiPage( $img ) {
        return true;
    }
    /**
     * Various checks against the uploaded file
     * - maximum upload size
     * - maximum number of embedded files
     * - maximum size of metadata
     * - identify-errors
     * - identify-warnings
     * - check for running-identify-service
     * @param string $fileName
     * @return Status
     */
    public function verifyUpload( $fileName ) {
        $status = Status::newGood();
        $meta = $this->getTiffImage( false, $fileName )->retrieveMetaData();
        if ( !$meta ) {
            wfDebug( __METHOD__ . ": unable to retrieve metadata" );
            $status->fatal( 'tiff_out_of_service' );
        } else {
            $ok = $this->verifyMetaData( $meta, $error );
            if ( !$ok ) {
                $this->getCachedTiffImage( $fileName )->resetMetaData();
                call_user_func_array( [ $status, 'fatal' ], $error );
            }
        }
        return $status;
    }
    /**
     * @param array $meta
     * @param array &$error
     * @return bool
     */
    private function verifyMetaData( $meta, &$error ) {
        global $wgTiffMaxEmbedFiles, $wgTiffMaxMetaSize;
        $errors = $this->getMetadataErrors( $meta );
        if ( $errors ) {
            $error = [ 'tiff_bad_file', $this->joinMessages( $errors ) ];
            wfDebug( __METHOD__ . "{$error[0]} " .
                $this->joinMessages( $errors, false ) );
            return false;
        }
        if ( $meta['page_count'] <= 0 || empty( $meta['page_data'] ) ) {
            $error = [ 'tiff_page_error', $meta['page_count'] ];
            wfDebug( __METHOD__ . "{$error[0]}" );
            return false;
        }
        if ( $wgTiffMaxEmbedFiles && $meta['page_count'] > $wgTiffMaxEmbedFiles ) {
            $error = [ 'tiff_too_much_embed_files', $meta['page_count'], $wgTiffMaxEmbedFiles ];
            wfDebug( __METHOD__ . "{$error[0]}" );
            return false;
        }
        $len = strlen( serialize( $meta ) );
        if ( ( $len + 1 ) > $wgTiffMaxMetaSize ) {
            $error = [ 'tiff_too_much_meta', $len, $wgTiffMaxMetaSize ];
            wfDebug( __METHOD__ . "{$error[0]}" );
            return false;
        }
        wfDebug( __METHOD__ . ": metadata is ok" );
        return true;
    }
    /**
     * Maps MagicWord-IDs to parameters.
     * In this case, width, page, and lossy.
     * @return array
     */
    public function getParamMap() {
        return [
            'img_width' => 'width',
            'img_page' => 'page',
            'img_lossy' => 'lossy',
        ];
    }
    /**
     * Checks whether parameters are valid and have valid values.
     * Check for lossy was added.
     * @param string $name
     * @param string $value
     * @return bool
     */
    public function validateParam( $name, $value ) {
        if ( in_array( $name, [ 'width', 'height', 'page', 'lossy' ] ) ) {
            if ( $name === 'page' && trim( $value ) !== (string)intval( $value ) ) {
                // Extra junk on the end of page, probably actually a caption
                // e.g. [[File:Foo.tiff|thumb|Page 3 of the document shows foo]]
                return false;
            }
            if ( $name == 'lossy' ) {
                # NOTE: make sure to use === for comparison. in PHP, '' == 0 and 'foo' == 1.
                if ( $value === 1 || $value === 0 || $value === '1' || $value === '0' ) {
                    return true;
                }
                if ( $value === 'true' || $value === 'false'
                    || $value === 'lossy' || $value === 'lossless'
                ) {
                    return true;
                }
                return false;
            } elseif ( $value <= 0 || $value > 65535 ) { // ImageMagick overflows for values > 65536
                return false;
            } else {
                return true;
            }
        } else {
            return false;
        }
    }
    /**
     * Creates parameter string for file name.
     * Page number was added.
     * @param array $params
     * @return string|false
     */
    public function makeParamString( $params ) {
        if (
            !isset( $params['width'] ) || !isset( $params['lossy'] ) || !isset( $params['page'] )
        ) {
            return false;
        }
        return "{$params['lossy']}-page{$params['page']}-{$params['width']}px";
    }
    /**
     * Parses parameter string into an array.
     * @param string $str
     * @return array|false
     */
    public function parseParamString( $str ) {
        if ( preg_match( '/^(\w+)-page(\d+)-(\d+)px$/', $str, $matches ) ) {
            return [ 'width' => $matches[3], 'page' => $matches[2], 'lossy' => $matches[1] ];
        }
        return false;
    }
    /**
     * The function is used to specify which parameters to File::transform() should be
     * passed through to thumb.php, in the case where the configuration specifies
     * thumb.php is to be used (e.g. $wgThumbnailScriptPath !== false). You should
     * pass through the same parameters as in makeParamString().
     * @param array $params
     * @return array
     */
    protected function getScriptParams( $params ) {
        return [
            'width' => $params['width'],
            'page' => $params['page'],
            'lossy' => $params['lossy'],
        ];
    }
    /**
     * Prepares param array and sets standard values.
     * Adds normalisation for parameter "lossy".
     * @param File $image
     * @param array &$params
     * @return bool
     */
    public function normaliseParams( $image, &$params ) {
        if ( isset( $params['page'] ) ) {
            $params['page'] = $this->adjustPage( $image, $params['page'] );
        } else {
            $params['page'] = $this->firstPage( $image );
        }
        if ( isset( $params['lossy'] ) ) {
            if ( in_array( $params['lossy'], [ 1, '1', 'true', 'lossy' ] ) ) {
                $params['lossy'] = 'lossy';
            } else {
                $params['lossy'] = 'lossless';
            }
        } else {
            $page = $params['page'];
            $data = $image->getMetadataArray();
            if ( !$this->isMetadataError( $data )
                && strtolower( $data['page_data'][$page]['alpha'] ?? '' ) == 'true'
            ) {
                // If there is an alpha channel, use png.
                $params['lossy'] = 'lossless';
            } else {
                $params['lossy'] = 'lossy';
            }
        }
        return parent::normaliseParams( $image, $params );
    }
    /**
     * @param array|false $metadata
     * @return bool|string[] a list of errors or an error flag (true = error)
     */
    private function getMetadataErrors( $metadata ) {
        if ( !$metadata ) {
            return true;
        } elseif ( !isset( $metadata['errors'] ) ) {
            return false;
        }
        return $metadata['errors'];
    }
    /**
     * Is metadata an error condition?
     * @param array|false $metadata Metadata to test
     * @return bool True if metadata is an error, false if it has normal info
     */
    private function isMetadataError( $metadata ) {
        $errors = $this->getMetadataErrors( $metadata );
        if ( is_array( $errors ) ) {
            return count( $errors ) > 0;
        } else {
            return $errors;
        }
    }
    /**
     * @param array|string $errors_raw
     * @param bool $to_html
     * @return bool|string
     */
    private function joinMessages( $errors_raw, $to_html = true ) {
        if ( is_array( $errors_raw ) ) {
            if ( !$errors_raw ) {
                return false;
            }
            $errors = [];
            foreach ( $errors_raw as $error ) {
                if ( $error === false || $error === null || $error === 0 || $error === '' ) {
                    continue;
                }
                $error = trim( $error );
                if ( $error === '' ) {
                    continue;
                }
                if ( $to_html ) {
                    $error = htmlspecialchars( $error );
                }
                $errors[] = $error;
            }
            if ( $to_html ) {
                return trim( implode( '<br />', $errors ) );
            } else {
                return trim( implode( ";\n", $errors ) );
            }
        }
        return $errors_raw;
    }
    /**
     * What method to use to scale this file
     *
     * @see TransformationalImageHandler::getScalerType
     * @param string $dstPath Path to store thumbnail
     * @param bool $checkDstPath Whether to verify destination path exists
     * @return callable Transform function to call.
     */
    protected function getScalerType( $dstPath, $checkDstPath = true ) {
        if ( !$dstPath && $checkDstPath ) {
            // We don't have the option of doing client side scaling for this filetype.
            throw new Exception( "Cannot create thumbnail, no destination path" );
        }
        return [ $this, 'transformIM' ];
    }
    /**
     * Actually scale the file (using ImageMagick).
     *
     * @param File $file File object
     * @param array $scalerParams Scaling options (see TransformationalImageHandler::doTransform)
     * @return bool|MediaTransformError False on success, an instance of MediaTransformError
     *   otherwise.
     * @note Success is noted by $scalerParams['dstPath'] no longer being a 0 byte file.
     */
    protected function transformIM( $file, $scalerParams ) {
        global $wgImageMagickConvertCommand, $wgMaxImageArea;
        $meta = $file->getMetadataArray();
        $errors = $this->getMetadataErrors( $meta );
        if ( $errors ) {
            $errors = $this->joinMessages( $errors );
            if ( is_string( $errors ) ) {
                // TODO: original error as param // TESTME
                return $this->doThumbError( $scalerParams, 'tiff_bad_file' );
            } else {
                return $this->doThumbError( $scalerParams, 'tiff_no_metadata' );
            }
        }
        if ( !$this->verifyMetaData( $meta, $error ) ) {
            return $this->doThumbError( $scalerParams, $error );
        }
        if ( !wfMkdirParents( dirname( $scalerParams['dstPath'] ), null, __METHOD__ ) ) {
            return $this->doThumbError( $scalerParams, 'thumbnail_dest_directory' );
        }
        // Get params and force width, height and page to be integers
        $width = intval( $scalerParams['physicalWidth'] );
        $height = intval( $scalerParams['physicalHeight'] );
        $page = intval( $scalerParams['page'] );
        $srcPath = $this->escapeMagickInput( $scalerParams['srcPath'], (string)( $page - 1 ) );
        $dstPath = $this->escapeMagickOutput( $scalerParams['dstPath'] );
        if ( $wgMaxImageArea
            && isset( $meta['page_data'][$page]['pixels'] )
            && $meta['page_data'][$page]['pixels'] > $wgMaxImageArea
        ) {
            return $this->doThumbError( $scalerParams, 'tiff_sourcefile_too_large' );
        }
        $command = $this->commandFactory->create()
            ->params(
                $wgImageMagickConvertCommand,
                $srcPath,
                '-depth', '8',
                '-resize', $width,
                $dstPath
            )
            ->includeStderr();
        $result = $command->execute();
        $exitCode = $result->getExitCode();
        if ( $exitCode !== 0 ) {
            $err = $result->getStdout();
            $cmd = $command->getCommandString();
            wfDebugLog(
                'thumbnail',
                "thumbnail failed on " . wfHostname() . "; error $exitCode \"$err\" from \"$cmd\""
            );
            return $this->getMediaTransformError( $scalerParams, $err );
        } else {
            return false; /* no error */
        }
    }
    /**
     * Get the thumbnail extension and MIME type for a given source MIME type
     * @param string $ext
     * @param string $mime
     * @param array|null $params
     * @return array thumbnail extension and MIME type
     */
    public function getThumbType( $ext, $mime, $params = null ) {
        // Make sure the file is actually a tiff image
        $tiffImageThumbType = parent::getThumbType( $ext, $mime, $params );
        if ( $tiffImageThumbType[1] !== 'image/tiff' ) {
            // We have some other file pretending to be a tiff image.
            return $tiffImageThumbType;
        }
        if ( isset( $params['lossy'] ) && $params['lossy'] == 'lossy' ) {
            return [ 'jpg', 'image/jpeg' ];
        } else {
            return [ 'png', 'image/png' ];
        }
    }
    /**
     * Returns the number of available pages/embedded files
     * @param File $image
     * @return int
     */
    public function pageCount( File $image ) {
        $data = $image->getMetadataArray();
        if ( $this->isMetadataError( $data ) ) {
            return 1;
        }
        return intval( $data['page_count'] );
    }
    /**
     * Returns the number of the first page in the file
     * @param File $image
     * @return int
     */
    private function firstPage( $image ) {
        $data = $image->getMetadataArray();
        if ( $this->isMetadataError( $data ) ) {
            return 1;
        }
        return intval( $data['first_page'] );
    }
    /**
     * Returns the number of the last page in the file
     * @param File $image
     * @return int
     */
    private function lastPage( $image ) {
        $data = $image->getMetadataArray();
        if ( $this->isMetadataError( $data ) ) {
            return 1;
        }
        return intval( $data['last_page'] );
    }
    /**
     * Returns a page number within range.
     * @param File $image
     * @param int|string $page
     * @return int
     */
    private function adjustPage( $image, $page ) {
        $page = intval( $page );
        if ( !$page || $page < $this->firstPage( $image ) ) {
            $page = $this->firstPage( $image );
        }
        if ( $page > $this->lastPage( $image ) ) {
            $page = $this->lastPage( $image );
        }
        return $page;
    }
    /**
     * Returns a new error message.
     * @param array $params
     * @param string|array $msg
     * @return MediaTransformError
     */
    protected function doThumbError( $params, $msg ) {
        global $wgThumbLimits;
        $errorParams = [];
        if ( empty( $params['width'] ) ) {
            $user = RequestContext::getMain()->getUser();
            // no usable width/height in the parameter array
            // only happens if we don't have image meta-data, and no
            // size was specified by the user.
            // we need to pick *some* size, and the preferred
            // thumbnail size seems sane.
            $sz = $this->userOptionsLookup->getOption( $user, 'thumbsize' );
            $errorParams['clientWidth'] = $wgThumbLimits[ $sz ];
            // we don't have a height or aspect ratio. make it square.
            $errorParams['clientHeight'] = $wgThumbLimits[ $sz ];
        } else {
            $errorParams['clientWidth'] = intval( $params['width'] );
            if ( !empty( $params['height'] ) ) {
                $errorParams['clientHeight'] = intval( $params['height'] );
            } else {
                // we don't have a height or aspect ratio. make it square.
                $errorParams['clientHeight'] = $errorParams['clientWidth'];
            }
        }
        return $this->getMediaTransformError( $errorParams, Message::newFromSpecifier( $msg )->text() );
    }
    /**
     * @param MediaHandlerState $state
     * @param string $path
     * @return array
     */
    public function getSizeAndMetadata( $state, $path ) {
        $metadata = $state->getHandlerState( 'TiffMetaArray' );
        if ( !$metadata ) {
            $metadata = $this->getTiffImage( $state, $path )->retrieveMetaData();
            $state->setHandlerState( 'TiffMetaArray', $metadata );
        }
        $gis = getimagesize( $path );
        if ( $gis ) {
            [ $width, $height ] = $gis;
        } else {
            $width = 0;
            $height = 0;
        }
        return [
            'width' => $width,
            'height' => $height,
            'metadata' => $metadata
        ];
    }
    /**
     * Check if the metadata string is valid for this handler.
     * If it returns false, Image will reload the metadata from the file and update the database
     * @param File $image
     * @return bool
     */
    public function isFileMetadataValid( $image ) {
        $metadata = $image->getMetadataArray();
        if ( isset( $metadata['errors'] ) ) {
            // In the case of a broken file, we do not want to reload the
            // metadata on every request.
            return self::METADATA_GOOD;
        }
        if ( !isset( $metadata['TIFF_METADATA_VERSION'] )
            || $metadata['TIFF_METADATA_VERSION'] != self::TIFF_METADATA_VERSION
        ) {
            return self::METADATA_BAD;
        }
        return self::METADATA_GOOD;
    }
    /**
     * Get an array structure that looks like this:
     *
     * [
     *  'visible' => [
     *    'Human-readable name' => 'Human readable value',
     *    ...
     *  ],
     *  'collapsed' => [
     *    'Human-readable name' => 'Human readable value',
     *    ...
     *  ]
     * ]
     * The UI will format this into a table where the visible fields are always
     * visible, and the collapsed fields are optionally visible.
     *
     * The function should return false if there is no metadata to display.
     *
     * @param File $image
     * @param bool|IContextSource $context Context to use (optional)
     * @return array|bool
     */
    public function formatMetadata( $image, $context = false ) {
        $result = [
            'visible' => [],
            'collapsed' => []
        ];
        $metadata = $image->getMetadata();
        if ( !$metadata ) {
            return false;
        }
        $exif = unserialize( $metadata );
        if ( !isset( $exif['exif'] ) || !$exif['exif'] ) {
            return false;
        }
        $exif = $exif['exif'];
        unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
        $formatted = FormatMetadata::getFormattedData( $exif, $context );
        // Sort fields into visible and collapsed
        $visibleFields = $this->visibleMetadataFields();
        foreach ( $formatted as $name => $value ) {
            $tag = strtolower( $name );
            $this->addMeta( $result,
                in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
                'exif',
                $tag,
                $value
            );
        }
        $meta = unserialize( $metadata );
        $errors_raw = $this->getMetadataErrors( $meta );
        if ( $errors_raw ) {
            $errors = $this->joinMessages( $errors_raw );
            $this->addMeta( $result,
                'collapsed',
                'metadata',
                'error',
                $errors
            );
            // XXX: need translation for <metadata-error>
        }
        if ( !empty( $meta['warnings'] ) ) {
            $warnings = $this->joinMessages( $meta['warnings'] );
            $this->addMeta( $result,
                'collapsed',
                'metadata',
                'warning',
                $warnings
            );
            // XXX: need translation for <metadata-warning>
        }
        return $result;
    }
    /**
     * Returns a PagedTiffImage or creates a new one if it doesn't exist.
     * @param MediaHandlerState|false $state The image object, or false if there isn't one
     * @param string $path path to the image?
     * @return PagedTiffImage
     */
    private function getTiffImage( $state, $path ) {
        if ( !$state ) {
            return $this->getCachedTiffImage( $path );
        }
        // If there is an Image object, we check whether there's already a TiffImage
        // instance in there; if not, a new instance is created and stored in the Image object
        $tiffImage = $state->getHandlerState( 'TiffImage' );
        if ( !$tiffImage ) {
            $tiffImage = $this->getCachedTiffImage( $path );
            $state->setHandlerState( 'TiffImage', $tiffImage );
        }
        return $tiffImage;
    }
    /**
     * Gets a PagedTiffImage from the cache, or creates one
     * @param string $path path to the image
     * @return PagedTiffImage
     */
    private function getCachedTiffImage( $path ) {
        $image = $this->knownImages->get( $path );
        if ( $image === null ) {
            $image = new PagedTiffImage( $this->commandFactory, $this->statsdFactory, $path );
            $this->knownImages->set( $path, $image );
        }
        return $image;
    }
    /**
     * Get an associative array of page dimensions
     * Currently "width" and "height" are understood, but this might be
     * expanded in the future.
     * @param File $image
     * @param int $page
     * @return int|false Returns false if unknown or if the document is not multi-page.
     */
    public function getPageDimensions( File $image, $page ) {
        // makeImageLink (Linker.php) sets $page to false if no page parameter
        // is set in wiki code
        $page = $this->adjustPage( $image, $page );
        $data = $image->getMetadataArray();
        return PagedTiffImage::getPageSize( $data, $page );
    }
    public function isExpensiveToThumbnail( $file ) {
        return $file->getSize() > self::EXPENSIVE_SIZE_LIMIT;
    }
    /**
     * What source thumbnail to use.
     *
     * This does not use MW's builtin bucket system, as it tries to take
     * advantage of the fact that VIPS can scale integer shrink factors
     * much more efficiently than non-integral scaling factors.
     *
     * @param File $file
     * @param array $params Parameters to transform file with.
     * @return array Array with keys path, width and height
     */
    protected function getThumbnailSource( $file, $params ) {
        /** @var MediaTransformOutput */
        $mto = $this->getIntermediaryStep( $file, $params );
        if ( $mto && !$mto->isError() ) {
            return [
                'path' => $mto->getLocalCopyPath(),
                'width' => $mto->getWidth(),
                'height' => $mto->getHeight(),
                // The path to the temporary file may be deleted when last
                // instance of the MediaTransformOutput is garbage collected,
                // so keep a reference around.
                'mto' => $mto
            ];
        } else {
            return parent::getThumbnailSource( $file, $params );
        }
    }
    /**
     * Get an intermediary sized thumb to do further rendering on
     *
     * Tiff files can be huge. This method gets a large thumbnail
     * to further scale things down. Size is chosen to be
     * efficient to scale in vips for those who use VipsScaler
     *
     * @param File $file
     * @param array $params Scaling parameters for original thumbnail
     * @return MediaTransformOutput|MediaTransformError|bool false if no in between step needed,
     *   MediaTransformError on error. False if the doTransform method returns false
     *   MediaTransformOutput on success.
     */
    private function getIntermediaryStep( $file, $params ) {
        global $wgTiffIntermediaryScaleStep, $wgThumbnailMinimumBucketDistance;
        $page = intval( $params['page'] );
        $page = $this->adjustPage( $file, $page );
        $srcWidth = $file->getWidth( $page );
        $srcHeight = $file->getHeight( $page );
        if ( $srcWidth <= $wgTiffIntermediaryScaleStep ) {
            // Image is already smaller than intermediary step or at that step
            return false;
        }
        $widthOfFinalThumb = $params['physicalWidth'];
        // Try and get a width that's easy for VipsScaler to work with
        // i.e. Is an integer shrink factor.
        $rx = floor( $srcWidth / ( $wgTiffIntermediaryScaleStep + 0.125 ) );
        $intermediaryWidth = intval( floor( $srcWidth / $rx ) );
        $intermediaryHeight = intval( floor( $srcHeight / $rx ) );
        // We need both the vertical and horizontal shrink factors to be
        // integers, and at the same time make sure that both vips and mediawiki
        // have the same height for a given width (MediaWiki makes the assumption
        // that the height of an image functionally depends on its width)
        for ( ; $rx >= 2; $rx-- ) {
            $intermediaryWidth = intval( floor( $srcWidth / $rx ) );
            $intermediaryHeight = intval( floor( $srcHeight / $rx ) );
            if ( $intermediaryHeight ==
                File::scaleHeight( $srcWidth, $srcHeight, $intermediaryWidth )
            ) {
                break;
            }
        }
        if (
            $intermediaryWidth <= $widthOfFinalThumb + $wgThumbnailMinimumBucketDistance || $rx < 2
        ) {
            // Need to scale the original full sized thumb
            return false;
        }
        static $isInThisFunction;
        if ( $isInThisFunction ) {
            // Sanity check, should never be reached
            throw new Exception( "Loop detected in " . __METHOD__ );
        }
        $isInThisFunction = true;
        $newParams = [
            'width' => $intermediaryWidth,
            'page' => $page,
            // Render a png, to avoid loss of quality when doing multi-step
            'lossy' => 'lossless'
        ];
        // RENDER_NOW causes rendering in this process if
        // thumb doesn't exist, but unlike RENDER_FORCE, will return
        // a cached thumb if available.
        $mto = $file->transform( $newParams, File::RENDER_NOW );
        $isInThisFunction = false;
        return $mto;
    }
}