(bug 33186) image rotate api
authorJan Gerber <jgerber@wikimedia.org>
Tue, 8 Jan 2013 11:35:55 +0000 (11:35 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 7 Mar 2013 21:38:10 +0000 (21:38 +0000)
add api action imagerotate to rotate images.
rotations are stored as a new version of the image.

Change-Id: Id15a92d19cda8256917e7e1e5ee4241012214102

12 files changed:
RELEASE-NOTES-1.21
includes/AutoLoader.php
includes/DefaultSettings.php
includes/api/ApiImageRotate.php [new file with mode: 0644]
includes/api/ApiMain.php
includes/media/Bitmap.php
includes/media/ExifBitmap.php
includes/media/Jpeg.php
includes/media/MediaHandler.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/language/messages.inc

index bda9254..d6ca336 100644 (file)
@@ -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.
index b1431a2..789538e 100644 (file)
@@ -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',
index eb87e7b..ec8c2f7 100644 (file)
@@ -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 (file)
index 0000000..3815d41
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+/**
+ *
+ * Created on January 3rd, 2013
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class ApiImageRotate extends ApiBase {
+
+       private $mPageSet = null;
+
+       public function __construct( $main, $action ) {
+               parent::__construct( $main, $action );
+       }
+
+       /**
+        * Add all items from $values into the result
+        * @param $result array output
+        * @param $values array values to add
+        * @param $flag string the name of the boolean flag to mark this element
+        * @param $name string if given, name of the value
+        */
+       private static function addValues( array &$result, $values, $flag = null, $name = null ) {
+               foreach ( $values as $val ) {
+                       if( $val instanceof Title ) {
+                               $v = array();
+                               ApiQueryBase::addTitleInfo( $v, $val );
+                       } elseif( $name !== null ) {
+                               $v = array( $name => $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=+\\',
+               );
+       }
+}
index 215bdac..2bd01e0 100644 (file)
@@ -83,6 +83,7 @@ class ApiMain extends ApiBase {
                'import' => 'ApiImport',
                'userrights' => 'ApiUserrights',
                'options' => 'ApiOptions',
+               'imagerotate' =>'ApiImageRotate',
        );
 
        /**
index a8c1731..0ad862d 100644 (file)
@@ -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.
index 1671ab2..1a761f3 100644 (file)
@@ -190,7 +190,7 @@ class ExifBitmapHandler extends BitmapHandler {
                }
 
                $data = $file->getMetadata();
-               return $this->getRotationForExif( $data );
+               return self::getRotationForExif( $data );
        }
 
        /**
index 5ea30f2..cd6f18c 100644 (file)
@@ -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 );
+               }
+       }
+
 }
index dca14c3..e35f440 100644 (file)
@@ -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;
+       }
 }
index 3cbb89d..1647184 100644 (file)
@@ -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',
+
 );
index 317b812..0cf597b 100644 (file)
@@ -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}}',
index c0b0126..70c7fbe 100644 (file)
@@ -4094,4 +4094,5 @@ Variants for Chinese language",
        'apierrors'             => 'API errors',
        'duration'              => 'Durations',
        'cachedspecial'         => 'SpecialCachedPage',
+       'rotation-comment'      => 'Image rotation message',
 );