Merge "Fix documentation of HistoryPager::lastLink() and make code more readable"
[lhc/web/wiklou.git] / includes / filerepo / file / File.php
index 1103e38..e970e38 100644 (file)
@@ -149,6 +149,9 @@ abstract class File {
        /** @var string Required Repository class type */
        protected $repoClass = 'FileRepo';
 
+       /** @var array Cache of tmp filepaths pointing to generated bucket thumbnails, keyed by width */
+       protected $tmpBucketedThumbCache = array();
+
        /**
         * Call this constructor from child classes.
         *
@@ -456,6 +459,50 @@ abstract class File {
                return false;
        }
 
+       /**
+        * Return the smallest bucket from $wgThumbnailBuckets which is at least
+        * $wgThumbnailMinimumBucketDistance larger than $desiredWidth. The returned bucket, if any,
+        * will always be bigger than $desiredWidth.
+        *
+        * @param int $desiredWidth
+        * @param int $page
+        * @return bool|int
+        */
+       public function getThumbnailBucket( $desiredWidth, $page = 1 ) {
+               global $wgThumbnailBuckets, $wgThumbnailMinimumBucketDistance;
+
+               $imageWidth = $this->getWidth( $page );
+
+               if ( $imageWidth === false ) {
+                       return false;
+               }
+
+               if ( $desiredWidth > $imageWidth ) {
+                       return false;
+               }
+
+               if ( !$wgThumbnailBuckets ) {
+                       return false;
+               }
+
+               $sortedBuckets = $wgThumbnailBuckets;
+
+               sort( $sortedBuckets );
+
+               foreach ( $sortedBuckets as $bucket ) {
+                       if ( $bucket > $imageWidth ) {
+                               return false;
+                       }
+
+                       if ( $bucket - $wgThumbnailMinimumBucketDistance > $desiredWidth ) {
+                               return $bucket;
+                       }
+               }
+
+               // Image is bigger than any available bucket
+               return false;
+       }
+
        /**
         * Returns ID or name of user who uploaded the file
         * STUB
@@ -877,9 +924,9 @@ abstract class File {
                        return null;
                }
                $extension = $this->getExtension();
-               list( $thumbExt, ) = $this->handler->getThumbType(
+               list( $thumbExt, ) = $this->getHandler()->getThumbType(
                        $extension, $this->getMimeType(), $params );
-               $thumbName = $this->handler->makeParamString( $params ) . '-' . $name;
+               $thumbName = $this->getHandler()->makeParamString( $params ) . '-' . $name;
                if ( $thumbExt != $extension ) {
                        $thumbName .= ".$thumbExt";
                }
@@ -947,7 +994,7 @@ abstract class File {
         * @return MediaTransformOutput|bool False on failure
         */
        function transform( $params, $flags = 0 ) {
-               global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch;
+               global $wgThumbnailEpoch;
 
                wfProfileIn( __METHOD__ );
                do {
@@ -1004,64 +1051,221 @@ abstract class File {
                                } elseif ( $flags & self::RENDER_FORCE ) {
                                        wfDebug( __METHOD__ . " forcing rendering per flag File::RENDER_FORCE\n" );
                                }
-                       }
 
-                       // If the backend is ready-only, don't keep generating thumbnails
-                       // only to return transformation errors, just return the error now.
-                       if ( $this->repo->getReadOnlyReason() !== false ) {
-                               $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
-                               break;
+                               // If the backend is ready-only, don't keep generating thumbnails
+                               // only to return transformation errors, just return the error now.
+                               if ( $this->repo->getReadOnlyReason() !== false ) {
+                                       $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
+                                       break;
+                               }
                        }
 
-                       // Create a temp FS file with the same extension and the thumbnail
-                       $thumbExt = FileBackend::extensionFromPath( $thumbPath );
-                       $tmpFile = TempFSFile::factory( 'transform_', $thumbExt );
+                       $tmpFile = $this->makeTransformTmpFile( $thumbPath );
+
                        if ( !$tmpFile ) {
                                $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
-                               break;
+                       } else {
+                               $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags );
                        }
-                       $tmpThumbPath = $tmpFile->getPath(); // path of 0-byte temp file
-
-                       // Actually render the thumbnail...
-                       wfProfileIn( __METHOD__ . '-doTransform' );
-                       $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $params );
-                       wfProfileOut( __METHOD__ . '-doTransform' );
-                       $tmpFile->bind( $thumb ); // keep alive with $thumb
-
-                       if ( !$thumb ) { // bad params?
-                               $thumb = false;
-                       } elseif ( $thumb->isError() ) { // transform error
-                               $this->lastError = $thumb->toText();
-                               // Ignore errors if requested
-                               if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
-                                       $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $params );
-                               }
-                       } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) {
-                               // Copy the thumbnail from the file system into storage...
-                               $disposition = $this->getThumbDisposition( $thumbName );
-                               $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition );
-                               if ( $status->isOK() ) {
-                                       $thumb->setStoragePath( $thumbPath );
-                               } else {
-                                       $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
+               } while ( false );
+
+               wfProfileOut( __METHOD__ );
+
+               return is_object( $thumb ) ? $thumb : false;
+       }
+
+       /**
+        * Generates a thumbnail according to the given parameters and saves it to storage
+        * @param TempFSFile $tmpFile Temporary file where the rendered thumbnail will be saved
+        * @param array $transformParams
+        * @param int $flags
+        * @return bool|MediaTransformOutput
+        */
+       public function generateAndSaveThumb( $tmpFile, $transformParams, $flags ) {
+               global $wgUseSquid, $wgIgnoreImageErrors;
+
+               $handler = $this->getHandler();
+
+               $normalisedParams = $transformParams;
+               $handler->normaliseParams( $this, $normalisedParams );
+
+               $thumbName = $this->thumbName( $normalisedParams );
+               $thumbUrl = $this->getThumbUrl( $thumbName );
+               $thumbPath = $this->getThumbPath( $thumbName ); // final thumb path
+
+               $tmpThumbPath = $tmpFile->getPath();
+
+               if ( $handler->supportsBucketing() ) {
+                       $this->generateBucketsIfNeeded( $normalisedParams, $flags );
+               }
+
+               // Actually render the thumbnail...
+               wfProfileIn( __METHOD__ . '-doTransform' );
+               $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams );
+               wfProfileOut( __METHOD__ . '-doTransform' );
+               $tmpFile->bind( $thumb ); // keep alive with $thumb
+
+               if ( !$thumb ) { // bad params?
+                       $thumb = false;
+               } elseif ( $thumb->isError() ) { // transform error
+                       $this->lastError = $thumb->toText();
+                       // Ignore errors if requested
+                       if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
+                               $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams );
+                       }
+               } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) {
+                       // Copy the thumbnail from the file system into storage...
+                       $disposition = $this->getThumbDisposition( $thumbName );
+                       $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition );
+                       if ( $status->isOK() ) {
+                               $thumb->setStoragePath( $thumbPath );
+                       } else {
+                               $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $transformParams, $flags );
+                       }
+                       // Give extensions a chance to do something with this thumbnail...
+                       wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) );
+               }
+
+               // Purge. Useful in the event of Core -> Squid connection failure or squid
+               // purge collisions from elsewhere during failure. Don't keep triggering for
+               // "thumbs" which have the main image URL though (bug 13776)
+               if ( $wgUseSquid ) {
+                       if ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL() ) {
+                               SquidUpdate::purge( array( $thumbUrl ) );
+                       }
+               }
+
+               return $thumb;
+       }
+
+       /**
+        * Generates chained bucketed thumbnails if needed
+        * @param array $params
+        * @param int $flags
+        * @return bool Whether at least one bucket was generated
+        */
+       protected function generateBucketsIfNeeded( $params, $flags = 0 ) {
+               if ( !$this->repo
+                       || !isset( $params['physicalWidth'] )
+                       || !isset( $params['physicalHeight'] )
+                       || !( $bucket = $this->getThumbnailBucket( $params['physicalWidth'] ) )
+                       || $bucket == $params['physicalWidth'] ) {
+                       return false;
+               }
+
+               $bucketPath = $this->getBucketThumbPath( $bucket );
+
+               if ( $this->repo->fileExists( $bucketPath ) ) {
+                       return false;
+               }
+
+               $params['physicalWidth'] = $bucket;
+               $params['width'] = $bucket;
+
+               $params = $this->getHandler()->sanitizeParamsForBucketing( $params );
+
+               $bucketName = $this->getBucketThumbName( $bucket );
+
+               $tmpFile = $this->makeTransformTmpFile( $bucketPath );
+
+               if ( !$tmpFile ) {
+                       return false;
+               }
+
+               $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags );
+
+               if ( !$thumb || $thumb->isError() ) {
+                       return false;
+               }
+
+               $this->tmpBucketedThumbCache[ $bucket ] = $tmpFile->getPath();
+               // For the caching to work, we need to make the tmp file survive as long as
+               // this object exists
+               $tmpFile->bind( $this );
+
+               return true;
+       }
+
+       /**
+        * Returns the most appropriate source image for the thumbnail, given a target thumbnail size
+        * @param array $params
+        * @return array source path and width/height of the source
+        */
+       public function getThumbnailSource( $params ) {
+               if ( $this->repo
+                       && $this->getHandler()->supportsBucketing()
+                       && isset( $params['physicalWidth'] )
+                       && $bucket = $this->getThumbnailBucket( $params['physicalWidth'] )
+               ) {
+                       if ( $this->getWidth() != 0 ) {
+                               $bucketHeight = round( $this->getHeight() * ( $bucket / $this->getWidth() ) );
+                       } else {
+                               $bucketHeight = 0;
+                       }
+
+                       // Try to avoid reading from storage if the file was generated by this script
+                       if ( isset( $this->tmpBucketedThumbCache[ $bucket ] ) ) {
+                               $tmpPath = $this->tmpBucketedThumbCache[ $bucket ];
+
+                               if ( file_exists( $tmpPath ) ) {
+                                       return array(
+                                               'path' => $tmpPath,
+                                               'width' => $bucket,
+                                               'height' => $bucketHeight
+                                       );
                                }
-                               // Give extensions a chance to do something with this thumbnail...
-                               wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) );
                        }
 
-                       // Purge. Useful in the event of Core -> Squid connection failure or squid
-                       // purge collisions from elsewhere during failure. Don't keep triggering for
-                       // "thumbs" which have the main image URL though (bug 13776)
-                       if ( $wgUseSquid ) {
-                               if ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL() ) {
-                                       SquidUpdate::purge( array( $thumbUrl ) );
+                       $bucketPath = $this->getBucketThumbPath( $bucket );
+
+                       if ( $this->repo->fileExists( $bucketPath ) ) {
+                               $fsFile = $this->repo->getLocalReference( $bucketPath );
+
+                               if ( $fsFile ) {
+                                       return array(
+                                               'path' => $fsFile->getPath(),
+                                               'width' => $bucket,
+                                               'height' => $bucketHeight
+                                       );
                                }
                        }
-               } while ( false );
+               }
 
-               wfProfileOut( __METHOD__ );
+               // Original file
+               return array(
+                       'path' => $this->getLocalRefPath(),
+                       'width' => $this->getWidth(),
+                       'height' => $this->getHeight()
+               );
+       }
 
-               return is_object( $thumb ) ? $thumb : false;
+       /**
+        * Returns the repo path of the thumb for a given bucket
+        * @param int $bucket
+        * @return string
+        */
+       protected function getBucketThumbPath( $bucket ) {
+               $thumbName = $this->getBucketThumbName( $bucket );
+               return $this->getThumbPath( $thumbName );
+       }
+
+       /**
+        * Returns the name of the thumb for a given bucket
+        * @param int $bucket
+        * @return string
+        */
+       protected function getBucketThumbName( $bucket ) {
+               return $this->thumbName( array( 'physicalWidth' => $bucket ) );
+       }
+
+       /**
+        * Creates a temp FS file with the same extension and the thumbnail
+        * @param string $thumbPath Thumbnail path
+        * @returns TempFSFile
+        */
+       protected function makeTransformTmpFile( $thumbPath ) {
+               $thumbExt = FileBackend::extensionFromPath( $thumbPath );
+               return TempFSFile::factory( 'transform_', $thumbExt );
        }
 
        /**
@@ -1741,7 +1945,7 @@ abstract class File {
                        return false;
                }
 
-               return $this->handler->getImageSize( $this, $filePath );
+               return $this->getHandler()->getImageSize( $this, $filePath );
        }
 
        /**