[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Base class for handlers which require transforming images in a 4 * similar way as BitmapHandler does. 5 * 6 * This was split from BitmapHandler on the basis that some extensions 7 * might want to work in a similar way to BitmapHandler, but for 8 * different formats. 9 * 10 * This program is free software; you can redistribute it and/or modify 11 * it under the terms of the GNU General Public License as published by 12 * the Free Software Foundation; either version 2 of the License, or 13 * (at your option) any later version. 14 * 15 * This program is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU General Public License for more details. 19 * 20 * You should have received a copy of the GNU General Public License along 21 * with this program; if not, write to the Free Software Foundation, Inc., 22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 23 * http://www.gnu.org/copyleft/gpl.html 24 * 25 * @file 26 * @ingroup Media 27 */ 28 29 /** 30 * Handler for images that need to be transformed 31 * 32 * @since 1.24 33 * @ingroup Media 34 */ 35 abstract class TransformationalImageHandler extends ImageHandler { 36 /** 37 * @param File $image 38 * @param array $params Transform parameters. Entries with the keys 'width' 39 * and 'height' are the respective screen width and height, while the keys 40 * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions. 41 * @return bool 42 */ 43 function normaliseParams( $image, &$params ) { 44 if ( !parent::normaliseParams( $image, $params ) ) { 45 return false; 46 } 47 48 # Obtain the source, pre-rotation dimensions 49 $srcWidth = $image->getWidth( $params['page'] ); 50 $srcHeight = $image->getHeight( $params['page'] ); 51 52 # Don't make an image bigger than the source 53 if ( $params['physicalWidth'] >= $srcWidth ) { 54 $params['physicalWidth'] = $srcWidth; 55 $params['physicalHeight'] = $srcHeight; 56 57 # Skip scaling limit checks if no scaling is required 58 # due to requested size being bigger than source. 59 if ( !$image->mustRender() ) { 60 return true; 61 } 62 } 63 64 # Check if the file is smaller than the maximum image area for thumbnailing 65 # For historical reasons, hook starts with BitmapHandler 66 $checkImageAreaHookResult = null; 67 wfRunHooks( 68 'BitmapHandlerCheckImageArea', 69 array( $image, &$params, &$checkImageAreaHookResult ) 70 ); 71 72 if ( is_null( $checkImageAreaHookResult ) ) { 73 global $wgMaxImageArea; 74 75 if ( $srcWidth * $srcHeight > $wgMaxImageArea 76 && !( $image->getMimeType() == 'image/jpeg' 77 && $this->getScalerType( false, false ) == 'im' ) 78 ) { 79 # Only ImageMagick can efficiently downsize jpg images without loading 80 # the entire file in memory 81 return false; 82 } 83 } else { 84 return $checkImageAreaHookResult; 85 } 86 87 return true; 88 } 89 90 /** 91 * Extracts the width/height if the image will be scaled before rotating 92 * 93 * This will match the physical size/aspect ratio of the original image 94 * prior to application of the rotation -- so for a portrait image that's 95 * stored as raw landscape with 90-degress rotation, the resulting size 96 * will be wider than it is tall. 97 * 98 * @param array $params Parameters as returned by normaliseParams 99 * @param int $rotation The rotation angle that will be applied 100 * @return array ($width, $height) array 101 */ 102 public function extractPreRotationDimensions( $params, $rotation ) { 103 if ( $rotation == 90 || $rotation == 270 ) { 104 # We'll resize before rotation, so swap the dimensions again 105 $width = $params['physicalHeight']; 106 $height = $params['physicalWidth']; 107 } else { 108 $width = $params['physicalWidth']; 109 $height = $params['physicalHeight']; 110 } 111 112 return array( $width, $height ); 113 } 114 115 /** 116 * Create a thumbnail. 117 * 118 * This sets up various parameters, and then calls a helper method 119 * based on $this->getScalerType in order to scale the image. 120 * 121 * @param File $image 122 * @param string $dstPath 123 * @param string $dstUrl 124 * @param array $params 125 * @param int $flags 126 * @return MediaTransformError|ThumbnailImage|TransformParameterError 127 */ 128 function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { 129 if ( !$this->normaliseParams( $image, $params ) ) { 130 return new TransformParameterError( $params ); 131 } 132 133 # Create a parameter array to pass to the scaler 134 $scalerParams = array( 135 # The size to which the image will be resized 136 'physicalWidth' => $params['physicalWidth'], 137 'physicalHeight' => $params['physicalHeight'], 138 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}", 139 # The size of the image on the page 140 'clientWidth' => $params['width'], 141 'clientHeight' => $params['height'], 142 # Comment as will be added to the Exif of the thumbnail 143 'comment' => isset( $params['descriptionUrl'] ) 144 ? "File source: {$params['descriptionUrl']}" 145 : '', 146 # Properties of the original image 147 'srcWidth' => $image->getWidth(), 148 'srcHeight' => $image->getHeight(), 149 'mimeType' => $image->getMimeType(), 150 'dstPath' => $dstPath, 151 'dstUrl' => $dstUrl, 152 ); 153 154 if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) { 155 $scalerParams['quality'] = 30; 156 } 157 158 // For subclasses that might be paged. 159 if ( $image->isMultipage() && isset( $params['page'] ) ) { 160 $scalerParams['page'] = intval( $params['page'] ); 161 } 162 163 # Determine scaler type 164 $scaler = $this->getScalerType( $dstPath ); 165 166 if ( is_array( $scaler ) ) { 167 $scalerName = get_class( $scaler[0] ); 168 } else { 169 $scalerName = $scaler; 170 } 171 172 wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " . 173 "thumbnail at $dstPath using scaler $scalerName\n" ); 174 175 if ( !$image->mustRender() && 176 $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] 177 && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] 178 && !isset( $scalerParams['quality'] ) 179 ) { 180 181 # normaliseParams (or the user) wants us to return the unscaled image 182 wfDebug( __METHOD__ . ": returning unscaled image\n" ); 183 184 return $this->getClientScalingThumbnailImage( $image, $scalerParams ); 185 } 186 187 if ( $scaler == 'client' ) { 188 # Client-side image scaling, use the source URL 189 # Using the destination URL in a TRANSFORM_LATER request would be incorrect 190 return $this->getClientScalingThumbnailImage( $image, $scalerParams ); 191 } 192 193 if ( $flags & self::TRANSFORM_LATER ) { 194 wfDebug( __METHOD__ . ": Transforming later per flags.\n" ); 195 $newParams = array( 196 'width' => $scalerParams['clientWidth'], 197 'height' => $scalerParams['clientHeight'] 198 ); 199 if ( isset( $params['quality'] ) ) { 200 $newParams['quality'] = $params['quality']; 201 } 202 if ( isset( $params['page'] ) && $params['page'] ) { 203 $newParams['page'] = $params['page']; 204 } 205 return new ThumbnailImage( $image, $dstUrl, false, $newParams ); 206 } 207 208 # Try to make a target path for the thumbnail 209 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { 210 wfDebug( __METHOD__ . ": Unable to create thumbnail destination " . 211 "directory, falling back to client scaling\n" ); 212 213 return $this->getClientScalingThumbnailImage( $image, $scalerParams ); 214 } 215 216 # Transform functions and binaries need a FS source file 217 $thumbnailSource = $this->getThumbnailSource( $image, $params ); 218 219 $scalerParams['srcPath'] = $thumbnailSource['path']; 220 $scalerParams['srcWidth'] = $thumbnailSource['width']; 221 $scalerParams['srcHeight'] = $thumbnailSource['height']; 222 223 if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy 224 wfDebugLog( 'thumbnail', 225 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"', 226 wfHostname(), $image->getName() ) ); 227 228 return new MediaTransformError( 'thumbnail_error', 229 $scalerParams['clientWidth'], $scalerParams['clientHeight'], 230 wfMessage( 'filemissing' )->text() 231 ); 232 } 233 234 # Try a hook. Called "Bitmap" for historical reasons. 235 /** @var $mto MediaTransformOutput */ 236 $mto = null; 237 wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) ); 238 if ( !is_null( $mto ) ) { 239 wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" ); 240 $scaler = 'hookaborted'; 241 } 242 243 // $scaler will return a MediaTransformError on failure, or false on success. 244 // If the scaler is succesful, it will have created a thumbnail at the destination 245 // path. 246 if ( is_array( $scaler ) && is_callable( $scaler ) ) { 247 // Allow subclasses to specify their own rendering methods. 248 $err = call_user_func( $scaler, $image, $scalerParams ); 249 } else { 250 switch ( $scaler ) { 251 case 'hookaborted': 252 # Handled by the hook above 253 $err = $mto->isError() ? $mto : false; 254 break; 255 case 'im': 256 $err = $this->transformImageMagick( $image, $scalerParams ); 257 break; 258 case 'custom': 259 $err = $this->transformCustom( $image, $scalerParams ); 260 break; 261 case 'imext': 262 $err = $this->transformImageMagickExt( $image, $scalerParams ); 263 break; 264 case 'gd': 265 default: 266 $err = $this->transformGd( $image, $scalerParams ); 267 break; 268 } 269 } 270 271 # Remove the file if a zero-byte thumbnail was created, or if there was an error 272 $removed = $this->removeBadFile( $dstPath, (bool)$err ); 273 if ( $err ) { 274 # transform returned MediaTransforError 275 return $err; 276 } elseif ( $removed ) { 277 # Thumbnail was zero-byte and had to be removed 278 return new MediaTransformError( 'thumbnail_error', 279 $scalerParams['clientWidth'], $scalerParams['clientHeight'], 280 wfMessage( 'unknown-error' )->text() 281 ); 282 } elseif ( $mto ) { 283 return $mto; 284 } else { 285 $newParams = array( 286 'width' => $scalerParams['clientWidth'], 287 'height' => $scalerParams['clientHeight'] 288 ); 289 if ( isset( $params['quality'] ) ) { 290 $newParams['quality'] = $params['quality']; 291 } 292 if ( isset( $params['page'] ) && $params['page'] ) { 293 $newParams['page'] = $params['page']; 294 } 295 return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams ); 296 } 297 } 298 299 /** 300 * Get the source file for the transform 301 * 302 * @param $file File 303 * @param $params Array 304 * @return Array Array with keys width, height and path. 305 */ 306 protected function getThumbnailSource( $file, $params ) { 307 return $file->getThumbnailSource( $params ); 308 } 309 310 /** 311 * Returns what sort of scaler type should be used. 312 * 313 * Values can be one of client, im, custom, gd, imext, or an array 314 * of object, method-name to call that specific method. 315 * 316 * If specifying a custom scaler command with array( Obj, method ), 317 * the method in question should take 2 parameters, a File object, 318 * and a $scalerParams array with various options (See doTransform 319 * for what is in $scalerParams). On error it should return a 320 * MediaTransformError object. On success it should return false, 321 * and simply make sure the thumbnail file is located at 322 * $scalerParams['dstPath']. 323 * 324 * If there is a problem with the output path, it returns "client" 325 * to do client side scaling. 326 * 327 * @param string $dstPath 328 * @param bool $checkDstPath Check that $dstPath is valid 329 * @return string|Callable One of client, im, custom, gd, imext, or a Callable array. 330 */ 331 abstract protected function getScalerType( $dstPath, $checkDstPath = true ); 332 333 /** 334 * Get a ThumbnailImage that respresents an image that will be scaled 335 * client side 336 * 337 * @param File $image File associated with this thumbnail 338 * @param array $scalerParams Array with scaler params 339 * @return ThumbnailImage 340 * 341 * @todo FIXME: No rotation support 342 */ 343 protected function getClientScalingThumbnailImage( $image, $scalerParams ) { 344 $params = array( 345 'width' => $scalerParams['clientWidth'], 346 'height' => $scalerParams['clientHeight'] 347 ); 348 349 return new ThumbnailImage( $image, $image->getURL(), null, $params ); 350 } 351 352 /** 353 * Transform an image using ImageMagick 354 * 355 * This is a stub method. The real method is in BitmapHander. 356 * 357 * @param File $image File associated with this thumbnail 358 * @param array $params Array with scaler params 359 * 360 * @return MediaTransformError Error object if error occurred, false (=no error) otherwise 361 */ 362 protected function transformImageMagick( $image, $params ) { 363 return $this->getMediaTransformError( $params, "Unimplemented" ); 364 } 365 366 /** 367 * Transform an image using the Imagick PHP extension 368 * 369 * This is a stub method. The real method is in BitmapHander. 370 * 371 * @param File $image File associated with this thumbnail 372 * @param array $params Array with scaler params 373 * 374 * @return MediaTransformError Error object if error occurred, false (=no error) otherwise 375 */ 376 protected function transformImageMagickExt( $image, $params ) { 377 return $this->getMediaTransformError( $params, "Unimplemented" ); 378 } 379 380 /** 381 * Transform an image using a custom command 382 * 383 * This is a stub method. The real method is in BitmapHander. 384 * 385 * @param File $image File associated with this thumbnail 386 * @param array $params Array with scaler params 387 * 388 * @return MediaTransformError Error object if error occurred, false (=no error) otherwise 389 */ 390 protected function transformCustom( $image, $params ) { 391 return $this->getMediaTransformError( $params, "Unimplemented" ); 392 } 393 394 /** 395 * Get a MediaTransformError with error 'thumbnail_error' 396 * 397 * @param array $params Parameter array as passed to the transform* functions 398 * @param string $errMsg Error message 399 * @return MediaTransformError 400 */ 401 public function getMediaTransformError( $params, $errMsg ) { 402 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'], 403 $params['clientHeight'], $errMsg ); 404 } 405 406 /** 407 * Transform an image using the built in GD library 408 * 409 * This is a stub method. The real method is in BitmapHander. 410 * 411 * @param File $image File associated with this thumbnail 412 * @param array $params Array with scaler params 413 * 414 * @return MediaTransformError Error object if error occurred, false (=no error) otherwise 415 */ 416 protected function transformGd( $image, $params ) { 417 return $this->getMediaTransformError( $params, "Unimplemented" ); 418 } 419 420 /** 421 * Escape a string for ImageMagick's property input (e.g. -set -comment) 422 * See InterpretImageProperties() in magick/property.c 423 * @param string $s 424 * @return string 425 */ 426 function escapeMagickProperty( $s ) { 427 // Double the backslashes 428 $s = str_replace( '\\', '\\\\', $s ); 429 // Double the percents 430 $s = str_replace( '%', '%%', $s ); 431 // Escape initial - or @ 432 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) { 433 $s = '\\' . $s; 434 } 435 436 return $s; 437 } 438 439 /** 440 * Escape a string for ImageMagick's input filenames. See ExpandFilenames() 441 * and GetPathComponent() in magick/utility.c. 442 * 443 * This won't work with an initial ~ or @, so input files should be prefixed 444 * with the directory name. 445 * 446 * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but 447 * it's broken in a way that doesn't involve trying to convert every file 448 * in a directory, so we're better off escaping and waiting for the bugfix 449 * to filter down to users. 450 * 451 * @param string $path The file path 452 * @param bool|string $scene The scene specification, or false if there is none 453 * @throws MWException 454 * @return string 455 */ 456 function escapeMagickInput( $path, $scene = false ) { 457 # Die on initial metacharacters (caller should prepend path) 458 $firstChar = substr( $path, 0, 1 ); 459 if ( $firstChar === '~' || $firstChar === '@' ) { 460 throw new MWException( __METHOD__ . ': cannot escape this path name' ); 461 } 462 463 # Escape glob chars 464 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path ); 465 466 return $this->escapeMagickPath( $path, $scene ); 467 } 468 469 /** 470 * Escape a string for ImageMagick's output filename. See 471 * InterpretImageFilename() in magick/image.c. 472 * @param string $path The file path 473 * @param bool|string $scene The scene specification, or false if there is none 474 * @return string 475 */ 476 function escapeMagickOutput( $path, $scene = false ) { 477 $path = str_replace( '%', '%%', $path ); 478 479 return $this->escapeMagickPath( $path, $scene ); 480 } 481 482 /** 483 * Armour a string against ImageMagick's GetPathComponent(). This is a 484 * helper function for escapeMagickInput() and escapeMagickOutput(). 485 * 486 * @param string $path The file path 487 * @param bool|string $scene The scene specification, or false if there is none 488 * @throws MWException 489 * @return string 490 */ 491 protected function escapeMagickPath( $path, $scene = false ) { 492 # Die on format specifiers (other than drive letters). The regex is 493 # meant to match all the formats you get from "convert -list format" 494 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) { 495 if ( wfIsWindows() && is_dir( $m[0] ) ) { 496 // OK, it's a drive letter 497 // ImageMagick has a similar exception, see IsMagickConflict() 498 } else { 499 throw new MWException( __METHOD__ . ': unexpected colon character in path name' ); 500 } 501 } 502 503 # If there are square brackets, add a do-nothing scene specification 504 # to force a literal interpretation 505 if ( $scene === false ) { 506 if ( strpos( $path, '[' ) !== false ) { 507 $path .= '[0--1]'; 508 } 509 } else { 510 $path .= "[$scene]"; 511 } 512 513 return $path; 514 } 515 516 /** 517 * Retrieve the version of the installed ImageMagick 518 * You can use PHPs version_compare() to use this value 519 * Value is cached for one hour. 520 * @return string Representing the IM version. 521 */ 522 protected function getMagickVersion() { 523 global $wgMemc; 524 525 $cache = $wgMemc->get( "imagemagick-version" ); 526 if ( !$cache ) { 527 global $wgImageMagickConvertCommand; 528 $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version'; 529 wfDebug( __METHOD__ . ": Running convert -version\n" ); 530 $retval = ''; 531 $return = wfShellExec( $cmd, $retval ); 532 $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches ); 533 if ( $x != 1 ) { 534 wfDebug( __METHOD__ . ": ImageMagick version check failed\n" ); 535 536 return null; 537 } 538 $wgMemc->set( "imagemagick-version", $matches[1], 3600 ); 539 540 return $matches[1]; 541 } 542 543 return $cache; 544 } 545 546 /** 547 * Returns whether the current scaler supports rotation. 548 * 549 * @since 1.24 No longer static 550 * @return bool 551 */ 552 public function canRotate() { 553 return false; 554 } 555 556 /** 557 * Should we automatically rotate an image based on exif 558 * 559 * @since 1.24 No longer static 560 * @see $wgEnableAutoRotation 561 * @return bool Whether auto rotation is enabled 562 */ 563 public function autoRotateEnabled() { 564 return false; 565 } 566 567 /** 568 * Rotate a thumbnail. 569 * 570 * This is a stub. See BitmapHandler::rotate. 571 * 572 * @param File $file 573 * @param array $params Rotate parameters. 574 * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 575 * @since 1.24 Is non-static. From 1.21 it was static 576 * @return bool 577 */ 578 public function rotate( $file, $params ) { 579 return new MediaTransformError( 'thumbnail_error', 0, 0, 580 get_class( $this ) . ' rotation not implemented' ); 581 } 582 583 /** 584 * Returns whether the file needs to be rendered. Returns true if the 585 * file requires rotation and we are able to rotate it. 586 * 587 * @param File $file 588 * @return bool 589 */ 590 public function mustRender( $file ) { 591 return $this->canRotate() && $this->getRotation( $file ) != 0; 592 } 593 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |