00001 <?php
00010 class BitmapHandler extends ImageHandler {
00011 function normaliseParams( $image, &$params ) {
00012 global $wgMaxImageArea;
00013 if ( !parent::normaliseParams( $image, $params ) ) {
00014 return false;
00015 }
00016
00017 $mimeType = $image->getMimeType();
00018 $srcWidth = $image->getWidth( $params['page'] );
00019 $srcHeight = $image->getHeight( $params['page'] );
00020
00021 # Don't thumbnail an image so big that it will fill hard drives and send servers into swap
00022 # JPEG has the handy property of allowing thumbnailing without full decompression, so we make
00023 # an exception for it.
00024 if ( $mimeType !== 'image/jpeg' &&
00025 $this->getImageArea( $image, $srcWidth, $srcHeight ) > $wgMaxImageArea )
00026 {
00027 return false;
00028 }
00029
00030 # Don't make an image bigger than the source
00031 $params['physicalWidth'] = $params['width'];
00032 $params['physicalHeight'] = $params['height'];
00033
00034 if ( $params['physicalWidth'] >= $srcWidth ) {
00035 $params['physicalWidth'] = $srcWidth;
00036 $params['physicalHeight'] = $srcHeight;
00037 return true;
00038 }
00039
00040 return true;
00041 }
00042
00043
00044
00045
00046 function getImageArea( $image, $width, $height ) {
00047 return $width * $height;
00048 }
00049
00050 function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
00051 global $wgUseImageMagick, $wgImageMagickConvertCommand, $wgImageMagickTempDir;
00052 global $wgCustomConvertCommand, $wgUseImageResize;
00053 global $wgSharpenParameter, $wgSharpenReductionThreshold;
00054 global $wgMaxAnimatedGifArea;
00055
00056 if ( !$this->normaliseParams( $image, $params ) ) {
00057 return new TransformParameterError( $params );
00058 }
00059 $physicalWidth = $params['physicalWidth'];
00060 $physicalHeight = $params['physicalHeight'];
00061 $clientWidth = $params['width'];
00062 $clientHeight = $params['height'];
00063 $comment = isset( $params['descriptionUrl'] ) ? "File source: ". $params['descriptionUrl'] : '';
00064 $srcWidth = $image->getWidth();
00065 $srcHeight = $image->getHeight();
00066 $mimeType = $image->getMimeType();
00067 $srcPath = $image->getPath();
00068 $retval = 0;
00069 wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" );
00070
00071 if ( !$image->mustRender() && $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) {
00072 # normaliseParams (or the user) wants us to return the unscaled image
00073 wfDebug( __METHOD__.": returning unscaled image\n" );
00074 return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
00075 }
00076
00077 if ( !$dstPath ) {
00078
00079 $scaler = 'client';
00080 } elseif( !$wgUseImageResize ) {
00081 $scaler = 'client';
00082 } elseif ( $wgUseImageMagick ) {
00083 $scaler = 'im';
00084 } elseif ( $wgCustomConvertCommand ) {
00085 $scaler = 'custom';
00086 } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
00087 $scaler = 'gd';
00088 } else {
00089 $scaler = 'client';
00090 }
00091 wfDebug( __METHOD__.": scaler $scaler\n" );
00092
00093 if ( $scaler == 'client' ) {
00094 # Client-side image scaling, use the source URL
00095 # Using the destination URL in a TRANSFORM_LATER request would be incorrect
00096 return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
00097 }
00098
00099 if ( $flags & self::TRANSFORM_LATER ) {
00100 wfDebug( __METHOD__.": Transforming later per flags.\n" );
00101 return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
00102 }
00103
00104 if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
00105 wfDebug( __METHOD__.": Unable to create thumbnail destination directory, falling back to client scaling\n" );
00106 return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
00107 }
00108
00109 if ( $scaler == 'im' ) {
00110 # use ImageMagick
00111
00112 $quality = '';
00113 $sharpen = '';
00114 $scene = false;
00115 $animation = '';
00116 if ( $mimeType == 'image/jpeg' ) {
00117 $quality = "-quality 80";
00118 # Sharpening, see bug 6193
00119 if ( ( $physicalWidth + $physicalHeight ) / ( $srcWidth + $srcHeight ) < $wgSharpenReductionThreshold ) {
00120 $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter );
00121 }
00122 } elseif ( $mimeType == 'image/png' ) {
00123 $quality = "-quality 95";
00124 } elseif( $mimeType == 'image/gif' ) {
00125 if( $srcWidth * $srcHeight > $wgMaxAnimatedGifArea ) {
00126
00127
00128 $scene = 0;
00129 } else {
00130
00131 $animation = ' -coalesce ';
00132 }
00133 }
00134
00135 if ( strval( $wgImageMagickTempDir ) !== '' ) {
00136 $tempEnv = 'MAGICK_TMPDIR=' . wfEscapeShellArg( $wgImageMagickTempDir ) . ' ';
00137 } else {
00138 $tempEnv = '';
00139 }
00140
00141 # Specify white background color, will be used for transparent images
00142 # in Internet Explorer/Windows instead of default black.
00143
00144 # Note, we specify "-size {$physicalWidth}" and NOT "-size {$physicalWidth}x{$physicalHeight}".
00145 # It seems that ImageMagick has a bug wherein it produces thumbnails of
00146 # the wrong size in the second case.
00147
00148 $cmd =
00149 $tempEnv .
00150 wfEscapeShellArg( $wgImageMagickConvertCommand ) .
00151 " {$quality} -background white -size {$physicalWidth} ".
00152 wfEscapeShellArg( $this->escapeMagickInput( $srcPath, $scene ) ) .
00153 $animation .
00154
00155
00156
00157 " -thumbnail " . wfEscapeShellArg( "{$physicalWidth}x{$physicalHeight}!" ) .
00158
00159 " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $comment ) ) .
00160 " -depth 8 $sharpen " .
00161 wfEscapeShellArg( $this->escapeMagickOutput( $dstPath ) ) . " 2>&1";
00162 wfDebug( __METHOD__.": running ImageMagick: $cmd\n" );
00163 wfProfileIn( 'convert' );
00164 $err = wfShellExec( $cmd, $retval );
00165 wfProfileOut( 'convert' );
00166 } elseif( $scaler == 'custom' ) {
00167 # Use a custom convert command
00168 # Variables: %s %d %w %h
00169 $src = wfEscapeShellArg( $srcPath );
00170 $dst = wfEscapeShellArg( $dstPath );
00171 $cmd = $wgCustomConvertCommand;
00172 $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
00173 $cmd = str_replace( '%h', $physicalHeight, str_replace( '%w', $physicalWidth, $cmd ) ); # Size
00174 wfDebug( __METHOD__.": Running custom convert command $cmd\n" );
00175 wfProfileIn( 'convert' );
00176 $err = wfShellExec( $cmd, $retval );
00177 wfProfileOut( 'convert' );
00178 } else {
00179 # Use PHP's builtin GD library functions.
00180 #
00181 # First find out what kind of file this is, and select the correct
00182 # input routine for this.
00183
00184 $typemap = array(
00185 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ),
00186 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ),
00187 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ),
00188 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ),
00189 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ),
00190 );
00191 if( !isset( $typemap[$mimeType] ) ) {
00192 $err = 'Image type not supported';
00193 wfDebug( "$err\n" );
00194 $errMsg = wfMsg ( 'thumbnail_image-type' );
00195 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg );
00196 }
00197 list( $loader, $colorStyle, $saveType ) = $typemap[$mimeType];
00198
00199 if( !function_exists( $loader ) ) {
00200 $err = "Incomplete GD library configuration: missing function $loader";
00201 wfDebug( "$err\n" );
00202 $errMsg = wfMsg ( 'thumbnail_gd-library', $loader );
00203 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg );
00204 }
00205
00206 if ( !file_exists( $srcPath ) ) {
00207 $err = "File seems to be missing: $srcPath";
00208 wfDebug( "$err\n" );
00209 $errMsg = wfMsg ( 'thumbnail_image-missing', $srcPath );
00210 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg );
00211 }
00212
00213 $src_image = call_user_func( $loader, $srcPath );
00214 $dst_image = imagecreatetruecolor( $physicalWidth, $physicalHeight );
00215
00216
00217
00218 $background = imagecolorallocate( $dst_image, 0, 0, 0 );
00219 imagecolortransparent( $dst_image, $background );
00220 imagealphablending( $dst_image, false );
00221
00222 if( $colorStyle == 'palette' ) {
00223
00224
00225 imagecopyresized( $dst_image, $src_image,
00226 0,0,0,0,
00227 $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) );
00228 } else {
00229 imagecopyresampled( $dst_image, $src_image,
00230 0,0,0,0,
00231 $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) );
00232 }
00233
00234 imagesavealpha( $dst_image, true );
00235
00236 call_user_func( $saveType, $dst_image, $dstPath );
00237 imagedestroy( $dst_image );
00238 imagedestroy( $src_image );
00239 $retval = 0;
00240 }
00241
00242 $removed = $this->removeBadFile( $dstPath, $retval );
00243 if ( $retval != 0 || $removed ) {
00244 wfDebugLog( 'thumbnail',
00245 sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
00246 wfHostname(), $retval, trim($err), $cmd ) );
00247 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
00248 } else {
00249 return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
00250 }
00251 }
00252
00257 function escapeMagickProperty( $s ) {
00258
00259 $s = str_replace( '\\', '\\\\', $s );
00260
00261 $s = str_replace( '%', '%%', $s );
00262
00263 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
00264 $s = '\\' . $s;
00265 }
00266 return $s;
00267 }
00268
00284 function escapeMagickInput( $path, $scene = false ) {
00285 # Die on initial metacharacters (caller should prepend path)
00286 $firstChar = substr( $path, 0, 1 );
00287 if ( $firstChar === '~' || $firstChar === '@' ) {
00288 throw new MWException( __METHOD__.': cannot escape this path name' );
00289 }
00290
00291 # Escape glob chars
00292 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
00293
00294 return $this->escapeMagickPath( $path, $scene );
00295 }
00296
00301 function escapeMagickOutput( $path, $scene = false ) {
00302 $path = str_replace( '%', '%%', $path );
00303 return $this->escapeMagickPath( $path, $scene );
00304 }
00305
00313 protected function escapeMagickPath( $path, $scene = false ) {
00314 # Die on format specifiers (other than drive letters). The regex is
00315 # meant to match all the formats you get from "convert -list format"
00316 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
00317 if ( wfIsWindows() && is_dir( $m[0] ) ) {
00318
00319
00320 } else {
00321 throw new MWException( __METHOD__.': unexpected colon character in path name' );
00322 }
00323 }
00324
00325 # If there are square brackets, add a do-nothing scene specification
00326 # to force a literal interpretation
00327 if ( $scene === false ) {
00328 if ( strpos( $path, '[' ) !== false ) {
00329 $path .= '[0--1]';
00330 }
00331 } else {
00332 $path .= "[$scene]";
00333 }
00334 return $path;
00335 }
00336
00337 static function imageJpegWrapper( $dst_image, $thumbPath ) {
00338 imageinterlace( $dst_image );
00339 imagejpeg( $dst_image, $thumbPath, 95 );
00340 }
00341
00342
00343 function getMetadata( $image, $filename ) {
00344 global $wgShowEXIF;
00345 if( $wgShowEXIF && file_exists( $filename ) ) {
00346 $exif = new Exif( $filename );
00347 $data = $exif->getFilteredData();
00348 if ( $data ) {
00349 $data['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
00350 return serialize( $data );
00351 } else {
00352 return '0';
00353 }
00354 } else {
00355 return '';
00356 }
00357 }
00358
00359 function getMetadataType( $image ) {
00360 return 'exif';
00361 }
00362
00363 function isMetadataValid( $image, $metadata ) {
00364 global $wgShowEXIF;
00365 if ( !$wgShowEXIF ) {
00366 # Metadata disabled and so an empty field is expected
00367 return true;
00368 }
00369 if ( $metadata === '0' ) {
00370 # Special value indicating that there is no EXIF data in the file
00371 return true;
00372 }
00373 $exif = @unserialize( $metadata );
00374 if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) ||
00375 $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version() )
00376 {
00377 # Wrong version
00378 wfDebug( __METHOD__.": wrong version\n" );
00379 return false;
00380 }
00381 return true;
00382 }
00383
00391 function visibleMetadataFields() {
00392 $fields = array();
00393 $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
00394 foreach( $lines as $line ) {
00395 $matches = array();
00396 if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
00397 $fields[] = $matches[1];
00398 }
00399 }
00400 $fields = array_map( 'strtolower', $fields );
00401 return $fields;
00402 }
00403
00404 function formatMetadata( $image ) {
00405 $result = array(
00406 'visible' => array(),
00407 'collapsed' => array()
00408 );
00409 $metadata = $image->getMetadata();
00410 if ( !$metadata ) {
00411 return false;
00412 }
00413 $exif = unserialize( $metadata );
00414 if ( !$exif ) {
00415 return false;
00416 }
00417 unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
00418 $format = new FormatExif( $exif );
00419
00420 $formatted = $format->getFormattedData();
00421
00422 $visibleFields = $this->visibleMetadataFields();
00423 foreach ( $formatted as $name => $value ) {
00424 $tag = strtolower( $name );
00425 self::addMeta( $result,
00426 in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
00427 'exif',
00428 $tag,
00429 $value
00430 );
00431 }
00432 return $result;
00433 }
00434 }