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