BitmapHandler.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. <?php
  2. /**
  3. * Generic handler for bitmap images.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Media
  22. */
  23. use MediaWiki\Shell\Shell;
  24. /**
  25. * Generic handler for bitmap images
  26. *
  27. * @ingroup Media
  28. */
  29. class BitmapHandler extends TransformationalImageHandler {
  30. /**
  31. * Returns which scaler type should be used. Creates parent directories
  32. * for $dstPath and returns 'client' on error
  33. *
  34. * @param string $dstPath
  35. * @param bool $checkDstPath
  36. * @return string|Callable One of client, im, custom, gd, imext or an array( object, method )
  37. */
  38. protected function getScalerType( $dstPath, $checkDstPath = true ) {
  39. global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
  40. if ( !$dstPath && $checkDstPath ) {
  41. # No output path available, client side scaling only
  42. $scaler = 'client';
  43. } elseif ( !$wgUseImageResize ) {
  44. $scaler = 'client';
  45. } elseif ( $wgUseImageMagick ) {
  46. $scaler = 'im';
  47. } elseif ( $wgCustomConvertCommand ) {
  48. $scaler = 'custom';
  49. } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
  50. $scaler = 'gd';
  51. } elseif ( class_exists( 'Imagick' ) ) {
  52. $scaler = 'imext';
  53. } else {
  54. $scaler = 'client';
  55. }
  56. return $scaler;
  57. }
  58. public function makeParamString( $params ) {
  59. $res = parent::makeParamString( $params );
  60. if ( isset( $params['interlace'] ) && $params['interlace'] ) {
  61. return "interlaced-{$res}";
  62. } else {
  63. return $res;
  64. }
  65. }
  66. public function parseParamString( $str ) {
  67. $remainder = preg_replace( '/^interlaced-/', '', $str );
  68. $params = parent::parseParamString( $remainder );
  69. if ( $params === false ) {
  70. return false;
  71. }
  72. $params['interlace'] = $str !== $remainder;
  73. return $params;
  74. }
  75. public function validateParam( $name, $value ) {
  76. if ( $name === 'interlace' ) {
  77. return $value === false || $value === true;
  78. } else {
  79. return parent::validateParam( $name, $value );
  80. }
  81. }
  82. /**
  83. * @param File $image
  84. * @param array &$params
  85. * @return bool
  86. */
  87. public function normaliseParams( $image, &$params ) {
  88. global $wgMaxInterlacingAreas;
  89. if ( !parent::normaliseParams( $image, $params ) ) {
  90. return false;
  91. }
  92. $mimeType = $image->getMimeType();
  93. $interlace = isset( $params['interlace'] ) && $params['interlace']
  94. && isset( $wgMaxInterlacingAreas[$mimeType] )
  95. && $this->getImageArea( $image ) <= $wgMaxInterlacingAreas[$mimeType];
  96. $params['interlace'] = $interlace;
  97. return true;
  98. }
  99. /**
  100. * Get ImageMagick subsampling factors for the target JPEG pixel format.
  101. *
  102. * @param string $pixelFormat one of 'yuv444', 'yuv422', 'yuv420'
  103. * @return string[] List of sampling factors
  104. */
  105. protected function imageMagickSubsampling( $pixelFormat ) {
  106. switch ( $pixelFormat ) {
  107. case 'yuv444':
  108. return [ '1x1', '1x1', '1x1' ];
  109. case 'yuv422':
  110. return [ '2x1', '1x1', '1x1' ];
  111. case 'yuv420':
  112. return [ '2x2', '1x1', '1x1' ];
  113. default:
  114. throw new MWException( 'Invalid pixel format for JPEG output' );
  115. }
  116. }
  117. /**
  118. * Transform an image using ImageMagick
  119. *
  120. * @param File $image File associated with this thumbnail
  121. * @param array $params Array with scaler params
  122. *
  123. * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
  124. */
  125. protected function transformImageMagick( $image, $params ) {
  126. # use ImageMagick
  127. global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
  128. $wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgJpegPixelFormat,
  129. $wgJpegQuality;
  130. $quality = [];
  131. $sharpen = [];
  132. $scene = false;
  133. $animation_pre = [];
  134. $animation_post = [];
  135. $decoderHint = [];
  136. $subsampling = [];
  137. if ( $params['mimeType'] == 'image/jpeg' ) {
  138. $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
  139. $quality = [ '-quality', $qualityVal ?: (string)$wgJpegQuality ]; // 80% by default
  140. if ( $params['interlace'] ) {
  141. $animation_post = [ '-interlace', 'JPEG' ];
  142. }
  143. # Sharpening, see T8193
  144. if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
  145. / ( $params['srcWidth'] + $params['srcHeight'] )
  146. < $wgSharpenReductionThreshold
  147. ) {
  148. $sharpen = [ '-sharpen', $wgSharpenParameter ];
  149. }
  150. if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
  151. // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
  152. $decoderHint = [ '-define', "jpeg:size={$params['physicalDimensions']}" ];
  153. }
  154. if ( $wgJpegPixelFormat ) {
  155. $factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
  156. $subsampling = [ '-sampling-factor', implode( ',', $factors ) ];
  157. }
  158. } elseif ( $params['mimeType'] == 'image/png' ) {
  159. $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
  160. if ( $params['interlace'] ) {
  161. $animation_post = [ '-interlace', 'PNG' ];
  162. }
  163. } elseif ( $params['mimeType'] == 'image/webp' ) {
  164. $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
  165. } elseif ( $params['mimeType'] == 'image/gif' ) {
  166. if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
  167. // Extract initial frame only; we're so big it'll
  168. // be a total drag. :P
  169. $scene = 0;
  170. } elseif ( $this->isAnimatedImage( $image ) ) {
  171. // Coalesce is needed to scale animated GIFs properly (T3017).
  172. $animation_pre = [ '-coalesce' ];
  173. // We optimize the output, but -optimize is broken,
  174. // use optimizeTransparency instead (T13822)
  175. if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
  176. $animation_post = [ '-fuzz', '5%', '-layers', 'optimizeTransparency' ];
  177. }
  178. }
  179. if ( $params['interlace'] && version_compare( $this->getMagickVersion(), "6.3.4" ) >= 0
  180. && !$this->isAnimatedImage( $image ) ) { // interlacing animated GIFs is a bad idea
  181. $animation_post[] = '-interlace';
  182. $animation_post[] = 'GIF';
  183. }
  184. } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
  185. // Before merging layers, we need to set the background
  186. // to be transparent to preserve alpha, as -layers merge
  187. // merges all layers on to a canvas filled with the
  188. // background colour. After merging we reset the background
  189. // to be white for the default background colour setting
  190. // in the PNG image (which is used in old IE)
  191. $animation_pre = [
  192. '-background', 'transparent',
  193. '-layers', 'merge',
  194. '-background', 'white',
  195. ];
  196. Wikimedia\suppressWarnings();
  197. $xcfMeta = unserialize( $image->getMetadata() );
  198. Wikimedia\restoreWarnings();
  199. if ( $xcfMeta
  200. && isset( $xcfMeta['colorType'] )
  201. && $xcfMeta['colorType'] === 'greyscale-alpha'
  202. && version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0
  203. ) {
  204. // T68323 - Greyscale images not rendered properly.
  205. // So only take the "red" channel.
  206. $channelOnly = [ '-channel', 'R', '-separate' ];
  207. $animation_pre = array_merge( $animation_pre, $channelOnly );
  208. }
  209. }
  210. // Use one thread only, to avoid deadlock bugs on OOM
  211. $env = [ 'OMP_NUM_THREADS' => 1 ];
  212. if ( strval( $wgImageMagickTempDir ) !== '' ) {
  213. $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
  214. }
  215. $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
  216. list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
  217. $cmd = Shell::escape( ...array_merge(
  218. [ $wgImageMagickConvertCommand ],
  219. $quality,
  220. // Specify white background color, will be used for transparent images
  221. // in Internet Explorer/Windows instead of default black.
  222. [ '-background', 'white' ],
  223. $decoderHint,
  224. [ $this->escapeMagickInput( $params['srcPath'], $scene ) ],
  225. $animation_pre,
  226. // For the -thumbnail option a "!" is needed to force exact size,
  227. // or ImageMagick may decide your ratio is wrong and slice off
  228. // a pixel.
  229. [ '-thumbnail', "{$width}x{$height}!" ],
  230. // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
  231. ( $params['comment'] !== ''
  232. ? [ '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) ]
  233. : [] ),
  234. // T108616: Avoid exposure of local file path
  235. [ '+set', 'Thumb::URI' ],
  236. [ '-depth', 8 ],
  237. $sharpen,
  238. [ '-rotate', "-$rotation" ],
  239. $subsampling,
  240. $animation_post,
  241. [ $this->escapeMagickOutput( $params['dstPath'] ) ] ) );
  242. wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
  243. $retval = 0;
  244. $err = wfShellExecWithStderr( $cmd, $retval, $env );
  245. if ( $retval !== 0 ) {
  246. $this->logErrorForExternalProcess( $retval, $err, $cmd );
  247. return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
  248. }
  249. return false; # No error
  250. }
  251. /**
  252. * Transform an image using the Imagick PHP extension
  253. *
  254. * @param File $image File associated with this thumbnail
  255. * @param array $params Array with scaler params
  256. *
  257. * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
  258. */
  259. protected function transformImageMagickExt( $image, $params ) {
  260. global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
  261. $wgJpegPixelFormat, $wgJpegQuality;
  262. try {
  263. $im = new Imagick();
  264. $im->readImage( $params['srcPath'] );
  265. if ( $params['mimeType'] == 'image/jpeg' ) {
  266. // Sharpening, see T8193
  267. if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
  268. / ( $params['srcWidth'] + $params['srcHeight'] )
  269. < $wgSharpenReductionThreshold
  270. ) {
  271. // Hack, since $wgSharpenParameter is written specifically for the command line convert
  272. list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
  273. $im->sharpenImage( $radius, $sigma );
  274. }
  275. $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
  276. $im->setCompressionQuality( $qualityVal ?: $wgJpegQuality );
  277. if ( $params['interlace'] ) {
  278. $im->setInterlaceScheme( Imagick::INTERLACE_JPEG );
  279. }
  280. if ( $wgJpegPixelFormat ) {
  281. $factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
  282. $im->setSamplingFactors( $factors );
  283. }
  284. } elseif ( $params['mimeType'] == 'image/png' ) {
  285. $im->setCompressionQuality( 95 );
  286. if ( $params['interlace'] ) {
  287. $im->setInterlaceScheme( Imagick::INTERLACE_PNG );
  288. }
  289. } elseif ( $params['mimeType'] == 'image/gif' ) {
  290. if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
  291. // Extract initial frame only; we're so big it'll
  292. // be a total drag. :P
  293. $im->setImageScene( 0 );
  294. } elseif ( $this->isAnimatedImage( $image ) ) {
  295. // Coalesce is needed to scale animated GIFs properly (T3017).
  296. $im = $im->coalesceImages();
  297. }
  298. // GIF interlacing is only available since 6.3.4
  299. $v = Imagick::getVersion();
  300. preg_match( '/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $v );
  301. if ( $params['interlace'] && version_compare( $v[1], '6.3.4' ) >= 0 ) {
  302. $im->setInterlaceScheme( Imagick::INTERLACE_GIF );
  303. }
  304. }
  305. $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
  306. list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
  307. $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
  308. // Call Imagick::thumbnailImage on each frame
  309. foreach ( $im as $i => $frame ) {
  310. if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
  311. return $this->getMediaTransformError( $params, "Error scaling frame $i" );
  312. }
  313. }
  314. $im->setImageDepth( 8 );
  315. if ( $rotation && !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
  316. return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
  317. }
  318. if ( $this->isAnimatedImage( $image ) ) {
  319. wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
  320. // This is broken somehow... can't find out how to fix it
  321. $result = $im->writeImages( $params['dstPath'], true );
  322. } else {
  323. $result = $im->writeImage( $params['dstPath'] );
  324. }
  325. if ( !$result ) {
  326. return $this->getMediaTransformError( $params,
  327. "Unable to write thumbnail to {$params['dstPath']}" );
  328. }
  329. } catch ( ImagickException $e ) {
  330. return $this->getMediaTransformError( $params, $e->getMessage() );
  331. }
  332. return false;
  333. }
  334. /**
  335. * Transform an image using a custom command
  336. *
  337. * @param File $image File associated with this thumbnail
  338. * @param array $params Array with scaler params
  339. *
  340. * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
  341. */
  342. protected function transformCustom( $image, $params ) {
  343. # Use a custom convert command
  344. global $wgCustomConvertCommand;
  345. # Variables: %s %d %w %h
  346. $src = Shell::escape( $params['srcPath'] );
  347. $dst = Shell::escape( $params['dstPath'] );
  348. $cmd = $wgCustomConvertCommand;
  349. $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
  350. $cmd = str_replace( '%h', Shell::escape( $params['physicalHeight'] ),
  351. str_replace( '%w', Shell::escape( $params['physicalWidth'] ), $cmd ) ); # Size
  352. wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
  353. $retval = 0;
  354. $err = wfShellExecWithStderr( $cmd, $retval );
  355. if ( $retval !== 0 ) {
  356. $this->logErrorForExternalProcess( $retval, $err, $cmd );
  357. return $this->getMediaTransformError( $params, $err );
  358. }
  359. return false; # No error
  360. }
  361. /**
  362. * Transform an image using the built in GD library
  363. *
  364. * @param File $image File associated with this thumbnail
  365. * @param array $params Array with scaler params
  366. *
  367. * @return MediaTransformError|bool Error object if error occurred, false (=no error) otherwise
  368. */
  369. protected function transformGd( $image, $params ) {
  370. # Use PHP's builtin GD library functions.
  371. # First find out what kind of file this is, and select the correct
  372. # input routine for this.
  373. $typemap = [
  374. 'image/gif' => [ 'imagecreatefromgif', 'palette', false, 'imagegif' ],
  375. 'image/jpeg' => [ 'imagecreatefromjpeg', 'truecolor', true,
  376. [ __CLASS__, 'imageJpegWrapper' ] ],
  377. 'image/png' => [ 'imagecreatefrompng', 'bits', false, 'imagepng' ],
  378. 'image/vnd.wap.wbmp' => [ 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ],
  379. 'image/xbm' => [ 'imagecreatefromxbm', 'palette', false, 'imagexbm' ],
  380. ];
  381. if ( !isset( $typemap[$params['mimeType']] ) ) {
  382. $err = 'Image type not supported';
  383. wfDebug( "$err\n" );
  384. $errMsg = wfMessage( 'thumbnail_image-type' )->text();
  385. return $this->getMediaTransformError( $params, $errMsg );
  386. }
  387. list( $loader, $colorStyle, $useQuality, $saveType ) = $typemap[$params['mimeType']];
  388. if ( !function_exists( $loader ) ) {
  389. $err = "Incomplete GD library configuration: missing function $loader";
  390. wfDebug( "$err\n" );
  391. $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
  392. return $this->getMediaTransformError( $params, $errMsg );
  393. }
  394. if ( !file_exists( $params['srcPath'] ) ) {
  395. $err = "File seems to be missing: {$params['srcPath']}";
  396. wfDebug( "$err\n" );
  397. $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
  398. return $this->getMediaTransformError( $params, $errMsg );
  399. }
  400. if ( filesize( $params['srcPath'] ) === 0 ) {
  401. $err = "Image file size seems to be zero.";
  402. wfDebug( "$err\n" );
  403. $errMsg = wfMessage( 'thumbnail_image-size-zero', $params['srcPath'] )->text();
  404. return $this->getMediaTransformError( $params, $errMsg );
  405. }
  406. $src_image = $loader( $params['srcPath'] );
  407. $rotation = function_exists( 'imagerotate' ) && !isset( $params['disableRotation'] ) ?
  408. $this->getRotation( $image ) :
  409. 0;
  410. list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
  411. $dst_image = imagecreatetruecolor( $width, $height );
  412. // Initialise the destination image to transparent instead of
  413. // the default solid black, to support PNG and GIF transparency nicely
  414. $background = imagecolorallocate( $dst_image, 0, 0, 0 );
  415. imagecolortransparent( $dst_image, $background );
  416. imagealphablending( $dst_image, false );
  417. if ( $colorStyle == 'palette' ) {
  418. // Don't resample for paletted GIF images.
  419. // It may just uglify them, and completely breaks transparency.
  420. imagecopyresized( $dst_image, $src_image,
  421. 0, 0, 0, 0,
  422. $width, $height,
  423. imagesx( $src_image ), imagesy( $src_image ) );
  424. } else {
  425. imagecopyresampled( $dst_image, $src_image,
  426. 0, 0, 0, 0,
  427. $width, $height,
  428. imagesx( $src_image ), imagesy( $src_image ) );
  429. }
  430. if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
  431. $rot_image = imagerotate( $dst_image, $rotation, 0 );
  432. imagedestroy( $dst_image );
  433. $dst_image = $rot_image;
  434. }
  435. imagesavealpha( $dst_image, true );
  436. $funcParams = [ $dst_image, $params['dstPath'] ];
  437. if ( $useQuality && isset( $params['quality'] ) ) {
  438. $funcParams[] = $params['quality'];
  439. }
  440. $saveType( ...$funcParams );
  441. imagedestroy( $dst_image );
  442. imagedestroy( $src_image );
  443. return false; # No error
  444. }
  445. /**
  446. * Callback for transformGd when transforming jpeg images.
  447. *
  448. * @param resource $dst_image Image resource of the original image
  449. * @param string $thumbPath File path to write the thumbnail image to
  450. * @param int|null $quality Quality of the thumbnail from 1-100,
  451. * or null to use default quality.
  452. */
  453. static function imageJpegWrapper( $dst_image, $thumbPath, $quality = null ) {
  454. global $wgJpegQuality;
  455. if ( $quality === null ) {
  456. $quality = $wgJpegQuality;
  457. }
  458. imageinterlace( $dst_image );
  459. imagejpeg( $dst_image, $thumbPath, $quality );
  460. }
  461. /**
  462. * Returns whether the current scaler supports rotation (im and gd do)
  463. *
  464. * @return bool
  465. */
  466. public function canRotate() {
  467. $scaler = $this->getScalerType( null, false );
  468. switch ( $scaler ) {
  469. case 'im':
  470. # ImageMagick supports autorotation
  471. return true;
  472. case 'imext':
  473. # Imagick::rotateImage
  474. return true;
  475. case 'gd':
  476. # GD's imagerotate function is used to rotate images, but not
  477. # all precompiled PHP versions have that function
  478. return function_exists( 'imagerotate' );
  479. default:
  480. # Other scalers don't support rotation
  481. return false;
  482. }
  483. }
  484. /**
  485. * @see $wgEnableAutoRotation
  486. * @return bool Whether auto rotation is enabled
  487. */
  488. public function autoRotateEnabled() {
  489. global $wgEnableAutoRotation;
  490. if ( $wgEnableAutoRotation === null ) {
  491. // Only enable auto-rotation when we actually can
  492. return $this->canRotate();
  493. }
  494. return $wgEnableAutoRotation;
  495. }
  496. /**
  497. * @param File $file
  498. * @param array $params Rotate parameters.
  499. * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
  500. * @since 1.21
  501. * @return bool|MediaTransformError
  502. */
  503. public function rotate( $file, $params ) {
  504. global $wgImageMagickConvertCommand;
  505. $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
  506. $scene = false;
  507. $scaler = $this->getScalerType( null, false );
  508. switch ( $scaler ) {
  509. case 'im':
  510. $cmd = Shell::escape( $wgImageMagickConvertCommand ) . " " .
  511. Shell::escape( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
  512. " -rotate " . Shell::escape( "-$rotation" ) . " " .
  513. Shell::escape( $this->escapeMagickOutput( $params['dstPath'] ) );
  514. wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
  515. $retval = 0;
  516. $err = wfShellExecWithStderr( $cmd, $retval );
  517. if ( $retval !== 0 ) {
  518. $this->logErrorForExternalProcess( $retval, $err, $cmd );
  519. return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
  520. }
  521. return false;
  522. case 'imext':
  523. $im = new Imagick();
  524. $im->readImage( $params['srcPath'] );
  525. if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
  526. return new MediaTransformError( 'thumbnail_error', 0, 0,
  527. "Error rotating $rotation degrees" );
  528. }
  529. $result = $im->writeImage( $params['dstPath'] );
  530. if ( !$result ) {
  531. return new MediaTransformError( 'thumbnail_error', 0, 0,
  532. "Unable to write image to {$params['dstPath']}" );
  533. }
  534. return false;
  535. default:
  536. return new MediaTransformError( 'thumbnail_error', 0, 0,
  537. "$scaler rotation not implemented" );
  538. }
  539. }
  540. }