Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
15.69% |
32 / 204 |
|
0.00% |
0 / 20 |
CRAP | |
0.00% |
0 / 1 |
| TransformationalImageHandler | |
15.69% |
32 / 204 |
|
0.00% |
0 / 20 |
4112.16 | |
0.00% |
0 / 1 |
| normaliseParams | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
| extractPreRotationDimensions | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| doTransform | |
28.57% |
32 / 112 |
|
0.00% |
0 / 1 |
593.30 | |||
| getThumbnailSource | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getScalerType | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| getClientScalingThumbnailImage | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| transformImageMagick | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| transformImageMagickExt | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| transformCustom | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getMediaTransformError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| transformGd | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| escapeMagickProperty | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
| escapeMagickInput | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| escapeMagickOutput | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| escapeMagickPath | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
| getMagickVersion | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 | |||
| canRotate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| autoRotateEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| rotate | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| mustRender | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
| isImageAreaOkForThumbnaling | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
| 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 | * @license GPL-2.0-or-later |
| 11 | * @file |
| 12 | * @ingroup Media |
| 13 | */ |
| 14 | |
| 15 | use MediaWiki\FileRepo\File\File; |
| 16 | use MediaWiki\HookContainer\HookRunner; |
| 17 | use MediaWiki\MainConfigNames; |
| 18 | use MediaWiki\MediaWikiServices; |
| 19 | use MediaWiki\Shell\Shell; |
| 20 | |
| 21 | /** |
| 22 | * Handler for images that need to be transformed |
| 23 | * |
| 24 | * @stable to extend |
| 25 | * |
| 26 | * @since 1.24 |
| 27 | * @ingroup Media |
| 28 | */ |
| 29 | abstract class TransformationalImageHandler extends ImageHandler { |
| 30 | /** |
| 31 | * @stable to override |
| 32 | * @param File $image |
| 33 | * @param array &$params Transform parameters. Entries with the keys 'width' |
| 34 | * and 'height' are the respective screen width and height, while the keys |
| 35 | * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions. |
| 36 | * @return bool |
| 37 | */ |
| 38 | public function normaliseParams( $image, &$params ) { |
| 39 | if ( !parent::normaliseParams( $image, $params ) ) { |
| 40 | return false; |
| 41 | } |
| 42 | |
| 43 | # Obtain the source, pre-rotation dimensions |
| 44 | $srcWidth = $image->getWidth( $params['page'] ); |
| 45 | $srcHeight = $image->getHeight( $params['page'] ); |
| 46 | |
| 47 | # Don't make an image bigger than the source |
| 48 | if ( $params['physicalWidth'] >= $srcWidth ) { |
| 49 | $params['physicalWidth'] = $srcWidth; |
| 50 | $params['physicalHeight'] = $srcHeight; |
| 51 | |
| 52 | # Skip scaling limit checks if no scaling is required |
| 53 | # due to requested size being bigger than source. |
| 54 | if ( !$image->mustRender() ) { |
| 55 | return true; |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | return true; |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Extracts the width/height if the image will be scaled before rotating |
| 64 | * |
| 65 | * This will match the physical size/aspect ratio of the original image |
| 66 | * prior to application of the rotation -- so for a portrait image that's |
| 67 | * stored as raw landscape with 90-degress rotation, the resulting size |
| 68 | * will be wider than it is tall. |
| 69 | * |
| 70 | * @param array $params Parameters as returned by normaliseParams |
| 71 | * @param int $rotation The rotation angle that will be applied |
| 72 | * @return array ($width, $height) array |
| 73 | */ |
| 74 | public function extractPreRotationDimensions( $params, $rotation ) { |
| 75 | if ( $rotation === 90 || $rotation === 270 ) { |
| 76 | // We'll resize before rotation, so swap the dimensions again |
| 77 | $width = $params['physicalHeight']; |
| 78 | $height = $params['physicalWidth']; |
| 79 | } else { |
| 80 | $width = $params['physicalWidth']; |
| 81 | $height = $params['physicalHeight']; |
| 82 | } |
| 83 | |
| 84 | return [ $width, $height ]; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Create a thumbnail. |
| 89 | * |
| 90 | * This sets up various parameters, and then calls a helper method |
| 91 | * based on $this->getScalerType in order to scale the image. |
| 92 | * @stable to override |
| 93 | * |
| 94 | * @param File $image |
| 95 | * @param string $dstPath |
| 96 | * @param string $dstUrl |
| 97 | * @param array $params |
| 98 | * @param int $flags |
| 99 | * @return MediaTransformError|ThumbnailImage|TransformParameterError |
| 100 | */ |
| 101 | public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { |
| 102 | if ( !$this->normaliseParams( $image, $params ) ) { |
| 103 | return new TransformParameterError( $params ); |
| 104 | } |
| 105 | |
| 106 | // Create a parameter array to pass to the scaler |
| 107 | $scalerParams = [ |
| 108 | // The size to which the image will be resized |
| 109 | 'physicalWidth' => $params['physicalWidth'], |
| 110 | 'physicalHeight' => $params['physicalHeight'], |
| 111 | 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}", |
| 112 | // The size of the image on the page |
| 113 | 'clientWidth' => $params['width'], |
| 114 | 'clientHeight' => $params['height'], |
| 115 | // Comment as will be added to the Exif of the thumbnail |
| 116 | 'comment' => isset( $params['descriptionUrl'] ) |
| 117 | ? "File source: {$params['descriptionUrl']}" |
| 118 | : '', |
| 119 | // Properties of the original image |
| 120 | 'srcWidth' => $image->getWidth(), |
| 121 | 'srcHeight' => $image->getHeight(), |
| 122 | 'mimeType' => $image->getMimeType(), |
| 123 | 'dstPath' => $dstPath, |
| 124 | 'dstUrl' => $dstUrl, |
| 125 | 'interlace' => $params['interlace'] ?? false, |
| 126 | 'isFilePageThumb' => $params['isFilePageThumb'] ?? false, |
| 127 | ]; |
| 128 | |
| 129 | if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) { |
| 130 | $scalerParams['quality'] = 30; |
| 131 | } |
| 132 | |
| 133 | // For subclasses that might be paged. |
| 134 | if ( $image->isMultipage() && isset( $params['page'] ) ) { |
| 135 | $scalerParams['page'] = (int)$params['page']; |
| 136 | } |
| 137 | |
| 138 | # Determine scaler type |
| 139 | $scaler = $this->getScalerType( $dstPath ); |
| 140 | |
| 141 | if ( is_array( $scaler ) ) { |
| 142 | $scalerName = get_class( $scaler[0] ); |
| 143 | } else { |
| 144 | $scalerName = $scaler; |
| 145 | } |
| 146 | |
| 147 | wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " . |
| 148 | "thumbnail of {$image->getPath()} at $dstPath using scaler $scalerName" ); |
| 149 | |
| 150 | if ( !$image->mustRender() && |
| 151 | $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] |
| 152 | && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] |
| 153 | && !isset( $scalerParams['quality'] ) |
| 154 | ) { |
| 155 | # normaliseParams (or the user) wants us to return the unscaled image |
| 156 | wfDebug( __METHOD__ . ": returning unscaled image" ); |
| 157 | |
| 158 | return $this->getClientScalingThumbnailImage( $image, $scalerParams ); |
| 159 | } |
| 160 | |
| 161 | if ( $scaler === 'client' ) { |
| 162 | # Client-side image scaling, use the source URL |
| 163 | # Using the destination URL in a TRANSFORM_LATER request would be incorrect |
| 164 | return $this->getClientScalingThumbnailImage( $image, $scalerParams ); |
| 165 | } |
| 166 | |
| 167 | if ( $image->isTransformedLocally() && !$this->isImageAreaOkForThumbnaling( $image, $params ) ) { |
| 168 | $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea ); |
| 169 | return new TransformTooBigImageAreaError( $params, $maxImageArea ); |
| 170 | } |
| 171 | |
| 172 | if ( $flags & self::TRANSFORM_LATER ) { |
| 173 | wfDebug( __METHOD__ . ": Transforming later per flags." ); |
| 174 | $newParams = [ |
| 175 | 'width' => $scalerParams['clientWidth'], |
| 176 | 'height' => $scalerParams['clientHeight'] |
| 177 | ]; |
| 178 | if ( isset( $params['quality'] ) ) { |
| 179 | $newParams['quality'] = $params['quality']; |
| 180 | } |
| 181 | if ( isset( $params['page'] ) && $params['page'] ) { |
| 182 | $newParams['page'] = $params['page']; |
| 183 | } |
| 184 | return new ThumbnailImage( $image, $dstUrl, false, $newParams ); |
| 185 | } |
| 186 | |
| 187 | # Try to make a target path for the thumbnail |
| 188 | if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { |
| 189 | wfDebug( __METHOD__ . ": Unable to create thumbnail destination " . |
| 190 | "directory, falling back to client scaling" ); |
| 191 | |
| 192 | return $this->getClientScalingThumbnailImage( $image, $scalerParams ); |
| 193 | } |
| 194 | |
| 195 | # Transform functions and binaries need a FS source file |
| 196 | $thumbnailSource = $this->getThumbnailSource( $image, $params ); |
| 197 | |
| 198 | // If the source isn't the original, disable EXIF rotation because it's already been applied |
| 199 | if ( $scalerParams['srcWidth'] != $thumbnailSource['width'] |
| 200 | || $scalerParams['srcHeight'] != $thumbnailSource['height'] ) { |
| 201 | $scalerParams['disableRotation'] = true; |
| 202 | } |
| 203 | |
| 204 | $scalerParams['srcPath'] = $thumbnailSource['path']; |
| 205 | $scalerParams['srcWidth'] = $thumbnailSource['width']; |
| 206 | $scalerParams['srcHeight'] = $thumbnailSource['height']; |
| 207 | |
| 208 | if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy |
| 209 | wfDebugLog( 'thumbnail', |
| 210 | sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"', |
| 211 | wfHostname(), $image->getName() ) ); |
| 212 | |
| 213 | return new MediaTransformError( 'thumbnail_error', |
| 214 | $scalerParams['clientWidth'], $scalerParams['clientHeight'], |
| 215 | wfMessage( 'filemissing' ) |
| 216 | ); |
| 217 | } |
| 218 | |
| 219 | // Try a hook. Called "Bitmap" for historical reasons. |
| 220 | /** @var MediaTransformOutput $mto */ |
| 221 | $mto = null; |
| 222 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
| 223 | ->onBitmapHandlerTransform( $this, $image, $scalerParams, $mto ); |
| 224 | if ( $mto !== null ) { |
| 225 | wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto" ); |
| 226 | $scaler = 'hookaborted'; |
| 227 | } |
| 228 | |
| 229 | // $scaler will return a MediaTransformError on failure, or false on success. |
| 230 | // If the scaler is successful, it will have created a thumbnail at the destination |
| 231 | // path. |
| 232 | if ( is_array( $scaler ) && is_callable( $scaler ) ) { |
| 233 | // Allow subclasses to specify their own rendering methods. |
| 234 | $err = $scaler( $image, $scalerParams ); |
| 235 | } else { |
| 236 | switch ( $scaler ) { |
| 237 | case 'hookaborted': |
| 238 | # Handled by the hook above |
| 239 | $err = $mto->isError() ? $mto : false; |
| 240 | break; |
| 241 | case 'im': |
| 242 | $err = $this->transformImageMagick( $image, $scalerParams ); |
| 243 | break; |
| 244 | case 'custom': |
| 245 | $err = $this->transformCustom( $image, $scalerParams ); |
| 246 | break; |
| 247 | case 'imext': |
| 248 | $err = $this->transformImageMagickExt( $image, $scalerParams ); |
| 249 | break; |
| 250 | case 'gd': |
| 251 | default: |
| 252 | $err = $this->transformGd( $image, $scalerParams ); |
| 253 | break; |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | // Remove the file if a zero-byte thumbnail was created, or if there was an error |
| 258 | // @phan-suppress-next-line PhanTypeMismatchArgument Relaying on bool/int conversion to cast objects correct |
| 259 | $removed = $this->removeBadFile( $dstPath, (bool)$err ); |
| 260 | if ( $err ) { |
| 261 | # transform returned MediaTransforError |
| 262 | return $err; |
| 263 | } |
| 264 | |
| 265 | if ( $removed ) { |
| 266 | // Thumbnail was zero-byte and had to be removed |
| 267 | return new MediaTransformError( 'thumbnail_error', |
| 268 | $scalerParams['clientWidth'], $scalerParams['clientHeight'], |
| 269 | wfMessage( 'unknown-error' ) |
| 270 | ); |
| 271 | } |
| 272 | |
| 273 | if ( $mto ) { |
| 274 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
| 275 | return $mto; |
| 276 | } |
| 277 | |
| 278 | $newParams = [ |
| 279 | 'width' => $scalerParams['clientWidth'], |
| 280 | 'height' => $scalerParams['clientHeight'] |
| 281 | ]; |
| 282 | if ( isset( $params['quality'] ) ) { |
| 283 | $newParams['quality'] = $params['quality']; |
| 284 | } |
| 285 | if ( isset( $params['page'] ) && $params['page'] ) { |
| 286 | $newParams['page'] = $params['page']; |
| 287 | } |
| 288 | return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams ); |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * Get the source file for the transform |
| 293 | * |
| 294 | * @param File $file |
| 295 | * @param array $params |
| 296 | * @return array Array with keys width, height and path. |
| 297 | */ |
| 298 | protected function getThumbnailSource( $file, $params ) { |
| 299 | return $file->getThumbnailSource( $params ); |
| 300 | } |
| 301 | |
| 302 | /** |
| 303 | * Returns what sort of scaler type should be used. |
| 304 | * |
| 305 | * Values can be one of client, im, custom, gd, imext, or an array |
| 306 | * of object, method-name to call that specific method. |
| 307 | * |
| 308 | * If specifying a custom scaler command with [ Obj, method ], |
| 309 | * the method in question should take 2 parameters, a File object, |
| 310 | * and a $scalerParams array with various options (See doTransform |
| 311 | * for what is in $scalerParams). On error it should return a |
| 312 | * MediaTransformError object. On success it should return false, |
| 313 | * and simply make sure the thumbnail file is located at |
| 314 | * $scalerParams['dstPath']. |
| 315 | * |
| 316 | * If there is a problem with the output path, it returns "client" |
| 317 | * to do client side scaling. |
| 318 | * |
| 319 | * @param string|null $dstPath |
| 320 | * @param bool $checkDstPath Check that $dstPath is valid |
| 321 | * @return string|callable One of client, im, custom, gd, imext, or a callable |
| 322 | */ |
| 323 | abstract protected function getScalerType( $dstPath, $checkDstPath = true ); |
| 324 | |
| 325 | /** |
| 326 | * Get a ThumbnailImage that respresents an image that will be scaled |
| 327 | * client side |
| 328 | * |
| 329 | * @stable to override |
| 330 | * @param File $image File associated with this thumbnail |
| 331 | * @param array $scalerParams Array with scaler params |
| 332 | * @return ThumbnailImage |
| 333 | * |
| 334 | * @todo FIXME: No rotation support |
| 335 | */ |
| 336 | protected function getClientScalingThumbnailImage( $image, $scalerParams ) { |
| 337 | $params = [ |
| 338 | 'width' => $scalerParams['clientWidth'], |
| 339 | 'height' => $scalerParams['clientHeight'] |
| 340 | ]; |
| 341 | |
| 342 | $url = $image->getUrl(); |
| 343 | if ( isset( $scalerParams['isFilePageThumb'] ) && $scalerParams['isFilePageThumb'] ) { |
| 344 | // Use a versioned URL on file description pages |
| 345 | $url = $image->getFilePageThumbUrl( $url ); |
| 346 | } |
| 347 | |
| 348 | return new ThumbnailImage( $image, $url, null, $params ); |
| 349 | } |
| 350 | |
| 351 | /** |
| 352 | * Transform an image using ImageMagick |
| 353 | * |
| 354 | * This is a stub method. The real method is in BitmapHandler. |
| 355 | * |
| 356 | * @stable to override |
| 357 | * @param File $image File associated with this thumbnail |
| 358 | * @param array $params Array with scaler params |
| 359 | * |
| 360 | * @return MediaTransformError|false 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 BitmapHandler. |
| 370 | * |
| 371 | * @stable to override |
| 372 | * @param File $image File associated with this thumbnail |
| 373 | * @param array $params Array with scaler params |
| 374 | * |
| 375 | * @return MediaTransformError Error object if error occurred, false (=no error) otherwise |
| 376 | */ |
| 377 | protected function transformImageMagickExt( $image, $params ) { |
| 378 | return $this->getMediaTransformError( $params, "Unimplemented" ); |
| 379 | } |
| 380 | |
| 381 | /** |
| 382 | * Transform an image using a custom command |
| 383 | * |
| 384 | * This is a stub method. The real method is in BitmapHandler. |
| 385 | * |
| 386 | * @stable to override |
| 387 | * @param File $image File associated with this thumbnail |
| 388 | * @param array $params Array with scaler params |
| 389 | * |
| 390 | * @return MediaTransformError Error object if error occurred, false (=no error) otherwise |
| 391 | */ |
| 392 | protected function transformCustom( $image, $params ) { |
| 393 | return $this->getMediaTransformError( $params, "Unimplemented" ); |
| 394 | } |
| 395 | |
| 396 | /** |
| 397 | * Get a MediaTransformError with error 'thumbnail_error' |
| 398 | * |
| 399 | * @param array $params Parameter array as passed to the transform* functions |
| 400 | * @param string $errMsg Error message |
| 401 | * @return MediaTransformError |
| 402 | */ |
| 403 | public function getMediaTransformError( $params, $errMsg ) { |
| 404 | return new MediaTransformError( 'thumbnail_error', $params['clientWidth'], |
| 405 | $params['clientHeight'], $errMsg ); |
| 406 | } |
| 407 | |
| 408 | /** |
| 409 | * Transform an image using the built in GD library |
| 410 | * |
| 411 | * This is a stub method. The real method is in BitmapHandler. |
| 412 | * |
| 413 | * @param File $image File associated with this thumbnail |
| 414 | * @param array $params Array with scaler params |
| 415 | * |
| 416 | * @return MediaTransformError Error object if error occurred, false (=no error) otherwise |
| 417 | */ |
| 418 | protected function transformGd( $image, $params ) { |
| 419 | return $this->getMediaTransformError( $params, "Unimplemented" ); |
| 420 | } |
| 421 | |
| 422 | /** |
| 423 | * Escape a string for ImageMagick's property input (e.g. -set -comment) |
| 424 | * See InterpretImageProperties() in magick/property.c |
| 425 | * @param string $s |
| 426 | * @return string |
| 427 | */ |
| 428 | protected function escapeMagickProperty( $s ) { |
| 429 | // Double the backslashes |
| 430 | $s = str_replace( '\\', '\\\\', $s ); |
| 431 | // Double the percents |
| 432 | $s = str_replace( '%', '%%', $s ); |
| 433 | // Escape initial - or @ |
| 434 | if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) { |
| 435 | $s = '\\' . $s; |
| 436 | } |
| 437 | |
| 438 | return $s; |
| 439 | } |
| 440 | |
| 441 | /** |
| 442 | * Escape a string for ImageMagick's input filenames. See ExpandFilenames() |
| 443 | * and GetPathComponent() in magick/utility.c. |
| 444 | * |
| 445 | * This won't work with an initial ~ or @, so input files should be prefixed |
| 446 | * with the directory name. |
| 447 | * |
| 448 | * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but |
| 449 | * it's broken in a way that doesn't involve trying to convert every file |
| 450 | * in a directory, so we're better off escaping and waiting for the bugfix |
| 451 | * to filter down to users. |
| 452 | * |
| 453 | * @param string $path The file path |
| 454 | * @param string|false $scene The scene specification, or false if there is none |
| 455 | * @return string |
| 456 | */ |
| 457 | protected function escapeMagickInput( $path, $scene = false ) { |
| 458 | # Die on initial metacharacters (caller should prepend path) |
| 459 | $firstChar = substr( $path, 0, 1 ); |
| 460 | if ( $firstChar === '~' || $firstChar === '@' ) { |
| 461 | throw new InvalidArgumentException( __METHOD__ . ': cannot escape this path name' ); |
| 462 | } |
| 463 | |
| 464 | # Escape glob chars |
| 465 | $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path ); |
| 466 | |
| 467 | return $this->escapeMagickPath( $path, $scene ); |
| 468 | } |
| 469 | |
| 470 | /** |
| 471 | * Escape a string for ImageMagick's output filename. See |
| 472 | * InterpretImageFilename() in magick/image.c. |
| 473 | * @param string $path The file path |
| 474 | * @param string|false $scene The scene specification, or false if there is none |
| 475 | * @return string |
| 476 | */ |
| 477 | protected function escapeMagickOutput( $path, $scene = false ) { |
| 478 | $path = str_replace( '%', '%%', $path ); |
| 479 | |
| 480 | return $this->escapeMagickPath( $path, $scene ); |
| 481 | } |
| 482 | |
| 483 | /** |
| 484 | * Armour a string against ImageMagick's GetPathComponent(). This is a |
| 485 | * helper function for escapeMagickInput() and escapeMagickOutput(). |
| 486 | * |
| 487 | * @param string $path The file path |
| 488 | * @param string|false $scene The scene specification, or false if there is none |
| 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 InvalidArgumentException( __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 ( str_contains( $path, '[' ) ) { |
| 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|false Representing the IM version; false on error |
| 521 | */ |
| 522 | protected function getMagickVersion() { |
| 523 | $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache(); |
| 524 | $method = __METHOD__; |
| 525 | return $cache->getWithSetCallback( |
| 526 | $cache->makeGlobalKey( 'imagemagick-version' ), |
| 527 | $cache::TTL_HOUR, |
| 528 | static function () use ( $method ) { |
| 529 | $imageMagickConvertCommand = MediaWikiServices::getInstance() |
| 530 | ->getMainConfig()->get( MainConfigNames::ImageMagickConvertCommand ); |
| 531 | |
| 532 | $cmd = Shell::escape( $imageMagickConvertCommand ) . ' -version'; |
| 533 | wfDebug( $method . ": Running convert -version" ); |
| 534 | $retval = ''; |
| 535 | $return = wfShellExecWithStderr( $cmd, $retval ); |
| 536 | $x = preg_match( |
| 537 | '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches |
| 538 | ); |
| 539 | if ( $x != 1 ) { |
| 540 | wfDebug( $method . ": ImageMagick version check failed" ); |
| 541 | return false; |
| 542 | } |
| 543 | |
| 544 | return $matches[1]; |
| 545 | } |
| 546 | ); |
| 547 | } |
| 548 | |
| 549 | /** |
| 550 | * Returns whether the current scaler supports rotation. |
| 551 | * |
| 552 | * @since 1.24 No longer static |
| 553 | * @stable to override |
| 554 | * @return bool |
| 555 | */ |
| 556 | public function canRotate() { |
| 557 | return false; |
| 558 | } |
| 559 | |
| 560 | /** |
| 561 | * Should we automatically rotate an image based on exif |
| 562 | * |
| 563 | * @since 1.24 No longer static |
| 564 | * @stable to override |
| 565 | * @see $wgEnableAutoRotation |
| 566 | * @return bool Whether auto rotation is enabled |
| 567 | */ |
| 568 | public function autoRotateEnabled() { |
| 569 | return false; |
| 570 | } |
| 571 | |
| 572 | /** |
| 573 | * Rotate a thumbnail. |
| 574 | * |
| 575 | * This is a stub. See BitmapHandler::rotate. |
| 576 | * |
| 577 | * @stable to override |
| 578 | * @param File $file |
| 579 | * @param array{rotation:int,srcPath:string,dstPath:string} $params Rotate parameters. |
| 580 | * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 |
| 581 | * @since 1.24 Is non-static. From 1.21 it was static |
| 582 | * @return MediaTransformError|false |
| 583 | */ |
| 584 | public function rotate( $file, $params ) { |
| 585 | return new MediaTransformError( 'thumbnail_error', 0, 0, |
| 586 | static::class . ' rotation not implemented' ); |
| 587 | } |
| 588 | |
| 589 | /** |
| 590 | * Returns whether the file needs to be rendered. Returns true if the |
| 591 | * file requires rotation and we are able to rotate it. |
| 592 | * |
| 593 | * @stable to override |
| 594 | * @param File $file |
| 595 | * @return bool |
| 596 | */ |
| 597 | public function mustRender( $file ) { |
| 598 | return $this->canRotate() && $this->getRotation( $file ) != 0; |
| 599 | } |
| 600 | |
| 601 | /** |
| 602 | * Check if the file is smaller than the maximum image area for thumbnailing. |
| 603 | * |
| 604 | * Runs the 'BitmapHandlerCheckImageArea' hook. |
| 605 | * |
| 606 | * @stable to override |
| 607 | * @param File $file |
| 608 | * @param array &$params |
| 609 | * @return bool |
| 610 | * @since 1.25 |
| 611 | */ |
| 612 | public function isImageAreaOkForThumbnaling( $file, &$params ) { |
| 613 | $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea ); |
| 614 | |
| 615 | # For historical reasons, hook starts with BitmapHandler |
| 616 | $checkImageAreaHookResult = null; |
| 617 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onBitmapHandlerCheckImageArea( |
| 618 | $file, $params, $checkImageAreaHookResult ); |
| 619 | |
| 620 | if ( $checkImageAreaHookResult !== null ) { |
| 621 | // was set by hook, so return that value |
| 622 | return (bool)$checkImageAreaHookResult; |
| 623 | } |
| 624 | |
| 625 | if ( $maxImageArea === false ) { |
| 626 | // Checking is disabled, fine to thumbnail |
| 627 | return true; |
| 628 | } |
| 629 | |
| 630 | $srcWidth = $file->getWidth( $params['page'] ); |
| 631 | $srcHeight = $file->getHeight( $params['page'] ); |
| 632 | |
| 633 | if ( $srcWidth * $srcHeight > $maxImageArea |
| 634 | && !( $file->getMimeType() === 'image/jpeg' |
| 635 | && $this->getScalerType( null, false ) === 'im' ) |
| 636 | ) { |
| 637 | # Only ImageMagick can efficiently downsize jpg images without loading |
| 638 | # the entire file in memory |
| 639 | return false; |
| 640 | } |
| 641 | return true; |
| 642 | } |
| 643 | } |