From e288e4036c28b36c561c423e9df65cf0a07190a1 Mon Sep 17 00:00:00 2001 From: Jan Gerber Date: Tue, 8 Jan 2013 11:35:55 +0000 Subject: [PATCH] (bug 33186) image rotate api add api action imagerotate to rotate images. rotations are stored as a new version of the image. Change-Id: Id15a92d19cda8256917e7e1e5ee4241012214102 --- RELEASE-NOTES-1.21 | 1 + includes/AutoLoader.php | 1 + includes/DefaultSettings.php | 7 + includes/api/ApiImageRotate.php | 216 +++++++++++++++++++++++++++++ includes/api/ApiMain.php | 1 + includes/media/Bitmap.php | 53 ++++++- includes/media/ExifBitmap.php | 2 +- includes/media/Jpeg.php | 32 +++++ includes/media/MediaHandler.php | 9 ++ languages/messages/MessagesEn.php | 3 + languages/messages/MessagesQqq.php | 3 + maintenance/language/messages.inc | 1 + 12 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 includes/api/ApiImageRotate.php diff --git a/RELEASE-NOTES-1.21 b/RELEASE-NOTES-1.21 index bda925478b..d6ca336b1b 100644 --- a/RELEASE-NOTES-1.21 +++ b/RELEASE-NOTES-1.21 @@ -114,6 +114,7 @@ production. oc, pl, pt, rm, ro, ru, rup, sco, sk, sl, smn, sq, sr, sv, tk, tl, tr, tt, uk, uz, vi. * Added 'CategoryAfterPageAdded' and 'CategoryAfterPageRemoved' hooks. +* (bug 33186) Add image rotation api "imagerotate" === Bug fixes in 1.21 === * (bug 40353) SpecialDoubleRedirect should support interwiki redirects. diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index b1431a2cb0..789538e8f4 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -360,6 +360,7 @@ $wgAutoloadLocalClasses = array( 'ApiFormatXmlRsd' => 'includes/api/ApiRsd.php', 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', 'ApiHelp' => 'includes/api/ApiHelp.php', + 'ApiImageRotate' => 'includes/api/ApiImageRotate.php', 'ApiImport' => 'includes/api/ApiImport.php', 'ApiImportReporter' => 'includes/api/ApiImport.php', 'ApiLogin' => 'includes/api/ApiLogin.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index eb87e7b613..ec8c2f77c4 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -832,6 +832,13 @@ $wgImageMagickTempDir = false; */ $wgCustomConvertCommand = false; +/** used for lossless jpeg rotation + * + * @since 1.21 + * **/ +$wgJpegTran = '/usr/bin/jpegtran'; + + /** * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some * image formats. diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php new file mode 100644 index 0000000000..3815d41070 --- /dev/null +++ b/includes/api/ApiImageRotate.php @@ -0,0 +1,216 @@ + $val ); + } else { + $v = $val; + } + if( $flag !== null ) { + $v[$flag] = ''; + } + $result[] = $v; + } + } + + + public function execute() { + $params = $this->extractRequestParams(); + $rotation = $params[ 'rotation' ]; + $user = $this->getUser(); + + if( is_null( $rotation ) || $rotation % 90 ) { + $this->dieUsage( "Rotation: {$rotation}", 'rotation must be multiple of 90 degrees' ); + } + + $pageSet = $this->getPageSet(); + $pageSet->execute(); + + $result = array(); + $result = array(); + + self::addValues( $result, $pageSet->getInvalidTitles(), 'invalid', 'title' ); + self::addValues( $result, $pageSet->getSpecialTitles(), 'special', 'title' ); + self::addValues( $result, $pageSet->getMissingPageIDs(), 'missing', 'pageid' ); + self::addValues( $result, $pageSet->getMissingRevisionIDs(), 'missing', 'revid' ); + self::addValues( $result, $pageSet->getMissingTitles(), 'missing' ); + self::addValues( $result, $pageSet->getInterwikiTitlesAsResult() ); + + foreach ( $pageSet->getTitles() as $title ) { + $file = wfFindFile( $title ); + + $r = array(); + $r[ 'title' ] = $title->getFullText(); + if ( !$file ) { + $r['missing'] = ''; + $r['result'] = 'Failure'; + $result[] = $r; + continue; + } + $handler = $file->getHandler(); + if ( !$handler || !$handler->canRotate() ) { + $r['invalid'] = ''; + $r['result'] = 'Failure'; + $result[] = $r; + continue; + } + + // Check whether we're allowed to rotate this file + $this->checkPermissions( $this->getUser(), $file->getTitle() ); + + $srcPath = $file->getLocalRefPath(); + $ext = strtolower( pathinfo( "$srcPath", PATHINFO_EXTENSION ) ); + $tmpFile = TempFSFile::factory( 'rotate_', $ext); + $dstPath = $tmpFile->getPath(); + $err = $handler->rotate( $file, array( + "srcPath" => $srcPath, + "dstPath" => $dstPath, + "rotation"=> $rotation + ) ); + if ( !$err ) { + $comment = wfMessage( 'rotate-comment' )->numParams( $rotation )->text(); + $status = $file->upload( $dstPath, + $comment, $comment, 0, false, false, $this->getUser() ); + if ( $status->isGood() ) { + $r['result'] = 'Success'; + } else { + $r['result'] = 'Failure'; + $r['errormessage'] = $this->getResult()->convertStatusToArray( $status ); + } + } else { + $r['result'] = 'Failure'; + $r['errormessage'] = $err->toText(); + } + $result[] = $r; + } + $apiResult = $this->getResult(); + $apiResult->setIndexedTagName( $result, 'page' ); + $apiResult->addValue( null, $this->getModuleName(), $result ); + } + + /** + * Get a cached instance of an ApiPageSet object + * @return ApiPageSet + */ + private function getPageSet() { + if ( $this->mPageSet === null ) { + $this->mPageSet = new ApiPageSet( $this, 0, NS_FILE); + } + return $this->mPageSet; + } + + /** + * Checks that the user has permissions to perform rotations. + * Dies with usage message on inadequate permissions. + * @param $user User The user to check. + */ + protected function checkPermissions( $user, $title ) { + $permissionErrors = array_merge( + $title->getUserPermissionsErrors( 'edit' , $user ), + $title->getUserPermissionsErrors( 'upload' , $user ) + ); + + if ( $permissionErrors ) { + $this->dieUsageMsg( $permissionErrors[0] ); + } + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams( $flags = 0 ) { + $pageSet = $this->getPageSet(); + $result = array( + 'rotation' => array( + ApiBase::PARAM_DFLT => 0, + ), + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + ); + if ( $flags ) { + $result += $this->getPageSet()->getFinalParams( $flags ); + } + return $result; + } + + public function getParamDescription() { + $pageSet = $this->getPageSet(); + return $pageSet->getParamDescription() + array( + 'rotation' => 'Degrees to rotate image, values can be 0, 90, 180 or 270', + 'token' => 'Edit token. You can get one of these through prop=info', + ); + } + + public function getDescription() { + return 'Rotate one or more images'; + } + + public function needsToken() { + return true; + } + + public function getTokenSalt() { + return ''; + } + + public function getPossibleErrors() { + $pageSet = $this->getPageSet(); + return array_merge( + parent::getPossibleErrors(), + $pageSet->getPossibleErrors() + ); + } + + public function getExamples() { + return array( + 'api.php?action=imagerotate&titles=Example.jpg&rotation=90&token=+\\', + ); + } +} diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 215bdac96f..2bd01e029b 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -83,6 +83,7 @@ class ApiMain extends ApiBase { 'import' => 'ApiImport', 'userrights' => 'ApiUserrights', 'options' => 'ApiOptions', + 'imagerotate' =>'ApiImageRotate', ); /** diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index a8c1731d4b..0ad862d8dc 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -634,7 +634,7 @@ class BitmapHandler extends ImageHandler { # Escape glob chars $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path ); - return $this->escapeMagickPath( $path, $scene ); + return self::escapeMagickPath( $path, $scene ); } /** @@ -644,7 +644,7 @@ class BitmapHandler extends ImageHandler { */ function escapeMagickOutput( $path, $scene = false ) { $path = str_replace( '%', '%%', $path ); - return $this->escapeMagickPath( $path, $scene ); + return self::escapeMagickPath( $path, $scene ); } /** @@ -755,6 +755,55 @@ class BitmapHandler extends ImageHandler { } } + /** + * @param $file File + * @param $params array Rotate parameters. + * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 + * @since 1.21 + * @return bool + */ + public static function rotate( $file, $params ) { + global $wgImageMagickConvertCommand; + + $rotation = ( $params[ 'rotation' ] + self::getRotation( $file ) ) % 360; + $scene = false; + + $scaler = self::getScalerType( null, false ); + switch ( $scaler ) { + case 'im': + $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " . + wfEscapeShellArg( self::escapeMagickInput( $params[ 'srcPath' ], $scene ) ) . + " -rotate -$rotation " . + wfEscapeShellArg( self::escapeMagickOutput( $params[ 'dstPath' ] ) ) . " 2>&1"; + wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" ); + wfProfileIn( 'convert' ); + $retval = 0; + $err = wfShellExec( $cmd, $retval, $env ); + wfProfileOut( 'convert' ); + if ( $retval !== 0 ) { + self::logErrorForExternalProcess( $retval, $err, $cmd ); + return new MediaTransformError( 'thumbnail_error', 0, 0, $err ); + } + return false; + case 'imext': + $im = new Imagick(); + $im->readImage( $params['srcPath'] ); + if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) { + return new MediaTransformError( 'thumbnail_error', 0, 0, + "Error rotating $rotation degrees" ); + } + $result = $im->writeImage( $params['dstPath'] ); + if ( !$result ) { + return new MediaTransformError( 'thumbnail_error', 0, 0, + "Unable to write image to {$params['dstPath']}" ); + } + return false; + default: + return new MediaTransformError( 'thumbnail_error', 0, 0, + "$scaler rotation not implemented" ); + } + } + /** * Rerurns whether the file needs to be rendered. Returns true if the * file requires rotation and we are able to rotate it. diff --git a/includes/media/ExifBitmap.php b/includes/media/ExifBitmap.php index 1671ab2584..1a761f3a70 100644 --- a/includes/media/ExifBitmap.php +++ b/includes/media/ExifBitmap.php @@ -190,7 +190,7 @@ class ExifBitmapHandler extends BitmapHandler { } $data = $file->getMetadata(); - return $this->getRotationForExif( $data ); + return self::getRotationForExif( $data ); } /** diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php index 5ea30f204d..cd6f18c0df 100644 --- a/includes/media/Jpeg.php +++ b/includes/media/Jpeg.php @@ -59,4 +59,36 @@ class JpegHandler extends ExifBitmapHandler { } } + /** + * @param $file File + * @param $params array Rotate parameters. + * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 + * @since 1.21 + * @return bool + */ + public static function rotate( $file, $params ) { + global $wgJpegTran; + + $rotation = ( $params[ 'rotation' ] + self::getRotation( $file ) ) % 360; + + if( $wgJpegTran && is_file( $wgJpegTran ) ){ + $cmd = wfEscapeShellArg( $wgJpegTran ) . + " -rotate " . wfEscapeShellArg( $rotation ) . + " -outfile " . wfEscapeShellArg( $params[ 'dstPath' ] ) . + " " . wfEscapeShellArg( $params[ 'srcPath' ] ) . " 2>&1"; + wfDebug( __METHOD__ . ": running jpgtran: $cmd\n" ); + wfProfileIn( 'jpegtran' ); + $retval = 0; + $err = wfShellExec( $cmd, $retval, $env ); + wfProfileOut( 'jpegtran' ); + if ( $retval !== 0 ) { + self::logErrorForExternalProcess( $retval, $err, $cmd ); + return new MediaTransformError( 'thumbnail_error', 0, 0, $err ); + } + return false; + } else { + return Bitmap::rotate( $file, $params ); + } + } + } diff --git a/includes/media/MediaHandler.php b/includes/media/MediaHandler.php index dca14c3510..e35f44082e 100644 --- a/includes/media/MediaHandler.php +++ b/includes/media/MediaHandler.php @@ -564,4 +564,13 @@ abstract class MediaHandler { public function filterThumbnailPurgeList( &$files, $options ) { // Do nothing } + + /* + * True if the handler can rotate the media + * @since 1.21 + * @return bool + */ + public static function canRotate() { + return false; + } } diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 3cbb89db2b..16471842b5 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -5001,4 +5001,7 @@ Otherwise, you can use the easy form below. Your comment will be added to the pa 'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}', 'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}', +#Rotation +'rotate-comment' => 'Image rotated by $1 {{PLURAL:$1|degree|degrees}} clockwise', + ); diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index 317b81285c..0cf597b83c 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -8782,6 +8782,9 @@ $4 is the gender of the target user.', 'duration-centuries' => '{{Related|Duration}}', 'duration-millennia' => '{{Related|Duration}}', +# Rotation +'rotation-comment' => 'comment set for new version uploaded after rotation', + # Unknown messages 'pageswithprop-legend' => 'Legend for the input form on [[Special:PagesWithProp]]. {{Identical|Page with page property}}', diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index c0b0126012..70c7fbe88e 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -4094,4 +4094,5 @@ Variants for Chinese language", 'apierrors' => 'API errors', 'duration' => 'Durations', 'cachedspecial' => 'SpecialCachedPage', + 'rotation-comment' => 'Image rotation message', ); -- 2.20.1