'BenchmarkSanitizer' => __DIR__ . '/maintenance/benchmarks/benchmarkSanitizer.php',
'BenchmarkTidy' => __DIR__ . '/maintenance/benchmarks/benchmarkTidy.php',
'Benchmarker' => __DIR__ . '/maintenance/benchmarks/Benchmarker.php',
- 'BitmapHandler' => __DIR__ . '/includes/media/Bitmap.php',
- 'BitmapHandler_ClientOnly' => __DIR__ . '/includes/media/Bitmap_ClientOnly.php',
+ 'BitmapHandler' => __DIR__ . '/includes/media/BitmapHandler.php',
+ 'BitmapHandler_ClientOnly' => __DIR__ . '/includes/media/BitmapHandler_ClientOnly.php',
'BitmapMetadataHandler' => __DIR__ . '/includes/media/BitmapMetadataHandler.php',
'Blob' => __DIR__ . '/includes/libs/rdbms/encasing/Blob.php',
'Block' => __DIR__ . '/includes/Block.php',
'BlockLevelPass' => __DIR__ . '/includes/parser/BlockLevelPass.php',
'BlockListPager' => __DIR__ . '/includes/specials/pagers/BlockListPager.php',
'BlockLogFormatter' => __DIR__ . '/includes/logging/BlockLogFormatter.php',
- 'BmpHandler' => __DIR__ . '/includes/media/BMP.php',
+ 'BmpHandler' => __DIR__ . '/includes/media/BmpHandler.php',
'BotPassword' => __DIR__ . '/includes/user/BotPassword.php',
'BrokenRedirectsPage' => __DIR__ . '/includes/specials/SpecialBrokenRedirects.php',
'BufferingStatsdDataFactory' => __DIR__ . '/includes/libs/stats/BufferingStatsdDataFactory.php',
'DiffOpDelete' => __DIR__ . '/includes/diff/DairikiDiff.php',
'DifferenceEngine' => __DIR__ . '/includes/diff/DifferenceEngine.php',
'Digit2Html' => __DIR__ . '/maintenance/language/digit2html.php',
- 'DjVuHandler' => __DIR__ . '/includes/media/DjVu.php',
+ 'DjVuHandler' => __DIR__ . '/includes/media/DjVuHandler.php',
'DjVuImage' => __DIR__ . '/includes/media/DjVuImage.php',
'DnsSrvDiscoverer' => __DIR__ . '/includes/libs/DnsSrvDiscoverer.php',
'DoubleRedirectJob' => __DIR__ . '/includes/jobqueue/jobs/DoubleRedirectJob.php',
'EventRelayerNull' => __DIR__ . '/includes/libs/eventrelayer/EventRelayerNull.php',
'ExecutableFinder' => __DIR__ . '/includes/utils/ExecutableFinder.php',
'Exif' => __DIR__ . '/includes/media/Exif.php',
- 'ExifBitmapHandler' => __DIR__ . '/includes/media/ExifBitmap.php',
+ 'ExifBitmapHandler' => __DIR__ . '/includes/media/ExifBitmapHandler.php',
'ExplodeIterator' => __DIR__ . '/includes/libs/ExplodeIterator.php',
'ExportProgressFilter' => __DIR__ . '/includes/export/ExportProgressFilter.php',
'ExportSites' => __DIR__ . '/maintenance/exportSites.php',
'FormatMetadata' => __DIR__ . '/includes/media/FormatMetadata.php',
'FormattedRCFeed' => __DIR__ . '/includes/rcfeed/FormattedRCFeed.php',
'FormlessAction' => __DIR__ . '/includes/actions/FormlessAction.php',
- 'GIFHandler' => __DIR__ . '/includes/media/GIF.php',
+ 'GIFHandler' => __DIR__ . '/includes/media/GIFHandler.php',
'GIFMetadataExtractor' => __DIR__ . '/includes/media/GIFMetadataExtractor.php',
'GanConverter' => __DIR__ . '/languages/classes/LanguageGan.php',
'GenderCache' => __DIR__ . '/includes/cache/GenderCache.php',
'JobQueueSecondTestQueue' => __DIR__ . '/includes/jobqueue/JobQueueSecondTestQueue.php',
'JobRunner' => __DIR__ . '/includes/jobqueue/JobRunner.php',
'JobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
- 'JpegHandler' => __DIR__ . '/includes/media/Jpeg.php',
+ 'JpegHandler' => __DIR__ . '/includes/media/JpegHandler.php',
'JpegMetadataExtractor' => __DIR__ . '/includes/media/JpegMetadataExtractor.php',
'JsonContent' => __DIR__ . '/includes/content/JsonContent.php',
'JsonContentHandler' => __DIR__ . '/includes/content/JsonContentHandler.php',
'Orphans' => __DIR__ . '/maintenance/orphans.php',
'OutputPage' => __DIR__ . '/includes/OutputPage.php',
'PHPVersionCheck' => __DIR__ . '/includes/PHPVersionCheck.php',
- 'PNGHandler' => __DIR__ . '/includes/media/PNG.php',
+ 'PNGHandler' => __DIR__ . '/includes/media/PNGHandler.php',
'PNGMetadataExtractor' => __DIR__ . '/includes/media/PNGMetadataExtractor.php',
'PPCustomFrame_DOM' => __DIR__ . '/includes/parser/Preprocessor_DOM.php',
'PPCustomFrame_Hash' => __DIR__ . '/includes/parser/Preprocessor_Hash.php',
'StubUserLang' => __DIR__ . '/includes/StubObject.php',
'SubmitAction' => __DIR__ . '/includes/actions/SubmitAction.php',
'SubpageImportTitleFactory' => __DIR__ . '/includes/title/SubpageImportTitleFactory.php',
- 'SvgHandler' => __DIR__ . '/includes/media/SVG.php',
+ 'SvgHandler' => __DIR__ . '/includes/media/SvgHandler.php',
'SwiftFileBackend' => __DIR__ . '/includes/libs/filebackend/SwiftFileBackend.php',
'SwiftFileBackendDirList' => __DIR__ . '/includes/libs/filebackend/SwiftFileBackend.php',
'SwiftFileBackendFileList' => __DIR__ . '/includes/libs/filebackend/SwiftFileBackend.php',
'ThumbnailImage' => __DIR__ . '/includes/media/MediaTransformOutput.php',
'ThumbnailRenderJob' => __DIR__ . '/includes/jobqueue/jobs/ThumbnailRenderJob.php',
'TidyUpBug37714' => __DIR__ . '/maintenance/tidyUpBug37714.php',
- 'TiffHandler' => __DIR__ . '/includes/media/Tiff.php',
+ 'TiffHandler' => __DIR__ . '/includes/media/TiffHandler.php',
'Timing' => __DIR__ . '/includes/libs/Timing.php',
'Title' => __DIR__ . '/includes/Title.php',
'TitleArray' => __DIR__ . '/includes/TitleArray.php',
'WebInstallerUpgrade' => __DIR__ . '/includes/installer/WebInstallerUpgrade.php',
'WebInstallerUpgradeDoc' => __DIR__ . '/includes/installer/WebInstallerUpgradeDoc.php',
'WebInstallerWelcome' => __DIR__ . '/includes/installer/WebInstallerWelcome.php',
- 'WebPHandler' => __DIR__ . '/includes/media/WebP.php',
+ 'WebPHandler' => __DIR__ . '/includes/media/WebPHandler.php',
'WebRequest' => __DIR__ . '/includes/WebRequest.php',
'WebRequestUpload' => __DIR__ . '/includes/WebRequestUpload.php',
'WebResponse' => __DIR__ . '/includes/WebResponse.php',
+++ /dev/null
-<?php
-/**
- * Handler for Microsoft's bitmap format.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Handler for Microsoft's bitmap format; getimagesize() doesn't
- * support these files
- *
- * @ingroup Media
- */
-class BmpHandler extends BitmapHandler {
- /**
- * @param File $file
- * @return bool
- */
- public function mustRender( $file ) {
- return true;
- }
-
- /**
- * Render files as PNG
- *
- * @param string $text
- * @param string $mime
- * @param array $params
- * @return array
- */
- function getThumbType( $text, $mime, $params = null ) {
- return [ 'png', 'image/png' ];
- }
-
- /**
- * Get width and height from the bmp header.
- *
- * @param File|FSFile $image
- * @param string $filename
- * @return array
- */
- function getImageSize( $image, $filename ) {
- $f = fopen( $filename, 'rb' );
- if ( !$f ) {
- return false;
- }
- $header = fread( $f, 54 );
- fclose( $f );
-
- // Extract binary form of width and height from the header
- $w = substr( $header, 18, 4 );
- $h = substr( $header, 22, 4 );
-
- // Convert the unsigned long 32 bits (little endian):
- try {
- $w = wfUnpack( 'V', $w, 4 );
- $h = wfUnpack( 'V', $h, 4 );
- } catch ( Exception $e ) {
- return false;
- }
-
- return [ $w[1], $h[1] ];
- }
-}
+++ /dev/null
-<?php
-/**
- * Generic handler for bitmap images.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Generic handler for bitmap images
- *
- * @ingroup Media
- */
-class BitmapHandler extends TransformationalImageHandler {
-
- /**
- * Returns which scaler type should be used. Creates parent directories
- * for $dstPath and returns 'client' on error
- *
- * @param string $dstPath
- * @param bool $checkDstPath
- * @return string|Callable One of client, im, custom, gd, imext or an array( object, method )
- */
- protected function getScalerType( $dstPath, $checkDstPath = true ) {
- global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
-
- if ( !$dstPath && $checkDstPath ) {
- # No output path available, client side scaling only
- $scaler = 'client';
- } elseif ( !$wgUseImageResize ) {
- $scaler = 'client';
- } elseif ( $wgUseImageMagick ) {
- $scaler = 'im';
- } elseif ( $wgCustomConvertCommand ) {
- $scaler = 'custom';
- } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
- $scaler = 'gd';
- } elseif ( class_exists( 'Imagick' ) ) {
- $scaler = 'imext';
- } else {
- $scaler = 'client';
- }
-
- return $scaler;
- }
-
- public function makeParamString( $params ) {
- $res = parent::makeParamString( $params );
- if ( isset( $params['interlace'] ) && $params['interlace'] ) {
- return "interlaced-{$res}";
- } else {
- return $res;
- }
- }
-
- public function parseParamString( $str ) {
- $remainder = preg_replace( '/^interlaced-/', '', $str );
- $params = parent::parseParamString( $remainder );
- if ( $params === false ) {
- return false;
- }
- $params['interlace'] = $str !== $remainder;
- return $params;
- }
-
- public function validateParam( $name, $value ) {
- if ( $name === 'interlace' ) {
- return $value === false || $value === true;
- } else {
- return parent::validateParam( $name, $value );
- }
- }
-
- /**
- * @param File $image
- * @param array &$params
- * @return bool
- */
- function normaliseParams( $image, &$params ) {
- global $wgMaxInterlacingAreas;
- if ( !parent::normaliseParams( $image, $params ) ) {
- return false;
- }
- $mimeType = $image->getMimeType();
- $interlace = isset( $params['interlace'] ) && $params['interlace']
- && isset( $wgMaxInterlacingAreas[$mimeType] )
- && $this->getImageArea( $image ) <= $wgMaxInterlacingAreas[$mimeType];
- $params['interlace'] = $interlace;
- return true;
- }
-
- /**
- * Get ImageMagick subsampling factors for the target JPEG pixel format.
- *
- * @param string $pixelFormat one of 'yuv444', 'yuv422', 'yuv420'
- * @return array of string keys
- */
- protected function imageMagickSubsampling( $pixelFormat ) {
- switch ( $pixelFormat ) {
- case 'yuv444':
- return [ '1x1', '1x1', '1x1' ];
- case 'yuv422':
- return [ '2x1', '1x1', '1x1' ];
- case 'yuv420':
- return [ '2x2', '1x1', '1x1' ];
- default:
- throw new MWException( 'Invalid pixel format for JPEG output' );
- }
- }
-
- /**
- * Transform an image using ImageMagick
- *
- * @param File $image File associated with this thumbnail
- * @param array $params Array with scaler params
- *
- * @return MediaTransformError|bool Error object if error occurred, false (=no error) otherwise
- */
- protected function transformImageMagick( $image, $params ) {
- # use ImageMagick
- global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
- $wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgJpegPixelFormat,
- $wgJpegQuality;
-
- $quality = [];
- $sharpen = [];
- $scene = false;
- $animation_pre = [];
- $animation_post = [];
- $decoderHint = [];
- $subsampling = [];
-
- if ( $params['mimeType'] == 'image/jpeg' ) {
- $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
- $quality = [ '-quality', $qualityVal ?: (string)$wgJpegQuality ]; // 80% by default
- if ( $params['interlace'] ) {
- $animation_post = [ '-interlace', 'JPEG' ];
- }
- # Sharpening, see T8193
- if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
- / ( $params['srcWidth'] + $params['srcHeight'] )
- < $wgSharpenReductionThreshold
- ) {
- $sharpen = [ '-sharpen', $wgSharpenParameter ];
- }
- if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
- // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
- $decoderHint = [ '-define', "jpeg:size={$params['physicalDimensions']}" ];
- }
- if ( $wgJpegPixelFormat ) {
- $factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
- $subsampling = [ '-sampling-factor', implode( ',', $factors ) ];
- }
- } elseif ( $params['mimeType'] == 'image/png' ) {
- $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
- if ( $params['interlace'] ) {
- $animation_post = [ '-interlace', 'PNG' ];
- }
- } elseif ( $params['mimeType'] == 'image/webp' ) {
- $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
- } elseif ( $params['mimeType'] == 'image/gif' ) {
- if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
- // Extract initial frame only; we're so big it'll
- // be a total drag. :P
- $scene = 0;
- } elseif ( $this->isAnimatedImage( $image ) ) {
- // Coalesce is needed to scale animated GIFs properly (T3017).
- $animation_pre = [ '-coalesce' ];
- // We optimize the output, but -optimize is broken,
- // use optimizeTransparency instead (T13822)
- if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
- $animation_post = [ '-fuzz', '5%', '-layers', 'optimizeTransparency' ];
- }
- }
- if ( $params['interlace'] && version_compare( $this->getMagickVersion(), "6.3.4" ) >= 0
- && !$this->isAnimatedImage( $image ) ) { // interlacing animated GIFs is a bad idea
- $animation_post[] = '-interlace';
- $animation_post[] = 'GIF';
- }
- } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
- // Before merging layers, we need to set the background
- // to be transparent to preserve alpha, as -layers merge
- // merges all layers on to a canvas filled with the
- // background colour. After merging we reset the background
- // to be white for the default background colour setting
- // in the PNG image (which is used in old IE)
- $animation_pre = [
- '-background', 'transparent',
- '-layers', 'merge',
- '-background', 'white',
- ];
- Wikimedia\suppressWarnings();
- $xcfMeta = unserialize( $image->getMetadata() );
- Wikimedia\restoreWarnings();
- if ( $xcfMeta
- && isset( $xcfMeta['colorType'] )
- && $xcfMeta['colorType'] === 'greyscale-alpha'
- && version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0
- ) {
- // T68323 - Greyscale images not rendered properly.
- // So only take the "red" channel.
- $channelOnly = [ '-channel', 'R', '-separate' ];
- $animation_pre = array_merge( $animation_pre, $channelOnly );
- }
- }
-
- // Use one thread only, to avoid deadlock bugs on OOM
- $env = [ 'OMP_NUM_THREADS' => 1 ];
- if ( strval( $wgImageMagickTempDir ) !== '' ) {
- $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
- }
-
- $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
- list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
-
- $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
- [ $wgImageMagickConvertCommand ],
- $quality,
- // Specify white background color, will be used for transparent images
- // in Internet Explorer/Windows instead of default black.
- [ '-background', 'white' ],
- $decoderHint,
- [ $this->escapeMagickInput( $params['srcPath'], $scene ) ],
- $animation_pre,
- // For the -thumbnail option a "!" is needed to force exact size,
- // or ImageMagick may decide your ratio is wrong and slice off
- // a pixel.
- [ '-thumbnail', "{$width}x{$height}!" ],
- // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
- ( $params['comment'] !== ''
- ? [ '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) ]
- : [] ),
- // T108616: Avoid exposure of local file path
- [ '+set', 'Thumb::URI' ],
- [ '-depth', 8 ],
- $sharpen,
- [ '-rotate', "-$rotation" ],
- $subsampling,
- $animation_post,
- [ $this->escapeMagickOutput( $params['dstPath'] ) ] ) );
-
- wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
- $retval = 0;
- $err = wfShellExecWithStderr( $cmd, $retval, $env );
-
- if ( $retval !== 0 ) {
- $this->logErrorForExternalProcess( $retval, $err, $cmd );
-
- return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
- }
-
- return false; # No error
- }
-
- /**
- * Transform an image using the Imagick PHP extension
- *
- * @param File $image File associated with this thumbnail
- * @param array $params Array with scaler params
- *
- * @return MediaTransformError Error|bool object if error occurred, false (=no error) otherwise
- */
- protected function transformImageMagickExt( $image, $params ) {
- global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
- $wgJpegPixelFormat, $wgJpegQuality;
-
- try {
- $im = new Imagick();
- $im->readImage( $params['srcPath'] );
-
- if ( $params['mimeType'] == 'image/jpeg' ) {
- // Sharpening, see T8193
- if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
- / ( $params['srcWidth'] + $params['srcHeight'] )
- < $wgSharpenReductionThreshold
- ) {
- // Hack, since $wgSharpenParameter is written specifically for the command line convert
- list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
- $im->sharpenImage( $radius, $sigma );
- }
- $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
- $im->setCompressionQuality( $qualityVal ?: $wgJpegQuality );
- if ( $params['interlace'] ) {
- $im->setInterlaceScheme( Imagick::INTERLACE_JPEG );
- }
- if ( $wgJpegPixelFormat ) {
- $factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
- $im->setSamplingFactors( $factors );
- }
- } elseif ( $params['mimeType'] == 'image/png' ) {
- $im->setCompressionQuality( 95 );
- if ( $params['interlace'] ) {
- $im->setInterlaceScheme( Imagick::INTERLACE_PNG );
- }
- } elseif ( $params['mimeType'] == 'image/gif' ) {
- if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
- // Extract initial frame only; we're so big it'll
- // be a total drag. :P
- $im->setImageScene( 0 );
- } elseif ( $this->isAnimatedImage( $image ) ) {
- // Coalesce is needed to scale animated GIFs properly (T3017).
- $im = $im->coalesceImages();
- }
- // GIF interlacing is only available since 6.3.4
- $v = Imagick::getVersion();
- preg_match( '/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $v );
-
- if ( $params['interlace'] && version_compare( $v[1], '6.3.4' ) >= 0 ) {
- $im->setInterlaceScheme( Imagick::INTERLACE_GIF );
- }
- }
-
- $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
- list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
-
- $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
-
- // Call Imagick::thumbnailImage on each frame
- foreach ( $im as $i => $frame ) {
- if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
- return $this->getMediaTransformError( $params, "Error scaling frame $i" );
- }
- }
- $im->setImageDepth( 8 );
-
- if ( $rotation ) {
- if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
- return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
- }
- }
-
- if ( $this->isAnimatedImage( $image ) ) {
- wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
- // This is broken somehow... can't find out how to fix it
- $result = $im->writeImages( $params['dstPath'], true );
- } else {
- $result = $im->writeImage( $params['dstPath'] );
- }
- if ( !$result ) {
- return $this->getMediaTransformError( $params,
- "Unable to write thumbnail to {$params['dstPath']}" );
- }
- } catch ( ImagickException $e ) {
- return $this->getMediaTransformError( $params, $e->getMessage() );
- }
-
- return false;
- }
-
- /**
- * Transform an image using a custom command
- *
- * @param File $image File associated with this thumbnail
- * @param array $params Array with scaler params
- *
- * @return MediaTransformError Error|bool object if error occurred, false (=no error) otherwise
- */
- protected function transformCustom( $image, $params ) {
- # Use a custom convert command
- global $wgCustomConvertCommand;
-
- # Variables: %s %d %w %h
- $src = wfEscapeShellArg( $params['srcPath'] );
- $dst = wfEscapeShellArg( $params['dstPath'] );
- $cmd = $wgCustomConvertCommand;
- $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
- $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
- str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
- wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
- $retval = 0;
- $err = wfShellExecWithStderr( $cmd, $retval );
-
- if ( $retval !== 0 ) {
- $this->logErrorForExternalProcess( $retval, $err, $cmd );
-
- return $this->getMediaTransformError( $params, $err );
- }
-
- return false; # No error
- }
-
- /**
- * Transform an image using the built in GD library
- *
- * @param File $image File associated with this thumbnail
- * @param array $params Array with scaler params
- *
- * @return MediaTransformError|bool Error object if error occurred, false (=no error) otherwise
- */
- protected function transformGd( $image, $params ) {
- # Use PHP's builtin GD library functions.
- # First find out what kind of file this is, and select the correct
- # input routine for this.
-
- $typemap = [
- 'image/gif' => [ 'imagecreatefromgif', 'palette', false, 'imagegif' ],
- 'image/jpeg' => [ 'imagecreatefromjpeg', 'truecolor', true,
- [ __CLASS__, 'imageJpegWrapper' ] ],
- 'image/png' => [ 'imagecreatefrompng', 'bits', false, 'imagepng' ],
- 'image/vnd.wap.wbmp' => [ 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ],
- 'image/xbm' => [ 'imagecreatefromxbm', 'palette', false, 'imagexbm' ],
- ];
-
- if ( !isset( $typemap[$params['mimeType']] ) ) {
- $err = 'Image type not supported';
- wfDebug( "$err\n" );
- $errMsg = wfMessage( 'thumbnail_image-type' )->text();
-
- return $this->getMediaTransformError( $params, $errMsg );
- }
- list( $loader, $colorStyle, $useQuality, $saveType ) = $typemap[$params['mimeType']];
-
- if ( !function_exists( $loader ) ) {
- $err = "Incomplete GD library configuration: missing function $loader";
- wfDebug( "$err\n" );
- $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
-
- return $this->getMediaTransformError( $params, $errMsg );
- }
-
- if ( !file_exists( $params['srcPath'] ) ) {
- $err = "File seems to be missing: {$params['srcPath']}";
- wfDebug( "$err\n" );
- $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
-
- return $this->getMediaTransformError( $params, $errMsg );
- }
-
- if ( filesize( $params['srcPath'] ) === 0 ) {
- $err = "Image file size seems to be zero.";
- wfDebug( "$err\n" );
- $errMsg = wfMessage( 'thumbnail_image-size-zero', $params['srcPath'] )->text();
-
- return $this->getMediaTransformError( $params, $errMsg );
- }
-
- $src_image = call_user_func( $loader, $params['srcPath'] );
-
- $rotation = function_exists( 'imagerotate' ) && !isset( $params['disableRotation'] ) ?
- $this->getRotation( $image ) :
- 0;
- list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
- $dst_image = imagecreatetruecolor( $width, $height );
-
- // Initialise the destination image to transparent instead of
- // the default solid black, to support PNG and GIF transparency nicely
- $background = imagecolorallocate( $dst_image, 0, 0, 0 );
- imagecolortransparent( $dst_image, $background );
- imagealphablending( $dst_image, false );
-
- if ( $colorStyle == 'palette' ) {
- // Don't resample for paletted GIF images.
- // It may just uglify them, and completely breaks transparency.
- imagecopyresized( $dst_image, $src_image,
- 0, 0, 0, 0,
- $width, $height,
- imagesx( $src_image ), imagesy( $src_image ) );
- } else {
- imagecopyresampled( $dst_image, $src_image,
- 0, 0, 0, 0,
- $width, $height,
- imagesx( $src_image ), imagesy( $src_image ) );
- }
-
- if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
- $rot_image = imagerotate( $dst_image, $rotation, 0 );
- imagedestroy( $dst_image );
- $dst_image = $rot_image;
- }
-
- imagesavealpha( $dst_image, true );
-
- $funcParams = [ $dst_image, $params['dstPath'] ];
- if ( $useQuality && isset( $params['quality'] ) ) {
- $funcParams[] = $params['quality'];
- }
- call_user_func_array( $saveType, $funcParams );
-
- imagedestroy( $dst_image );
- imagedestroy( $src_image );
-
- return false; # No error
- }
-
- /**
- * Callback for transformGd when transforming jpeg images.
- *
- * @param resource $dst_image Image resource of the original image
- * @param string $thumbPath File path to write the thumbnail image to
- * @param int|null $quality Quality of the thumbnail from 1-100,
- * or null to use default quality.
- */
- static function imageJpegWrapper( $dst_image, $thumbPath, $quality = null ) {
- global $wgJpegQuality;
-
- if ( $quality === null ) {
- $quality = $wgJpegQuality;
- }
-
- imageinterlace( $dst_image );
- imagejpeg( $dst_image, $thumbPath, $quality );
- }
-
- /**
- * Returns whether the current scaler supports rotation (im and gd do)
- *
- * @return bool
- */
- public function canRotate() {
- $scaler = $this->getScalerType( null, false );
- switch ( $scaler ) {
- case 'im':
- # ImageMagick supports autorotation
- return true;
- case 'imext':
- # Imagick::rotateImage
- return true;
- case 'gd':
- # GD's imagerotate function is used to rotate images, but not
- # all precompiled PHP versions have that function
- return function_exists( 'imagerotate' );
- default:
- # Other scalers don't support rotation
- return false;
- }
- }
-
- /**
- * @see $wgEnableAutoRotation
- * @return bool Whether auto rotation is enabled
- */
- public function autoRotateEnabled() {
- global $wgEnableAutoRotation;
-
- if ( $wgEnableAutoRotation === null ) {
- // Only enable auto-rotation when we actually can
- return $this->canRotate();
- }
-
- return $wgEnableAutoRotation;
- }
-
- /**
- * @param File $file
- * @param array $params Rotate parameters.
- * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
- * @since 1.21
- * @return bool|MediaTransformError
- */
- public function rotate( $file, $params ) {
- global $wgImageMagickConvertCommand;
-
- $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
- $scene = false;
-
- $scaler = $this->getScalerType( null, false );
- switch ( $scaler ) {
- case 'im':
- $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
- wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
- " -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
- wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
- wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
- $retval = 0;
- $err = wfShellExecWithStderr( $cmd, $retval );
- if ( $retval !== 0 ) {
- $this->logErrorForExternalProcess( $retval, $err, $cmd );
-
- return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
- }
-
- return false;
- case 'imext':
- $im = new Imagick();
- $im->readImage( $params['srcPath'] );
- if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
- return new MediaTransformError( 'thumbnail_error', 0, 0,
- "Error rotating $rotation degrees" );
- }
- $result = $im->writeImage( $params['dstPath'] );
- if ( !$result ) {
- return new MediaTransformError( 'thumbnail_error', 0, 0,
- "Unable to write image to {$params['dstPath']}" );
- }
-
- return false;
- default:
- return new MediaTransformError( 'thumbnail_error', 0, 0,
- "$scaler rotation not implemented" );
- }
- }
-}
--- /dev/null
+<?php
+/**
+ * Generic handler for bitmap images.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Generic handler for bitmap images
+ *
+ * @ingroup Media
+ */
+class BitmapHandler extends TransformationalImageHandler {
+
+ /**
+ * Returns which scaler type should be used. Creates parent directories
+ * for $dstPath and returns 'client' on error
+ *
+ * @param string $dstPath
+ * @param bool $checkDstPath
+ * @return string|Callable One of client, im, custom, gd, imext or an array( object, method )
+ */
+ protected function getScalerType( $dstPath, $checkDstPath = true ) {
+ global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
+
+ if ( !$dstPath && $checkDstPath ) {
+ # No output path available, client side scaling only
+ $scaler = 'client';
+ } elseif ( !$wgUseImageResize ) {
+ $scaler = 'client';
+ } elseif ( $wgUseImageMagick ) {
+ $scaler = 'im';
+ } elseif ( $wgCustomConvertCommand ) {
+ $scaler = 'custom';
+ } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
+ $scaler = 'gd';
+ } elseif ( class_exists( 'Imagick' ) ) {
+ $scaler = 'imext';
+ } else {
+ $scaler = 'client';
+ }
+
+ return $scaler;
+ }
+
+ public function makeParamString( $params ) {
+ $res = parent::makeParamString( $params );
+ if ( isset( $params['interlace'] ) && $params['interlace'] ) {
+ return "interlaced-{$res}";
+ } else {
+ return $res;
+ }
+ }
+
+ public function parseParamString( $str ) {
+ $remainder = preg_replace( '/^interlaced-/', '', $str );
+ $params = parent::parseParamString( $remainder );
+ if ( $params === false ) {
+ return false;
+ }
+ $params['interlace'] = $str !== $remainder;
+ return $params;
+ }
+
+ public function validateParam( $name, $value ) {
+ if ( $name === 'interlace' ) {
+ return $value === false || $value === true;
+ } else {
+ return parent::validateParam( $name, $value );
+ }
+ }
+
+ /**
+ * @param File $image
+ * @param array &$params
+ * @return bool
+ */
+ function normaliseParams( $image, &$params ) {
+ global $wgMaxInterlacingAreas;
+ if ( !parent::normaliseParams( $image, $params ) ) {
+ return false;
+ }
+ $mimeType = $image->getMimeType();
+ $interlace = isset( $params['interlace'] ) && $params['interlace']
+ && isset( $wgMaxInterlacingAreas[$mimeType] )
+ && $this->getImageArea( $image ) <= $wgMaxInterlacingAreas[$mimeType];
+ $params['interlace'] = $interlace;
+ return true;
+ }
+
+ /**
+ * Get ImageMagick subsampling factors for the target JPEG pixel format.
+ *
+ * @param string $pixelFormat one of 'yuv444', 'yuv422', 'yuv420'
+ * @return array of string keys
+ */
+ protected function imageMagickSubsampling( $pixelFormat ) {
+ switch ( $pixelFormat ) {
+ case 'yuv444':
+ return [ '1x1', '1x1', '1x1' ];
+ case 'yuv422':
+ return [ '2x1', '1x1', '1x1' ];
+ case 'yuv420':
+ return [ '2x2', '1x1', '1x1' ];
+ default:
+ throw new MWException( 'Invalid pixel format for JPEG output' );
+ }
+ }
+
+ /**
+ * Transform an image using ImageMagick
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError|bool Error object if error occurred, false (=no error) otherwise
+ */
+ protected function transformImageMagick( $image, $params ) {
+ # use ImageMagick
+ global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
+ $wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgJpegPixelFormat,
+ $wgJpegQuality;
+
+ $quality = [];
+ $sharpen = [];
+ $scene = false;
+ $animation_pre = [];
+ $animation_post = [];
+ $decoderHint = [];
+ $subsampling = [];
+
+ if ( $params['mimeType'] == 'image/jpeg' ) {
+ $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
+ $quality = [ '-quality', $qualityVal ?: (string)$wgJpegQuality ]; // 80% by default
+ if ( $params['interlace'] ) {
+ $animation_post = [ '-interlace', 'JPEG' ];
+ }
+ # Sharpening, see T8193
+ if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
+ / ( $params['srcWidth'] + $params['srcHeight'] )
+ < $wgSharpenReductionThreshold
+ ) {
+ $sharpen = [ '-sharpen', $wgSharpenParameter ];
+ }
+ if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
+ // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
+ $decoderHint = [ '-define', "jpeg:size={$params['physicalDimensions']}" ];
+ }
+ if ( $wgJpegPixelFormat ) {
+ $factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
+ $subsampling = [ '-sampling-factor', implode( ',', $factors ) ];
+ }
+ } elseif ( $params['mimeType'] == 'image/png' ) {
+ $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
+ if ( $params['interlace'] ) {
+ $animation_post = [ '-interlace', 'PNG' ];
+ }
+ } elseif ( $params['mimeType'] == 'image/webp' ) {
+ $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
+ } elseif ( $params['mimeType'] == 'image/gif' ) {
+ if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
+ // Extract initial frame only; we're so big it'll
+ // be a total drag. :P
+ $scene = 0;
+ } elseif ( $this->isAnimatedImage( $image ) ) {
+ // Coalesce is needed to scale animated GIFs properly (T3017).
+ $animation_pre = [ '-coalesce' ];
+ // We optimize the output, but -optimize is broken,
+ // use optimizeTransparency instead (T13822)
+ if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
+ $animation_post = [ '-fuzz', '5%', '-layers', 'optimizeTransparency' ];
+ }
+ }
+ if ( $params['interlace'] && version_compare( $this->getMagickVersion(), "6.3.4" ) >= 0
+ && !$this->isAnimatedImage( $image ) ) { // interlacing animated GIFs is a bad idea
+ $animation_post[] = '-interlace';
+ $animation_post[] = 'GIF';
+ }
+ } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
+ // Before merging layers, we need to set the background
+ // to be transparent to preserve alpha, as -layers merge
+ // merges all layers on to a canvas filled with the
+ // background colour. After merging we reset the background
+ // to be white for the default background colour setting
+ // in the PNG image (which is used in old IE)
+ $animation_pre = [
+ '-background', 'transparent',
+ '-layers', 'merge',
+ '-background', 'white',
+ ];
+ Wikimedia\suppressWarnings();
+ $xcfMeta = unserialize( $image->getMetadata() );
+ Wikimedia\restoreWarnings();
+ if ( $xcfMeta
+ && isset( $xcfMeta['colorType'] )
+ && $xcfMeta['colorType'] === 'greyscale-alpha'
+ && version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0
+ ) {
+ // T68323 - Greyscale images not rendered properly.
+ // So only take the "red" channel.
+ $channelOnly = [ '-channel', 'R', '-separate' ];
+ $animation_pre = array_merge( $animation_pre, $channelOnly );
+ }
+ }
+
+ // Use one thread only, to avoid deadlock bugs on OOM
+ $env = [ 'OMP_NUM_THREADS' => 1 ];
+ if ( strval( $wgImageMagickTempDir ) !== '' ) {
+ $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
+ }
+
+ $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
+ list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
+
+ $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
+ [ $wgImageMagickConvertCommand ],
+ $quality,
+ // Specify white background color, will be used for transparent images
+ // in Internet Explorer/Windows instead of default black.
+ [ '-background', 'white' ],
+ $decoderHint,
+ [ $this->escapeMagickInput( $params['srcPath'], $scene ) ],
+ $animation_pre,
+ // For the -thumbnail option a "!" is needed to force exact size,
+ // or ImageMagick may decide your ratio is wrong and slice off
+ // a pixel.
+ [ '-thumbnail', "{$width}x{$height}!" ],
+ // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
+ ( $params['comment'] !== ''
+ ? [ '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) ]
+ : [] ),
+ // T108616: Avoid exposure of local file path
+ [ '+set', 'Thumb::URI' ],
+ [ '-depth', 8 ],
+ $sharpen,
+ [ '-rotate', "-$rotation" ],
+ $subsampling,
+ $animation_post,
+ [ $this->escapeMagickOutput( $params['dstPath'] ) ] ) );
+
+ wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
+ $retval = 0;
+ $err = wfShellExecWithStderr( $cmd, $retval, $env );
+
+ if ( $retval !== 0 ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+
+ return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
+ }
+
+ return false; # No error
+ }
+
+ /**
+ * Transform an image using the Imagick PHP extension
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError Error|bool object if error occurred, false (=no error) otherwise
+ */
+ protected function transformImageMagickExt( $image, $params ) {
+ global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
+ $wgJpegPixelFormat, $wgJpegQuality;
+
+ try {
+ $im = new Imagick();
+ $im->readImage( $params['srcPath'] );
+
+ if ( $params['mimeType'] == 'image/jpeg' ) {
+ // Sharpening, see T8193
+ if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
+ / ( $params['srcWidth'] + $params['srcHeight'] )
+ < $wgSharpenReductionThreshold
+ ) {
+ // Hack, since $wgSharpenParameter is written specifically for the command line convert
+ list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
+ $im->sharpenImage( $radius, $sigma );
+ }
+ $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
+ $im->setCompressionQuality( $qualityVal ?: $wgJpegQuality );
+ if ( $params['interlace'] ) {
+ $im->setInterlaceScheme( Imagick::INTERLACE_JPEG );
+ }
+ if ( $wgJpegPixelFormat ) {
+ $factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
+ $im->setSamplingFactors( $factors );
+ }
+ } elseif ( $params['mimeType'] == 'image/png' ) {
+ $im->setCompressionQuality( 95 );
+ if ( $params['interlace'] ) {
+ $im->setInterlaceScheme( Imagick::INTERLACE_PNG );
+ }
+ } elseif ( $params['mimeType'] == 'image/gif' ) {
+ if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
+ // Extract initial frame only; we're so big it'll
+ // be a total drag. :P
+ $im->setImageScene( 0 );
+ } elseif ( $this->isAnimatedImage( $image ) ) {
+ // Coalesce is needed to scale animated GIFs properly (T3017).
+ $im = $im->coalesceImages();
+ }
+ // GIF interlacing is only available since 6.3.4
+ $v = Imagick::getVersion();
+ preg_match( '/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $v );
+
+ if ( $params['interlace'] && version_compare( $v[1], '6.3.4' ) >= 0 ) {
+ $im->setInterlaceScheme( Imagick::INTERLACE_GIF );
+ }
+ }
+
+ $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
+ list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
+
+ $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
+
+ // Call Imagick::thumbnailImage on each frame
+ foreach ( $im as $i => $frame ) {
+ if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
+ return $this->getMediaTransformError( $params, "Error scaling frame $i" );
+ }
+ }
+ $im->setImageDepth( 8 );
+
+ if ( $rotation ) {
+ if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
+ return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
+ }
+ }
+
+ if ( $this->isAnimatedImage( $image ) ) {
+ wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
+ // This is broken somehow... can't find out how to fix it
+ $result = $im->writeImages( $params['dstPath'], true );
+ } else {
+ $result = $im->writeImage( $params['dstPath'] );
+ }
+ if ( !$result ) {
+ return $this->getMediaTransformError( $params,
+ "Unable to write thumbnail to {$params['dstPath']}" );
+ }
+ } catch ( ImagickException $e ) {
+ return $this->getMediaTransformError( $params, $e->getMessage() );
+ }
+
+ return false;
+ }
+
+ /**
+ * Transform an image using a custom command
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError Error|bool object if error occurred, false (=no error) otherwise
+ */
+ protected function transformCustom( $image, $params ) {
+ # Use a custom convert command
+ global $wgCustomConvertCommand;
+
+ # Variables: %s %d %w %h
+ $src = wfEscapeShellArg( $params['srcPath'] );
+ $dst = wfEscapeShellArg( $params['dstPath'] );
+ $cmd = $wgCustomConvertCommand;
+ $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
+ $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
+ str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
+ wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
+ $retval = 0;
+ $err = wfShellExecWithStderr( $cmd, $retval );
+
+ if ( $retval !== 0 ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+
+ return $this->getMediaTransformError( $params, $err );
+ }
+
+ return false; # No error
+ }
+
+ /**
+ * Transform an image using the built in GD library
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError|bool Error object if error occurred, false (=no error) otherwise
+ */
+ protected function transformGd( $image, $params ) {
+ # Use PHP's builtin GD library functions.
+ # First find out what kind of file this is, and select the correct
+ # input routine for this.
+
+ $typemap = [
+ 'image/gif' => [ 'imagecreatefromgif', 'palette', false, 'imagegif' ],
+ 'image/jpeg' => [ 'imagecreatefromjpeg', 'truecolor', true,
+ [ __CLASS__, 'imageJpegWrapper' ] ],
+ 'image/png' => [ 'imagecreatefrompng', 'bits', false, 'imagepng' ],
+ 'image/vnd.wap.wbmp' => [ 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ],
+ 'image/xbm' => [ 'imagecreatefromxbm', 'palette', false, 'imagexbm' ],
+ ];
+
+ if ( !isset( $typemap[$params['mimeType']] ) ) {
+ $err = 'Image type not supported';
+ wfDebug( "$err\n" );
+ $errMsg = wfMessage( 'thumbnail_image-type' )->text();
+
+ return $this->getMediaTransformError( $params, $errMsg );
+ }
+ list( $loader, $colorStyle, $useQuality, $saveType ) = $typemap[$params['mimeType']];
+
+ if ( !function_exists( $loader ) ) {
+ $err = "Incomplete GD library configuration: missing function $loader";
+ wfDebug( "$err\n" );
+ $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
+
+ return $this->getMediaTransformError( $params, $errMsg );
+ }
+
+ if ( !file_exists( $params['srcPath'] ) ) {
+ $err = "File seems to be missing: {$params['srcPath']}";
+ wfDebug( "$err\n" );
+ $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
+
+ return $this->getMediaTransformError( $params, $errMsg );
+ }
+
+ if ( filesize( $params['srcPath'] ) === 0 ) {
+ $err = "Image file size seems to be zero.";
+ wfDebug( "$err\n" );
+ $errMsg = wfMessage( 'thumbnail_image-size-zero', $params['srcPath'] )->text();
+
+ return $this->getMediaTransformError( $params, $errMsg );
+ }
+
+ $src_image = call_user_func( $loader, $params['srcPath'] );
+
+ $rotation = function_exists( 'imagerotate' ) && !isset( $params['disableRotation'] ) ?
+ $this->getRotation( $image ) :
+ 0;
+ list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
+ $dst_image = imagecreatetruecolor( $width, $height );
+
+ // Initialise the destination image to transparent instead of
+ // the default solid black, to support PNG and GIF transparency nicely
+ $background = imagecolorallocate( $dst_image, 0, 0, 0 );
+ imagecolortransparent( $dst_image, $background );
+ imagealphablending( $dst_image, false );
+
+ if ( $colorStyle == 'palette' ) {
+ // Don't resample for paletted GIF images.
+ // It may just uglify them, and completely breaks transparency.
+ imagecopyresized( $dst_image, $src_image,
+ 0, 0, 0, 0,
+ $width, $height,
+ imagesx( $src_image ), imagesy( $src_image ) );
+ } else {
+ imagecopyresampled( $dst_image, $src_image,
+ 0, 0, 0, 0,
+ $width, $height,
+ imagesx( $src_image ), imagesy( $src_image ) );
+ }
+
+ if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
+ $rot_image = imagerotate( $dst_image, $rotation, 0 );
+ imagedestroy( $dst_image );
+ $dst_image = $rot_image;
+ }
+
+ imagesavealpha( $dst_image, true );
+
+ $funcParams = [ $dst_image, $params['dstPath'] ];
+ if ( $useQuality && isset( $params['quality'] ) ) {
+ $funcParams[] = $params['quality'];
+ }
+ call_user_func_array( $saveType, $funcParams );
+
+ imagedestroy( $dst_image );
+ imagedestroy( $src_image );
+
+ return false; # No error
+ }
+
+ /**
+ * Callback for transformGd when transforming jpeg images.
+ *
+ * @param resource $dst_image Image resource of the original image
+ * @param string $thumbPath File path to write the thumbnail image to
+ * @param int|null $quality Quality of the thumbnail from 1-100,
+ * or null to use default quality.
+ */
+ static function imageJpegWrapper( $dst_image, $thumbPath, $quality = null ) {
+ global $wgJpegQuality;
+
+ if ( $quality === null ) {
+ $quality = $wgJpegQuality;
+ }
+
+ imageinterlace( $dst_image );
+ imagejpeg( $dst_image, $thumbPath, $quality );
+ }
+
+ /**
+ * Returns whether the current scaler supports rotation (im and gd do)
+ *
+ * @return bool
+ */
+ public function canRotate() {
+ $scaler = $this->getScalerType( null, false );
+ switch ( $scaler ) {
+ case 'im':
+ # ImageMagick supports autorotation
+ return true;
+ case 'imext':
+ # Imagick::rotateImage
+ return true;
+ case 'gd':
+ # GD's imagerotate function is used to rotate images, but not
+ # all precompiled PHP versions have that function
+ return function_exists( 'imagerotate' );
+ default:
+ # Other scalers don't support rotation
+ return false;
+ }
+ }
+
+ /**
+ * @see $wgEnableAutoRotation
+ * @return bool Whether auto rotation is enabled
+ */
+ public function autoRotateEnabled() {
+ global $wgEnableAutoRotation;
+
+ if ( $wgEnableAutoRotation === null ) {
+ // Only enable auto-rotation when we actually can
+ return $this->canRotate();
+ }
+
+ return $wgEnableAutoRotation;
+ }
+
+ /**
+ * @param File $file
+ * @param array $params Rotate parameters.
+ * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
+ * @since 1.21
+ * @return bool|MediaTransformError
+ */
+ public function rotate( $file, $params ) {
+ global $wgImageMagickConvertCommand;
+
+ $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
+ $scene = false;
+
+ $scaler = $this->getScalerType( null, false );
+ switch ( $scaler ) {
+ case 'im':
+ $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
+ wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
+ " -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
+ wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
+ wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
+ $retval = 0;
+ $err = wfShellExecWithStderr( $cmd, $retval );
+ if ( $retval !== 0 ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+
+ return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
+ }
+
+ return false;
+ case 'imext':
+ $im = new Imagick();
+ $im->readImage( $params['srcPath'] );
+ if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "Error rotating $rotation degrees" );
+ }
+ $result = $im->writeImage( $params['dstPath'] );
+ if ( !$result ) {
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "Unable to write image to {$params['dstPath']}" );
+ }
+
+ return false;
+ default:
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "$scaler rotation not implemented" );
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * Handler for bitmap images that will be resized by clients.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for bitmap images that will be resized by clients.
+ *
+ * This is not used by default but can be assigned to some image types
+ * using $wgMediaHandlers.
+ *
+ * @ingroup Media
+ */
+// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
+class BitmapHandler_ClientOnly extends BitmapHandler {
+
+ /**
+ * @param File $image
+ * @param array &$params
+ * @return bool
+ */
+ function normaliseParams( $image, &$params ) {
+ return ImageHandler::normaliseParams( $image, $params );
+ }
+
+ /**
+ * @param File $image
+ * @param string $dstPath
+ * @param string $dstUrl
+ * @param array $params
+ * @param int $flags
+ * @return ThumbnailImage|TransformParameterError
+ */
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ if ( !$this->normaliseParams( $image, $params ) ) {
+ return new TransformParameterError( $params );
+ }
+
+ return new ThumbnailImage( $image, $image->getUrl(), $image->getLocalRefPath(), $params );
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for bitmap images that will be resized by clients.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Handler for bitmap images that will be resized by clients.
- *
- * This is not used by default but can be assigned to some image types
- * using $wgMediaHandlers.
- *
- * @ingroup Media
- */
-// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
-class BitmapHandler_ClientOnly extends BitmapHandler {
-
- /**
- * @param File $image
- * @param array &$params
- * @return bool
- */
- function normaliseParams( $image, &$params ) {
- return ImageHandler::normaliseParams( $image, $params );
- }
-
- /**
- * @param File $image
- * @param string $dstPath
- * @param string $dstUrl
- * @param array $params
- * @param int $flags
- * @return ThumbnailImage|TransformParameterError
- */
- function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- if ( !$this->normaliseParams( $image, $params ) ) {
- return new TransformParameterError( $params );
- }
-
- return new ThumbnailImage( $image, $image->getUrl(), $image->getLocalRefPath(), $params );
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for Microsoft's bitmap format.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for Microsoft's bitmap format; getimagesize() doesn't
+ * support these files
+ *
+ * @ingroup Media
+ */
+class BmpHandler extends BitmapHandler {
+ /**
+ * @param File $file
+ * @return bool
+ */
+ public function mustRender( $file ) {
+ return true;
+ }
+
+ /**
+ * Render files as PNG
+ *
+ * @param string $text
+ * @param string $mime
+ * @param array $params
+ * @return array
+ */
+ function getThumbType( $text, $mime, $params = null ) {
+ return [ 'png', 'image/png' ];
+ }
+
+ /**
+ * Get width and height from the bmp header.
+ *
+ * @param File|FSFile $image
+ * @param string $filename
+ * @return array
+ */
+ function getImageSize( $image, $filename ) {
+ $f = fopen( $filename, 'rb' );
+ if ( !$f ) {
+ return false;
+ }
+ $header = fread( $f, 54 );
+ fclose( $f );
+
+ // Extract binary form of width and height from the header
+ $w = substr( $header, 18, 4 );
+ $h = substr( $header, 22, 4 );
+
+ // Convert the unsigned long 32 bits (little endian):
+ try {
+ $w = wfUnpack( 'V', $w, 4 );
+ $h = wfUnpack( 'V', $h, 4 );
+ } catch ( Exception $e ) {
+ return false;
+ }
+
+ return [ $w[1], $h[1] ];
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for DjVu images.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Handler for DjVu images
- *
- * @ingroup Media
- */
-class DjVuHandler extends ImageHandler {
- const EXPENSIVE_SIZE_LIMIT = 10485760; // 10MiB
-
- /**
- * @return bool
- */
- function isEnabled() {
- global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML;
- if ( !$wgDjvuRenderer || ( !$wgDjvuDump && !$wgDjvuToXML ) ) {
- wfDebug( "DjVu is disabled, please set \$wgDjvuRenderer and \$wgDjvuDump\n" );
-
- return false;
- } else {
- return true;
- }
- }
-
- /**
- * @param File $file
- * @return bool
- */
- public function mustRender( $file ) {
- return true;
- }
-
- /**
- * True if creating thumbnails from the file is large or otherwise resource-intensive.
- * @param File $file
- * @return bool
- */
- public function isExpensiveToThumbnail( $file ) {
- return $file->getSize() > static::EXPENSIVE_SIZE_LIMIT;
- }
-
- /**
- * @param File $file
- * @return bool
- */
- public function isMultiPage( $file ) {
- return true;
- }
-
- /**
- * @return array
- */
- public function getParamMap() {
- return [
- 'img_width' => 'width',
- 'img_page' => 'page',
- ];
- }
-
- /**
- * @param string $name
- * @param mixed $value
- * @return bool
- */
- public function validateParam( $name, $value ) {
- if ( $name === 'page' && trim( $value ) !== (string)intval( $value ) ) {
- // Extra junk on the end of page, probably actually a caption
- // e.g. [[File:Foo.djvu|thumb|Page 3 of the document shows foo]]
- return false;
- }
- if ( in_array( $name, [ 'width', 'height', 'page' ] ) ) {
- if ( $value <= 0 ) {
- return false;
- } else {
- return true;
- }
- } else {
- return false;
- }
- }
-
- /**
- * @param array $params
- * @return bool|string
- */
- public function makeParamString( $params ) {
- $page = isset( $params['page'] ) ? $params['page'] : 1;
- if ( !isset( $params['width'] ) ) {
- return false;
- }
-
- return "page{$page}-{$params['width']}px";
- }
-
- /**
- * @param string $str
- * @return array|bool
- */
- public function parseParamString( $str ) {
- $m = false;
- if ( preg_match( '/^page(\d+)-(\d+)px$/', $str, $m ) ) {
- return [ 'width' => $m[2], 'page' => $m[1] ];
- } else {
- return false;
- }
- }
-
- /**
- * @param array $params
- * @return array
- */
- function getScriptParams( $params ) {
- return [
- 'width' => $params['width'],
- 'page' => $params['page'],
- ];
- }
-
- /**
- * @param File $image
- * @param string $dstPath
- * @param string $dstUrl
- * @param array $params
- * @param int $flags
- * @return MediaTransformError|ThumbnailImage|TransformParameterError
- */
- function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- global $wgDjvuRenderer, $wgDjvuPostProcessor;
-
- if ( !$this->normaliseParams( $image, $params ) ) {
- return new TransformParameterError( $params );
- }
- $width = $params['width'];
- $height = $params['height'];
- $page = $params['page'];
-
- if ( $flags & self::TRANSFORM_LATER ) {
- $params = [
- 'width' => $width,
- 'height' => $height,
- 'page' => $page
- ];
-
- return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
- }
-
- if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
- return new MediaTransformError(
- 'thumbnail_error',
- $width,
- $height,
- wfMessage( 'thumbnail_dest_directory' )
- );
- }
-
- // Get local copy source for shell scripts
- // Thumbnail extraction is very inefficient for large files.
- // Provide a way to pool count limit the number of downloaders.
- if ( $image->getSize() >= 1e7 ) { // 10MB
- $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $image->getName() ),
- [
- 'doWork' => function () use ( $image ) {
- return $image->getLocalRefPath();
- }
- ]
- );
- $srcPath = $work->execute();
- } else {
- $srcPath = $image->getLocalRefPath();
- }
-
- if ( $srcPath === false ) { // Failed to get local copy
- wfDebugLog( 'thumbnail',
- sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
- wfHostname(), $image->getName() ) );
-
- return new MediaTransformError( 'thumbnail_error',
- $params['width'], $params['height'],
- wfMessage( 'filemissing' )
- );
- }
-
- # Use a subshell (brackets) to aggregate stderr from both pipeline commands
- # before redirecting it to the overall stdout. This works in both Linux and Windows XP.
- $cmd = '(' . wfEscapeShellArg(
- $wgDjvuRenderer,
- "-format=ppm",
- "-page={$page}",
- "-size={$params['physicalWidth']}x{$params['physicalHeight']}",
- $srcPath );
- if ( $wgDjvuPostProcessor ) {
- $cmd .= " | {$wgDjvuPostProcessor}";
- }
- $cmd .= ' > ' . wfEscapeShellArg( $dstPath ) . ') 2>&1';
- wfDebug( __METHOD__ . ": $cmd\n" );
- $retval = '';
- $err = wfShellExec( $cmd, $retval );
-
- $removed = $this->removeBadFile( $dstPath, $retval );
- if ( $retval != 0 || $removed ) {
- $this->logErrorForExternalProcess( $retval, $err, $cmd );
- return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
- } else {
- $params = [
- 'width' => $width,
- 'height' => $height,
- 'page' => $page
- ];
-
- return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
- }
- }
-
- /**
- * Cache an instance of DjVuImage in an Image object, return that instance
- *
- * @param File|FSFile $image
- * @param string $path
- * @return DjVuImage
- */
- function getDjVuImage( $image, $path ) {
- if ( !$image ) {
- $deja = new DjVuImage( $path );
- } elseif ( !isset( $image->dejaImage ) ) {
- $deja = $image->dejaImage = new DjVuImage( $path );
- } else {
- $deja = $image->dejaImage;
- }
-
- return $deja;
- }
-
- /**
- * Get metadata, unserializing it if neccessary.
- *
- * @param File $file The DjVu file in question
- * @return string XML metadata as a string.
- * @throws MWException
- */
- private function getUnserializedMetadata( File $file ) {
- $metadata = $file->getMetadata();
- if ( substr( $metadata, 0, 3 ) === '<?xml' ) {
- // Old style. Not serialized but instead just a raw string of XML.
- return $metadata;
- }
-
- Wikimedia\suppressWarnings();
- $unser = unserialize( $metadata );
- Wikimedia\restoreWarnings();
- if ( is_array( $unser ) ) {
- if ( isset( $unser['error'] ) ) {
- return false;
- } elseif ( isset( $unser['xml'] ) ) {
- return $unser['xml'];
- } else {
- // Should never ever reach here.
- throw new MWException( "Error unserializing DjVu metadata." );
- }
- }
-
- // unserialize failed. Guess it wasn't really serialized after all,
- return $metadata;
- }
-
- /**
- * Cache a document tree for the DjVu XML metadata
- * @param File $image
- * @param bool $gettext DOCUMENT (Default: false)
- * @return bool|SimpleXMLElement
- */
- public function getMetaTree( $image, $gettext = false ) {
- if ( $gettext && isset( $image->djvuTextTree ) ) {
- return $image->djvuTextTree;
- }
- if ( !$gettext && isset( $image->dejaMetaTree ) ) {
- return $image->dejaMetaTree;
- }
-
- $metadata = $this->getUnserializedMetadata( $image );
- if ( !$this->isMetadataValid( $image, $metadata ) ) {
- wfDebug( "DjVu XML metadata is invalid or missing, should have been fixed in upgradeRow\n" );
-
- return false;
- }
-
- $trees = $this->extractTreesFromMetadata( $metadata );
- $image->djvuTextTree = $trees['TextTree'];
- $image->dejaMetaTree = $trees['MetaTree'];
-
- if ( $gettext ) {
- return $image->djvuTextTree;
- } else {
- return $image->dejaMetaTree;
- }
- }
-
- /**
- * Extracts metadata and text trees from metadata XML in string form
- * @param string $metadata XML metadata as a string
- * @return array
- */
- protected function extractTreesFromMetadata( $metadata ) {
- Wikimedia\suppressWarnings();
- try {
- // Set to false rather than null to avoid further attempts
- $metaTree = false;
- $textTree = false;
- $tree = new SimpleXMLElement( $metadata, LIBXML_PARSEHUGE );
- if ( $tree->getName() == 'mw-djvu' ) {
- /** @var SimpleXMLElement $b */
- foreach ( $tree->children() as $b ) {
- if ( $b->getName() == 'DjVuTxt' ) {
- // @todo File::djvuTextTree and File::dejaMetaTree are declared
- // dynamically. Add a public File::$data to facilitate this?
- $textTree = $b;
- } elseif ( $b->getName() == 'DjVuXML' ) {
- $metaTree = $b;
- }
- }
- } else {
- $metaTree = $tree;
- }
- } catch ( Exception $e ) {
- wfDebug( "Bogus multipage XML metadata\n" );
- }
- Wikimedia\restoreWarnings();
-
- return [ 'MetaTree' => $metaTree, 'TextTree' => $textTree ];
- }
-
- function getImageSize( $image, $path ) {
- return $this->getDjVuImage( $image, $path )->getImageSize();
- }
-
- function getThumbType( $ext, $mime, $params = null ) {
- global $wgDjvuOutputExtension;
- static $mime;
- if ( !isset( $mime ) ) {
- $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
- $mime = $magic->guessTypesForExtension( $wgDjvuOutputExtension );
- }
-
- return [ $wgDjvuOutputExtension, $mime ];
- }
-
- function getMetadata( $image, $path ) {
- wfDebug( "Getting DjVu metadata for $path\n" );
-
- $xml = $this->getDjVuImage( $image, $path )->retrieveMetaData();
- if ( $xml === false ) {
- // Special value so that we don't repetitively try and decode a broken file.
- return serialize( [ 'error' => 'Error extracting metadata' ] );
- } else {
- return serialize( [ 'xml' => $xml ] );
- }
- }
-
- function getMetadataType( $image ) {
- return 'djvuxml';
- }
-
- function isMetadataValid( $image, $metadata ) {
- return !empty( $metadata ) && $metadata != serialize( [] );
- }
-
- function pageCount( File $image ) {
- $info = $this->getDimensionInfo( $image );
-
- return $info ? $info['pageCount'] : false;
- }
-
- function getPageDimensions( File $image, $page ) {
- $index = $page - 1; // MW starts pages at 1
-
- $info = $this->getDimensionInfo( $image );
- if ( $info && isset( $info['dimensionsByPage'][$index] ) ) {
- return $info['dimensionsByPage'][$index];
- }
-
- return false;
- }
-
- protected function getDimensionInfo( File $file ) {
- $cache = ObjectCache::getMainWANInstance();
- return $cache->getWithSetCallback(
- $cache->makeKey( 'file-djvu', 'dimensions', $file->getSha1() ),
- $cache::TTL_INDEFINITE,
- function () use ( $file ) {
- $tree = $this->getMetaTree( $file );
- return $this->getDimensionInfoFromMetaTree( $tree );
- },
- [ 'pcTTL' => $cache::TTL_INDEFINITE ]
- );
- }
-
- /**
- * Given an XML metadata tree, returns dimension information about the document
- * @param bool|SimpleXMLElement $metatree The file's XML metadata tree
- * @return bool|array
- */
- protected function getDimensionInfoFromMetaTree( $metatree ) {
- if ( !$metatree ) {
- return false;
- }
-
- $dimsByPage = [];
- $count = count( $metatree->xpath( '//OBJECT' ) );
- for ( $i = 0; $i < $count; $i++ ) {
- $o = $metatree->BODY[0]->OBJECT[$i];
- if ( $o ) {
- $dimsByPage[$i] = [
- 'width' => (int)$o['width'],
- 'height' => (int)$o['height'],
- ];
- } else {
- $dimsByPage[$i] = false;
- }
- }
-
- return [ 'pageCount' => $count, 'dimensionsByPage' => $dimsByPage ];
- }
-
- /**
- * @param File $image
- * @param int $page Page number to get information for
- * @return bool|string Page text or false when no text found.
- */
- function getPageText( File $image, $page ) {
- $tree = $this->getMetaTree( $image, true );
- if ( !$tree ) {
- return false;
- }
-
- $o = $tree->BODY[0]->PAGE[$page - 1];
- if ( $o ) {
- $txt = $o['value'];
-
- return $txt;
- } else {
- return false;
- }
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for DjVu images.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for DjVu images
+ *
+ * @ingroup Media
+ */
+class DjVuHandler extends ImageHandler {
+ const EXPENSIVE_SIZE_LIMIT = 10485760; // 10MiB
+
+ /**
+ * @return bool
+ */
+ function isEnabled() {
+ global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML;
+ if ( !$wgDjvuRenderer || ( !$wgDjvuDump && !$wgDjvuToXML ) ) {
+ wfDebug( "DjVu is disabled, please set \$wgDjvuRenderer and \$wgDjvuDump\n" );
+
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @param File $file
+ * @return bool
+ */
+ public function mustRender( $file ) {
+ return true;
+ }
+
+ /**
+ * True if creating thumbnails from the file is large or otherwise resource-intensive.
+ * @param File $file
+ * @return bool
+ */
+ public function isExpensiveToThumbnail( $file ) {
+ return $file->getSize() > static::EXPENSIVE_SIZE_LIMIT;
+ }
+
+ /**
+ * @param File $file
+ * @return bool
+ */
+ public function isMultiPage( $file ) {
+ return true;
+ }
+
+ /**
+ * @return array
+ */
+ public function getParamMap() {
+ return [
+ 'img_width' => 'width',
+ 'img_page' => 'page',
+ ];
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ * @return bool
+ */
+ public function validateParam( $name, $value ) {
+ if ( $name === 'page' && trim( $value ) !== (string)intval( $value ) ) {
+ // Extra junk on the end of page, probably actually a caption
+ // e.g. [[File:Foo.djvu|thumb|Page 3 of the document shows foo]]
+ return false;
+ }
+ if ( in_array( $name, [ 'width', 'height', 'page' ] ) ) {
+ if ( $value <= 0 ) {
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param array $params
+ * @return bool|string
+ */
+ public function makeParamString( $params ) {
+ $page = isset( $params['page'] ) ? $params['page'] : 1;
+ if ( !isset( $params['width'] ) ) {
+ return false;
+ }
+
+ return "page{$page}-{$params['width']}px";
+ }
+
+ /**
+ * @param string $str
+ * @return array|bool
+ */
+ public function parseParamString( $str ) {
+ $m = false;
+ if ( preg_match( '/^page(\d+)-(\d+)px$/', $str, $m ) ) {
+ return [ 'width' => $m[2], 'page' => $m[1] ];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param array $params
+ * @return array
+ */
+ function getScriptParams( $params ) {
+ return [
+ 'width' => $params['width'],
+ 'page' => $params['page'],
+ ];
+ }
+
+ /**
+ * @param File $image
+ * @param string $dstPath
+ * @param string $dstUrl
+ * @param array $params
+ * @param int $flags
+ * @return MediaTransformError|ThumbnailImage|TransformParameterError
+ */
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ global $wgDjvuRenderer, $wgDjvuPostProcessor;
+
+ if ( !$this->normaliseParams( $image, $params ) ) {
+ return new TransformParameterError( $params );
+ }
+ $width = $params['width'];
+ $height = $params['height'];
+ $page = $params['page'];
+
+ if ( $flags & self::TRANSFORM_LATER ) {
+ $params = [
+ 'width' => $width,
+ 'height' => $height,
+ 'page' => $page
+ ];
+
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
+ }
+
+ if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
+ return new MediaTransformError(
+ 'thumbnail_error',
+ $width,
+ $height,
+ wfMessage( 'thumbnail_dest_directory' )
+ );
+ }
+
+ // Get local copy source for shell scripts
+ // Thumbnail extraction is very inefficient for large files.
+ // Provide a way to pool count limit the number of downloaders.
+ if ( $image->getSize() >= 1e7 ) { // 10MB
+ $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $image->getName() ),
+ [
+ 'doWork' => function () use ( $image ) {
+ return $image->getLocalRefPath();
+ }
+ ]
+ );
+ $srcPath = $work->execute();
+ } else {
+ $srcPath = $image->getLocalRefPath();
+ }
+
+ if ( $srcPath === false ) { // Failed to get local copy
+ wfDebugLog( 'thumbnail',
+ sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
+ wfHostname(), $image->getName() ) );
+
+ return new MediaTransformError( 'thumbnail_error',
+ $params['width'], $params['height'],
+ wfMessage( 'filemissing' )
+ );
+ }
+
+ # Use a subshell (brackets) to aggregate stderr from both pipeline commands
+ # before redirecting it to the overall stdout. This works in both Linux and Windows XP.
+ $cmd = '(' . wfEscapeShellArg(
+ $wgDjvuRenderer,
+ "-format=ppm",
+ "-page={$page}",
+ "-size={$params['physicalWidth']}x{$params['physicalHeight']}",
+ $srcPath );
+ if ( $wgDjvuPostProcessor ) {
+ $cmd .= " | {$wgDjvuPostProcessor}";
+ }
+ $cmd .= ' > ' . wfEscapeShellArg( $dstPath ) . ') 2>&1';
+ wfDebug( __METHOD__ . ": $cmd\n" );
+ $retval = '';
+ $err = wfShellExec( $cmd, $retval );
+
+ $removed = $this->removeBadFile( $dstPath, $retval );
+ if ( $retval != 0 || $removed ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+ return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
+ } else {
+ $params = [
+ 'width' => $width,
+ 'height' => $height,
+ 'page' => $page
+ ];
+
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
+ }
+ }
+
+ /**
+ * Cache an instance of DjVuImage in an Image object, return that instance
+ *
+ * @param File|FSFile $image
+ * @param string $path
+ * @return DjVuImage
+ */
+ function getDjVuImage( $image, $path ) {
+ if ( !$image ) {
+ $deja = new DjVuImage( $path );
+ } elseif ( !isset( $image->dejaImage ) ) {
+ $deja = $image->dejaImage = new DjVuImage( $path );
+ } else {
+ $deja = $image->dejaImage;
+ }
+
+ return $deja;
+ }
+
+ /**
+ * Get metadata, unserializing it if neccessary.
+ *
+ * @param File $file The DjVu file in question
+ * @return string XML metadata as a string.
+ * @throws MWException
+ */
+ private function getUnserializedMetadata( File $file ) {
+ $metadata = $file->getMetadata();
+ if ( substr( $metadata, 0, 3 ) === '<?xml' ) {
+ // Old style. Not serialized but instead just a raw string of XML.
+ return $metadata;
+ }
+
+ Wikimedia\suppressWarnings();
+ $unser = unserialize( $metadata );
+ Wikimedia\restoreWarnings();
+ if ( is_array( $unser ) ) {
+ if ( isset( $unser['error'] ) ) {
+ return false;
+ } elseif ( isset( $unser['xml'] ) ) {
+ return $unser['xml'];
+ } else {
+ // Should never ever reach here.
+ throw new MWException( "Error unserializing DjVu metadata." );
+ }
+ }
+
+ // unserialize failed. Guess it wasn't really serialized after all,
+ return $metadata;
+ }
+
+ /**
+ * Cache a document tree for the DjVu XML metadata
+ * @param File $image
+ * @param bool $gettext DOCUMENT (Default: false)
+ * @return bool|SimpleXMLElement
+ */
+ public function getMetaTree( $image, $gettext = false ) {
+ if ( $gettext && isset( $image->djvuTextTree ) ) {
+ return $image->djvuTextTree;
+ }
+ if ( !$gettext && isset( $image->dejaMetaTree ) ) {
+ return $image->dejaMetaTree;
+ }
+
+ $metadata = $this->getUnserializedMetadata( $image );
+ if ( !$this->isMetadataValid( $image, $metadata ) ) {
+ wfDebug( "DjVu XML metadata is invalid or missing, should have been fixed in upgradeRow\n" );
+
+ return false;
+ }
+
+ $trees = $this->extractTreesFromMetadata( $metadata );
+ $image->djvuTextTree = $trees['TextTree'];
+ $image->dejaMetaTree = $trees['MetaTree'];
+
+ if ( $gettext ) {
+ return $image->djvuTextTree;
+ } else {
+ return $image->dejaMetaTree;
+ }
+ }
+
+ /**
+ * Extracts metadata and text trees from metadata XML in string form
+ * @param string $metadata XML metadata as a string
+ * @return array
+ */
+ protected function extractTreesFromMetadata( $metadata ) {
+ Wikimedia\suppressWarnings();
+ try {
+ // Set to false rather than null to avoid further attempts
+ $metaTree = false;
+ $textTree = false;
+ $tree = new SimpleXMLElement( $metadata, LIBXML_PARSEHUGE );
+ if ( $tree->getName() == 'mw-djvu' ) {
+ /** @var SimpleXMLElement $b */
+ foreach ( $tree->children() as $b ) {
+ if ( $b->getName() == 'DjVuTxt' ) {
+ // @todo File::djvuTextTree and File::dejaMetaTree are declared
+ // dynamically. Add a public File::$data to facilitate this?
+ $textTree = $b;
+ } elseif ( $b->getName() == 'DjVuXML' ) {
+ $metaTree = $b;
+ }
+ }
+ } else {
+ $metaTree = $tree;
+ }
+ } catch ( Exception $e ) {
+ wfDebug( "Bogus multipage XML metadata\n" );
+ }
+ Wikimedia\restoreWarnings();
+
+ return [ 'MetaTree' => $metaTree, 'TextTree' => $textTree ];
+ }
+
+ function getImageSize( $image, $path ) {
+ return $this->getDjVuImage( $image, $path )->getImageSize();
+ }
+
+ function getThumbType( $ext, $mime, $params = null ) {
+ global $wgDjvuOutputExtension;
+ static $mime;
+ if ( !isset( $mime ) ) {
+ $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
+ $mime = $magic->guessTypesForExtension( $wgDjvuOutputExtension );
+ }
+
+ return [ $wgDjvuOutputExtension, $mime ];
+ }
+
+ function getMetadata( $image, $path ) {
+ wfDebug( "Getting DjVu metadata for $path\n" );
+
+ $xml = $this->getDjVuImage( $image, $path )->retrieveMetaData();
+ if ( $xml === false ) {
+ // Special value so that we don't repetitively try and decode a broken file.
+ return serialize( [ 'error' => 'Error extracting metadata' ] );
+ } else {
+ return serialize( [ 'xml' => $xml ] );
+ }
+ }
+
+ function getMetadataType( $image ) {
+ return 'djvuxml';
+ }
+
+ function isMetadataValid( $image, $metadata ) {
+ return !empty( $metadata ) && $metadata != serialize( [] );
+ }
+
+ function pageCount( File $image ) {
+ $info = $this->getDimensionInfo( $image );
+
+ return $info ? $info['pageCount'] : false;
+ }
+
+ function getPageDimensions( File $image, $page ) {
+ $index = $page - 1; // MW starts pages at 1
+
+ $info = $this->getDimensionInfo( $image );
+ if ( $info && isset( $info['dimensionsByPage'][$index] ) ) {
+ return $info['dimensionsByPage'][$index];
+ }
+
+ return false;
+ }
+
+ protected function getDimensionInfo( File $file ) {
+ $cache = ObjectCache::getMainWANInstance();
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'file-djvu', 'dimensions', $file->getSha1() ),
+ $cache::TTL_INDEFINITE,
+ function () use ( $file ) {
+ $tree = $this->getMetaTree( $file );
+ return $this->getDimensionInfoFromMetaTree( $tree );
+ },
+ [ 'pcTTL' => $cache::TTL_INDEFINITE ]
+ );
+ }
+
+ /**
+ * Given an XML metadata tree, returns dimension information about the document
+ * @param bool|SimpleXMLElement $metatree The file's XML metadata tree
+ * @return bool|array
+ */
+ protected function getDimensionInfoFromMetaTree( $metatree ) {
+ if ( !$metatree ) {
+ return false;
+ }
+
+ $dimsByPage = [];
+ $count = count( $metatree->xpath( '//OBJECT' ) );
+ for ( $i = 0; $i < $count; $i++ ) {
+ $o = $metatree->BODY[0]->OBJECT[$i];
+ if ( $o ) {
+ $dimsByPage[$i] = [
+ 'width' => (int)$o['width'],
+ 'height' => (int)$o['height'],
+ ];
+ } else {
+ $dimsByPage[$i] = false;
+ }
+ }
+
+ return [ 'pageCount' => $count, 'dimensionsByPage' => $dimsByPage ];
+ }
+
+ /**
+ * @param File $image
+ * @param int $page Page number to get information for
+ * @return bool|string Page text or false when no text found.
+ */
+ function getPageText( File $image, $page ) {
+ $tree = $this->getMetaTree( $image, true );
+ if ( !$tree ) {
+ return false;
+ }
+
+ $o = $tree->BODY[0]->PAGE[$page - 1];
+ if ( $o ) {
+ $txt = $o['value'];
+
+ return $txt;
+ } else {
+ return false;
+ }
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for bitmap images with exif metadata.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Stuff specific to JPEG and (built-in) TIFF handler.
- * All metadata related, since both JPEG and TIFF support Exif.
- *
- * @ingroup Media
- */
-class ExifBitmapHandler extends BitmapHandler {
- const BROKEN_FILE = '-1'; // error extracting metadata
- const OLD_BROKEN_FILE = '0'; // outdated error extracting metadata.
-
- function convertMetadataVersion( $metadata, $version = 1 ) {
- // basically flattens arrays.
- $version = intval( explode( ';', $version, 2 )[0] );
- if ( $version < 1 || $version >= 2 ) {
- return $metadata;
- }
-
- $avoidHtml = true;
-
- if ( !is_array( $metadata ) ) {
- $metadata = unserialize( $metadata );
- }
- if ( !isset( $metadata['MEDIAWIKI_EXIF_VERSION'] ) || $metadata['MEDIAWIKI_EXIF_VERSION'] != 2 ) {
- return $metadata;
- }
-
- // Treat Software as a special case because in can contain
- // an array of (SoftwareName, Version).
- if ( isset( $metadata['Software'] )
- && is_array( $metadata['Software'] )
- && is_array( $metadata['Software'][0] )
- && isset( $metadata['Software'][0][0] )
- && isset( $metadata['Software'][0][1] )
- ) {
- $metadata['Software'] = $metadata['Software'][0][0] . ' (Version '
- . $metadata['Software'][0][1] . ')';
- }
-
- $formatter = new FormatMetadata;
-
- // ContactInfo also has to be dealt with specially
- if ( isset( $metadata['Contact'] ) ) {
- $metadata['Contact'] =
- $formatter->collapseContactInfo(
- $metadata['Contact'] );
- }
-
- foreach ( $metadata as &$val ) {
- if ( is_array( $val ) ) {
- $val = $formatter->flattenArrayReal( $val, 'ul', $avoidHtml );
- }
- }
- $metadata['MEDIAWIKI_EXIF_VERSION'] = 1;
-
- return $metadata;
- }
-
- /**
- * @param File $image
- * @param array $metadata
- * @return bool|int
- */
- function isMetadataValid( $image, $metadata ) {
- global $wgShowEXIF;
- if ( !$wgShowEXIF ) {
- # Metadata disabled and so an empty field is expected
- return self::METADATA_GOOD;
- }
- if ( $metadata === self::OLD_BROKEN_FILE ) {
- # Old special value indicating that there is no Exif data in the file.
- # or that there was an error well extracting the metadata.
- wfDebug( __METHOD__ . ": back-compat version\n" );
-
- return self::METADATA_COMPATIBLE;
- }
- if ( $metadata === self::BROKEN_FILE ) {
- return self::METADATA_GOOD;
- }
- Wikimedia\suppressWarnings();
- $exif = unserialize( $metadata );
- Wikimedia\restoreWarnings();
- if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
- || $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version()
- ) {
- if ( isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
- && $exif['MEDIAWIKI_EXIF_VERSION'] == 1
- ) {
- // back-compatible but old
- wfDebug( __METHOD__ . ": back-compat version\n" );
-
- return self::METADATA_COMPATIBLE;
- }
- # Wrong (non-compatible) version
- wfDebug( __METHOD__ . ": wrong version\n" );
-
- return self::METADATA_BAD;
- }
-
- return self::METADATA_GOOD;
- }
-
- /**
- * @param File $image
- * @param bool|IContextSource $context Context to use (optional)
- * @return array|bool
- */
- function formatMetadata( $image, $context = false ) {
- $meta = $this->getCommonMetaArray( $image );
- if ( count( $meta ) === 0 ) {
- return false;
- }
-
- return $this->formatMetadataHelper( $meta, $context );
- }
-
- public function getCommonMetaArray( File $file ) {
- $metadata = $file->getMetadata();
- if ( $metadata === self::OLD_BROKEN_FILE
- || $metadata === self::BROKEN_FILE
- || $this->isMetadataValid( $file, $metadata ) === self::METADATA_BAD
- ) {
- // So we don't try and display metadata from PagedTiffHandler
- // for example when using InstantCommons.
- return [];
- }
-
- $exif = unserialize( $metadata );
- if ( !$exif ) {
- return [];
- }
- unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
-
- return $exif;
- }
-
- function getMetadataType( $image ) {
- return 'exif';
- }
-
- /**
- * Wrapper for base classes ImageHandler::getImageSize() that checks for
- * rotation reported from metadata and swaps the sizes to match.
- *
- * @param File|FSFile $image
- * @param string $path
- * @return array
- */
- function getImageSize( $image, $path ) {
- $gis = parent::getImageSize( $image, $path );
-
- // Don't just call $image->getMetadata(); FSFile::getPropsFromPath() calls us with a bogus object.
- // This may mean we read EXIF data twice on initial upload.
- if ( $this->autoRotateEnabled() ) {
- $meta = $this->getMetadata( $image, $path );
- $rotation = $this->getRotationForExif( $meta );
- } else {
- $rotation = 0;
- }
-
- if ( $rotation == 90 || $rotation == 270 ) {
- $width = $gis[0];
- $gis[0] = $gis[1];
- $gis[1] = $width;
- }
-
- return $gis;
- }
-
- /**
- * On supporting image formats, try to read out the low-level orientation
- * of the file and return the angle that the file needs to be rotated to
- * be viewed.
- *
- * This information is only useful when manipulating the original file;
- * the width and height we normally work with is logical, and will match
- * any produced output views.
- *
- * @param File $file
- * @return int 0, 90, 180 or 270
- */
- public function getRotation( $file ) {
- if ( !$this->autoRotateEnabled() ) {
- return 0;
- }
-
- $data = $file->getMetadata();
-
- return $this->getRotationForExif( $data );
- }
-
- /**
- * Given a chunk of serialized Exif metadata, return the orientation as
- * degrees of rotation.
- *
- * @param string $data
- * @return int 0, 90, 180 or 270
- * @todo FIXME: Orientation can include flipping as well; see if this is an issue!
- */
- protected function getRotationForExif( $data ) {
- if ( !$data ) {
- return 0;
- }
- Wikimedia\suppressWarnings();
- $data = unserialize( $data );
- Wikimedia\restoreWarnings();
- if ( isset( $data['Orientation'] ) ) {
- # See http://sylvana.net/jpegcrop/exif_orientation.html
- switch ( $data['Orientation'] ) {
- case 8:
- return 90;
- case 3:
- return 180;
- case 6:
- return 270;
- default:
- return 0;
- }
- }
-
- return 0;
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for bitmap images with exif metadata.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Stuff specific to JPEG and (built-in) TIFF handler.
+ * All metadata related, since both JPEG and TIFF support Exif.
+ *
+ * @ingroup Media
+ */
+class ExifBitmapHandler extends BitmapHandler {
+ const BROKEN_FILE = '-1'; // error extracting metadata
+ const OLD_BROKEN_FILE = '0'; // outdated error extracting metadata.
+
+ function convertMetadataVersion( $metadata, $version = 1 ) {
+ // basically flattens arrays.
+ $version = intval( explode( ';', $version, 2 )[0] );
+ if ( $version < 1 || $version >= 2 ) {
+ return $metadata;
+ }
+
+ $avoidHtml = true;
+
+ if ( !is_array( $metadata ) ) {
+ $metadata = unserialize( $metadata );
+ }
+ if ( !isset( $metadata['MEDIAWIKI_EXIF_VERSION'] ) || $metadata['MEDIAWIKI_EXIF_VERSION'] != 2 ) {
+ return $metadata;
+ }
+
+ // Treat Software as a special case because in can contain
+ // an array of (SoftwareName, Version).
+ if ( isset( $metadata['Software'] )
+ && is_array( $metadata['Software'] )
+ && is_array( $metadata['Software'][0] )
+ && isset( $metadata['Software'][0][0] )
+ && isset( $metadata['Software'][0][1] )
+ ) {
+ $metadata['Software'] = $metadata['Software'][0][0] . ' (Version '
+ . $metadata['Software'][0][1] . ')';
+ }
+
+ $formatter = new FormatMetadata;
+
+ // ContactInfo also has to be dealt with specially
+ if ( isset( $metadata['Contact'] ) ) {
+ $metadata['Contact'] =
+ $formatter->collapseContactInfo(
+ $metadata['Contact'] );
+ }
+
+ foreach ( $metadata as &$val ) {
+ if ( is_array( $val ) ) {
+ $val = $formatter->flattenArrayReal( $val, 'ul', $avoidHtml );
+ }
+ }
+ $metadata['MEDIAWIKI_EXIF_VERSION'] = 1;
+
+ return $metadata;
+ }
+
+ /**
+ * @param File $image
+ * @param array $metadata
+ * @return bool|int
+ */
+ function isMetadataValid( $image, $metadata ) {
+ global $wgShowEXIF;
+ if ( !$wgShowEXIF ) {
+ # Metadata disabled and so an empty field is expected
+ return self::METADATA_GOOD;
+ }
+ if ( $metadata === self::OLD_BROKEN_FILE ) {
+ # Old special value indicating that there is no Exif data in the file.
+ # or that there was an error well extracting the metadata.
+ wfDebug( __METHOD__ . ": back-compat version\n" );
+
+ return self::METADATA_COMPATIBLE;
+ }
+ if ( $metadata === self::BROKEN_FILE ) {
+ return self::METADATA_GOOD;
+ }
+ Wikimedia\suppressWarnings();
+ $exif = unserialize( $metadata );
+ Wikimedia\restoreWarnings();
+ if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
+ || $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version()
+ ) {
+ if ( isset( $exif['MEDIAWIKI_EXIF_VERSION'] )
+ && $exif['MEDIAWIKI_EXIF_VERSION'] == 1
+ ) {
+ // back-compatible but old
+ wfDebug( __METHOD__ . ": back-compat version\n" );
+
+ return self::METADATA_COMPATIBLE;
+ }
+ # Wrong (non-compatible) version
+ wfDebug( __METHOD__ . ": wrong version\n" );
+
+ return self::METADATA_BAD;
+ }
+
+ return self::METADATA_GOOD;
+ }
+
+ /**
+ * @param File $image
+ * @param bool|IContextSource $context Context to use (optional)
+ * @return array|bool
+ */
+ function formatMetadata( $image, $context = false ) {
+ $meta = $this->getCommonMetaArray( $image );
+ if ( count( $meta ) === 0 ) {
+ return false;
+ }
+
+ return $this->formatMetadataHelper( $meta, $context );
+ }
+
+ public function getCommonMetaArray( File $file ) {
+ $metadata = $file->getMetadata();
+ if ( $metadata === self::OLD_BROKEN_FILE
+ || $metadata === self::BROKEN_FILE
+ || $this->isMetadataValid( $file, $metadata ) === self::METADATA_BAD
+ ) {
+ // So we don't try and display metadata from PagedTiffHandler
+ // for example when using InstantCommons.
+ return [];
+ }
+
+ $exif = unserialize( $metadata );
+ if ( !$exif ) {
+ return [];
+ }
+ unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
+
+ return $exif;
+ }
+
+ function getMetadataType( $image ) {
+ return 'exif';
+ }
+
+ /**
+ * Wrapper for base classes ImageHandler::getImageSize() that checks for
+ * rotation reported from metadata and swaps the sizes to match.
+ *
+ * @param File|FSFile $image
+ * @param string $path
+ * @return array
+ */
+ function getImageSize( $image, $path ) {
+ $gis = parent::getImageSize( $image, $path );
+
+ // Don't just call $image->getMetadata(); FSFile::getPropsFromPath() calls us with a bogus object.
+ // This may mean we read EXIF data twice on initial upload.
+ if ( $this->autoRotateEnabled() ) {
+ $meta = $this->getMetadata( $image, $path );
+ $rotation = $this->getRotationForExif( $meta );
+ } else {
+ $rotation = 0;
+ }
+
+ if ( $rotation == 90 || $rotation == 270 ) {
+ $width = $gis[0];
+ $gis[0] = $gis[1];
+ $gis[1] = $width;
+ }
+
+ return $gis;
+ }
+
+ /**
+ * On supporting image formats, try to read out the low-level orientation
+ * of the file and return the angle that the file needs to be rotated to
+ * be viewed.
+ *
+ * This information is only useful when manipulating the original file;
+ * the width and height we normally work with is logical, and will match
+ * any produced output views.
+ *
+ * @param File $file
+ * @return int 0, 90, 180 or 270
+ */
+ public function getRotation( $file ) {
+ if ( !$this->autoRotateEnabled() ) {
+ return 0;
+ }
+
+ $data = $file->getMetadata();
+
+ return $this->getRotationForExif( $data );
+ }
+
+ /**
+ * Given a chunk of serialized Exif metadata, return the orientation as
+ * degrees of rotation.
+ *
+ * @param string $data
+ * @return int 0, 90, 180 or 270
+ * @todo FIXME: Orientation can include flipping as well; see if this is an issue!
+ */
+ protected function getRotationForExif( $data ) {
+ if ( !$data ) {
+ return 0;
+ }
+ Wikimedia\suppressWarnings();
+ $data = unserialize( $data );
+ Wikimedia\restoreWarnings();
+ if ( isset( $data['Orientation'] ) ) {
+ # See http://sylvana.net/jpegcrop/exif_orientation.html
+ switch ( $data['Orientation'] ) {
+ case 8:
+ return 90;
+ case 3:
+ return 180;
+ case 6:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ return 0;
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for GIF images.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Handler for GIF images.
- *
- * @ingroup Media
- */
-class GIFHandler extends BitmapHandler {
- const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata.
-
- function getMetadata( $image, $filename ) {
- try {
- $parsedGIFMetadata = BitmapMetadataHandler::GIF( $filename );
- } catch ( Exception $e ) {
- // Broken file?
- wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
-
- return self::BROKEN_FILE;
- }
-
- return serialize( $parsedGIFMetadata );
- }
-
- /**
- * @param File $image
- * @param bool|IContextSource $context Context to use (optional)
- * @return array|bool
- */
- function formatMetadata( $image, $context = false ) {
- $meta = $this->getCommonMetaArray( $image );
- if ( count( $meta ) === 0 ) {
- return false;
- }
-
- return $this->formatMetadataHelper( $meta, $context );
- }
-
- /**
- * Return the standard metadata elements for #filemetadata parser func.
- * @param File $image
- * @return array|bool
- */
- public function getCommonMetaArray( File $image ) {
- $meta = $image->getMetadata();
-
- if ( !$meta ) {
- return [];
- }
- $meta = unserialize( $meta );
- if ( !isset( $meta['metadata'] ) ) {
- return [];
- }
- unset( $meta['metadata']['_MW_GIF_VERSION'] );
-
- return $meta['metadata'];
- }
-
- /**
- * @todo Add unit tests
- *
- * @param File $image
- * @return bool
- */
- function getImageArea( $image ) {
- $ser = $image->getMetadata();
- if ( $ser ) {
- $metadata = unserialize( $ser );
-
- return $image->getWidth() * $image->getHeight() * $metadata['frameCount'];
- } else {
- return $image->getWidth() * $image->getHeight();
- }
- }
-
- /**
- * @param File $image
- * @return bool
- */
- function isAnimatedImage( $image ) {
- $ser = $image->getMetadata();
- if ( $ser ) {
- $metadata = unserialize( $ser );
- if ( $metadata['frameCount'] > 1 ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * We cannot animate thumbnails that are bigger than a particular size
- * @param File $file
- * @return bool
- */
- function canAnimateThumbnail( $file ) {
- global $wgMaxAnimatedGifArea;
- $answer = $this->getImageArea( $file ) <= $wgMaxAnimatedGifArea;
-
- return $answer;
- }
-
- function getMetadataType( $image ) {
- return 'parsed-gif';
- }
-
- function isMetadataValid( $image, $metadata ) {
- if ( $metadata === self::BROKEN_FILE ) {
- // Do not repetitivly regenerate metadata on broken file.
- return self::METADATA_GOOD;
- }
-
- Wikimedia\suppressWarnings();
- $data = unserialize( $metadata );
- Wikimedia\restoreWarnings();
-
- if ( !$data || !is_array( $data ) ) {
- wfDebug( __METHOD__ . " invalid GIF metadata\n" );
-
- return self::METADATA_BAD;
- }
-
- if ( !isset( $data['metadata']['_MW_GIF_VERSION'] )
- || $data['metadata']['_MW_GIF_VERSION'] != GIFMetadataExtractor::VERSION
- ) {
- wfDebug( __METHOD__ . " old but compatible GIF metadata\n" );
-
- return self::METADATA_COMPATIBLE;
- }
-
- return self::METADATA_GOOD;
- }
-
- /**
- * @param File $image
- * @return string
- */
- function getLongDesc( $image ) {
- global $wgLang;
-
- $original = parent::getLongDesc( $image );
-
- Wikimedia\suppressWarnings();
- $metadata = unserialize( $image->getMetadata() );
- Wikimedia\restoreWarnings();
-
- if ( !$metadata || $metadata['frameCount'] <= 1 ) {
- return $original;
- }
-
- /* Preserve original image info string, but strip the last char ')' so we can add even more */
- $info = [];
- $info[] = $original;
-
- if ( $metadata['looped'] ) {
- $info[] = wfMessage( 'file-info-gif-looped' )->parse();
- }
-
- if ( $metadata['frameCount'] > 1 ) {
- $info[] = wfMessage( 'file-info-gif-frames' )->numParams( $metadata['frameCount'] )->parse();
- }
-
- if ( $metadata['duration'] ) {
- $info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
- }
-
- return $wgLang->commaList( $info );
- }
-
- /**
- * Return the duration of the GIF file.
- *
- * Shown in the &query=imageinfo&iiprop=size api query.
- *
- * @param File $file
- * @return float The duration of the file.
- */
- public function getLength( $file ) {
- $serMeta = $file->getMetadata();
- Wikimedia\suppressWarnings();
- $metadata = unserialize( $serMeta );
- Wikimedia\restoreWarnings();
-
- if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
- return 0.0;
- } else {
- return (float)$metadata['duration'];
- }
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for GIF images.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for GIF images.
+ *
+ * @ingroup Media
+ */
+class GIFHandler extends BitmapHandler {
+ const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata.
+
+ function getMetadata( $image, $filename ) {
+ try {
+ $parsedGIFMetadata = BitmapMetadataHandler::GIF( $filename );
+ } catch ( Exception $e ) {
+ // Broken file?
+ wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+
+ return self::BROKEN_FILE;
+ }
+
+ return serialize( $parsedGIFMetadata );
+ }
+
+ /**
+ * @param File $image
+ * @param bool|IContextSource $context Context to use (optional)
+ * @return array|bool
+ */
+ function formatMetadata( $image, $context = false ) {
+ $meta = $this->getCommonMetaArray( $image );
+ if ( count( $meta ) === 0 ) {
+ return false;
+ }
+
+ return $this->formatMetadataHelper( $meta, $context );
+ }
+
+ /**
+ * Return the standard metadata elements for #filemetadata parser func.
+ * @param File $image
+ * @return array|bool
+ */
+ public function getCommonMetaArray( File $image ) {
+ $meta = $image->getMetadata();
+
+ if ( !$meta ) {
+ return [];
+ }
+ $meta = unserialize( $meta );
+ if ( !isset( $meta['metadata'] ) ) {
+ return [];
+ }
+ unset( $meta['metadata']['_MW_GIF_VERSION'] );
+
+ return $meta['metadata'];
+ }
+
+ /**
+ * @todo Add unit tests
+ *
+ * @param File $image
+ * @return bool
+ */
+ function getImageArea( $image ) {
+ $ser = $image->getMetadata();
+ if ( $ser ) {
+ $metadata = unserialize( $ser );
+
+ return $image->getWidth() * $image->getHeight() * $metadata['frameCount'];
+ } else {
+ return $image->getWidth() * $image->getHeight();
+ }
+ }
+
+ /**
+ * @param File $image
+ * @return bool
+ */
+ function isAnimatedImage( $image ) {
+ $ser = $image->getMetadata();
+ if ( $ser ) {
+ $metadata = unserialize( $ser );
+ if ( $metadata['frameCount'] > 1 ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * We cannot animate thumbnails that are bigger than a particular size
+ * @param File $file
+ * @return bool
+ */
+ function canAnimateThumbnail( $file ) {
+ global $wgMaxAnimatedGifArea;
+ $answer = $this->getImageArea( $file ) <= $wgMaxAnimatedGifArea;
+
+ return $answer;
+ }
+
+ function getMetadataType( $image ) {
+ return 'parsed-gif';
+ }
+
+ function isMetadataValid( $image, $metadata ) {
+ if ( $metadata === self::BROKEN_FILE ) {
+ // Do not repetitivly regenerate metadata on broken file.
+ return self::METADATA_GOOD;
+ }
+
+ Wikimedia\suppressWarnings();
+ $data = unserialize( $metadata );
+ Wikimedia\restoreWarnings();
+
+ if ( !$data || !is_array( $data ) ) {
+ wfDebug( __METHOD__ . " invalid GIF metadata\n" );
+
+ return self::METADATA_BAD;
+ }
+
+ if ( !isset( $data['metadata']['_MW_GIF_VERSION'] )
+ || $data['metadata']['_MW_GIF_VERSION'] != GIFMetadataExtractor::VERSION
+ ) {
+ wfDebug( __METHOD__ . " old but compatible GIF metadata\n" );
+
+ return self::METADATA_COMPATIBLE;
+ }
+
+ return self::METADATA_GOOD;
+ }
+
+ /**
+ * @param File $image
+ * @return string
+ */
+ function getLongDesc( $image ) {
+ global $wgLang;
+
+ $original = parent::getLongDesc( $image );
+
+ Wikimedia\suppressWarnings();
+ $metadata = unserialize( $image->getMetadata() );
+ Wikimedia\restoreWarnings();
+
+ if ( !$metadata || $metadata['frameCount'] <= 1 ) {
+ return $original;
+ }
+
+ /* Preserve original image info string, but strip the last char ')' so we can add even more */
+ $info = [];
+ $info[] = $original;
+
+ if ( $metadata['looped'] ) {
+ $info[] = wfMessage( 'file-info-gif-looped' )->parse();
+ }
+
+ if ( $metadata['frameCount'] > 1 ) {
+ $info[] = wfMessage( 'file-info-gif-frames' )->numParams( $metadata['frameCount'] )->parse();
+ }
+
+ if ( $metadata['duration'] ) {
+ $info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
+ }
+
+ return $wgLang->commaList( $info );
+ }
+
+ /**
+ * Return the duration of the GIF file.
+ *
+ * Shown in the &query=imageinfo&iiprop=size api query.
+ *
+ * @param File $file
+ * @return float The duration of the file.
+ */
+ public function getLength( $file ) {
+ $serMeta = $file->getMetadata();
+ Wikimedia\suppressWarnings();
+ $metadata = unserialize( $serMeta );
+ Wikimedia\restoreWarnings();
+
+ if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
+ return 0.0;
+ } else {
+ return (float)$metadata['duration'];
+ }
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for JPEG images.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * JPEG specific handler.
- * Inherits most stuff from BitmapHandler, just here to do the metadata handler differently.
- *
- * Metadata stuff common to Jpeg and built-in Tiff (not PagedTiffHandler) is
- * in ExifBitmapHandler.
- *
- * @ingroup Media
- */
-class JpegHandler extends ExifBitmapHandler {
- const SRGB_EXIF_COLOR_SPACE = 'sRGB';
- const SRGB_ICC_PROFILE_DESCRIPTION = 'sRGB IEC61966-2.1';
-
- function normaliseParams( $image, &$params ) {
- if ( !parent::normaliseParams( $image, $params ) ) {
- return false;
- }
- if ( isset( $params['quality'] ) && !self::validateQuality( $params['quality'] ) ) {
- return false;
- }
- return true;
- }
-
- public function validateParam( $name, $value ) {
- if ( $name === 'quality' ) {
- return self::validateQuality( $value );
- } else {
- return parent::validateParam( $name, $value );
- }
- }
-
- /** Validate and normalize quality value to be between 1 and 100 (inclusive).
- * @param int $value Quality value, will be converted to integer or 0 if invalid
- * @return bool True if the value is valid
- */
- private static function validateQuality( $value ) {
- return $value === 'low';
- }
-
- public function makeParamString( $params ) {
- // Prepend quality as "qValue-". This has to match parseParamString() below
- $res = parent::makeParamString( $params );
- if ( $res && isset( $params['quality'] ) ) {
- $res = "q{$params['quality']}-$res";
- }
- return $res;
- }
-
- public function parseParamString( $str ) {
- // $str contains "qlow-200px" or "200px" strings because thumb.php would strip the filename
- // first - check if the string begins with "qlow-", and if so, treat it as quality.
- // Pass the first portion, or the whole string if "qlow-" not found, to the parent
- // The parsing must match the makeParamString() above
- $res = false;
- $m = false;
- if ( preg_match( '/q([^-]+)-(.*)$/', $str, $m ) ) {
- $v = $m[1];
- if ( self::validateQuality( $v ) ) {
- $res = parent::parseParamString( $m[2] );
- if ( $res ) {
- $res['quality'] = $v;
- }
- }
- } else {
- $res = parent::parseParamString( $str );
- }
- return $res;
- }
-
- function getScriptParams( $params ) {
- $res = parent::getScriptParams( $params );
- if ( isset( $params['quality'] ) ) {
- $res['quality'] = $params['quality'];
- }
- return $res;
- }
-
- function getMetadata( $image, $filename ) {
- try {
- $meta = BitmapMetadataHandler::Jpeg( $filename );
- if ( !is_array( $meta ) ) {
- // This should never happen, but doesn't hurt to be paranoid.
- throw new MWException( 'Metadata array is not an array' );
- }
- $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
-
- return serialize( $meta );
- } catch ( Exception $e ) {
- // BitmapMetadataHandler throws an exception in certain exceptional
- // cases like if file does not exist.
- wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
-
- /* This used to use 0 (ExifBitmapHandler::OLD_BROKEN_FILE) for the cases
- * * No metadata in the file
- * * Something is broken in the file.
- * However, if the metadata support gets expanded then you can't tell if the 0 is from
- * a broken file, or just no props found. A broken file is likely to stay broken, but
- * a file which had no props could have props once the metadata support is improved.
- * Thus switch to using -1 to denote only a broken file, and use an array with only
- * MEDIAWIKI_EXIF_VERSION to denote no props.
- */
-
- return ExifBitmapHandler::BROKEN_FILE;
- }
- }
-
- /**
- * @param File $file
- * @param array $params Rotate parameters.
- * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
- * @since 1.21
- * @return bool|MediaTransformError
- */
- public function rotate( $file, $params ) {
- global $wgJpegTran;
-
- $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
-
- if ( $wgJpegTran && is_executable( $wgJpegTran ) ) {
- $cmd = wfEscapeShellArg( $wgJpegTran ) .
- " -rotate " . wfEscapeShellArg( $rotation ) .
- " -outfile " . wfEscapeShellArg( $params['dstPath'] ) .
- " " . wfEscapeShellArg( $params['srcPath'] );
- wfDebug( __METHOD__ . ": running jpgtran: $cmd\n" );
- $retval = 0;
- $err = wfShellExecWithStderr( $cmd, $retval );
- if ( $retval !== 0 ) {
- $this->logErrorForExternalProcess( $retval, $err, $cmd );
-
- return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
- }
-
- return false;
- } else {
- return parent::rotate( $file, $params );
- }
- }
-
- public function supportsBucketing() {
- return true;
- }
-
- public function sanitizeParamsForBucketing( $params ) {
- $params = parent::sanitizeParamsForBucketing( $params );
-
- // Quality needs to be cleared for bucketing. Buckets need to be default quality
- if ( isset( $params['quality'] ) ) {
- unset( $params['quality'] );
- }
-
- return $params;
- }
-
- /**
- * @inheritDoc
- */
- protected function transformImageMagick( $image, $params ) {
- global $wgUseTinyRGBForJPGThumbnails;
-
- $ret = parent::transformImageMagick( $image, $params );
-
- if ( $ret ) {
- return $ret;
- }
-
- if ( $wgUseTinyRGBForJPGThumbnails ) {
- // T100976 If the profile embedded in the JPG is sRGB, swap it for the smaller
- // (and free) TinyRGB
-
- /**
- * We'll want to replace the color profile for JPGs:
- * * in the sRGB color space, or with the sRGB profile
- * (other profiles will be left untouched)
- * * without color space or profile, in which case browsers
- * should assume sRGB, but don't always do (e.g. on wide-gamut
- * monitors (unless it's meant for low bandwith)
- * @see https://phabricator.wikimedia.org/T134498
- */
- $colorSpaces = [ self::SRGB_EXIF_COLOR_SPACE, '-' ];
- $profiles = [ self::SRGB_ICC_PROFILE_DESCRIPTION ];
-
- // we'll also add TinyRGB profile to images lacking a profile, but
- // only if they're not low quality (which are meant to save bandwith
- // and we don't want to increase the filesize by adding a profile)
- if ( isset( $params['quality'] ) && $params['quality'] > 30 ) {
- $profiles[] = '-';
- }
-
- $this->swapICCProfile(
- $params['dstPath'],
- $colorSpaces,
- $profiles,
- realpath( __DIR__ ) . '/tinyrgb.icc'
- );
- }
-
- return false;
- }
-
- /**
- * Swaps an embedded ICC profile for another, if found.
- * Depends on exiftool, no-op if not installed.
- * @param string $filepath File to be manipulated (will be overwritten)
- * @param array $colorSpaces Only process files with this/these Color Space(s)
- * @param array $oldProfileStrings Exact name(s) of color profile to look for
- * (the one that will be replaced)
- * @param string $profileFilepath ICC profile file to apply to the file
- * @since 1.26
- * @return bool
- */
- public function swapICCProfile( $filepath, array $colorSpaces,
- array $oldProfileStrings, $profileFilepath
- ) {
- global $wgExiftool;
-
- if ( !$wgExiftool || !is_executable( $wgExiftool ) ) {
- return false;
- }
-
- $cmd = wfEscapeShellArg( $wgExiftool,
- '-EXIF:ColorSpace',
- '-ICC_Profile:ProfileDescription',
- '-S',
- '-T',
- $filepath
- );
-
- $output = wfShellExecWithStderr( $cmd, $retval );
-
- // Explode EXIF data into an array with [0 => Color Space, 1 => Device Model Desc]
- $data = explode( "\t", trim( $output ) );
-
- if ( $retval !== 0 ) {
- return false;
- }
-
- // Make a regex out of the source data to match it to an array of color
- // spaces in a case-insensitive way
- $colorSpaceRegex = '/'.preg_quote( $data[0], '/' ).'/i';
- if ( empty( preg_grep( $colorSpaceRegex, $colorSpaces ) ) ) {
- // We can't establish that this file matches the color space, don't process it
- return false;
- }
-
- $profileRegex = '/'.preg_quote( $data[1], '/' ).'/i';
- if ( empty( preg_grep( $profileRegex, $oldProfileStrings ) ) ) {
- // We can't establish that this file has the expected ICC profile, don't process it
- return false;
- }
-
- $cmd = wfEscapeShellArg( $wgExiftool,
- '-overwrite_original',
- '-icc_profile<=' . $profileFilepath,
- $filepath
- );
-
- $output = wfShellExecWithStderr( $cmd, $retval );
-
- if ( $retval !== 0 ) {
- $this->logErrorForExternalProcess( $retval, $output, $cmd );
-
- return false;
- }
-
- return true;
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for JPEG images.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * JPEG specific handler.
+ * Inherits most stuff from BitmapHandler, just here to do the metadata handler differently.
+ *
+ * Metadata stuff common to Jpeg and built-in Tiff (not PagedTiffHandler) is
+ * in ExifBitmapHandler.
+ *
+ * @ingroup Media
+ */
+class JpegHandler extends ExifBitmapHandler {
+ const SRGB_EXIF_COLOR_SPACE = 'sRGB';
+ const SRGB_ICC_PROFILE_DESCRIPTION = 'sRGB IEC61966-2.1';
+
+ function normaliseParams( $image, &$params ) {
+ if ( !parent::normaliseParams( $image, $params ) ) {
+ return false;
+ }
+ if ( isset( $params['quality'] ) && !self::validateQuality( $params['quality'] ) ) {
+ return false;
+ }
+ return true;
+ }
+
+ public function validateParam( $name, $value ) {
+ if ( $name === 'quality' ) {
+ return self::validateQuality( $value );
+ } else {
+ return parent::validateParam( $name, $value );
+ }
+ }
+
+ /** Validate and normalize quality value to be between 1 and 100 (inclusive).
+ * @param int $value Quality value, will be converted to integer or 0 if invalid
+ * @return bool True if the value is valid
+ */
+ private static function validateQuality( $value ) {
+ return $value === 'low';
+ }
+
+ public function makeParamString( $params ) {
+ // Prepend quality as "qValue-". This has to match parseParamString() below
+ $res = parent::makeParamString( $params );
+ if ( $res && isset( $params['quality'] ) ) {
+ $res = "q{$params['quality']}-$res";
+ }
+ return $res;
+ }
+
+ public function parseParamString( $str ) {
+ // $str contains "qlow-200px" or "200px" strings because thumb.php would strip the filename
+ // first - check if the string begins with "qlow-", and if so, treat it as quality.
+ // Pass the first portion, or the whole string if "qlow-" not found, to the parent
+ // The parsing must match the makeParamString() above
+ $res = false;
+ $m = false;
+ if ( preg_match( '/q([^-]+)-(.*)$/', $str, $m ) ) {
+ $v = $m[1];
+ if ( self::validateQuality( $v ) ) {
+ $res = parent::parseParamString( $m[2] );
+ if ( $res ) {
+ $res['quality'] = $v;
+ }
+ }
+ } else {
+ $res = parent::parseParamString( $str );
+ }
+ return $res;
+ }
+
+ function getScriptParams( $params ) {
+ $res = parent::getScriptParams( $params );
+ if ( isset( $params['quality'] ) ) {
+ $res['quality'] = $params['quality'];
+ }
+ return $res;
+ }
+
+ function getMetadata( $image, $filename ) {
+ try {
+ $meta = BitmapMetadataHandler::Jpeg( $filename );
+ if ( !is_array( $meta ) ) {
+ // This should never happen, but doesn't hurt to be paranoid.
+ throw new MWException( 'Metadata array is not an array' );
+ }
+ $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
+
+ return serialize( $meta );
+ } catch ( Exception $e ) {
+ // BitmapMetadataHandler throws an exception in certain exceptional
+ // cases like if file does not exist.
+ wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+
+ /* This used to use 0 (ExifBitmapHandler::OLD_BROKEN_FILE) for the cases
+ * * No metadata in the file
+ * * Something is broken in the file.
+ * However, if the metadata support gets expanded then you can't tell if the 0 is from
+ * a broken file, or just no props found. A broken file is likely to stay broken, but
+ * a file which had no props could have props once the metadata support is improved.
+ * Thus switch to using -1 to denote only a broken file, and use an array with only
+ * MEDIAWIKI_EXIF_VERSION to denote no props.
+ */
+
+ return ExifBitmapHandler::BROKEN_FILE;
+ }
+ }
+
+ /**
+ * @param File $file
+ * @param array $params Rotate parameters.
+ * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
+ * @since 1.21
+ * @return bool|MediaTransformError
+ */
+ public function rotate( $file, $params ) {
+ global $wgJpegTran;
+
+ $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
+
+ if ( $wgJpegTran && is_executable( $wgJpegTran ) ) {
+ $cmd = wfEscapeShellArg( $wgJpegTran ) .
+ " -rotate " . wfEscapeShellArg( $rotation ) .
+ " -outfile " . wfEscapeShellArg( $params['dstPath'] ) .
+ " " . wfEscapeShellArg( $params['srcPath'] );
+ wfDebug( __METHOD__ . ": running jpgtran: $cmd\n" );
+ $retval = 0;
+ $err = wfShellExecWithStderr( $cmd, $retval );
+ if ( $retval !== 0 ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+
+ return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
+ }
+
+ return false;
+ } else {
+ return parent::rotate( $file, $params );
+ }
+ }
+
+ public function supportsBucketing() {
+ return true;
+ }
+
+ public function sanitizeParamsForBucketing( $params ) {
+ $params = parent::sanitizeParamsForBucketing( $params );
+
+ // Quality needs to be cleared for bucketing. Buckets need to be default quality
+ if ( isset( $params['quality'] ) ) {
+ unset( $params['quality'] );
+ }
+
+ return $params;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function transformImageMagick( $image, $params ) {
+ global $wgUseTinyRGBForJPGThumbnails;
+
+ $ret = parent::transformImageMagick( $image, $params );
+
+ if ( $ret ) {
+ return $ret;
+ }
+
+ if ( $wgUseTinyRGBForJPGThumbnails ) {
+ // T100976 If the profile embedded in the JPG is sRGB, swap it for the smaller
+ // (and free) TinyRGB
+
+ /**
+ * We'll want to replace the color profile for JPGs:
+ * * in the sRGB color space, or with the sRGB profile
+ * (other profiles will be left untouched)
+ * * without color space or profile, in which case browsers
+ * should assume sRGB, but don't always do (e.g. on wide-gamut
+ * monitors (unless it's meant for low bandwith)
+ * @see https://phabricator.wikimedia.org/T134498
+ */
+ $colorSpaces = [ self::SRGB_EXIF_COLOR_SPACE, '-' ];
+ $profiles = [ self::SRGB_ICC_PROFILE_DESCRIPTION ];
+
+ // we'll also add TinyRGB profile to images lacking a profile, but
+ // only if they're not low quality (which are meant to save bandwith
+ // and we don't want to increase the filesize by adding a profile)
+ if ( isset( $params['quality'] ) && $params['quality'] > 30 ) {
+ $profiles[] = '-';
+ }
+
+ $this->swapICCProfile(
+ $params['dstPath'],
+ $colorSpaces,
+ $profiles,
+ realpath( __DIR__ ) . '/tinyrgb.icc'
+ );
+ }
+
+ return false;
+ }
+
+ /**
+ * Swaps an embedded ICC profile for another, if found.
+ * Depends on exiftool, no-op if not installed.
+ * @param string $filepath File to be manipulated (will be overwritten)
+ * @param array $colorSpaces Only process files with this/these Color Space(s)
+ * @param array $oldProfileStrings Exact name(s) of color profile to look for
+ * (the one that will be replaced)
+ * @param string $profileFilepath ICC profile file to apply to the file
+ * @since 1.26
+ * @return bool
+ */
+ public function swapICCProfile( $filepath, array $colorSpaces,
+ array $oldProfileStrings, $profileFilepath
+ ) {
+ global $wgExiftool;
+
+ if ( !$wgExiftool || !is_executable( $wgExiftool ) ) {
+ return false;
+ }
+
+ $cmd = wfEscapeShellArg( $wgExiftool,
+ '-EXIF:ColorSpace',
+ '-ICC_Profile:ProfileDescription',
+ '-S',
+ '-T',
+ $filepath
+ );
+
+ $output = wfShellExecWithStderr( $cmd, $retval );
+
+ // Explode EXIF data into an array with [0 => Color Space, 1 => Device Model Desc]
+ $data = explode( "\t", trim( $output ) );
+
+ if ( $retval !== 0 ) {
+ return false;
+ }
+
+ // Make a regex out of the source data to match it to an array of color
+ // spaces in a case-insensitive way
+ $colorSpaceRegex = '/'.preg_quote( $data[0], '/' ).'/i';
+ if ( empty( preg_grep( $colorSpaceRegex, $colorSpaces ) ) ) {
+ // We can't establish that this file matches the color space, don't process it
+ return false;
+ }
+
+ $profileRegex = '/'.preg_quote( $data[1], '/' ).'/i';
+ if ( empty( preg_grep( $profileRegex, $oldProfileStrings ) ) ) {
+ // We can't establish that this file has the expected ICC profile, don't process it
+ return false;
+ }
+
+ $cmd = wfEscapeShellArg( $wgExiftool,
+ '-overwrite_original',
+ '-icc_profile<=' . $profileFilepath,
+ $filepath
+ );
+
+ $output = wfShellExecWithStderr( $cmd, $retval );
+
+ if ( $retval !== 0 ) {
+ $this->logErrorForExternalProcess( $retval, $output, $cmd );
+
+ return false;
+ }
+
+ return true;
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for PNG images.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Handler for PNG images.
- *
- * @ingroup Media
- */
-class PNGHandler extends BitmapHandler {
- const BROKEN_FILE = '0';
-
- /**
- * @param File|FSFile $image
- * @param string $filename
- * @return string
- */
- function getMetadata( $image, $filename ) {
- try {
- $metadata = BitmapMetadataHandler::PNG( $filename );
- } catch ( Exception $e ) {
- // Broken file?
- wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
-
- return self::BROKEN_FILE;
- }
-
- return serialize( $metadata );
- }
-
- /**
- * @param File $image
- * @param bool|IContextSource $context Context to use (optional)
- * @return array|bool
- */
- function formatMetadata( $image, $context = false ) {
- $meta = $this->getCommonMetaArray( $image );
- if ( count( $meta ) === 0 ) {
- return false;
- }
-
- return $this->formatMetadataHelper( $meta, $context );
- }
-
- /**
- * Get a file type independent array of metadata.
- *
- * @param File $image
- * @return array The metadata array
- */
- public function getCommonMetaArray( File $image ) {
- $meta = $image->getMetadata();
-
- if ( !$meta ) {
- return [];
- }
- $meta = unserialize( $meta );
- if ( !isset( $meta['metadata'] ) ) {
- return [];
- }
- unset( $meta['metadata']['_MW_PNG_VERSION'] );
-
- return $meta['metadata'];
- }
-
- /**
- * @param File $image
- * @return bool
- */
- function isAnimatedImage( $image ) {
- $ser = $image->getMetadata();
- if ( $ser ) {
- $metadata = unserialize( $ser );
- if ( $metadata['frameCount'] > 1 ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * We do not support making APNG thumbnails, so always false
- * @param File $image
- * @return bool False
- */
- function canAnimateThumbnail( $image ) {
- return false;
- }
-
- function getMetadataType( $image ) {
- return 'parsed-png';
- }
-
- function isMetadataValid( $image, $metadata ) {
- if ( $metadata === self::BROKEN_FILE ) {
- // Do not repetitivly regenerate metadata on broken file.
- return self::METADATA_GOOD;
- }
-
- Wikimedia\suppressWarnings();
- $data = unserialize( $metadata );
- Wikimedia\restoreWarnings();
-
- if ( !$data || !is_array( $data ) ) {
- wfDebug( __METHOD__ . " invalid png metadata\n" );
-
- return self::METADATA_BAD;
- }
-
- if ( !isset( $data['metadata']['_MW_PNG_VERSION'] )
- || $data['metadata']['_MW_PNG_VERSION'] != PNGMetadataExtractor::VERSION
- ) {
- wfDebug( __METHOD__ . " old but compatible png metadata\n" );
-
- return self::METADATA_COMPATIBLE;
- }
-
- return self::METADATA_GOOD;
- }
-
- /**
- * @param File $image
- * @return string
- */
- function getLongDesc( $image ) {
- global $wgLang;
- $original = parent::getLongDesc( $image );
-
- Wikimedia\suppressWarnings();
- $metadata = unserialize( $image->getMetadata() );
- Wikimedia\restoreWarnings();
-
- if ( !$metadata || $metadata['frameCount'] <= 0 ) {
- return $original;
- }
-
- $info = [];
- $info[] = $original;
-
- if ( $metadata['loopCount'] == 0 ) {
- $info[] = wfMessage( 'file-info-png-looped' )->parse();
- } elseif ( $metadata['loopCount'] > 1 ) {
- $info[] = wfMessage( 'file-info-png-repeat' )->numParams( $metadata['loopCount'] )->parse();
- }
-
- if ( $metadata['frameCount'] > 0 ) {
- $info[] = wfMessage( 'file-info-png-frames' )->numParams( $metadata['frameCount'] )->parse();
- }
-
- if ( $metadata['duration'] ) {
- $info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
- }
-
- return $wgLang->commaList( $info );
- }
-
- /**
- * Return the duration of an APNG file.
- *
- * Shown in the &query=imageinfo&iiprop=size api query.
- *
- * @param File $file
- * @return float The duration of the file.
- */
- public function getLength( $file ) {
- $serMeta = $file->getMetadata();
- Wikimedia\suppressWarnings();
- $metadata = unserialize( $serMeta );
- Wikimedia\restoreWarnings();
-
- if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
- return 0.0;
- } else {
- return (float)$metadata['duration'];
- }
- }
-
- // PNGs should be easy to support, but it will need some sharpening applied
- // and another user test to check if the perceived quality change is noticeable
- public function supportsBucketing() {
- return false;
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for PNG images.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for PNG images.
+ *
+ * @ingroup Media
+ */
+class PNGHandler extends BitmapHandler {
+ const BROKEN_FILE = '0';
+
+ /**
+ * @param File|FSFile $image
+ * @param string $filename
+ * @return string
+ */
+ function getMetadata( $image, $filename ) {
+ try {
+ $metadata = BitmapMetadataHandler::PNG( $filename );
+ } catch ( Exception $e ) {
+ // Broken file?
+ wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+
+ return self::BROKEN_FILE;
+ }
+
+ return serialize( $metadata );
+ }
+
+ /**
+ * @param File $image
+ * @param bool|IContextSource $context Context to use (optional)
+ * @return array|bool
+ */
+ function formatMetadata( $image, $context = false ) {
+ $meta = $this->getCommonMetaArray( $image );
+ if ( count( $meta ) === 0 ) {
+ return false;
+ }
+
+ return $this->formatMetadataHelper( $meta, $context );
+ }
+
+ /**
+ * Get a file type independent array of metadata.
+ *
+ * @param File $image
+ * @return array The metadata array
+ */
+ public function getCommonMetaArray( File $image ) {
+ $meta = $image->getMetadata();
+
+ if ( !$meta ) {
+ return [];
+ }
+ $meta = unserialize( $meta );
+ if ( !isset( $meta['metadata'] ) ) {
+ return [];
+ }
+ unset( $meta['metadata']['_MW_PNG_VERSION'] );
+
+ return $meta['metadata'];
+ }
+
+ /**
+ * @param File $image
+ * @return bool
+ */
+ function isAnimatedImage( $image ) {
+ $ser = $image->getMetadata();
+ if ( $ser ) {
+ $metadata = unserialize( $ser );
+ if ( $metadata['frameCount'] > 1 ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * We do not support making APNG thumbnails, so always false
+ * @param File $image
+ * @return bool False
+ */
+ function canAnimateThumbnail( $image ) {
+ return false;
+ }
+
+ function getMetadataType( $image ) {
+ return 'parsed-png';
+ }
+
+ function isMetadataValid( $image, $metadata ) {
+ if ( $metadata === self::BROKEN_FILE ) {
+ // Do not repetitivly regenerate metadata on broken file.
+ return self::METADATA_GOOD;
+ }
+
+ Wikimedia\suppressWarnings();
+ $data = unserialize( $metadata );
+ Wikimedia\restoreWarnings();
+
+ if ( !$data || !is_array( $data ) ) {
+ wfDebug( __METHOD__ . " invalid png metadata\n" );
+
+ return self::METADATA_BAD;
+ }
+
+ if ( !isset( $data['metadata']['_MW_PNG_VERSION'] )
+ || $data['metadata']['_MW_PNG_VERSION'] != PNGMetadataExtractor::VERSION
+ ) {
+ wfDebug( __METHOD__ . " old but compatible png metadata\n" );
+
+ return self::METADATA_COMPATIBLE;
+ }
+
+ return self::METADATA_GOOD;
+ }
+
+ /**
+ * @param File $image
+ * @return string
+ */
+ function getLongDesc( $image ) {
+ global $wgLang;
+ $original = parent::getLongDesc( $image );
+
+ Wikimedia\suppressWarnings();
+ $metadata = unserialize( $image->getMetadata() );
+ Wikimedia\restoreWarnings();
+
+ if ( !$metadata || $metadata['frameCount'] <= 0 ) {
+ return $original;
+ }
+
+ $info = [];
+ $info[] = $original;
+
+ if ( $metadata['loopCount'] == 0 ) {
+ $info[] = wfMessage( 'file-info-png-looped' )->parse();
+ } elseif ( $metadata['loopCount'] > 1 ) {
+ $info[] = wfMessage( 'file-info-png-repeat' )->numParams( $metadata['loopCount'] )->parse();
+ }
+
+ if ( $metadata['frameCount'] > 0 ) {
+ $info[] = wfMessage( 'file-info-png-frames' )->numParams( $metadata['frameCount'] )->parse();
+ }
+
+ if ( $metadata['duration'] ) {
+ $info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
+ }
+
+ return $wgLang->commaList( $info );
+ }
+
+ /**
+ * Return the duration of an APNG file.
+ *
+ * Shown in the &query=imageinfo&iiprop=size api query.
+ *
+ * @param File $file
+ * @return float The duration of the file.
+ */
+ public function getLength( $file ) {
+ $serMeta = $file->getMetadata();
+ Wikimedia\suppressWarnings();
+ $metadata = unserialize( $serMeta );
+ Wikimedia\restoreWarnings();
+
+ if ( !$metadata || !isset( $metadata['duration'] ) || !$metadata['duration'] ) {
+ return 0.0;
+ } else {
+ return (float)$metadata['duration'];
+ }
+ }
+
+ // PNGs should be easy to support, but it will need some sharpening applied
+ // and another user test to check if the perceived quality change is noticeable
+ public function supportsBucketing() {
+ return false;
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for SVG images.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-use Wikimedia\ScopedCallback;
-
-/**
- * Handler for SVG images.
- *
- * @ingroup Media
- */
-class SvgHandler extends ImageHandler {
- const SVG_METADATA_VERSION = 2;
-
- /** @var array A list of metadata tags that can be converted
- * to the commonly used exif tags. This allows messages
- * to be reused, and consistent tag names for {{#formatmetadata:..}}
- */
- private static $metaConversion = [
- 'originalwidth' => 'ImageWidth',
- 'originalheight' => 'ImageLength',
- 'description' => 'ImageDescription',
- 'title' => 'ObjectName',
- ];
-
- function isEnabled() {
- global $wgSVGConverters, $wgSVGConverter;
- if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) {
- wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" );
-
- return false;
- } else {
- return true;
- }
- }
-
- public function mustRender( $file ) {
- return true;
- }
-
- function isVectorized( $file ) {
- return true;
- }
-
- /**
- * @param File $file
- * @return bool
- */
- function isAnimatedImage( $file ) {
- # @todo Detect animated SVGs
- $metadata = $file->getMetadata();
- if ( $metadata ) {
- $metadata = $this->unpackMetadata( $metadata );
- if ( isset( $metadata['animated'] ) ) {
- return $metadata['animated'];
- }
- }
-
- return false;
- }
-
- /**
- * Which languages (systemLanguage attribute) is supported.
- *
- * @note This list is not guaranteed to be exhaustive.
- * To avoid OOM errors, we only look at first bit of a file.
- * Thus all languages on this list are present in the file,
- * but its possible for the file to have a language not on
- * this list.
- *
- * @param File $file
- * @return array Array of language codes, or empty if no language switching supported.
- */
- public function getAvailableLanguages( File $file ) {
- $metadata = $file->getMetadata();
- $langList = [];
- if ( $metadata ) {
- $metadata = $this->unpackMetadata( $metadata );
- if ( isset( $metadata['translations'] ) ) {
- foreach ( $metadata['translations'] as $lang => $langType ) {
- if ( $langType === SVGReader::LANG_FULL_MATCH ) {
- $langList[] = strtolower( $lang );
- }
- }
- }
- }
- return array_unique( $langList );
- }
-
- /**
- * SVG's systemLanguage matching rules state:
- * 'The `systemLanguage` attribute ... [e]valuates to "true" if one of the languages indicated
- * by user preferences exactly equals one of the languages given in the value of this parameter,
- * or if one of the languages indicated by user preferences exactly equals a prefix of one of
- * the languages given in the value of this parameter such that the first tag character
- * following the prefix is "-".'
- *
- * Return the first element of $svgLanguages that matches $userPreferredLanguage
- *
- * @see https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
- * @param string $userPreferredLanguage
- * @param array $svgLanguages
- * @return string|null
- */
- public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
- foreach ( $svgLanguages as $svgLang ) {
- if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
- return $svgLang;
- }
- $trimmedSvgLang = $svgLang;
- while ( strpos( $trimmedSvgLang, '-' ) !== false ) {
- $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
- if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
- return $svgLang;
- }
- }
- }
- return null;
- }
-
- /**
- * What language to render file in if none selected
- *
- * @param File $file Language code
- * @return string
- */
- public function getDefaultRenderLanguage( File $file ) {
- return 'en';
- }
-
- /**
- * We do not support making animated svg thumbnails
- * @param File $file
- * @return bool
- */
- function canAnimateThumbnail( $file ) {
- return false;
- }
-
- /**
- * @param File $image
- * @param array &$params
- * @return bool
- */
- function normaliseParams( $image, &$params ) {
- global $wgSVGMaxSize;
- if ( !parent::normaliseParams( $image, $params ) ) {
- return false;
- }
- # Don't make an image bigger than wgMaxSVGSize on the smaller side
- if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
- if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
- $srcWidth = $image->getWidth( $params['page'] );
- $srcHeight = $image->getHeight( $params['page'] );
- $params['physicalWidth'] = $wgSVGMaxSize;
- $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
- }
- } else {
- if ( $params['physicalHeight'] > $wgSVGMaxSize ) {
- $srcWidth = $image->getWidth( $params['page'] );
- $srcHeight = $image->getHeight( $params['page'] );
- $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $wgSVGMaxSize );
- $params['physicalHeight'] = $wgSVGMaxSize;
- }
- }
-
- return true;
- }
-
- /**
- * @param File $image
- * @param string $dstPath
- * @param string $dstUrl
- * @param array $params
- * @param int $flags
- * @return bool|MediaTransformError|ThumbnailImage|TransformParameterError
- */
- function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- if ( !$this->normaliseParams( $image, $params ) ) {
- return new TransformParameterError( $params );
- }
- $clientWidth = $params['width'];
- $clientHeight = $params['height'];
- $physicalWidth = $params['physicalWidth'];
- $physicalHeight = $params['physicalHeight'];
- $lang = isset( $params['lang'] ) ? $params['lang'] : $this->getDefaultRenderLanguage( $image );
-
- if ( $flags & self::TRANSFORM_LATER ) {
- return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
- }
-
- $metadata = $this->unpackMetadata( $image->getMetadata() );
- if ( isset( $metadata['error'] ) ) { // sanity check
- $err = wfMessage( 'svg-long-error', $metadata['error']['message'] );
-
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
- }
-
- if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
- wfMessage( 'thumbnail_dest_directory' ) );
- }
-
- $srcPath = $image->getLocalRefPath();
- if ( $srcPath === false ) { // Failed to get local copy
- wfDebugLog( 'thumbnail',
- sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
- wfHostname(), $image->getName() ) );
-
- return new MediaTransformError( 'thumbnail_error',
- $params['width'], $params['height'],
- wfMessage( 'filemissing' )
- );
- }
-
- // Make a temp dir with a symlink to the local copy in it.
- // This plays well with rsvg-convert policy for external entities.
- // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
- $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
- $lnPath = "$tmpDir/" . basename( $srcPath );
- $ok = mkdir( $tmpDir, 0771 );
- if ( !$ok ) {
- wfDebugLog( 'thumbnail',
- sprintf( 'Thumbnail failed on %s: could not create temporary directory %s',
- wfHostname(), $tmpDir ) );
- return new MediaTransformError( 'thumbnail_error',
- $params['width'], $params['height'],
- wfMessage( 'thumbnail-temp-create' )->text()
- );
- }
- $ok = symlink( $srcPath, $lnPath );
- /** @noinspection PhpUnusedLocalVariableInspection */
- $cleaner = new ScopedCallback( function () use ( $tmpDir, $lnPath ) {
- Wikimedia\suppressWarnings();
- unlink( $lnPath );
- rmdir( $tmpDir );
- Wikimedia\restoreWarnings();
- } );
- if ( !$ok ) {
- wfDebugLog( 'thumbnail',
- sprintf( 'Thumbnail failed on %s: could not link %s to %s',
- wfHostname(), $lnPath, $srcPath ) );
- return new MediaTransformError( 'thumbnail_error',
- $params['width'], $params['height'],
- wfMessage( 'thumbnail-temp-create' )
- );
- }
-
- $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
- if ( $status === true ) {
- return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
- } else {
- return $status; // MediaTransformError
- }
- }
-
- /**
- * Transform an SVG file to PNG
- * This function can be called outside of thumbnail contexts
- * @param string $srcPath
- * @param string $dstPath
- * @param string $width
- * @param string $height
- * @param bool|string $lang Language code of the language to render the SVG in
- * @throws MWException
- * @return bool|MediaTransformError
- */
- public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
- global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
- $err = false;
- $retval = '';
- if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
- if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
- // This is a PHP callable
- $func = $wgSVGConverters[$wgSVGConverter][0];
- $args = array_merge( [ $srcPath, $dstPath, $width, $height, $lang ],
- array_slice( $wgSVGConverters[$wgSVGConverter], 1 ) );
- if ( !is_callable( $func ) ) {
- throw new MWException( "$func is not callable" );
- }
- $err = call_user_func_array( $func, $args );
- $retval = (bool)$err;
- } else {
- // External command
- $cmd = str_replace(
- [ '$path/', '$width', '$height', '$input', '$output' ],
- [ $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "",
- intval( $width ),
- intval( $height ),
- wfEscapeShellArg( $srcPath ),
- wfEscapeShellArg( $dstPath ) ],
- $wgSVGConverters[$wgSVGConverter]
- );
-
- $env = [];
- if ( $lang !== false ) {
- $env['LANG'] = $lang;
- }
-
- wfDebug( __METHOD__ . ": $cmd\n" );
- $err = wfShellExecWithStderr( $cmd, $retval, $env );
- }
- }
- $removed = $this->removeBadFile( $dstPath, $retval );
- if ( $retval != 0 || $removed ) {
- $this->logErrorForExternalProcess( $retval, $err, $cmd );
- return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
- }
-
- return true;
- }
-
- public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
- $im = new Imagick( $srcPath );
- $im->setImageFormat( 'png' );
- $im->setBackgroundColor( 'transparent' );
- $im->setImageDepth( 8 );
-
- if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
- return 'Could not resize image';
- }
- if ( !$im->writeImage( $dstPath ) ) {
- return "Could not write to $dstPath";
- }
- }
-
- /**
- * @param File|FSFile $file
- * @param string $path Unused
- * @param bool|array $metadata
- * @return array
- */
- function getImageSize( $file, $path, $metadata = false ) {
- if ( $metadata === false && $file instanceof File ) {
- $metadata = $file->getMetadata();
- }
- $metadata = $this->unpackMetadata( $metadata );
-
- if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
- return [ $metadata['width'], $metadata['height'], 'SVG',
- "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" ];
- } else { // error
- return [ 0, 0, 'SVG', "width=\"0\" height=\"0\"" ];
- }
- }
-
- function getThumbType( $ext, $mime, $params = null ) {
- return [ 'png', 'image/png' ];
- }
-
- /**
- * Subtitle for the image. Different from the base
- * class so it can be denoted that SVG's have
- * a "nominal" resolution, and not a fixed one,
- * as well as so animation can be denoted.
- *
- * @param File $file
- * @return string
- */
- function getLongDesc( $file ) {
- global $wgLang;
-
- $metadata = $this->unpackMetadata( $file->getMetadata() );
- if ( isset( $metadata['error'] ) ) {
- return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
- }
-
- $size = $wgLang->formatSize( $file->getSize() );
-
- if ( $this->isAnimatedImage( $file ) ) {
- $msg = wfMessage( 'svg-long-desc-animated' );
- } else {
- $msg = wfMessage( 'svg-long-desc' );
- }
-
- $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
-
- return $msg->parse();
- }
-
- /**
- * @param File|FSFile $file
- * @param string $filename
- * @return string Serialised metadata
- */
- function getMetadata( $file, $filename ) {
- $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
- try {
- $metadata += SVGMetadataExtractor::getMetadata( $filename );
- } catch ( Exception $e ) { // @todo SVG specific exceptions
- // File not found, broken, etc.
- $metadata['error'] = [
- 'message' => $e->getMessage(),
- 'code' => $e->getCode()
- ];
- wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
- }
-
- return serialize( $metadata );
- }
-
- function unpackMetadata( $metadata ) {
- Wikimedia\suppressWarnings();
- $unser = unserialize( $metadata );
- Wikimedia\restoreWarnings();
- if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
- return $unser;
- } else {
- return false;
- }
- }
-
- function getMetadataType( $image ) {
- return 'parsed-svg';
- }
-
- function isMetadataValid( $image, $metadata ) {
- $meta = $this->unpackMetadata( $metadata );
- if ( $meta === false ) {
- return self::METADATA_BAD;
- }
- if ( !isset( $meta['originalWidth'] ) ) {
- // Old but compatible
- return self::METADATA_COMPATIBLE;
- }
-
- return self::METADATA_GOOD;
- }
-
- protected function visibleMetadataFields() {
- $fields = [ 'objectname', 'imagedescription' ];
-
- return $fields;
- }
-
- /**
- * @param File $file
- * @param bool|IContextSource $context Context to use (optional)
- * @return array|bool
- */
- function formatMetadata( $file, $context = false ) {
- $result = [
- 'visible' => [],
- 'collapsed' => []
- ];
- $metadata = $file->getMetadata();
- if ( !$metadata ) {
- return false;
- }
- $metadata = $this->unpackMetadata( $metadata );
- if ( !$metadata || isset( $metadata['error'] ) ) {
- return false;
- }
-
- /* @todo Add a formatter
- $format = new FormatSVG( $metadata );
- $formatted = $format->getFormattedData();
- */
-
- // Sort fields into visible and collapsed
- $visibleFields = $this->visibleMetadataFields();
-
- $showMeta = false;
- foreach ( $metadata as $name => $value ) {
- $tag = strtolower( $name );
- if ( isset( self::$metaConversion[$tag] ) ) {
- $tag = strtolower( self::$metaConversion[$tag] );
- } else {
- // Do not output other metadata not in list
- continue;
- }
- $showMeta = true;
- self::addMeta( $result,
- in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
- 'exif',
- $tag,
- $value
- );
- }
-
- return $showMeta ? $result : false;
- }
-
- /**
- * @param string $name Parameter name
- * @param mixed $value Parameter value
- * @return bool Validity
- */
- public function validateParam( $name, $value ) {
- if ( in_array( $name, [ 'width', 'height' ] ) ) {
- // Reject negative heights, widths
- return ( $value > 0 );
- } elseif ( $name == 'lang' ) {
- // Validate $code
- if ( $value === '' || !Language::isValidCode( $value ) ) {
- return false;
- }
-
- return true;
- }
-
- // Only lang, width and height are acceptable keys
- return false;
- }
-
- /**
- * @param array $params Name=>value pairs of parameters
- * @return string Filename to use
- */
- public function makeParamString( $params ) {
- $lang = '';
- if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
- $lang = 'lang' . strtolower( $params['lang'] ) . '-';
- }
- if ( !isset( $params['width'] ) ) {
- return false;
- }
-
- return "$lang{$params['width']}px";
- }
-
- public function parseParamString( $str ) {
- $m = false;
- if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $str, $m ) ) {
- return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
- } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
- return [ 'width' => $m[1], 'lang' => 'en' ];
- } else {
- return false;
- }
- }
-
- public function getParamMap() {
- return [ 'img_lang' => 'lang', 'img_width' => 'width' ];
- }
-
- /**
- * @param array $params
- * @return array
- */
- function getScriptParams( $params ) {
- $scriptParams = [ 'width' => $params['width'] ];
- if ( isset( $params['lang'] ) ) {
- $scriptParams['lang'] = $params['lang'];
- }
-
- return $scriptParams;
- }
-
- public function getCommonMetaArray( File $file ) {
- $metadata = $file->getMetadata();
- if ( !$metadata ) {
- return [];
- }
- $metadata = $this->unpackMetadata( $metadata );
- if ( !$metadata || isset( $metadata['error'] ) ) {
- return [];
- }
- $stdMetadata = [];
- foreach ( $metadata as $name => $value ) {
- $tag = strtolower( $name );
- if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
- // Skip these. In the exif metadata stuff, it is assumed these
- // are measured in px, which is not the case here.
- continue;
- }
- if ( isset( self::$metaConversion[$tag] ) ) {
- $tag = self::$metaConversion[$tag];
- $stdMetadata[$tag] = $value;
- }
- }
-
- return $stdMetadata;
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for SVG images.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+use Wikimedia\ScopedCallback;
+
+/**
+ * Handler for SVG images.
+ *
+ * @ingroup Media
+ */
+class SvgHandler extends ImageHandler {
+ const SVG_METADATA_VERSION = 2;
+
+ /** @var array A list of metadata tags that can be converted
+ * to the commonly used exif tags. This allows messages
+ * to be reused, and consistent tag names for {{#formatmetadata:..}}
+ */
+ private static $metaConversion = [
+ 'originalwidth' => 'ImageWidth',
+ 'originalheight' => 'ImageLength',
+ 'description' => 'ImageDescription',
+ 'title' => 'ObjectName',
+ ];
+
+ function isEnabled() {
+ global $wgSVGConverters, $wgSVGConverter;
+ if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) {
+ wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" );
+
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public function mustRender( $file ) {
+ return true;
+ }
+
+ function isVectorized( $file ) {
+ return true;
+ }
+
+ /**
+ * @param File $file
+ * @return bool
+ */
+ function isAnimatedImage( $file ) {
+ # @todo Detect animated SVGs
+ $metadata = $file->getMetadata();
+ if ( $metadata ) {
+ $metadata = $this->unpackMetadata( $metadata );
+ if ( isset( $metadata['animated'] ) ) {
+ return $metadata['animated'];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Which languages (systemLanguage attribute) is supported.
+ *
+ * @note This list is not guaranteed to be exhaustive.
+ * To avoid OOM errors, we only look at first bit of a file.
+ * Thus all languages on this list are present in the file,
+ * but its possible for the file to have a language not on
+ * this list.
+ *
+ * @param File $file
+ * @return array Array of language codes, or empty if no language switching supported.
+ */
+ public function getAvailableLanguages( File $file ) {
+ $metadata = $file->getMetadata();
+ $langList = [];
+ if ( $metadata ) {
+ $metadata = $this->unpackMetadata( $metadata );
+ if ( isset( $metadata['translations'] ) ) {
+ foreach ( $metadata['translations'] as $lang => $langType ) {
+ if ( $langType === SVGReader::LANG_FULL_MATCH ) {
+ $langList[] = strtolower( $lang );
+ }
+ }
+ }
+ }
+ return array_unique( $langList );
+ }
+
+ /**
+ * SVG's systemLanguage matching rules state:
+ * 'The `systemLanguage` attribute ... [e]valuates to "true" if one of the languages indicated
+ * by user preferences exactly equals one of the languages given in the value of this parameter,
+ * or if one of the languages indicated by user preferences exactly equals a prefix of one of
+ * the languages given in the value of this parameter such that the first tag character
+ * following the prefix is "-".'
+ *
+ * Return the first element of $svgLanguages that matches $userPreferredLanguage
+ *
+ * @see https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
+ * @param string $userPreferredLanguage
+ * @param array $svgLanguages
+ * @return string|null
+ */
+ public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
+ foreach ( $svgLanguages as $svgLang ) {
+ if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
+ return $svgLang;
+ }
+ $trimmedSvgLang = $svgLang;
+ while ( strpos( $trimmedSvgLang, '-' ) !== false ) {
+ $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
+ if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
+ return $svgLang;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * What language to render file in if none selected
+ *
+ * @param File $file Language code
+ * @return string
+ */
+ public function getDefaultRenderLanguage( File $file ) {
+ return 'en';
+ }
+
+ /**
+ * We do not support making animated svg thumbnails
+ * @param File $file
+ * @return bool
+ */
+ function canAnimateThumbnail( $file ) {
+ return false;
+ }
+
+ /**
+ * @param File $image
+ * @param array &$params
+ * @return bool
+ */
+ function normaliseParams( $image, &$params ) {
+ global $wgSVGMaxSize;
+ if ( !parent::normaliseParams( $image, $params ) ) {
+ return false;
+ }
+ # Don't make an image bigger than wgMaxSVGSize on the smaller side
+ if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
+ if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
+ $srcWidth = $image->getWidth( $params['page'] );
+ $srcHeight = $image->getHeight( $params['page'] );
+ $params['physicalWidth'] = $wgSVGMaxSize;
+ $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
+ }
+ } else {
+ if ( $params['physicalHeight'] > $wgSVGMaxSize ) {
+ $srcWidth = $image->getWidth( $params['page'] );
+ $srcHeight = $image->getHeight( $params['page'] );
+ $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $wgSVGMaxSize );
+ $params['physicalHeight'] = $wgSVGMaxSize;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param File $image
+ * @param string $dstPath
+ * @param string $dstUrl
+ * @param array $params
+ * @param int $flags
+ * @return bool|MediaTransformError|ThumbnailImage|TransformParameterError
+ */
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ if ( !$this->normaliseParams( $image, $params ) ) {
+ return new TransformParameterError( $params );
+ }
+ $clientWidth = $params['width'];
+ $clientHeight = $params['height'];
+ $physicalWidth = $params['physicalWidth'];
+ $physicalHeight = $params['physicalHeight'];
+ $lang = isset( $params['lang'] ) ? $params['lang'] : $this->getDefaultRenderLanguage( $image );
+
+ if ( $flags & self::TRANSFORM_LATER ) {
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
+ }
+
+ $metadata = $this->unpackMetadata( $image->getMetadata() );
+ if ( isset( $metadata['error'] ) ) { // sanity check
+ $err = wfMessage( 'svg-long-error', $metadata['error']['message'] );
+
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
+ }
+
+ if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
+ wfMessage( 'thumbnail_dest_directory' ) );
+ }
+
+ $srcPath = $image->getLocalRefPath();
+ if ( $srcPath === false ) { // Failed to get local copy
+ wfDebugLog( 'thumbnail',
+ sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
+ wfHostname(), $image->getName() ) );
+
+ return new MediaTransformError( 'thumbnail_error',
+ $params['width'], $params['height'],
+ wfMessage( 'filemissing' )
+ );
+ }
+
+ // Make a temp dir with a symlink to the local copy in it.
+ // This plays well with rsvg-convert policy for external entities.
+ // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
+ $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
+ $lnPath = "$tmpDir/" . basename( $srcPath );
+ $ok = mkdir( $tmpDir, 0771 );
+ if ( !$ok ) {
+ wfDebugLog( 'thumbnail',
+ sprintf( 'Thumbnail failed on %s: could not create temporary directory %s',
+ wfHostname(), $tmpDir ) );
+ return new MediaTransformError( 'thumbnail_error',
+ $params['width'], $params['height'],
+ wfMessage( 'thumbnail-temp-create' )->text()
+ );
+ }
+ $ok = symlink( $srcPath, $lnPath );
+ /** @noinspection PhpUnusedLocalVariableInspection */
+ $cleaner = new ScopedCallback( function () use ( $tmpDir, $lnPath ) {
+ Wikimedia\suppressWarnings();
+ unlink( $lnPath );
+ rmdir( $tmpDir );
+ Wikimedia\restoreWarnings();
+ } );
+ if ( !$ok ) {
+ wfDebugLog( 'thumbnail',
+ sprintf( 'Thumbnail failed on %s: could not link %s to %s',
+ wfHostname(), $lnPath, $srcPath ) );
+ return new MediaTransformError( 'thumbnail_error',
+ $params['width'], $params['height'],
+ wfMessage( 'thumbnail-temp-create' )
+ );
+ }
+
+ $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
+ if ( $status === true ) {
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
+ } else {
+ return $status; // MediaTransformError
+ }
+ }
+
+ /**
+ * Transform an SVG file to PNG
+ * This function can be called outside of thumbnail contexts
+ * @param string $srcPath
+ * @param string $dstPath
+ * @param string $width
+ * @param string $height
+ * @param bool|string $lang Language code of the language to render the SVG in
+ * @throws MWException
+ * @return bool|MediaTransformError
+ */
+ public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
+ global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
+ $err = false;
+ $retval = '';
+ if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
+ if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
+ // This is a PHP callable
+ $func = $wgSVGConverters[$wgSVGConverter][0];
+ $args = array_merge( [ $srcPath, $dstPath, $width, $height, $lang ],
+ array_slice( $wgSVGConverters[$wgSVGConverter], 1 ) );
+ if ( !is_callable( $func ) ) {
+ throw new MWException( "$func is not callable" );
+ }
+ $err = call_user_func_array( $func, $args );
+ $retval = (bool)$err;
+ } else {
+ // External command
+ $cmd = str_replace(
+ [ '$path/', '$width', '$height', '$input', '$output' ],
+ [ $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "",
+ intval( $width ),
+ intval( $height ),
+ wfEscapeShellArg( $srcPath ),
+ wfEscapeShellArg( $dstPath ) ],
+ $wgSVGConverters[$wgSVGConverter]
+ );
+
+ $env = [];
+ if ( $lang !== false ) {
+ $env['LANG'] = $lang;
+ }
+
+ wfDebug( __METHOD__ . ": $cmd\n" );
+ $err = wfShellExecWithStderr( $cmd, $retval, $env );
+ }
+ }
+ $removed = $this->removeBadFile( $dstPath, $retval );
+ if ( $retval != 0 || $removed ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+ return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
+ }
+
+ return true;
+ }
+
+ public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
+ $im = new Imagick( $srcPath );
+ $im->setImageFormat( 'png' );
+ $im->setBackgroundColor( 'transparent' );
+ $im->setImageDepth( 8 );
+
+ if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
+ return 'Could not resize image';
+ }
+ if ( !$im->writeImage( $dstPath ) ) {
+ return "Could not write to $dstPath";
+ }
+ }
+
+ /**
+ * @param File|FSFile $file
+ * @param string $path Unused
+ * @param bool|array $metadata
+ * @return array
+ */
+ function getImageSize( $file, $path, $metadata = false ) {
+ if ( $metadata === false && $file instanceof File ) {
+ $metadata = $file->getMetadata();
+ }
+ $metadata = $this->unpackMetadata( $metadata );
+
+ if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
+ return [ $metadata['width'], $metadata['height'], 'SVG',
+ "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" ];
+ } else { // error
+ return [ 0, 0, 'SVG', "width=\"0\" height=\"0\"" ];
+ }
+ }
+
+ function getThumbType( $ext, $mime, $params = null ) {
+ return [ 'png', 'image/png' ];
+ }
+
+ /**
+ * Subtitle for the image. Different from the base
+ * class so it can be denoted that SVG's have
+ * a "nominal" resolution, and not a fixed one,
+ * as well as so animation can be denoted.
+ *
+ * @param File $file
+ * @return string
+ */
+ function getLongDesc( $file ) {
+ global $wgLang;
+
+ $metadata = $this->unpackMetadata( $file->getMetadata() );
+ if ( isset( $metadata['error'] ) ) {
+ return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
+ }
+
+ $size = $wgLang->formatSize( $file->getSize() );
+
+ if ( $this->isAnimatedImage( $file ) ) {
+ $msg = wfMessage( 'svg-long-desc-animated' );
+ } else {
+ $msg = wfMessage( 'svg-long-desc' );
+ }
+
+ $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
+
+ return $msg->parse();
+ }
+
+ /**
+ * @param File|FSFile $file
+ * @param string $filename
+ * @return string Serialised metadata
+ */
+ function getMetadata( $file, $filename ) {
+ $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
+ try {
+ $metadata += SVGMetadataExtractor::getMetadata( $filename );
+ } catch ( Exception $e ) { // @todo SVG specific exceptions
+ // File not found, broken, etc.
+ $metadata['error'] = [
+ 'message' => $e->getMessage(),
+ 'code' => $e->getCode()
+ ];
+ wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+ }
+
+ return serialize( $metadata );
+ }
+
+ function unpackMetadata( $metadata ) {
+ Wikimedia\suppressWarnings();
+ $unser = unserialize( $metadata );
+ Wikimedia\restoreWarnings();
+ if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
+ return $unser;
+ } else {
+ return false;
+ }
+ }
+
+ function getMetadataType( $image ) {
+ return 'parsed-svg';
+ }
+
+ function isMetadataValid( $image, $metadata ) {
+ $meta = $this->unpackMetadata( $metadata );
+ if ( $meta === false ) {
+ return self::METADATA_BAD;
+ }
+ if ( !isset( $meta['originalWidth'] ) ) {
+ // Old but compatible
+ return self::METADATA_COMPATIBLE;
+ }
+
+ return self::METADATA_GOOD;
+ }
+
+ protected function visibleMetadataFields() {
+ $fields = [ 'objectname', 'imagedescription' ];
+
+ return $fields;
+ }
+
+ /**
+ * @param File $file
+ * @param bool|IContextSource $context Context to use (optional)
+ * @return array|bool
+ */
+ function formatMetadata( $file, $context = false ) {
+ $result = [
+ 'visible' => [],
+ 'collapsed' => []
+ ];
+ $metadata = $file->getMetadata();
+ if ( !$metadata ) {
+ return false;
+ }
+ $metadata = $this->unpackMetadata( $metadata );
+ if ( !$metadata || isset( $metadata['error'] ) ) {
+ return false;
+ }
+
+ /* @todo Add a formatter
+ $format = new FormatSVG( $metadata );
+ $formatted = $format->getFormattedData();
+ */
+
+ // Sort fields into visible and collapsed
+ $visibleFields = $this->visibleMetadataFields();
+
+ $showMeta = false;
+ foreach ( $metadata as $name => $value ) {
+ $tag = strtolower( $name );
+ if ( isset( self::$metaConversion[$tag] ) ) {
+ $tag = strtolower( self::$metaConversion[$tag] );
+ } else {
+ // Do not output other metadata not in list
+ continue;
+ }
+ $showMeta = true;
+ self::addMeta( $result,
+ in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
+ 'exif',
+ $tag,
+ $value
+ );
+ }
+
+ return $showMeta ? $result : false;
+ }
+
+ /**
+ * @param string $name Parameter name
+ * @param mixed $value Parameter value
+ * @return bool Validity
+ */
+ public function validateParam( $name, $value ) {
+ if ( in_array( $name, [ 'width', 'height' ] ) ) {
+ // Reject negative heights, widths
+ return ( $value > 0 );
+ } elseif ( $name == 'lang' ) {
+ // Validate $code
+ if ( $value === '' || !Language::isValidCode( $value ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // Only lang, width and height are acceptable keys
+ return false;
+ }
+
+ /**
+ * @param array $params Name=>value pairs of parameters
+ * @return string Filename to use
+ */
+ public function makeParamString( $params ) {
+ $lang = '';
+ if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
+ $lang = 'lang' . strtolower( $params['lang'] ) . '-';
+ }
+ if ( !isset( $params['width'] ) ) {
+ return false;
+ }
+
+ return "$lang{$params['width']}px";
+ }
+
+ public function parseParamString( $str ) {
+ $m = false;
+ if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $str, $m ) ) {
+ return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
+ } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
+ return [ 'width' => $m[1], 'lang' => 'en' ];
+ } else {
+ return false;
+ }
+ }
+
+ public function getParamMap() {
+ return [ 'img_lang' => 'lang', 'img_width' => 'width' ];
+ }
+
+ /**
+ * @param array $params
+ * @return array
+ */
+ function getScriptParams( $params ) {
+ $scriptParams = [ 'width' => $params['width'] ];
+ if ( isset( $params['lang'] ) ) {
+ $scriptParams['lang'] = $params['lang'];
+ }
+
+ return $scriptParams;
+ }
+
+ public function getCommonMetaArray( File $file ) {
+ $metadata = $file->getMetadata();
+ if ( !$metadata ) {
+ return [];
+ }
+ $metadata = $this->unpackMetadata( $metadata );
+ if ( !$metadata || isset( $metadata['error'] ) ) {
+ return [];
+ }
+ $stdMetadata = [];
+ foreach ( $metadata as $name => $value ) {
+ $tag = strtolower( $name );
+ if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
+ // Skip these. In the exif metadata stuff, it is assumed these
+ // are measured in px, which is not the case here.
+ continue;
+ }
+ if ( isset( self::$metaConversion[$tag] ) ) {
+ $tag = self::$metaConversion[$tag];
+ $stdMetadata[$tag] = $value;
+ }
+ }
+
+ return $stdMetadata;
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for Tiff images.
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Handler for Tiff images.
- *
- * @ingroup Media
- */
-class TiffHandler extends ExifBitmapHandler {
- const EXPENSIVE_SIZE_LIMIT = 10485760; // TIFF files over 10M are considered expensive to thumbnail
-
- /**
- * Conversion to PNG for inline display can be disabled here...
- * Note scaling should work with ImageMagick, but may not with GD scaling.
- *
- * Files pulled from an another MediaWiki instance via ForeignAPIRepo /
- * InstantCommons will have thumbnails managed from the remote instance,
- * so we can skip this check.
- *
- * @param File $file
- * @return bool
- */
- public function canRender( $file ) {
- global $wgTiffThumbnailType;
-
- return (bool)$wgTiffThumbnailType
- || $file->getRepo() instanceof ForeignAPIRepo;
- }
-
- /**
- * Browsers don't support TIFF inline generally...
- * For inline display, we need to convert to PNG.
- *
- * @param File $file
- * @return bool
- */
- public function mustRender( $file ) {
- return true;
- }
-
- /**
- * @param string $ext
- * @param string $mime
- * @param array $params
- * @return bool
- */
- function getThumbType( $ext, $mime, $params = null ) {
- global $wgTiffThumbnailType;
-
- return $wgTiffThumbnailType;
- }
-
- /**
- * @param File|FSFile $image
- * @param string $filename
- * @throws MWException
- * @return string
- */
- function getMetadata( $image, $filename ) {
- global $wgShowEXIF;
-
- if ( $wgShowEXIF ) {
- try {
- $meta = BitmapMetadataHandler::Tiff( $filename );
- if ( !is_array( $meta ) ) {
- // This should never happen, but doesn't hurt to be paranoid.
- throw new MWException( 'Metadata array is not an array' );
- }
- $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
-
- return serialize( $meta );
- } catch ( Exception $e ) {
- // BitmapMetadataHandler throws an exception in certain exceptional
- // cases like if file does not exist.
- wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
-
- return ExifBitmapHandler::BROKEN_FILE;
- }
- } else {
- return '';
- }
- }
-
- public function isExpensiveToThumbnail( $file ) {
- return $file->getSize() > static::EXPENSIVE_SIZE_LIMIT;
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for Tiff images.
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for Tiff images.
+ *
+ * @ingroup Media
+ */
+class TiffHandler extends ExifBitmapHandler {
+ const EXPENSIVE_SIZE_LIMIT = 10485760; // TIFF files over 10M are considered expensive to thumbnail
+
+ /**
+ * Conversion to PNG for inline display can be disabled here...
+ * Note scaling should work with ImageMagick, but may not with GD scaling.
+ *
+ * Files pulled from an another MediaWiki instance via ForeignAPIRepo /
+ * InstantCommons will have thumbnails managed from the remote instance,
+ * so we can skip this check.
+ *
+ * @param File $file
+ * @return bool
+ */
+ public function canRender( $file ) {
+ global $wgTiffThumbnailType;
+
+ return (bool)$wgTiffThumbnailType
+ || $file->getRepo() instanceof ForeignAPIRepo;
+ }
+
+ /**
+ * Browsers don't support TIFF inline generally...
+ * For inline display, we need to convert to PNG.
+ *
+ * @param File $file
+ * @return bool
+ */
+ public function mustRender( $file ) {
+ return true;
+ }
+
+ /**
+ * @param string $ext
+ * @param string $mime
+ * @param array $params
+ * @return bool
+ */
+ function getThumbType( $ext, $mime, $params = null ) {
+ global $wgTiffThumbnailType;
+
+ return $wgTiffThumbnailType;
+ }
+
+ /**
+ * @param File|FSFile $image
+ * @param string $filename
+ * @throws MWException
+ * @return string
+ */
+ function getMetadata( $image, $filename ) {
+ global $wgShowEXIF;
+
+ if ( $wgShowEXIF ) {
+ try {
+ $meta = BitmapMetadataHandler::Tiff( $filename );
+ if ( !is_array( $meta ) ) {
+ // This should never happen, but doesn't hurt to be paranoid.
+ throw new MWException( 'Metadata array is not an array' );
+ }
+ $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
+
+ return serialize( $meta );
+ } catch ( Exception $e ) {
+ // BitmapMetadataHandler throws an exception in certain exceptional
+ // cases like if file does not exist.
+ wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+
+ return ExifBitmapHandler::BROKEN_FILE;
+ }
+ } else {
+ return '';
+ }
+ }
+
+ public function isExpensiveToThumbnail( $file ) {
+ return $file->getSize() > static::EXPENSIVE_SIZE_LIMIT;
+ }
+}
+++ /dev/null
-<?php
-/**
- * Handler for Google's WebP format <https://developers.google.com/speed/webp/>
- *
- * 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
- *
- * @file
- * @ingroup Media
- */
-
-/**
- * Handler for Google's WebP format <https://developers.google.com/speed/webp/>
- *
- * @ingroup Media
- */
-class WebPHandler extends BitmapHandler {
- const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata.
- /**
- * @var int Minimum chunk header size to be able to read all header types
- */
- const MINIMUM_CHUNK_HEADER_LENGTH = 18;
- /**
- * @var int version of the metadata stored in db records
- */
- const _MW_WEBP_VERSION = 1;
-
- const VP8X_ICC = 32;
- const VP8X_ALPHA = 16;
- const VP8X_EXIF = 8;
- const VP8X_XMP = 4;
- const VP8X_ANIM = 2;
-
- public function getMetadata( $image, $filename ) {
- $parsedWebPData = self::extractMetadata( $filename );
- if ( !$parsedWebPData ) {
- return self::BROKEN_FILE;
- }
-
- $parsedWebPData['metadata']['_MW_WEBP_VERSION'] = self::_MW_WEBP_VERSION;
- return serialize( $parsedWebPData );
- }
-
- public function getMetadataType( $image ) {
- return 'parsed-webp';
- }
-
- public function isMetadataValid( $image, $metadata ) {
- if ( $metadata === self::BROKEN_FILE ) {
- // Do not repetitivly regenerate metadata on broken file.
- return self::METADATA_GOOD;
- }
-
- Wikimedia\suppressWarnings();
- $data = unserialize( $metadata );
- Wikimedia\restoreWarnings();
-
- if ( !$data || !is_array( $data ) ) {
- wfDebug( __METHOD__ . " invalid WebP metadata\n" );
-
- return self::METADATA_BAD;
- }
-
- if ( !isset( $data['metadata']['_MW_WEBP_VERSION'] )
- || $data['metadata']['_MW_WEBP_VERSION'] != self::_MW_WEBP_VERSION
- ) {
- wfDebug( __METHOD__ . " old but compatible WebP metadata\n" );
-
- return self::METADATA_COMPATIBLE;
- }
- return self::METADATA_GOOD;
- }
-
- /**
- * Extracts the image size and WebP type from a file
- *
- * @param string $filename
- * @return array|bool Header data array with entries 'compression', 'width' and 'height',
- * where 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'. False if
- * file is not a valid WebP file.
- */
- public static function extractMetadata( $filename ) {
- wfDebugLog( 'WebP', __METHOD__ . ": Extracting metadata from $filename\n" );
-
- $info = RiffExtractor::findChunksFromFile( $filename, 100 );
- if ( $info === false ) {
- wfDebugLog( 'WebP', __METHOD__ . ": Not a valid RIFF file\n" );
- return false;
- }
-
- if ( $info['fourCC'] != 'WEBP' ) {
- wfDebugLog( 'WebP', __METHOD__ . ': FourCC was not WEBP: ' .
- bin2hex( $info['fourCC'] ) . " \n" );
- return false;
- }
-
- $metadata = self::extractMetadataFromChunks( $info['chunks'], $filename );
- if ( !$metadata ) {
- wfDebugLog( 'WebP', __METHOD__ . ": No VP8 chunks found\n" );
- return false;
- }
-
- return $metadata;
- }
-
- /**
- * Extracts the image size and WebP type from a file based on the chunk list
- * @param array $chunks Chunks as extracted by RiffExtractor
- * @param string $filename
- * @return array Header data array with entries 'compression', 'width' and 'height', where
- * 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'
- */
- public static function extractMetadataFromChunks( $chunks, $filename ) {
- $vp8Info = [];
-
- foreach ( $chunks as $chunk ) {
- if ( !in_array( $chunk['fourCC'], [ 'VP8 ', 'VP8L', 'VP8X' ] ) ) {
- // Not a chunk containing interesting metadata
- continue;
- }
-
- $chunkHeader = file_get_contents( $filename, false, null,
- $chunk['start'], self::MINIMUM_CHUNK_HEADER_LENGTH );
- wfDebugLog( 'WebP', __METHOD__ . ": {$chunk['fourCC']}\n" );
-
- switch ( $chunk['fourCC'] ) {
- case 'VP8 ':
- return array_merge( $vp8Info,
- self::decodeLossyChunkHeader( $chunkHeader ) );
- case 'VP8L':
- return array_merge( $vp8Info,
- self::decodeLosslessChunkHeader( $chunkHeader ) );
- case 'VP8X':
- $vp8Info = array_merge( $vp8Info,
- self::decodeExtendedChunkHeader( $chunkHeader ) );
- // Continue looking for other chunks to improve the metadata
- break;
- }
- }
- return $vp8Info;
- }
-
- /**
- * Decodes a lossy chunk header
- * @param string $header First few bytes of the header, expected to be at least 18 bytes long
- * @return bool|array See WebPHandler::decodeHeader
- */
- protected static function decodeLossyChunkHeader( $header ) {
- // Bytes 0-3 are 'VP8 '
- // Bytes 4-7 are the VP8 stream size
- // Bytes 8-10 are the frame tag
- // Bytes 11-13 are 0x9D 0x01 0x2A called the sync code
- $syncCode = substr( $header, 11, 3 );
- if ( $syncCode != "\x9D\x01\x2A" ) {
- wfDebugLog( 'WebP', __METHOD__ . ': Invalid sync code: ' .
- bin2hex( $syncCode ) . "\n" );
- return [];
- }
- // Bytes 14-17 are image size
- $imageSize = unpack( 'v2', substr( $header, 14, 4 ) );
- // Image sizes are 14 bit, 2 MSB are scaling parameters which are ignored here
- return [
- 'compression' => 'lossy',
- 'width' => $imageSize[1] & 0x3FFF,
- 'height' => $imageSize[2] & 0x3FFF
- ];
- }
-
- /**
- * Decodes a lossless chunk header
- * @param string $header First few bytes of the header, expected to be at least 13 bytes long
- * @return bool|array See WebPHandler::decodeHeader
- */
- public static function decodeLosslessChunkHeader( $header ) {
- // Bytes 0-3 are 'VP8L'
- // Bytes 4-7 are chunk stream size
- // Byte 8 is 0x2F called the signature
- if ( $header{8} != "\x2F" ) {
- wfDebugLog( 'WebP', __METHOD__ . ': Invalid signature: ' .
- bin2hex( $header{8} ) . "\n" );
- return [];
- }
- // Bytes 9-12 contain the image size
- // Bits 0-13 are width-1; bits 15-27 are height-1
- $imageSize = unpack( 'C4', substr( $header, 9, 4 ) );
- return [
- 'compression' => 'lossless',
- 'width' => ( $imageSize[1] | ( ( $imageSize[2] & 0x3F ) << 8 ) ) + 1,
- 'height' => ( ( ( $imageSize[2] & 0xC0 ) >> 6 ) |
- ( $imageSize[3] << 2 ) | ( ( $imageSize[4] & 0x03 ) << 10 ) ) + 1
- ];
- }
-
- /**
- * Decodes an extended chunk header
- * @param string $header First few bytes of the header, expected to be at least 18 bytes long
- * @return bool|array See WebPHandler::decodeHeader
- */
- public static function decodeExtendedChunkHeader( $header ) {
- // Bytes 0-3 are 'VP8X'
- // Byte 4-7 are chunk length
- // Byte 8-11 are a flag bytes
- $flags = unpack( 'c', substr( $header, 8, 1 ) );
-
- // Byte 12-17 are image size (24 bits)
- $width = unpack( 'V', substr( $header, 12, 3 ) . "\x00" );
- $height = unpack( 'V', substr( $header, 15, 3 ) . "\x00" );
-
- return [
- 'compression' => 'unknown',
- 'animated' => ( $flags[1] & self::VP8X_ANIM ) == self::VP8X_ANIM,
- 'transparency' => ( $flags[1] & self::VP8X_ALPHA ) == self::VP8X_ALPHA,
- 'width' => ( $width[1] & 0xFFFFFF ) + 1,
- 'height' => ( $height[1] & 0xFFFFFF ) + 1
- ];
- }
-
- public function getImageSize( $file, $path, $metadata = false ) {
- if ( $file === null ) {
- $metadata = self::getMetadata( $file, $path );
- }
- if ( $metadata === false && $file instanceof File ) {
- $metadata = $file->getMetadata();
- }
-
- Wikimedia\suppressWarnings();
- $metadata = unserialize( $metadata );
- Wikimedia\restoreWarnings();
-
- if ( $metadata == false ) {
- return false;
- }
- return [ $metadata['width'], $metadata['height'] ];
- }
-
- /**
- * @param File $file
- * @return bool True, not all browsers support WebP
- */
- public function mustRender( $file ) {
- return true;
- }
-
- /**
- * @param File $file
- * @return bool False if we are unable to render this image
- */
- public function canRender( $file ) {
- if ( self::isAnimatedImage( $file ) ) {
- return false;
- }
- return true;
- }
-
- /**
- * @param File $image
- * @return bool
- */
- public function isAnimatedImage( $image ) {
- $ser = $image->getMetadata();
- if ( $ser ) {
- $metadata = unserialize( $ser );
- if ( isset( $metadata['animated'] ) && $metadata['animated'] === true ) {
- return true;
- }
- }
-
- return false;
- }
-
- public function canAnimateThumbnail( $file ) {
- return false;
- }
-
- /**
- * Render files as PNG
- *
- * @param string $ext
- * @param string $mime
- * @param array|null $params
- * @return array
- */
- public function getThumbType( $ext, $mime, $params = null ) {
- return [ 'png', 'image/png' ];
- }
-
- /**
- * Must use "im" for XCF
- *
- * @param string $dstPath
- * @param bool $checkDstPath
- * @return string
- */
- protected function getScalerType( $dstPath, $checkDstPath = true ) {
- return 'im';
- }
-}
--- /dev/null
+<?php
+/**
+ * Handler for Google's WebP format <https://developers.google.com/speed/webp/>
+ *
+ * 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
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for Google's WebP format <https://developers.google.com/speed/webp/>
+ *
+ * @ingroup Media
+ */
+class WebPHandler extends BitmapHandler {
+ const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata.
+ /**
+ * @var int Minimum chunk header size to be able to read all header types
+ */
+ const MINIMUM_CHUNK_HEADER_LENGTH = 18;
+ /**
+ * @var int version of the metadata stored in db records
+ */
+ const _MW_WEBP_VERSION = 1;
+
+ const VP8X_ICC = 32;
+ const VP8X_ALPHA = 16;
+ const VP8X_EXIF = 8;
+ const VP8X_XMP = 4;
+ const VP8X_ANIM = 2;
+
+ public function getMetadata( $image, $filename ) {
+ $parsedWebPData = self::extractMetadata( $filename );
+ if ( !$parsedWebPData ) {
+ return self::BROKEN_FILE;
+ }
+
+ $parsedWebPData['metadata']['_MW_WEBP_VERSION'] = self::_MW_WEBP_VERSION;
+ return serialize( $parsedWebPData );
+ }
+
+ public function getMetadataType( $image ) {
+ return 'parsed-webp';
+ }
+
+ public function isMetadataValid( $image, $metadata ) {
+ if ( $metadata === self::BROKEN_FILE ) {
+ // Do not repetitivly regenerate metadata on broken file.
+ return self::METADATA_GOOD;
+ }
+
+ Wikimedia\suppressWarnings();
+ $data = unserialize( $metadata );
+ Wikimedia\restoreWarnings();
+
+ if ( !$data || !is_array( $data ) ) {
+ wfDebug( __METHOD__ . " invalid WebP metadata\n" );
+
+ return self::METADATA_BAD;
+ }
+
+ if ( !isset( $data['metadata']['_MW_WEBP_VERSION'] )
+ || $data['metadata']['_MW_WEBP_VERSION'] != self::_MW_WEBP_VERSION
+ ) {
+ wfDebug( __METHOD__ . " old but compatible WebP metadata\n" );
+
+ return self::METADATA_COMPATIBLE;
+ }
+ return self::METADATA_GOOD;
+ }
+
+ /**
+ * Extracts the image size and WebP type from a file
+ *
+ * @param string $filename
+ * @return array|bool Header data array with entries 'compression', 'width' and 'height',
+ * where 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'. False if
+ * file is not a valid WebP file.
+ */
+ public static function extractMetadata( $filename ) {
+ wfDebugLog( 'WebP', __METHOD__ . ": Extracting metadata from $filename\n" );
+
+ $info = RiffExtractor::findChunksFromFile( $filename, 100 );
+ if ( $info === false ) {
+ wfDebugLog( 'WebP', __METHOD__ . ": Not a valid RIFF file\n" );
+ return false;
+ }
+
+ if ( $info['fourCC'] != 'WEBP' ) {
+ wfDebugLog( 'WebP', __METHOD__ . ': FourCC was not WEBP: ' .
+ bin2hex( $info['fourCC'] ) . " \n" );
+ return false;
+ }
+
+ $metadata = self::extractMetadataFromChunks( $info['chunks'], $filename );
+ if ( !$metadata ) {
+ wfDebugLog( 'WebP', __METHOD__ . ": No VP8 chunks found\n" );
+ return false;
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * Extracts the image size and WebP type from a file based on the chunk list
+ * @param array $chunks Chunks as extracted by RiffExtractor
+ * @param string $filename
+ * @return array Header data array with entries 'compression', 'width' and 'height', where
+ * 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'
+ */
+ public static function extractMetadataFromChunks( $chunks, $filename ) {
+ $vp8Info = [];
+
+ foreach ( $chunks as $chunk ) {
+ if ( !in_array( $chunk['fourCC'], [ 'VP8 ', 'VP8L', 'VP8X' ] ) ) {
+ // Not a chunk containing interesting metadata
+ continue;
+ }
+
+ $chunkHeader = file_get_contents( $filename, false, null,
+ $chunk['start'], self::MINIMUM_CHUNK_HEADER_LENGTH );
+ wfDebugLog( 'WebP', __METHOD__ . ": {$chunk['fourCC']}\n" );
+
+ switch ( $chunk['fourCC'] ) {
+ case 'VP8 ':
+ return array_merge( $vp8Info,
+ self::decodeLossyChunkHeader( $chunkHeader ) );
+ case 'VP8L':
+ return array_merge( $vp8Info,
+ self::decodeLosslessChunkHeader( $chunkHeader ) );
+ case 'VP8X':
+ $vp8Info = array_merge( $vp8Info,
+ self::decodeExtendedChunkHeader( $chunkHeader ) );
+ // Continue looking for other chunks to improve the metadata
+ break;
+ }
+ }
+ return $vp8Info;
+ }
+
+ /**
+ * Decodes a lossy chunk header
+ * @param string $header First few bytes of the header, expected to be at least 18 bytes long
+ * @return bool|array See WebPHandler::decodeHeader
+ */
+ protected static function decodeLossyChunkHeader( $header ) {
+ // Bytes 0-3 are 'VP8 '
+ // Bytes 4-7 are the VP8 stream size
+ // Bytes 8-10 are the frame tag
+ // Bytes 11-13 are 0x9D 0x01 0x2A called the sync code
+ $syncCode = substr( $header, 11, 3 );
+ if ( $syncCode != "\x9D\x01\x2A" ) {
+ wfDebugLog( 'WebP', __METHOD__ . ': Invalid sync code: ' .
+ bin2hex( $syncCode ) . "\n" );
+ return [];
+ }
+ // Bytes 14-17 are image size
+ $imageSize = unpack( 'v2', substr( $header, 14, 4 ) );
+ // Image sizes are 14 bit, 2 MSB are scaling parameters which are ignored here
+ return [
+ 'compression' => 'lossy',
+ 'width' => $imageSize[1] & 0x3FFF,
+ 'height' => $imageSize[2] & 0x3FFF
+ ];
+ }
+
+ /**
+ * Decodes a lossless chunk header
+ * @param string $header First few bytes of the header, expected to be at least 13 bytes long
+ * @return bool|array See WebPHandler::decodeHeader
+ */
+ public static function decodeLosslessChunkHeader( $header ) {
+ // Bytes 0-3 are 'VP8L'
+ // Bytes 4-7 are chunk stream size
+ // Byte 8 is 0x2F called the signature
+ if ( $header{8} != "\x2F" ) {
+ wfDebugLog( 'WebP', __METHOD__ . ': Invalid signature: ' .
+ bin2hex( $header{8} ) . "\n" );
+ return [];
+ }
+ // Bytes 9-12 contain the image size
+ // Bits 0-13 are width-1; bits 15-27 are height-1
+ $imageSize = unpack( 'C4', substr( $header, 9, 4 ) );
+ return [
+ 'compression' => 'lossless',
+ 'width' => ( $imageSize[1] | ( ( $imageSize[2] & 0x3F ) << 8 ) ) + 1,
+ 'height' => ( ( ( $imageSize[2] & 0xC0 ) >> 6 ) |
+ ( $imageSize[3] << 2 ) | ( ( $imageSize[4] & 0x03 ) << 10 ) ) + 1
+ ];
+ }
+
+ /**
+ * Decodes an extended chunk header
+ * @param string $header First few bytes of the header, expected to be at least 18 bytes long
+ * @return bool|array See WebPHandler::decodeHeader
+ */
+ public static function decodeExtendedChunkHeader( $header ) {
+ // Bytes 0-3 are 'VP8X'
+ // Byte 4-7 are chunk length
+ // Byte 8-11 are a flag bytes
+ $flags = unpack( 'c', substr( $header, 8, 1 ) );
+
+ // Byte 12-17 are image size (24 bits)
+ $width = unpack( 'V', substr( $header, 12, 3 ) . "\x00" );
+ $height = unpack( 'V', substr( $header, 15, 3 ) . "\x00" );
+
+ return [
+ 'compression' => 'unknown',
+ 'animated' => ( $flags[1] & self::VP8X_ANIM ) == self::VP8X_ANIM,
+ 'transparency' => ( $flags[1] & self::VP8X_ALPHA ) == self::VP8X_ALPHA,
+ 'width' => ( $width[1] & 0xFFFFFF ) + 1,
+ 'height' => ( $height[1] & 0xFFFFFF ) + 1
+ ];
+ }
+
+ public function getImageSize( $file, $path, $metadata = false ) {
+ if ( $file === null ) {
+ $metadata = self::getMetadata( $file, $path );
+ }
+ if ( $metadata === false && $file instanceof File ) {
+ $metadata = $file->getMetadata();
+ }
+
+ Wikimedia\suppressWarnings();
+ $metadata = unserialize( $metadata );
+ Wikimedia\restoreWarnings();
+
+ if ( $metadata == false ) {
+ return false;
+ }
+ return [ $metadata['width'], $metadata['height'] ];
+ }
+
+ /**
+ * @param File $file
+ * @return bool True, not all browsers support WebP
+ */
+ public function mustRender( $file ) {
+ return true;
+ }
+
+ /**
+ * @param File $file
+ * @return bool False if we are unable to render this image
+ */
+ public function canRender( $file ) {
+ if ( self::isAnimatedImage( $file ) ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param File $image
+ * @return bool
+ */
+ public function isAnimatedImage( $image ) {
+ $ser = $image->getMetadata();
+ if ( $ser ) {
+ $metadata = unserialize( $ser );
+ if ( isset( $metadata['animated'] ) && $metadata['animated'] === true ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function canAnimateThumbnail( $file ) {
+ return false;
+ }
+
+ /**
+ * Render files as PNG
+ *
+ * @param string $ext
+ * @param string $mime
+ * @param array|null $params
+ * @return array
+ */
+ public function getThumbType( $ext, $mime, $params = null ) {
+ return [ 'png', 'image/png' ];
+ }
+
+ /**
+ * Must use "im" for XCF
+ *
+ * @param string $dstPath
+ * @param bool $checkDstPath
+ * @return string
+ */
+ protected function getScalerType( $dstPath, $checkDstPath = true ) {
+ return 'im';
+ }
+}
if ( meta && meta.tiff && meta.tiff.Orientation ) {
rotation = ( 360 - ( function () {
- // See includes/media/Bitmap.php
+ // See BitmapHandler class in PHP
switch ( meta.tiff.Orientation.value ) {
case 8:
return 90;