MediaWiki  REL1_24
TransformationalImageHandler.php
Go to the documentation of this file.
00001 <?php
00035 abstract class TransformationalImageHandler extends ImageHandler {
00043     function normaliseParams( $image, &$params ) {
00044         if ( !parent::normaliseParams( $image, $params ) ) {
00045             return false;
00046         }
00047 
00048         # Obtain the source, pre-rotation dimensions
00049         $srcWidth = $image->getWidth( $params['page'] );
00050         $srcHeight = $image->getHeight( $params['page'] );
00051 
00052         # Don't make an image bigger than the source
00053         if ( $params['physicalWidth'] >= $srcWidth ) {
00054             $params['physicalWidth'] = $srcWidth;
00055             $params['physicalHeight'] = $srcHeight;
00056 
00057             # Skip scaling limit checks if no scaling is required
00058             # due to requested size being bigger than source.
00059             if ( !$image->mustRender() ) {
00060                 return true;
00061             }
00062         }
00063 
00064         # Check if the file is smaller than the maximum image area for thumbnailing
00065         # For historical reasons, hook starts with BitmapHandler
00066         $checkImageAreaHookResult = null;
00067         wfRunHooks(
00068             'BitmapHandlerCheckImageArea',
00069             array( $image, &$params, &$checkImageAreaHookResult )
00070         );
00071 
00072         if ( is_null( $checkImageAreaHookResult ) ) {
00073             global $wgMaxImageArea;
00074 
00075             if ( $srcWidth * $srcHeight > $wgMaxImageArea
00076                 && !( $image->getMimeType() == 'image/jpeg'
00077                     && $this->getScalerType( false, false ) == 'im' )
00078             ) {
00079                 # Only ImageMagick can efficiently downsize jpg images without loading
00080                 # the entire file in memory
00081                 return false;
00082             }
00083         } else {
00084             return $checkImageAreaHookResult;
00085         }
00086 
00087         return true;
00088     }
00089 
00102     public function extractPreRotationDimensions( $params, $rotation ) {
00103         if ( $rotation == 90 || $rotation == 270 ) {
00104             # We'll resize before rotation, so swap the dimensions again
00105             $width = $params['physicalHeight'];
00106             $height = $params['physicalWidth'];
00107         } else {
00108             $width = $params['physicalWidth'];
00109             $height = $params['physicalHeight'];
00110         }
00111 
00112         return array( $width, $height );
00113     }
00114 
00128     function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
00129         if ( !$this->normaliseParams( $image, $params ) ) {
00130             return new TransformParameterError( $params );
00131         }
00132 
00133         # Create a parameter array to pass to the scaler
00134         $scalerParams = array(
00135             # The size to which the image will be resized
00136             'physicalWidth' => $params['physicalWidth'],
00137             'physicalHeight' => $params['physicalHeight'],
00138             'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
00139             # The size of the image on the page
00140             'clientWidth' => $params['width'],
00141             'clientHeight' => $params['height'],
00142             # Comment as will be added to the Exif of the thumbnail
00143             'comment' => isset( $params['descriptionUrl'] )
00144                 ? "File source: {$params['descriptionUrl']}"
00145                 : '',
00146             # Properties of the original image
00147             'srcWidth' => $image->getWidth(),
00148             'srcHeight' => $image->getHeight(),
00149             'mimeType' => $image->getMimeType(),
00150             'dstPath' => $dstPath,
00151             'dstUrl' => $dstUrl,
00152         );
00153 
00154         if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
00155             $scalerParams['quality'] = 30;
00156         }
00157 
00158         // For subclasses that might be paged.
00159         if ( $image->isMultipage() && isset( $params['page'] ) ) {
00160             $scalerParams['page'] = intval( $params['page'] );
00161         }
00162 
00163         # Determine scaler type
00164         $scaler = $this->getScalerType( $dstPath );
00165 
00166         if ( is_array( $scaler ) ) {
00167             $scalerName = get_class( $scaler[0] );
00168         } else {
00169             $scalerName = $scaler;
00170         }
00171 
00172         wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
00173             "thumbnail at $dstPath using scaler $scalerName\n" );
00174 
00175         if ( !$image->mustRender() &&
00176             $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
00177             && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
00178             && !isset( $scalerParams['quality'] )
00179         ) {
00180 
00181             # normaliseParams (or the user) wants us to return the unscaled image
00182             wfDebug( __METHOD__ . ": returning unscaled image\n" );
00183 
00184             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00185         }
00186 
00187         if ( $scaler == 'client' ) {
00188             # Client-side image scaling, use the source URL
00189             # Using the destination URL in a TRANSFORM_LATER request would be incorrect
00190             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00191         }
00192 
00193         if ( $flags & self::TRANSFORM_LATER ) {
00194             wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
00195             $newParams = array(
00196                 'width' => $scalerParams['clientWidth'],
00197                 'height' => $scalerParams['clientHeight']
00198             );
00199             if ( isset( $params['quality'] ) ) {
00200                 $newParams['quality'] = $params['quality'];
00201             }
00202             if ( isset( $params['page'] ) && $params['page'] ) {
00203                 $newParams['page'] = $params['page'];
00204             }
00205             return new ThumbnailImage( $image, $dstUrl, false, $newParams );
00206         }
00207 
00208         # Try to make a target path for the thumbnail
00209         if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
00210             wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
00211                 "directory, falling back to client scaling\n" );
00212 
00213             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00214         }
00215 
00216         # Transform functions and binaries need a FS source file
00217         $thumbnailSource = $this->getThumbnailSource( $image, $params );
00218 
00219         $scalerParams['srcPath'] = $thumbnailSource['path'];
00220         $scalerParams['srcWidth'] = $thumbnailSource['width'];
00221         $scalerParams['srcHeight'] = $thumbnailSource['height'];
00222 
00223         if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
00224             wfDebugLog( 'thumbnail',
00225                 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
00226                     wfHostname(), $image->getName() ) );
00227 
00228             return new MediaTransformError( 'thumbnail_error',
00229                 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
00230                 wfMessage( 'filemissing' )->text()
00231             );
00232         }
00233 
00234         # Try a hook. Called "Bitmap" for historical reasons.
00235 
00236         $mto = null;
00237         wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
00238         if ( !is_null( $mto ) ) {
00239             wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
00240             $scaler = 'hookaborted';
00241         }
00242 
00243         // $scaler will return a MediaTransformError on failure, or false on success.
00244         // If the scaler is succesful, it will have created a thumbnail at the destination
00245         // path.
00246         if ( is_array( $scaler ) && is_callable( $scaler ) ) {
00247             // Allow subclasses to specify their own rendering methods.
00248             $err = call_user_func( $scaler, $image, $scalerParams );
00249         } else {
00250             switch ( $scaler ) {
00251                 case 'hookaborted':
00252                     # Handled by the hook above
00253                     $err = $mto->isError() ? $mto : false;
00254                     break;
00255                 case 'im':
00256                     $err = $this->transformImageMagick( $image, $scalerParams );
00257                     break;
00258                 case 'custom':
00259                     $err = $this->transformCustom( $image, $scalerParams );
00260                     break;
00261                 case 'imext':
00262                     $err = $this->transformImageMagickExt( $image, $scalerParams );
00263                     break;
00264                 case 'gd':
00265                 default:
00266                     $err = $this->transformGd( $image, $scalerParams );
00267                     break;
00268             }
00269         }
00270 
00271         # Remove the file if a zero-byte thumbnail was created, or if there was an error
00272         $removed = $this->removeBadFile( $dstPath, (bool)$err );
00273         if ( $err ) {
00274             # transform returned MediaTransforError
00275             return $err;
00276         } elseif ( $removed ) {
00277             # Thumbnail was zero-byte and had to be removed
00278             return new MediaTransformError( 'thumbnail_error',
00279                 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
00280                 wfMessage( 'unknown-error' )->text()
00281             );
00282         } elseif ( $mto ) {
00283             return $mto;
00284         } else {
00285             $newParams = array(
00286                 'width' => $scalerParams['clientWidth'],
00287                 'height' => $scalerParams['clientHeight']
00288             );
00289             if ( isset( $params['quality'] ) ) {
00290                 $newParams['quality'] = $params['quality'];
00291             }
00292             if ( isset( $params['page'] ) && $params['page'] ) {
00293                 $newParams['page'] = $params['page'];
00294             }
00295             return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
00296         }
00297     }
00298 
00306     protected function getThumbnailSource( $file, $params ) {
00307         return $file->getThumbnailSource( $params );
00308     }
00309 
00331     abstract protected function getScalerType( $dstPath, $checkDstPath = true );
00332 
00343     protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
00344         $params = array(
00345             'width' => $scalerParams['clientWidth'],
00346             'height' => $scalerParams['clientHeight']
00347         );
00348 
00349         return new ThumbnailImage( $image, $image->getURL(), null, $params );
00350     }
00351 
00362     protected function transformImageMagick( $image, $params ) {
00363         return $this->getMediaTransformError( $params, "Unimplemented" );
00364     }
00365 
00376     protected function transformImageMagickExt( $image, $params ) {
00377         return $this->getMediaTransformError( $params, "Unimplemented" );
00378     }
00379 
00390     protected function transformCustom( $image, $params ) {
00391         return $this->getMediaTransformError( $params, "Unimplemented" );
00392     }
00393 
00401     public function getMediaTransformError( $params, $errMsg ) {
00402         return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
00403             $params['clientHeight'], $errMsg );
00404     }
00405 
00416     protected function transformGd( $image, $params ) {
00417         return $this->getMediaTransformError( $params, "Unimplemented" );
00418     }
00419 
00426     function escapeMagickProperty( $s ) {
00427         // Double the backslashes
00428         $s = str_replace( '\\', '\\\\', $s );
00429         // Double the percents
00430         $s = str_replace( '%', '%%', $s );
00431         // Escape initial - or @
00432         if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
00433             $s = '\\' . $s;
00434         }
00435 
00436         return $s;
00437     }
00438 
00456     function escapeMagickInput( $path, $scene = false ) {
00457         # Die on initial metacharacters (caller should prepend path)
00458         $firstChar = substr( $path, 0, 1 );
00459         if ( $firstChar === '~' || $firstChar === '@' ) {
00460             throw new MWException( __METHOD__ . ': cannot escape this path name' );
00461         }
00462 
00463         # Escape glob chars
00464         $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
00465 
00466         return $this->escapeMagickPath( $path, $scene );
00467     }
00468 
00476     function escapeMagickOutput( $path, $scene = false ) {
00477         $path = str_replace( '%', '%%', $path );
00478 
00479         return $this->escapeMagickPath( $path, $scene );
00480     }
00481 
00491     protected function escapeMagickPath( $path, $scene = false ) {
00492         # Die on format specifiers (other than drive letters). The regex is
00493         # meant to match all the formats you get from "convert -list format"
00494         if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
00495             if ( wfIsWindows() && is_dir( $m[0] ) ) {
00496                 // OK, it's a drive letter
00497                 // ImageMagick has a similar exception, see IsMagickConflict()
00498             } else {
00499                 throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
00500             }
00501         }
00502 
00503         # If there are square brackets, add a do-nothing scene specification
00504         # to force a literal interpretation
00505         if ( $scene === false ) {
00506             if ( strpos( $path, '[' ) !== false ) {
00507                 $path .= '[0--1]';
00508             }
00509         } else {
00510             $path .= "[$scene]";
00511         }
00512 
00513         return $path;
00514     }
00515 
00522     protected function getMagickVersion() {
00523         global $wgMemc;
00524 
00525         $cache = $wgMemc->get( "imagemagick-version" );
00526         if ( !$cache ) {
00527             global $wgImageMagickConvertCommand;
00528             $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
00529             wfDebug( __METHOD__ . ": Running convert -version\n" );
00530             $retval = '';
00531             $return = wfShellExec( $cmd, $retval );
00532             $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
00533             if ( $x != 1 ) {
00534                 wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
00535 
00536                 return null;
00537             }
00538             $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
00539 
00540             return $matches[1];
00541         }
00542 
00543         return $cache;
00544     }
00545 
00552     public function canRotate() {
00553         return false;
00554     }
00555 
00563     public function autoRotateEnabled() {
00564         return false;
00565     }
00566 
00578     public function rotate( $file, $params ) {
00579         return new MediaTransformError( 'thumbnail_error', 0, 0,
00580             get_class( $this ) . ' rotation not implemented' );
00581     }
00582 
00590     public function mustRender( $file ) {
00591         return $this->canRotate() && $this->getRotation( $file ) != 0;
00592     }
00593 }