From b15d8cffc454ad41191312a3c5e33069578bdd75 Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Fri, 20 Apr 2007 12:31:36 +0000 Subject: [PATCH] * Introduced media handler modules for file-type specific operations: thumbnailing, img_metadata, capabilities, etc. * Deprecated $wgUseImageResize, thumbnailing will be enabled unconditionally. * Fixed interaction of page parameter to ImagePage with the HTML file cache * Improved error reporting for image thumbnailing * Fixed MIME type for SVG files, will be silently changed from image/svg to image/svg+xml after loading from the database. * Workaround for djvutoxml bug #1704049 (poor performance). Use djvudump instead. * Fixed odd behaviour in ImagePage on DjVu thumbnailing errors * Improved error reporting for image thumbnailing * Added sharpening option for ImageMagick thumbnailing * Removed Image::selectPage(), added page parameters to getWidth() and getHeight(), deprecated Image::renderThumb() and Image::getThumbnail() * Changed default contents of img_metadata to empty string instead of a:0:{} * Moved responsibility for respecting $wgGenerateThumbnailOnParse from the UI to Image.php --- RELEASE-NOTES | 4 + config/index.php | 4 - includes/Article.php | 2 + includes/AutoLoader.php | 24 +- includes/DefaultSettings.php | 49 +- includes/DjVuImage.php | 110 +++- includes/Exif.php | 12 +- includes/Image.php | 983 ++++++++---------------------- includes/ImageGallery.php | 5 +- includes/ImagePage.php | 66 +- includes/Linker.php | 206 +++---- includes/MediaTransformOutput.php | 158 +++++ includes/MimeMagic.php | 4 +- includes/Parser.php | 28 +- includes/media/Bitmap.php | 226 +++++++ includes/media/DjVu.php | 203 ++++++ includes/media/Generic.php | 292 +++++++++ includes/media/SVG.php | 94 +++ includes/mime.info | 2 +- languages/messages/MessagesEn.php | 8 +- skins/common/common.css | 13 +- skins/monobook/main.css | 10 + thumb.php | 95 ++- 23 files changed, 1636 insertions(+), 962 deletions(-) create mode 100644 includes/MediaTransformOutput.php create mode 100644 includes/media/Bitmap.php create mode 100644 includes/media/DjVu.php create mode 100644 includes/media/Generic.php create mode 100644 includes/media/SVG.php diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 7d5ebc5950..8c8fe1500c 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -31,6 +31,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN * Added rate limiter for Special:Emailuser * Private logs can now be created using $wgLogRestrictions * (Bug 8590) limited HTML is now always enabled ($wgUserHtml = true). +* Deprecated $wgUseImageResize, thumbnailing will be enabled unconditionally. == New features since 1.9 == @@ -117,6 +118,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN * Introduce 'SearchUpdate' hook; see docs/hooks.txt for more information * Introduce 'mywatchlist' message; used on personal menu to link to watchlist page * Introduce magic word {{NUMBEROFEDITS}} +* Introduced media handlers for file-type specific operations. == Bugfixes since 1.9 == @@ -327,6 +329,8 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN a random page, and will give an error message if none really can be found instead of sending the user to the main page like they used to * Fix object variable used for displaying "not-patrolled" CSS class on list +* Fixed interaction of page parameter to ImagePage with the HTML file cache +* == Maintenance == diff --git a/config/index.php b/config/index.php index 4b987cded4..b392a6d7c2 100644 --- a/config/index.php +++ b/config/index.php @@ -483,8 +483,6 @@ if( $conf->HaveGD ) { } } -$conf->UseImageResize = $conf->HaveGD || $conf->ImageMagick; - $conf->IP = dirname( dirname( __FILE__ ) ); print "
  • Installation directory: " . htmlspecialchars( $conf->IP ) . "
  • \n"; @@ -1302,7 +1300,6 @@ function escapePhpString( $string ) { } function writeLocalSettings( $conf ) { - $conf->UseImageResize = $conf->UseImageResize ? 'true' : 'false'; $conf->PasswordSender = $conf->EmergencyContact; $magic = ($conf->ImageMagick ? "" : "# "); $convert = ($conf->ImageMagick ? $conf->ImageMagick : "/usr/bin/convert" ); @@ -1448,7 +1445,6 @@ if ( \$wgCommandLineMode ) { ## To enable image uploads, make sure the 'images' directory ## is writable, then set this to true: \$wgEnableUploads = false; -\$wgUseImageResize = {$conf->UseImageResize}; {$magic}\$wgUseImageMagick = true; {$magic}\$wgImageMagickConvertCommand = \"{$convert}\"; diff --git a/includes/Article.php b/includes/Article.php index 1fb42b30b0..c48e2ed130 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -2460,6 +2460,7 @@ class Article { $diff = $wgRequest->getVal( 'diff' ); $redirect = $wgRequest->getVal( 'redirect' ); $printable = $wgRequest->getVal( 'printable' ); + $page = $wgRequest->getVal( 'page' ); return $wgUseFileCache and (!$wgShowIPinHeader) @@ -2472,6 +2473,7 @@ class Article { and (!isset($diff)) and (!isset($redirect)) and (!isset($printable)) + and !isset($page) and (!$this->mRedirectedFrom); } diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 5d334da081..23d0384a22 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -8,6 +8,7 @@ function __autoload($className) { global $wgAutoloadClasses; static $localClasses = array( + # Includes 'AjaxDispatcher' => 'includes/AjaxDispatcher.php', 'AjaxCachePolicy' => 'includes/AjaxFunctions.php', 'AjaxResponse' => 'includes/AjaxResponse.php', @@ -115,6 +116,10 @@ function __autoload($className) { 'MacBinary' => 'includes/MacBinary.php', 'MagicWord' => 'includes/MagicWord.php', 'MathRenderer' => 'includes/Math.php', + 'MediaTransformOutput' => 'includes/MediaTransformOutput.php', + 'ThumbnailImage' => 'includes/MediaTransformOutput.php', + 'MediaTransformError' => 'includes/MediaTransformOutput.php', + 'TransformParameterError' => 'includes/MediaTransformOutput.php', 'MessageCache' => 'includes/MessageCache.php', 'MimeMagic' => 'includes/MimeMagic.php', 'Namespace' => 'includes/Namespace.php', @@ -128,6 +133,7 @@ function __autoload($className) { 'ParserOutput' => 'includes/ParserOutput.php', 'ParserOptions' => 'includes/ParserOptions.php', 'ParserCache' => 'includes/ParserCache.php', + 'PatrolLog' => 'includes/PatrolLog.php', 'ProfilerSimple' => 'includes/ProfilerSimple.php', 'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php', 'Profiler' => 'includes/Profiler.php', @@ -196,6 +202,7 @@ function __autoload($className) { 'PopularPagesPage' => 'includes/SpecialPopularpages.php', 'PreferencesForm' => 'includes/SpecialPreferences.php', 'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php', + 'PasswordResetForm' => 'includes/SpecialResetpass.php', 'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php', 'RevisionDeleter' => 'includes/SpecialRevisiondelete.php', 'SpecialSearch' => 'includes/SpecialSearch.php', @@ -240,15 +247,26 @@ function __autoload($className) { 'Xml' => 'includes/Xml.php', 'ZhClient' => 'includes/ZhClient.php', 'memcached' => 'includes/memcached-client.php', + + # Media + 'BitmapHandler' => 'includes/media/Bitmap.php', + 'DjVuHandler' => 'includes/media/DjVu.php', + 'MediaHandler' => 'includes/media/Generic.php', + 'ImageHandler' => 'includes/media/Generic.php', + 'SvgHandler' => 'includes/media/SVG.php', + + # Normal 'UtfNormal' => 'includes/normal/UtfNormal.php', + + # Templates 'UsercreateTemplate' => 'includes/templates/Userlogin.php', 'UserloginTemplate' => 'includes/templates/Userlogin.php', + + # Languages 'Language' => 'languages/Language.php', - 'PasswordResetForm' => 'includes/SpecialResetpass.php', - 'PatrolLog' => 'includes/PatrolLog.php', 'RandomPage' => 'includes/SpecialRandompage.php', - // API classes + # API 'ApiBase' => 'includes/api/ApiBase.php', 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', 'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index cd49b0adec..54a2686c70 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1443,8 +1443,18 @@ $wgSiteNotice = ''; # Images settings # -/** dynamic server side image resizing ("Thumbnails") */ -$wgUseImageResize = false; +/** + * Plugins for media file type handling. + * Each entry in the array maps a MIME type to a class name + */ +$wgMediaHandlers = array( + 'image/jpeg' => 'BitmapHandler', + 'image/png' => 'BitmapHandler', + 'image/gif' => 'BitmapHandler', + 'image/svg+xml' => 'SvgHandler', + 'image/vnd.djvu' => 'DjVuHandler', +); + /** * Resizing can be done using PHP's internal image libraries or using @@ -1458,6 +1468,12 @@ $wgUseImageMagick = false; /** The convert command shipped with ImageMagick */ $wgImageMagickConvertCommand = '/usr/bin/convert'; +/** Sharpening parameter to ImageMagick */ +$wgSharpenParameter = '0x0.4'; + +/** Reduction in linear dimensions below which sharpening will be enabled */ +$wgSharpenReductionThreshold = 0.85; + /** * Use another resizing converter, e.g. GraphicMagick * %s will be replaced with the source path, %d with the destination @@ -1523,6 +1539,10 @@ $wgIgnoreImageErrors = false; */ $wgGenerateThumbnailOnParse = true; +/** Obsolete, always true, kept for compatibility with extensions */ +$wgUseImageResize = true; + + /** Set $wgCommandLineMode if it's not set already, to avoid notices */ if( !isset( $wgCommandLineMode ) ) { $wgCommandLineMode = false; @@ -2277,7 +2297,7 @@ $wgTrustedMediaFormats= array( MEDIATYPE_BITMAP, //all bitmap formats MEDIATYPE_AUDIO, //all audio formats MEDIATYPE_VIDEO, //all plain video formats - "image/svg", //svg (only needed if inline rendering of svg is not supported) + "image/svg+xml", //svg (only needed if inline rendering of svg is not supported) "application/pdf", //PDF files #"application/x-shockwave-flash", //flash/shockwave movie ); @@ -2380,7 +2400,7 @@ $wgReservedUsernames = array( * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't * perform basic stuff like MIME detection and which are vulnerable to further idiots uploading * crap files as images. When this directive is on, will be allowed in files with - * an "image/svg" MIME type. You should leave this disabled if your web server is misconfigured + * an "image/svg+xml" MIME type. You should leave this disabled if your web server is misconfigured * and doesn't send appropriate MIME types for SVG images. */ $wgAllowTitlesInSVG = false; @@ -2406,19 +2426,32 @@ $wgMaxShellFileSize = 102400; /** * DJVU settings - * Path of the djvutoxml executable + * Path of the djvudump executable * Enable this and $wgDjvuRenderer to enable djvu rendering */ -# $wgDjvuToXML = 'djvutoxml'; -$wgDjvuToXML = null; +# $wgDjvuDump = 'djvudump'; +$wgDjvuDump = null; /** * Path of the ddjvu DJVU renderer - * Enable this and $wgDjvuToXML to enable djvu rendering + * Enable this and $wgDjvuDump to enable djvu rendering */ # $wgDjvuRenderer = 'ddjvu'; $wgDjvuRenderer = null; +/** + * Path of the djvutoxml executable + * This works like djvudump except much, much slower as of version 3.5. + * + * For now I recommend you use djvudump instead. The djvuxml output is + * probably more stable, so we'll switch back to it as soon as they fix + * the efficiency problem. + * http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 + */ +# $wgDjvuToXML = 'djvutoxml'; +$wgDjvuToXML = null; + + /** * Shell command for the DJVU post processor * Default: pnmtopng, since ddjvu generates ppm output diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index c63791c79b..cd85147711 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -220,17 +220,121 @@ class DjVuImage { * @return string */ function retrieveMetaData() { - global $wgDjvuToXML; - if ( isset( $wgDjvuToXML ) ) { + global $wgDjvuToXML, $wgDjvuDump; + if ( isset( $wgDjvuDump ) ) { + # djvudump is faster as of version 3.5 + # http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 + wfProfileIn( 'djvudump' ); + $cmd = wfEscapeShellArg( $wgDjvuDump ) . ' ' . wfEscapeShellArg( $this->mFilename ); + $dump = wfShellExec( $cmd ); + $xml = $this->convertDumpToXML( $dump ); + wfProfileOut( 'djvudump' ); + } elseif ( isset( $wgDjvuToXML ) ) { + wfProfileIn( 'djvutoxml' ); $cmd = wfEscapeShellArg( $wgDjvuToXML ) . ' --without-anno --without-text ' . wfEscapeShellArg( $this->mFilename ); $xml = wfShellExec( $cmd ); + wfProfileOut( 'djvutoxml' ); } else { $xml = null; } return $xml; } - + + /** + * Hack to temporarily work around djvutoxml bug + */ + function convertDumpToXML( $dump ) { + if ( strval( $dump ) == '' ) { + return false; + } + + $xml = <<<EOT +<?xml version="1.0" ?> +<!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd"> +<DjVuXML> +<HEAD></HEAD> +<BODY> +EOT; + + $dump = str_replace( "\r", '', $dump ); + $line = strtok( $dump, "\n" ); + $m = false; + $good = false; + if ( preg_match( '/^( *)FORM:DJVU/', $line, $m ) ) { + # Single-page + if ( $this->parseFormDjvu( $line, $xml ) ) { + $good = true; + } else { + return false; + } + } elseif ( preg_match( '/^( *)FORM:DJVM/', $line, $m ) ) { + # Multi-page + $parentLevel = strlen( $m[1] ); + # Find DIRM + $line = strtok( "\n" ); + while ( $line !== false ) { + $childLevel = strspn( $line, ' ' ); + if ( $childLevel <= $parentLevel ) { + # End of chunk + break; + } + + if ( preg_match( '/^ *DIRM.*indirect/', $line ) ) { + wfDebug( "Indirect multi-page DjVu document, bad for server!\n" ); + return false; + } + if ( preg_match( '/^ *FORM:DJVU/', $line ) ) { + # Found page + if ( $this->parseFormDjvu( $line, $xml ) ) { + $good = true; + } else { + return false; + } + } + $line = strtok( "\n" ); + } + } + if ( !$good ) { + return false; + } + + $xml .= "</BODY>\n</DjVuXML>\n"; + return $xml; + } + + function parseFormDjvu( $line, &$xml ) { + $parentLevel = strspn( $line, ' ' ); + $line = strtok( "\n" ); + + # Find INFO + while ( $line !== false ) { + $childLevel = strspn( $line, ' ' ); + if ( $childLevel <= $parentLevel ) { + # End of chunk + break; + } + + if ( preg_match( '/^ *INFO *\[\d*\] *DjVu *(\d+)x(\d+), *\w*, *(\d+) *dpi, *gamma=([0-9.-]+)/', $line, $m ) ) { + $xml .= Xml::tags( 'OBJECT', + array( + #'data' => '', + #'type' => 'image/x.djvu', + 'height' => $m[2], + 'width' => $m[1], + #'usemap' => '', + ), + "\n" . + Xml::element( 'PARAM', array( 'name' => 'DPI', 'value' => $m[3] ) ) . "\n" . + Xml::element( 'PARAM', array( 'name' => 'GAMMA', 'value' => $m[4] ) ) . "\n" + ) . "\n"; + return true; + } + $line = strtok( "\n" ); + } + # Not found + return false; + } } diff --git a/includes/Exif.php b/includes/Exif.php index a3689088b2..f9de28a170 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -93,9 +93,9 @@ class Exif { var $basename; /** - * The private log to log to + * The private log to log to, e.g. 'exif' */ - var $log = 'exif'; + var $log = false; //@} @@ -561,7 +561,10 @@ class Exif { * @param $fname String: * @param $action Mixed: , default NULL. */ - function debug( $in, $fname, $action = NULL ) { + function debug( $in, $fname, $action = NULL ) { + if ( !$this->log ) { + return; + } $type = gettype( $in ); $class = ucfirst( __CLASS__ ); if ( $type === 'array' ) @@ -586,6 +589,9 @@ class Exif { * @param $io Boolean: Specify whether we're beginning or ending */ function debugFile( $fname, $io ) { + if ( !$this->log ) { + return; + } $class = ucfirst( __CLASS__ ); if ( $io ) { wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" ); diff --git a/includes/Image.php b/includes/Image.php index 6a5e730e5c..c6b23aca87 100644 --- a/includes/Image.php +++ b/includes/Image.php @@ -27,7 +27,8 @@ class Image const DELETED_FILE = 1; const DELETED_COMMENT = 2; const DELETED_USER = 4; - const DELETED_RESTRICTED = 8; + const DELETED_RESTRICTED = 8; + const RENDER_NOW = 1; /**#@+ * @private @@ -85,13 +86,12 @@ class Image } $this->title =& $title; $this->name = $title->getDBkey(); - $this->metadata = serialize ( array() ) ; + $this->metadata = ''; $n = strrpos( $this->name, '.' ); $this->extension = Image::normalizeExtension( $n ? substr( $this->name, $n + 1 ) : '' ); $this->historyLine = 0; - $this->page = 1; $this->dataLoaded = false; } @@ -244,7 +244,6 @@ class Image $this->fileExists = file_exists( $this->imagePath ); $this->fromSharedDirectory = false; $gis = array(); - $deja = false; if (!$this->fileExists) wfDebug(__METHOD__.': '.$this->imagePath." not found locally!\n"); @@ -268,18 +267,26 @@ class Image $this->mime = $magic->guessMimeType($this->imagePath,true); $this->type = $magic->getMediaType($this->imagePath,$this->mime); + $handler = MediaHandler::getHandler( $this->mime ); # Get size in bytes $this->size = filesize( $this->imagePath ); - # Height and width - $gis = self::getImageSize( $this->imagePath, $this->mime, $deja ); + # Height, width and metadata + if ( $handler ) { + $gis = $handler->getImageSize( $this, $this->imagePath ); + $this->metadata = $handler->getMetadata( $this, $this->imagePath ); + } else { + $gis = false; + $this->metadata = ''; + } wfDebug(__METHOD__.': '.$this->imagePath." loaded, ".$this->size." bytes, ".$this->mime.".\n"); } else { $this->mime = NULL; $this->type = MEDIATYPE_UNKNOWN; + $this->metadata = ''; wfDebug(__METHOD__.': '.$this->imagePath." NOT FOUND!\n"); } @@ -298,13 +305,6 @@ class Image # as ther's only one thread of execution, this should be safe anyway. $this->dataLoaded = true; - - if ( $deja ) { - $this->metadata = $deja->retrieveMetaData(); - } else { - $this->metadata = serialize( $this->retrieveExifData( $this->imagePath ) ); - } - if ( isset( $gis['bits'] ) ) $this->bits = $gis['bits']; else $this->bits = 0; @@ -331,9 +331,7 @@ class Image $this->loadFromRow( $row ); $this->imagePath = $this->getFullPath(); // Check for rows from a previous schema, quietly upgrade them - if ( is_null($this->type) ) { - $this->upgradeRow(); - } + $this->maybeUpgradeRow(); } elseif ( $wgUseSharedUploads && $wgSharedUploadDBname ) { # In case we're on a wgCapitalLinks=false wiki, we # capitalize the first letter of the filename before @@ -354,9 +352,7 @@ class Image $this->loadFromRow( $row ); // Check for rows from a previous schema, quietly upgrade them - if ( is_null($this->type) ) { - $this->upgradeRow(); - } + $this->maybeUpgradeRow(); } } @@ -368,7 +364,7 @@ class Image $this->type = 0; $this->fileExists = false; $this->fromSharedDirectory = false; - $this->metadata = serialize ( array() ) ; + $this->metadata = ''; $this->mime = false; } @@ -395,9 +391,7 @@ class Image if (!$minor) $minor= "unknown"; $this->mime = $major.'/'.$minor; } - $this->metadata = $row->img_metadata; - if ( $this->metadata == "" ) $this->metadata = serialize ( array() ) ; $this->dataLoaded = true; } @@ -424,8 +418,21 @@ class Image } /** - * Metadata was loaded from the database, but the row had a marker indicating it needs to be - * upgraded from the 1.4 schema, which had no width, height, bits or type. Upgrade the row. + * Upgrade a row if it needs it + */ + function maybeUpgradeRow() { + if ( is_null($this->type) || $this->mime == 'image/svg' ) { + $this->upgradeRow(); + } else { + $handler = $this->getHandler(); + if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) { + $this->upgradeRow(); + } + } + } + + /** + * Fix assorted version-related problems with the image row by reloading it from the file */ function upgradeRow() { global $wgDBname, $wgSharedUploadDBname; @@ -450,7 +457,7 @@ class Image list( $major, $minor ) = self::splitMime( $this->mime ); - wfDebug(__METHOD__.': upgrading '.$this->name." to 1.5 schema\n"); + wfDebug(__METHOD__.': upgrading '.$this->name." to the current schema\n"); $dbw->update( 'image', array( @@ -543,23 +550,49 @@ class Image /** * Return the width of the image * - * Returns -1 if the file specified is not a known image type + * Returns false on error * @public */ - function getWidth() { + function getWidth( $page = 1 ) { $this->load(); - return $this->width; + if ( $this->isMultipage() ) { + $dim = $this->getHandler()->getPageDimensions( $this, $page ); + if ( $dim ) { + return $dim['width']; + } else { + return false; + } + } else { + return $this->width; + } } /** * Return the height of the image * - * Returns -1 if the file specified is not a known image type + * Returns false on error * @public */ - function getHeight() { + function getHeight( $page = 1 ) { + $this->load(); + if ( $this->isMultipage() ) { + $dim = $this->getHandler()->getPageDimensions( $this, $page ); + if ( $dim ) { + return $dim['height']; + } else { + return false; + } + } else { + return $this->height; + } + } + + /** + * Get handler-specific metadata + */ + function getMetadata() { $this->load(); - return $this->height; + return $this->metadata; } /** @@ -599,58 +632,10 @@ class Image * @todo remember the result of this check. */ function canRender() { - global $wgUseImageMagick, $wgDjvuRenderer; - - if( $this->getWidth()<=0 || $this->getHeight()<=0 ) return false; - - $mime= $this->getMimeType(); - - if (!$mime || $mime==='unknown' || $mime==='unknown/unknown') return false; - - #if it's SVG, check if there's a converter enabled - if ($mime === 'image/svg' || $mime == 'image/svg+xml' ) { - global $wgSVGConverters, $wgSVGConverter; - - if ($wgSVGConverter && isset( $wgSVGConverters[$wgSVGConverter])) { - wfDebug( "Image::canRender: SVG is ready!\n" ); - return true; - } else { - wfDebug( "Image::canRender: SVG renderer missing\n" ); - } - } - - #image formats available on ALL browsers - if ( $mime === 'image/gif' - || $mime === 'image/png' - || $mime === 'image/jpeg' ) return true; - - #image formats that can be converted to the above formats - if ($wgUseImageMagick) { - #convertable by ImageMagick (there are more...) - if ( $mime === 'image/vnd.wap.wbmp' - || $mime === 'image/x-xbitmap' - || $mime === 'image/x-xpixmap' - #|| $mime === 'image/x-icon' #file may be split into multiple parts - || $mime === 'image/x-portable-anymap' - || $mime === 'image/x-portable-bitmap' - || $mime === 'image/x-portable-graymap' - || $mime === 'image/x-portable-pixmap' - #|| $mime === 'image/x-photoshop' #this takes a lot of CPU and RAM! - || $mime === 'image/x-rgb' - || $mime === 'image/x-bmp' - || $mime === 'image/tiff' ) return true; - } - else { - #convertable by the PHP GD image lib - if ( $mime === 'image/vnd.wap.wbmp' - || $mime === 'image/x-xbitmap' ) return true; - } - if ( $mime === 'image/vnd.djvu' && isset( $wgDjvuRenderer ) && $wgDjvuRenderer ) return true; - - return false; + $handler = $this->getHandler(); + return $handler && $handler->canRender(); } - /** * Return true if the file is of a type that can't be directly * rendered by typical browsers and needs to be re-rasterized. @@ -662,13 +647,8 @@ class Image * @return bool */ function mustRender() { - $mime= $this->getMimeType(); - - if ( $mime === "image/gif" - || $mime === "image/png" - || $mime === "image/jpeg" ) return false; - - return true; + $handler = $this->getHandler(); + return $handler && $handler->mustRender(); } /** @@ -734,15 +714,7 @@ class Image * @public */ function getEscapeLocalURL( $query=false) { - $this->getTitle(); - if ( $query === false ) { - if ( $this->page != 1 ) { - $query = 'page=' . $this->page; - } else { - $query = ''; - } - } - return $this->title->escapeLocalURL( $query ); + return $this->getTitle()->escapeLocalURL( $query ); } /** @@ -790,77 +762,72 @@ class Image * @todo document * @private */ - function thumbUrl( $width, $subdir='thumb') { + function thumbUrl( $thumbName ) { global $wgUploadPath, $wgUploadBaseUrl, $wgSharedUploadPath; - global $wgSharedThumbnailScriptPath, $wgThumbnailScriptPath; - - // Generate thumb.php URL if possible - $script = false; - $url = false; + if($this->fromSharedDirectory) { + $base = ''; + $path = $wgSharedUploadPath; + } else { + $base = $wgUploadBaseUrl; + $path = $wgUploadPath; + } + if ( Image::isHashed( $this->fromSharedDirectory ) ) { + $url = "{$base}{$path}/thumb" . + wfGetHashPath($this->name, $this->fromSharedDirectory) + . $this->name.'/'.$thumbName; + $url = wfUrlencode( $url ); + } else { + $url = "{$base}{$path}/thumb/{$thumbName}"; + } + return $url; + } + function getTransformScript() { + global $wgSharedThumbnailScriptPath, $wgThumbnailScriptPath; if ( $this->fromSharedDirectory ) { - if ( $wgSharedThumbnailScriptPath ) { - $script = $wgSharedThumbnailScriptPath; - } + $script = $wgSharedThumbnailScriptPath; } else { - if ( $wgThumbnailScriptPath ) { - $script = $wgThumbnailScriptPath; - } + $script = $wgThumbnailScriptPath; } if ( $script ) { - $url = $script . '?f=' . urlencode( $this->name ) . '&w=' . urlencode( $width ); - if( $this->mustRender() ) { - $url.= '&r=1'; - } + return "$script?f=" . urlencode( $this->name ); } else { - $name = $this->thumbName( $width ); - if($this->fromSharedDirectory) { - $base = ''; - $path = $wgSharedUploadPath; - } else { - $base = $wgUploadBaseUrl; - $path = $wgUploadPath; - } - if ( Image::isHashed( $this->fromSharedDirectory ) ) { - $url = "{$base}{$path}/{$subdir}" . - wfGetHashPath($this->name, $this->fromSharedDirectory) - . $this->name.'/'.$name; - $url = wfUrlencode( $url ); - } else { - $url = "{$base}{$path}/{$subdir}/{$name}"; - } + return false; } - return array( $script !== false, $url ); } /** - * Return the file name of a thumbnail of the specified width + * Get a ThumbnailImage which is the same size as the source + */ + function getUnscaledThumb( $page = false ) { + if ( $page ) { + $params = array( + 'page' => $page, + 'width' => $this->getWidth( $page ) + ); + } else { + $params = array( 'width' => $this->getWidth() ); + } + return $this->transform( $params ); + } + + /** + * Return the file name of a thumbnail with the specified parameters * - * @param integer $width Width of the thumbnail image - * @param boolean $shared Does the thumbnail come from the shared repository? + * @param array $params Handler-specific parameters * @private */ - function thumbName( $width ) { - global $wgDjvuOutputExtension; - $thumb = $width."px-".$this->name; - if ( $this->page != 1 ) { - $thumb = "page{$this->page}-$thumb"; + function thumbName( $params ) { + $handler = $this->getHandler(); + if ( !$handler ) { + return null; } - - if( $this->mustRender() ) { - if( $this->canRender() ) { - list( $ext, $mime ) = self::getThumbType( $this->extension, $this->mime ); - if ( $ext != $this->extension ) { - $thumb .= ".$ext"; - } - } - else { - #should we use iconThumb here to get a symbolic thumbnail? - #or should we fail with an internal error? - return NULL; //can't make bitmap - } + list( $thumbExt, $thumbMime ) = self::getThumbType( $this->extension, $this->mime ); + $thumbName = $handler->makeParamString( $params ) . '-' . $this->name; + if ( $thumbExt != $this->extension ) { + $thumbName .= ".$thumbExt"; } - return $thumb; + return $thumbName; } /** @@ -879,9 +846,13 @@ class Image * @param integer $height maximum height of the image (optional) * @public */ - function createThumb( $width, $height=-1 ) { - $thumb = $this->getThumbnail( $width, $height ); - if( is_null( $thumb ) ) return ''; + function createThumb( $width, $height = -1 ) { + $params = array( 'width' => $width ); + if ( $height != -1 ) { + $params['height'] = $height; + } + $thumb = $this->transform( $params ); + if( is_null( $thumb ) || $thumb->isError() ) return ''; return $thumb->getUrl(); } @@ -899,149 +870,89 @@ class Image * * @return ThumbnailImage or null on failure * @public + * + * @deprecated use transform() */ function getThumbnail( $width, $height=-1, $render = true ) { - wfProfileIn( __METHOD__ ); - if ($this->canRender()) { - if ( $height > 0 ) { - $this->load(); - if ( $width > $this->width * $height / $this->height ) { - $width = wfFitBoxWidth( $this->width, $this->height, $height ); - } - } - if ( $render ) { - $thumb = $this->renderThumb( $width ); - } else { - // Don't render, just return the URL - if ( $this->validateThumbParams( $width, $height ) ) { - if ( !$this->mustRender() && $width == $this->width && $height == $this->height ) { - $url = $this->getURL(); - } else { - list( /* $isScriptUrl */, $url ) = $this->thumbUrl( $width ); - } - $thumb = new ThumbnailImage( $url, $width, $height ); - } else { - $thumb = null; - } - } - } else { - // not a bitmap or renderable image, don't try. - $thumb = $this->iconThumb(); + $params = array( 'width' => $width ); + if ( $height != -1 ) { + $params['height'] = $height; } - wfProfileOut( __METHOD__ ); - return $thumb; + $flags = $render ? self::RENDER_NOW : 0; + return $this->transform( $params, $flags ); } - - /** - * @return ThumbnailImage - */ - function iconThumb() { - global $wgStylePath, $wgStyleDirectory; - - $try = array( 'fileicon-' . $this->extension . '.png', 'fileicon.png' ); - foreach( $try as $icon ) { - $path = '/common/images/icons/' . $icon; - $filepath = $wgStyleDirectory . $path; - if( file_exists( $filepath ) ) { - return new ThumbnailImage( $wgStylePath . $path, 120, 120 ); - } - } - return null; - } - + /** - * Validate thumbnail parameters and fill in the correct height + * Transform a media file * - * @param integer &$width Specified width (input/output) - * @param integer &$height Height (output only) - * @return false to indicate that an error should be returned to the user. + * @param array $params An associative array of handler-specific parameters. Typical + * keys are width, height and page. + * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering + * @return MediaTransformOutput */ - function validateThumbParams( &$width, &$height ) { - global $wgSVGMaxSize, $wgMaxImageArea; + function transform( $params, $flags = 0 ) { + global $wgGenerateThumbnailOnParse, $wgUseSquid, $wgIgnoreImageErrors; - $this->load(); + wfProfileIn( __METHOD__ ); + do { + $handler = $this->getHandler(); + if ( !$handler || !$handler->canRender() ) { + // not a bitmap or renderable image, don't try. + $thumb = $this->iconThumb(); + break; + } - if ( ! $this->exists() ) - { - # If there is no image, there will be no thumbnail - return false; - } + $script = $this->getTransformScript(); + if ( $script && !($flags & self::RENDER_NOW) ) { + // Use a script to transform on client request + $thumb = $handler->getScriptedTransform( $this, $script, $params ); + break; + } - $width = intval( $width ); + $handler->normaliseParams( $this, $params ); + list( $thumbExt, $thumbMime ) = self::getThumbType( $this->extension, $this->mime ); + $thumbName = $this->thumbName( $params ); + $thumbPath = wfImageThumbDir( $this->name, $this->fromSharedDirectory ) . "/$thumbName"; + $thumbUrl = $this->thumbUrl( $thumbName ); - # Sanity check $width - if( $width <= 0 || $this->width <= 0) { - # BZZZT - return false; - } + $this->migrateThumbFile( $thumbName ); - # Don't thumbnail an image so big that it will fill hard drives and send servers into swap - # JPEG has the handy property of allowing thumbnailing without full decompression, so we make - # an exception for it. - if ( $this->getMediaType() == MEDIATYPE_BITMAP && - $this->getMimeType() !== 'image/jpeg' && - $this->width * $this->height > $wgMaxImageArea ) - { - return false; - } + if ( file_exists( $thumbPath ) ) { + $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } - # Don't make an image bigger than the source, or wgMaxSVGSize for SVGs - if ( $this->mustRender() ) { - $width = min( $width, $wgSVGMaxSize ); - } elseif ( $width > $this->width - 1 ) { - $width = $this->width; - $height = $this->height; - return true; - } + if ( !$wgGenerateThumbnailOnParse && !($flags & self::RENDER_NOW ) ) { + $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } + $thumb = $handler->doTransform( $this, $thumbPath, $thumbUrl, $params ); + + // Ignore errors if requested + if ( !$thumb ) { + $thumb = null; + } elseif ( $thumb->isError() ) { + $this->lastError = $thumb->toText(); + if ( $wgIgnoreImageErrors ) { + $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + } + } + + if ( $wgUseSquid ) { + wfPurgeSquidServers( array( $thumbUrl ) ); + } + } while (false); - $height = self::scaleHeight( $this->width, $this->height, $width ); - return true; + wfProfileOut( __METHOD__ ); + return $thumb; } /** - * Create a thumbnail of the image having the specified width. - * The thumbnail will not be created if the width is larger than the - * image's width. Let the browser do the scaling in this case. - * The thumbnail is stored on disk and is only computed if the thumbnail - * file does not exist OR if it is older than the image. - * Returns an object which can return the pathname, URL, and physical - * pixel size of the thumbnail -- or null on failure. - * - * @return ThumbnailImage or null on failure - * @private + * Fix thumbnail files from 1.4 or before, with extreme prejudice */ - function renderThumb( $width, $useScript = true ) { - global $wgUseSquid, $wgThumbnailEpoch; - - wfProfileIn( __METHOD__ ); - - $this->load(); - $height = -1; - if ( !$this->validateThumbParams( $width, $height ) ) { - # Validation error - wfProfileOut( __METHOD__ ); - return null; - } - - if ( !$this->mustRender() && $width == $this->width && $height == $this->height ) { - # validateThumbParams (or the user) wants us to return the unscaled image - $thumb = new ThumbnailImage( $this->getURL(), $width, $height ); - wfProfileOut( __METHOD__ ); - return $thumb; - } - - list( $isScriptUrl, $url ) = $this->thumbUrl( $width ); - if ( $isScriptUrl && $useScript ) { - // Use thumb.php to render the image - $thumb = new ThumbnailImage( $url, $width, $height ); - wfProfileOut( __METHOD__ ); - return $thumb; - } - - $thumbName = $this->thumbName( $width, $this->fromSharedDirectory ); + function migrateThumbFile( $thumbName ) { $thumbDir = wfImageThumbDir( $this->name, $this->fromSharedDirectory ); - $thumbPath = $thumbDir.'/'.$thumbName; - + $thumbPath = "$thumbDir/$thumbName"; if ( is_dir( $thumbPath ) ) { // Directory where file should be // This happened occasionally due to broken migration code in 1.5 @@ -1054,247 +965,50 @@ class Image break; } } - // Code below will ask if it exists, and the answer is now no + // Doesn't exist anymore clearstatcache(); } - - $done = true; - if ( !file_exists( $thumbPath ) || - filemtime( $thumbPath ) < wfTimestamp( TS_UNIX, $wgThumbnailEpoch ) ) - { - // Create the directory if it doesn't exist - if ( is_file( $thumbDir ) ) { - // File where thumb directory should be, destroy if possible - @unlink( $thumbDir ); - } - wfMkdirParents( $thumbDir ); - - $oldThumbPath = wfDeprecatedThumbDir( $thumbName, 'thumb', $this->fromSharedDirectory ). - '/'.$thumbName; - $done = false; - - // Migration from old directory structure - if ( is_file( $oldThumbPath ) ) { - if ( filemtime($oldThumbPath) >= filemtime($this->imagePath) ) { - if ( file_exists( $thumbPath ) ) { - if ( !is_dir( $thumbPath ) ) { - // Old image in the way of rename - unlink( $thumbPath ); - } else { - // This should have been dealt with already - throw new MWException( "Directory where image should be: $thumbPath" ); - } - } - // Rename the old image into the new location - rename( $oldThumbPath, $thumbPath ); - $done = true; - } else { - unlink( $oldThumbPath ); - } - } - if ( !$done ) { - $this->lastError = self::reallyRenderThumb( $this->imagePath, $thumbPath, $this->mime, - $width, $height, $this->page ); - if ( $this->lastError === true ) { - $done = true; - } elseif( $GLOBALS['wgIgnoreImageErrors'] ) { - // Log the error but output anyway. - // With luck it's a transitory error... - $done = true; - } - - # Purge squid - # This has to be done after the image is updated and present for all machines on NFS, - # or else the old version might be stored into the squid again - if ( $wgUseSquid ) { - $urlArr = array( $url ); - wfPurgeSquidServers($urlArr); - } - } - } - - if ( $done ) { - $thumb = new ThumbnailImage( $url, $width, $height, $thumbPath ); - } else { - $thumb = null; + if ( is_file( $thumbDir ) ) { + // File where directory should be + unlink( $thumbDir ); + // Doesn't exist anymore + clearstatcache(); } - wfProfileOut( __METHOD__ ); - return $thumb; - } // END OF function renderThumb - - /** - * Really render a thumbnail - * Call this only for images for which canRender() returns true. - * - * @param string $source Source filename - * @param string $destination Destination filename - * @param string $mime MIME type of source - * @param integer $width Destination width in pixels - * @param integer $height Destination height in pixels - * @param integer $page Which page of a multi-page document to display. Ignored - * for source MIME types which do not support multiple pages. - */ - static function reallyRenderThumb( $source, $destination, $mime, $width, $height, $page = false ) { - global $wgSVGConverters, $wgSVGConverter; - global $wgUseImageMagick, $wgImageMagickConvertCommand; - global $wgCustomConvertCommand; - global $wgDjvuRenderer, $wgDjvuPostProcessor; - - $err = false; - $cmd = ""; - $retval = 0; - - if( $mime == "image/svg" || $mime == 'image/svg+xml' ) { - #Right now we have only SVG - - global $wgSVGConverters, $wgSVGConverter; - if( isset( $wgSVGConverters[$wgSVGConverter] ) ) { - global $wgSVGConverterPath; - $cmd = str_replace( - array( '$path/', '$width', '$height', '$input', '$output' ), - array( $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "", - intval( $width ), - intval( $height ), - wfEscapeShellArg( $source ), - wfEscapeShellArg( $destination ) ), - $wgSVGConverters[$wgSVGConverter] ) . " 2>&1"; - wfProfileIn( 'rsvg' ); - wfDebug( "reallyRenderThumb SVG: $cmd\n" ); - $err = wfShellExec( $cmd, $retval ); - wfProfileOut( 'rsvg' ); - } - } elseif ( $mime === "image/vnd.djvu" && $wgDjvuRenderer ) { - // DJVU image - // The file contains several images. First, extract the - // page in hi-res, if it doesn't yet exist. Then, thumbnail - // it. - - $cmd = wfEscapeShellArg( $wgDjvuRenderer ) . " -format=ppm -page={$page} -size=${width}x${height} " . - wfEscapeShellArg( $source ); - if ( $wgDjvuPostProcessor ) { - $cmd .= " | {$wgDjvuPostProcessor}"; - } - $cmd .= ' > ' . wfEscapeShellArg($destination); - wfProfileIn( 'ddjvu' ); - wfDebug( "reallyRenderThumb DJVU: $cmd\n" ); - $err = wfShellExec( $cmd, $retval ); - wfProfileOut( 'ddjvu' ); - - } elseif ( $wgUseImageMagick ) { - # use ImageMagick - - if ( $mime == 'image/jpeg' ) { - $quality = "-quality 80"; // 80% - } elseif ( $mime == 'image/png' ) { - $quality = "-quality 95"; // zlib 9, adaptive filtering - } else { - $quality = ''; // default - } + } - # Specify white background color, will be used for transparent images - # in Internet Explorer/Windows instead of default black. - - # Note, we specify "-size {$width}" and NOT "-size {$width}x{$height}". - # It seems that ImageMagick has a bug wherein it produces thumbnails of - # the wrong size in the second case. - - $cmd = wfEscapeShellArg($wgImageMagickConvertCommand) . - " {$quality} -background white -size {$width} ". - wfEscapeShellArg($source) . - // Coalesce is needed to scale animated GIFs properly (bug 1017). - ' -coalesce ' . - // For the -resize option a "!" is needed to force exact size, - // or ImageMagick may decide your ratio is wrong and slice off - // a pixel. - " -thumbnail " . wfEscapeShellArg( "{$width}x{$height}!" ) . - " -depth 8 " . - wfEscapeShellArg($destination) . " 2>&1"; - wfDebug("reallyRenderThumb: running ImageMagick: $cmd\n"); - wfProfileIn( 'convert' ); - $err = wfShellExec( $cmd, $retval ); - wfProfileOut( 'convert' ); - } elseif( $wgCustomConvertCommand ) { - # Use a custom convert command - # Variables: %s %d %w %h - $src = wfEscapeShellArg( $source ); - $dst = wfEscapeShellArg( $destination ); - $cmd = $wgCustomConvertCommand; - $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames - $cmd = str_replace( '%h', $height, str_replace( '%w', $width, $cmd ) ); # Size - wfDebug( "reallyRenderThumb: Running custom convert command $cmd\n" ); - wfProfileIn( 'convert' ); - $err = wfShellExec( $cmd, $retval ); - wfProfileOut( 'convert' ); - } else { - # Use PHP's builtin GD library functions. - # - # First find out what kind of file this is, and select the correct - # input routine for this. - - $typemap = array( - 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ), - 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ), - 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ), - 'image/vnd.wap.wmbp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ), - 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ), - ); - if( !isset( $typemap[$mime] ) ) { - $err = 'Image type not supported'; - wfDebug( "$err\n" ); - return $err; - } - list( $loader, $colorStyle, $saveType ) = $typemap[$mime]; + /** + * Get a MediaHandler instance for this image + */ + function getHandler() { + return MediaHandler::getHandler( $this->getMimeType() ); + } - if( !function_exists( $loader ) ) { - $err = "Incomplete GD library configuration: missing function $loader"; - wfDebug( "$err\n" ); - return $err; - } + /** + * Get a ThumbnailImage representing a file type icon + * @return ThumbnailImage + */ + function iconThumb() { + global $wgStylePath, $wgStyleDirectory; - $src_image = call_user_func( $loader, $source ); - $dst_image = imagecreatetruecolor( $width, $height ); - imagecopyresampled( $dst_image, $src_image, - 0,0,0,0, - $width, $height, imagesx( $src_image ), imagesy( $src_image ) ); - call_user_func( $saveType, $dst_image, $destination ); - imagedestroy( $dst_image ); - imagedestroy( $src_image ); - } - - # - # Check for zero-sized thumbnails. Those can be generated when - # no disk space is available or some other error occurs - # - $removed = false; - if( file_exists( $destination ) ) { - $thumbstat = stat( $destination ); - if( $thumbstat['size'] == 0 || $retval != 0 ) { - wfDebugLog( 'thumbnail', - sprintf( 'Removing bad %d-byte thumbnail "%s"', - $thumbstat['size'], $destination ) ); - unlink( $destination ); - $removed = true; + $try = array( 'fileicon-' . $this->extension . '.png', 'fileicon.png' ); + foreach( $try as $icon ) { + $path = '/common/images/icons/' . $icon; + $filepath = $wgStyleDirectory . $path; + if( file_exists( $filepath ) ) { + return new ThumbnailImage( $wgStylePath . $path, 120, 120 ); } } - if ( $retval != 0 || $removed ) { - wfDebugLog( 'thumbnail', - sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', - wfHostname(), $retval, trim($err), $cmd ) ); - return wfMsg( 'thumbnail_error', $err ); - } else { - return true; - } + return null; } + /** + * Get last thumbnailing error. + * Largely obsolete. + */ function getLastError() { return $this->lastError; } - static function imageJpegWrapper( $dst_image, $thumbPath ) { - imageinterlace( $dst_image ); - imagejpeg( $dst_image, $thumbPath, 95 ); - } - /** * Get all thumbnail names previously generated for this image */ @@ -1328,7 +1042,7 @@ class Image */ function purgeMetadataCache() { clearstatcache(); - $this->loadFromFile(); + $this->upgradeRow(); $this->saveToCache(); } @@ -1348,7 +1062,7 @@ class Image foreach ( $files as $file ) { $m = array(); if ( preg_match( '/^(\d+)px/', $file, $m ) ) { - list( /* $isScriptUrl */, $url ) = $this->thumbUrl( $m[1] ); + $url = $this->thumbUrl( $m[1] ); $urls[] = $url; @unlink( "$dir/$file" ); } @@ -1391,6 +1105,9 @@ class Image $update->doUpdate(); } + /** + * Check the image table schema on the given connection for subtle problems + */ function checkDBSchema(&$db) { static $checkDone = false; global $wgCheckDBSchema; @@ -1711,74 +1428,22 @@ class Image return $retVal; } - /** - * Retrive Exif data from the file and prune unrecognized tags - * and/or tags with invalid contents - * - * @param $filename - * @return array - */ - private function retrieveExifData( $filename ) { - global $wgShowEXIF; - - /* - if ( $this->getMimeType() !== "image/jpeg" ) - return array(); - */ - - if( $wgShowEXIF && file_exists( $filename ) ) { - $exif = new Exif( $filename ); - return $exif->getFilteredData(); - } - - return array(); - } - function getExifData() { global $wgRequest; - if ( $this->metadata === '0' || $this->mime == 'image/vnd.djvu' ) + $handler = $this->getHandler(); + if ( !$handler || $handler->getMetadataType( $this ) != 'exif' ) { return array(); - - $purge = $wgRequest->getVal( 'action' ) == 'purge'; - $ret = unserialize( $this->metadata ); - - $oldver = isset( $ret['MEDIAWIKI_EXIF_VERSION'] ) ? $ret['MEDIAWIKI_EXIF_VERSION'] : 0; - $newver = Exif::version(); - - if ( !count( $ret ) || $purge || $oldver != $newver ) { - $this->purgeMetadataCache(); - $this->updateExifData( $newver ); } - if ( isset( $ret['MEDIAWIKI_EXIF_VERSION'] ) ) - unset( $ret['MEDIAWIKI_EXIF_VERSION'] ); - $format = new FormatExif( $ret ); - - return $format->getFormattedData(); - } - - function updateExifData( $version ) { - if ( $this->getImagePath() === false ) # Not a local image - return; - - # Get EXIF data from image - $exif = $this->retrieveExifData( $this->imagePath ); - if ( count( $exif ) ) { - $exif['MEDIAWIKI_EXIF_VERSION'] = $version; - $this->metadata = serialize( $exif ); - } else { - $this->metadata = '0'; + if ( !$this->metadata ) { + return array(); } + $exif = unserialize( $this->metadata ); + if ( !$exif ) { + return array(); + } + $format = new FormatExif( $exif ); - # Update EXIF data in database - $dbw = wfGetDB( DB_MASTER ); - - $this->checkDBSchema($dbw); - - $dbw->update( 'image', - array( 'img_metadata' => $this->metadata ), - array( 'img_name' => $this->name ), - __METHOD__ - ); + return $format->getFormattedData(); } /** @@ -2145,12 +1810,17 @@ class Image // an archived file revision. if( is_null( $row->fa_metadata ) ) { $tempFile = $store->filePath( $row->fa_storage_key ); - $metadata = serialize( $this->retrieveExifData( $tempFile ) ); $magic = MimeMagic::singleton(); $mime = $magic->guessMimeType( $tempFile, true ); $media_type = $magic->getMediaType( $tempFile, $mime ); list( $major_mime, $minor_mime ) = self::splitMime( $mime ); + $handler = MediaHandler::getHandler( $mime ); + if ( $handler ) { + $metadata = $handler->getMetadata( $image, $tempFile ); + } else { + $metadata = ''; + } } else { $metadata = $row->fa_metadata; $major_mime = $row->fa_major_mime; @@ -2257,64 +1927,6 @@ class Image return $revisions; } - /** - * Select a page from a multipage document. Determines the page used for - * rendering thumbnails. - * - * @param $page Integer: page number, starting with 1 - */ - function selectPage( $page ) { - if( $this->initializeMultiPageXML() ) { - wfDebug( __METHOD__." selecting page $page \n" ); - $this->page = $page; - $o = $this->multiPageXML->BODY[0]->OBJECT[$page-1]; - $this->height = intval( $o['height'] ); - $this->width = intval( $o['width'] ); - } else { - wfDebug( __METHOD__." selectPage($page) for bogus multipage xml on '$this->name'\n" ); - return; - } - } - - /** - * Lazy-initialize multipage XML metadata for DjVu files. - * @return bool true if $this->multiPageXML is set up and ready; - * false if corrupt or otherwise failing - */ - function initializeMultiPageXML() { - $this->load(); - if ( isset( $this->multiPageXML ) ) { - return true; - } - - # - # Check for files uploaded prior to DJVU support activation, - # or damaged. - # - if( empty( $this->metadata ) || $this->metadata == serialize( array() ) ) { - $deja = new DjVuImage( $this->imagePath ); - $this->metadata = $deja->retrieveMetaData(); - $this->purgeMetadataCache(); - - # Update metadata in the database - $dbw = wfGetDB( DB_MASTER ); - $dbw->update( 'image', - array( 'img_metadata' => $this->metadata ), - array( 'img_name' => $this->name ), - __METHOD__ - ); - } - wfSuppressWarnings(); - try { - $this->multiPageXML = new SimpleXMLElement( $this->metadata ); - } catch( Exception $e ) { - wfDebug( "Bogus multipage XML metadata on '$this->name'\n" ); - $this->multiPageXML = null; - } - wfRestoreWarnings(); - return isset( $this->multiPageXML ); - } - /** * Returns 'true' if this image is a multipage document, e.g. a DJVU * document. @@ -2322,7 +1934,8 @@ class Image * @return Bool */ function isMultipage() { - return ( $this->mime == 'image/vnd.djvu' ); + $handler = $this->getHandler(); + return $handler && $handler->isMultiPage(); } /** @@ -2330,13 +1943,10 @@ class Image * documents which aren't multipage documents */ function pageCount() { - if ( ! $this->isMultipage() ) { - return null; - } - if( $this->initializeMultiPageXML() ) { - return count( $this->multiPageXML->xpath( '//OBJECT' ) ); + $handler = $this->getHandler(); + if ( $handler && $handler->isMultiPage() ) { + return $handler->pageCount( $this ); } else { - wfDebug( "Requested pageCount() for bogus multi-page metadata for '$this->name'\n" ); return null; } } @@ -2358,7 +1968,11 @@ class Image */ static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) { // Exact integer multiply followed by division - return round( $srcHeight * $dstWidth / $srcWidth ); + if ( $srcWidth == 0 ) { + return 0; + } else { + return round( $srcHeight * $dstWidth / $srcWidth ); + } } /** @@ -2366,28 +1980,11 @@ class Image * can't be determined. * * @param string $fileName The filename - * @param string $mimeType The MIME type of the file - * @param object $deja Filled with a DjVu object if the mime type is image/vnd.djvu * @return array */ - static function getImageSize( $fileName, $mimeType, &$deja ) { - $magic =& MimeMagic::singleton(); - if( $mimeType == 'image/svg' || $mimeType == 'image/svg+xml' ) { - $gis = wfGetSVGsize( $fileName ); - } elseif( $mimeType == 'image/vnd.djvu' ) { - wfSuppressWarnings(); - $deja = new DjVuImage( $fileName ); - $gis = $deja->getImageSize(); - wfRestoreWarnings(); - } elseif ( !$magic->isPHPImageType( $mimeType ) ) { - # Don't try to get the width and height of sound and video files, that's bad for performance - $gis = false; - } else { - wfSuppressWarnings(); - $gis = getimagesize( $fileName ); - wfRestoreWarnings(); - } - return $gis; + function getImageSize( $fileName ) { + $handler = $this->getHandler(); + return $handler->getImageSize( $this, $fileName ); } /** @@ -2395,22 +1992,14 @@ class Image * @return array thumbnail extension and MIME type */ static function getThumbType( $ext, $mime ) { - switch ( $mime ) { - case 'image/svg': - case 'image/svg+xml': - $ext = 'png'; - $mime = 'image/png'; - break; - case 'image/vnd.djvu': - $ext = $GLOBALS['wgDjvuOutputExtension']; - $magic = MimeMagic::singleton(); - $mime = $magic->guessTypesForExtension( $ext ); - break; + $handler = MediaHandler::getHandler( $mime ); + if ( $handler ) { + return $handler->getThumbType( $ext, $mime ); + } else { + return array( $ext, $mime ); } - return array( $ext, $mime ); } - } //class class ArchivedFile @@ -2519,60 +2108,6 @@ class ArchivedFile } } -/** - * Wrapper class for thumbnail images - */ -class ThumbnailImage { - /** - * @param string $path Filesystem path to the thumb - * @param string $url URL path to the thumb - * @private - */ - function ThumbnailImage( $url, $width, $height, $path = false ) { - $this->url = $url; - $this->width = round( $width ); - $this->height = round( $height ); - # These should be integers when they get here. - # If not, there's a bug somewhere. But let's at - # least produce valid HTML code regardless. - $this->path = $path; - } - - /** - * @return string The thumbnail URL - */ - function getUrl() { - return $this->url; - } - - /** - * Return HTML <img ... /> tag for the thumbnail, will include - * width and height attributes and a blank alt text (as required). - * - * You can set or override additional attributes by passing an - * associative array of name => data pairs. The data will be escaped - * for HTML output, so should be in plaintext. - * - * @param array $attribs - * @return string - * @public - */ - function toHtml( $attribs = array() ) { - $attribs['src'] = $this->url; - $attribs['width'] = $this->width; - $attribs['height'] = $this->height; - if( !isset( $attribs['alt'] ) ) $attribs['alt'] = ''; - - $html = '<img '; - foreach( $attribs as $name => $data ) { - $html .= $name . '="' . htmlspecialchars( $data ) . '" '; - } - $html .= '/>'; - return $html; - } - -} - /** * Aliases for backwards compatibility with 1.6 */ diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 86bf786d33..036d71783c 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -183,7 +183,7 @@ class ImageGallery * */ function toHTML() { - global $wgLang, $wgGenerateThumbnailOnParse; + global $wgLang; $sk = $this->getSkin(); @@ -191,6 +191,7 @@ class ImageGallery if( $this->mCaption ) $s .= "\n\t<caption>{$this->mCaption}</caption>"; + $params = array( 'width' => $this->mWidths, 'height' => $this->mHeights ); $i = 0; foreach ( $this->mImages as $pair ) { $img =& $pair[0]; @@ -206,7 +207,7 @@ class ImageGallery # The image is blacklisted, just show it as a text link. $thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">' . $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '</div>'; - } elseif( !( $thumb = $img->getThumbnail( $this->mWidths, $this->mHeights, $wgGenerateThumbnailOnParse ) ) ) { + } elseif( !( $thumb = $img->transform( $params ) ) ) { # Error generating thumbnail. $thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">' . htmlspecialchars( $img->getLastError() ) . '</div>'; diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 8fc47c0ff6..7d3414a8db 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -166,11 +166,9 @@ class ImagePage extends Article { function openShowImage() { global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang; - global $wgUseImageResize, $wgGenerateThumbnailOnParse; $full_url = $this->img->getURL(); - $anchoropen = ''; - $anchorclose = ''; + $linkAttribs = false; $sizeSel = intval( $wgUser->getOption( 'imagesize') ); if( !isset( $wgImageLimits[$sizeSel] ) ) { $sizeSel = User::getDefaultOption( 'imagesize' ); @@ -190,10 +188,11 @@ class ImagePage extends Article { if ( $this->img->exists() ) { # image $page = $wgRequest->getIntOrNull( 'page' ); - if ( ! is_null( $page ) ) { - $this->img->selectPage( $page ); - } else { + if ( is_null( $page ) ) { + $params = array(); $page = 1; + } else { + $params = array( 'page' => $page ); } $width_orig = $this->img->getWidth(); $width = $width_orig; @@ -201,6 +200,7 @@ class ImagePage extends Article { $height = $height_orig; $mime = $this->img->getMimeType(); $showLink = false; + $linkAttribs = array( 'href' => $full_url ); if ( $this->img->allowInlineDisplay() and $width and $height) { # image @@ -223,39 +223,33 @@ class ImagePage extends Article { # Note that $height <= $maxHeight now, but might not be identical # because of rounding. } + } + $params['width'] = $width; + $thumbnail = $this->img->transform( $params ); - if( $wgUseImageResize ) { - $thumbnail = $this->img->getThumbnail( $width, -1, $wgGenerateThumbnailOnParse ); - if ( $thumbnail == null ) { - $url = $this->img->getViewURL(); - } else { - $url = $thumbnail->getURL(); - } - } else { - # No resize ability? Show the full image, but scale - # it down in the browser so it fits on the page. - $url = $this->img->getViewURL(); - } - $anchoropen = "<a href=\"{$full_url}\">"; - $anchorclose = "</a><br />"; - if( $this->img->mustRender() ) { - $showLink = true; - } else { - $anchorclose .= wfMsg('show-big-image-thumb', $width, $height ) . - '<br />' . "\n$anchoropen{$msgbig}</a> " . $msgsize; - } - } else { - $url = $this->img->getViewURL(); + $anchorclose = "<br />"; + if( $this->img->mustRender() ) { $showLink = true; + } else { + $anchorclose .= + wfMsg('show-big-image-thumb', $width, $height ) . + '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . ' ' . $msgsize; } if ( $this->img->isMultipage() ) { $wgOut->addHTML( '<table class="multipageimage"><tr><td>' ); } - $wgOut->addHTML( '<div class="fullImageLink" id="file">' . $anchoropen . - "<img border=\"0\" src=\"{$url}\" width=\"{$width}\" height=\"{$height}\" alt=\"" . - htmlspecialchars( $this->img->getTitle()->getPrefixedText() ).'" />' . $anchorclose . '</div>' ); + $imgAttribs = array( + 'border' => 0, + 'alt' => $this->img->getTitle()->getPrefixedText() + ); + + if ( $thumbnail ) { + $wgOut->addHTML( '<div class="fullImageLink" id="file">' . + $thumbnail->toHtml( $imgAttribs, $linkAttribs ) . + $anchorclose . '</div>' ); + } if ( $this->img->isMultipage() ) { $count = $this->img->pageCount(); @@ -263,17 +257,17 @@ class ImagePage extends Article { if ( $page > 1 ) { $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false ); $link = $sk->makeLinkObj( $this->mTitle, $label, 'page='. ($page-1) ); - $this->img->selectPage( $page - 1 ); - $thumb1 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none' ); + $thumb1 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none', + array( 'page' => $page - 1 ) ); } else { $thumb1 = ''; } if ( $page < $count ) { $label = wfMsg( 'imgmultipagenext' ); - $this->img->selectPage( $page + 1 ); $link = $sk->makeLinkObj( $this->mTitle, $label, 'page='. ($page+1) ); - $thumb2 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none' ); + $thumb2 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none', + array( 'page' => $page + 1 ) ); } else { $thumb2 = ''; } @@ -294,7 +288,7 @@ class ImagePage extends Article { htmlspecialchars( wfMsg( 'imgmultigo' ) ) . '"></form>'; $wgOut->addHTML( '</td><td><div class="multipageimagenavbox">' . - "$select<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" ); + "$select<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" ); } } else { #if direct link is allowed but it's not a renderable image, show an icon. diff --git a/includes/Linker.php b/includes/Linker.php index b2167638a3..c34fa211ec 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -429,25 +429,19 @@ class Linker { } /** @todo document */ - function makeImageLinkObj( $nt, $label, $alt, $align = '', $width = false, $height = false, $framed = false, - $thumb = false, $manual_thumb = '', $page = null, $valign = '' ) + function makeImageLinkObj( $nt, $label, $alt, $align = '', $params = array(), $framed = false, + $thumb = false, $manual_thumb = '', $valign = '' ) { - global $wgContLang, $wgUser, $wgThumbLimits, $wgGenerateThumbnailOnParse; + global $wgContLang, $wgUser, $wgThumbLimits; $img = new Image( $nt ); - if ( ! is_null( $page ) ) { - $img->selectPage( $page ); - } - if ( !$img->allowInlineDisplay() && $img->exists() ) { return $this->makeKnownLinkObj( $nt ); } - $url = $img->getViewURL(); $error = $prefix = $postfix = ''; - - wfDebug( "makeImageLinkObj: '$width'x'$height', \"$label\"\n" ); + $page = isset( $params['page'] ) ? $params['page'] : false; if ( 'center' == $align ) { @@ -456,6 +450,16 @@ class Linker { $align = 'none'; } + if ( !isset( $params['width'] ) ) { + $wopt = $wgUser->getOption( 'thumbsize' ); + + if( !isset( $wgThumbLimits[$wopt] ) ) { + $wopt = User::getDefaultOption( 'thumbsize' ); + } + + $params['width'] = min( $img->getWidth( $page ), $wgThumbLimits[$wopt] ); + } + if ( $thumb || $framed ) { # Create a thumbnail. Alignment depends on language @@ -468,73 +472,39 @@ class Linker { if ( $align == '' ) { $align = $wgContLang->isRTL() ? 'left' : 'right'; } - - - if ( $width === false ) { - $wopt = $wgUser->getOption( 'thumbsize' ); - - if( !isset( $wgThumbLimits[$wopt] ) ) { - $wopt = User::getDefaultOption( 'thumbsize' ); - } - - $width = min( $img->getWidth(), $wgThumbLimits[$wopt] ); - } - - return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $width, $height, $framed, $manual_thumb ).$postfix; + return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $params, $framed, $manual_thumb ).$postfix; } - if ( $width && $img->exists() ) { - - # Create a resized image, without the additional thumbnail - # features - - if ( $height == false ) - $height = -1; - if ( $manual_thumb == '') { - $thumb = $img->getThumbnail( $width, $height, $wgGenerateThumbnailOnParse ); - if ( $thumb ) { - // In most cases, $width = $thumb->width or $height = $thumb->height. - // If not, we're scaling the image larger than it can be scaled, - // so we send to the browser a smaller thumbnail, and let the client do the scaling. - - if ($height != -1 && $width > $thumb->width * $height / $thumb->height) { - // $height is the limiting factor, not $width - // set $width to the largest it can be, such that the resulting - // scaled height is at most $height - $width = floor($thumb->width * $height / $thumb->height); - } - $height = round($thumb->height * $width / $thumb->width); + if ( $params['width'] && $img->exists() ) { + # Create a resized image, without the additional thumbnail features + $thumb = $img->transform( $params ); + } else { + $thumb = false; + } - wfDebug( "makeImageLinkObj: client-size set to '$width x $height'\n" ); - $url = $thumb->getUrl(); - } else { - $error = htmlspecialchars( $img->getLastError() ); - // Do client-side scaling... - $height = intval( $img->getHeight() * $width / $img->getWidth() ); - } - } + if ( $page ) { + $query = 'page=' . urlencode( $page ); } else { - $width = $img->width; - $height = $img->height; + $query = ''; + } + $u = $nt->getLocalURL( $query ); + $imgAttribs = array( + 'alt' => $alt, + 'longdesc' => $u + ); + if ( $valign ) { + $imgAttribs['style'] = "vertical-align: $valign"; } + $linkAttribs = array( + 'href' => $u, + 'class' => 'image', + 'title' => $alt + ); - wfDebug( "makeImageLinkObj2: '$width'x'$height'\n" ); - $u = $nt->escapeLocalURL(); - if ( $error ) { - $s = $error; - } elseif ( $url == '' ) { + if ( !$thumb ) { $s = $this->makeBrokenImageLinkObj( $img->getTitle() ); - //$s .= "<br />{$alt}<br />{$url}<br />\n"; } else { - $s = '<a href="'.$u.'" class="image" title="'.$alt.'">' . - '<img src="'.$url.'" alt="'.$alt.'" ' . - ( $width - ? ( 'width="'.$width.'" height="'.$height.'" ' ) - : '' ) . - ( $valign - ? ( 'style="vertical-align: '.$valign.'" ' ) - : '' ) . - 'longdesc="'.$u.'" /></a>'; + $s = $thumb->toHtml( $imgAttribs, $linkAttribs ); } if ( '' != $align ) { $s = "<div class=\"float{$align}\"><span>{$s}</span></div>"; @@ -546,86 +516,64 @@ class Linker { * Make HTML for a thumbnail including image, border and caption * $img is an Image object */ - function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $boxwidth = 180, $boxheight=false, $framed=false , $manual_thumb = "" ) { - global $wgStylePath, $wgContLang, $wgGenerateThumbnailOnParse; + function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manual_thumb = "" ) { + global $wgStylePath, $wgContLang; $thumbUrl = ''; $error = ''; - $width = $height = 0; - if ( $img->exists() ) { - $width = $img->getWidth(); - $height = $img->getHeight(); - } - if ( 0 == $width || 0 == $height ) { - $width = $height = 180; - } - if ( $boxwidth == 0 ) { - $boxwidth = 180; + $page = isset( $params['page'] ) ? $params['page'] : false; + + if ( empty( $params['width'] ) ) { + $params['width'] = 180; } - if ( $framed ) { + $thumb = false; + if ( $manual_thumb != '' ) { + # Use manually specified thumbnail + $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); + if( $manual_title ) { + $manual_img = new Image( $manual_title ); + $thumb = $manual_img->getUnscaledThumb(); + } + } elseif ( $framed ) { // Use image dimensions, don't scale - $boxwidth = $width; - $boxheight = $height; - $thumbUrl = $img->getViewURL(); + $thumb = $img->getUnscaledThumb( $page ); } else { - if ( $boxheight === false ) - $boxheight = -1; - if ( '' == $manual_thumb ) { - $thumb = $img->getThumbnail( $boxwidth, $boxheight, $wgGenerateThumbnailOnParse ); - if ( $thumb ) { - $thumbUrl = $thumb->getUrl(); - $boxwidth = $thumb->width; - $boxheight = $thumb->height; - } else { - $error = $img->getLastError(); - } - } + $thumb = $img->transform( $params ); } - $oboxwidth = $boxwidth + 2; - if ( $manual_thumb != '' ) # Use manually specified thumbnail - { - $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); #new Title ( $manual_thumb ) ; - if( $manual_title ) { - $manual_img = new Image( $manual_title ); - $thumbUrl = $manual_img->getViewURL(); - if ( $manual_img->exists() ) - { - $width = $manual_img->getWidth(); - $height = $manual_img->getHeight(); - $boxwidth = $width ; - $boxheight = $height ; - $oboxwidth = $boxwidth + 2 ; - } - } + if ( $thumb ) { + $outerWidth = $thumb->getWidth() + 2; + } else { + $outerWidth = $params['width'] + 2; } - $u = $img->getEscapeLocalURL(); + $query = $page ? 'page=' . urlencode( $page ) : ''; + $u = $img->getTitle()->getLocalURL( $query ); $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right'; $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : ''; - $s = "<div class=\"thumb t{$align}\"><div class=\"thumbinner\" style=\"width:{$oboxwidth}px;\">"; - if( $thumbUrl == '' ) { - // Couldn't generate thumbnail? Scale the image client-side. - $thumbUrl = $img->getViewURL(); - if( $boxheight == -1 ) { - // Approximate... - $boxheight = round( $height * $boxwidth / $width ); - } - } - if ( $error ) { - $s .= htmlspecialchars( $error ); + $s = "<div class=\"thumb t{$align}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">"; + if ( !$thumb ) { + $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); $zoomicon = ''; } elseif( !$img->exists() ) { $s .= $this->makeBrokenImageLinkObj( $img->getTitle() ); $zoomicon = ''; } else { - $s .= '<a href="'.$u.'" class="internal" title="'.$alt.'">'. - '<img src="'.$thumbUrl.'" alt="'.$alt.'" ' . - 'width="'.$boxwidth.'" height="'.$boxheight.'" ' . - 'longdesc="'.$u.'" class="thumbimage" /></a>'; + $imgAttribs = array( + 'alt' => $alt, + 'longdesc' => $u, + 'class' => 'thumbimage' + ); + $linkAttribs = array( + 'href' => $u, + 'title' => $alt, + 'class' => 'internal' + ); + + $s .= $thumb->toHtml( $imgAttribs, $linkAttribs ); if ( $framed ) { $zoomicon=""; } else { diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php new file mode 100644 index 0000000000..5004fcc9cc --- /dev/null +++ b/includes/MediaTransformOutput.php @@ -0,0 +1,158 @@ +<?php + +/** + * Base class for the output of MediaHandler::doTransform() and Image::transform(). + */ +abstract class MediaTransformOutput { + /** + * Get the width of the output box + */ + function getWidth() { + return $this->width; + } + + /** + * Get the height of the output box + */ + function getHeight() { + return $this->height; + } + + /** + * @return string The thumbnail URL + */ + function getUrl() { + return $this->url; + } + + /** + * @return string Destination file path (local filesystem) + */ + function getPath() { + return $this->path; + } + + /** + * Fetch HTML for this transform output + * @param array $attribs Advisory associative array of HTML attributes supplied + * by the linker. These can be incorporated into the output in any way. + * @param array $linkAttribs Attributes of a suggested enclosing <a> tag. + * May be ignored. + */ + abstract function toHtml( $attribs = array() , $linkAttribs = false ); + + /** + * This will be overridden to return true in error classes + */ + function isError() { + return false; + } + + /** + * Wrap some XHTML text in an anchor tag with the given attributes + */ + protected function linkWrap( $linkAttribs, $contents ) { + if ( $linkAttribs ) { + return Xml::tags( 'a', $linkAttribs, $contents ); + } else { + return $contents; + } + } +} + + +/** + * Media transform output for images + */ +class ThumbnailImage extends MediaTransformOutput { + /** + * @param string $path Filesystem path to the thumb + * @param string $url URL path to the thumb + * @private + */ + function ThumbnailImage( $url, $width, $height, $path = false ) { + $this->url = $url; + # These should be integers when they get here. + # If not, there's a bug somewhere. But let's at + # least produce valid HTML code regardless. + $this->width = round( $width ); + $this->height = round( $height ); + $this->path = $path; + } + + /** + * Return HTML <img ... /> tag for the thumbnail, will include + * width and height attributes and a blank alt text (as required). + * + * You can set or override additional attributes by passing an + * associative array of name => data pairs. The data will be escaped + * for HTML output, so should be in plaintext. + * + * If $linkAttribs is given, the image will be enclosed in an <a> tag. + * + * @param array $attribs + * @param array $linkAttribs + * @return string + * @public + */ + function toHtml( $attribs = array(), $linkAttribs = false ) { + $attribs['src'] = $this->url; + $attribs['width'] = $this->width; + $attribs['height'] = $this->height; + if( !isset( $attribs['alt'] ) ) $attribs['alt'] = ''; + return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) ); + } + +} + +/** + * Basic media transform error class + */ +class MediaTransformError extends MediaTransformOutput { + var $htmlMsg, $textMsg, $width, $height, $url, $path; + + function __construct( $msg, $width, $height /*, ... */ ) { + $args = array_slice( func_get_args(), 3 ); + $htmlArgs = array_map( 'htmlspecialchars', $args ); + $htmlArgs = array_map( 'nl2br', $htmlArgs ); + + $this->htmlMsg = wfMsgReplaceArgs( htmlspecialchars( wfMsgGetKey( $msg, true ) ), $htmlArgs ); + $this->textMsg = wfMsgReal( $msg, $args ); + $this->width = intval( $width ); + $this->height = intval( $height ); + $this->url = false; + $this->path = false; + } + + function toHtml( $attribs = array(), $linkAttribs = false ) { + return "<table class=\"MediaTransformError\" style=\"" . + "width: {$this->width}px; height: {$this->height}px;\"><tr><td>" . + $this->htmlMsg . + "</td></tr></table>"; + } + + function toText() { + return $this->textMsg; + } + + function getHtmlMsg() { + return $this->htmlMsg; + } + + function isError() { + return true; + } +} + +/** + * Shortcut class for parameter validation errors + */ +class TransformParameterError extends MediaTransformError { + function __construct( $params ) { + parent::__construct( 'thumbnail_error', + max( @$params['width'], 180 ), max( @$params['height'], 180 ), + wfMsg( 'thumbnail_invalid_params' ) ); + } +} + +?> diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 9142ccebda..516a3cda51 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -22,7 +22,7 @@ image/x-bmp bmp image/gif gif image/jpeg jpeg jpg jpe image/png png -image/svg+xml svg +image/svg+xml image/svg svg image/tiff tiff tif image/vnd.djvu djvu image/x-portable-pixmap ppm @@ -51,7 +51,7 @@ image/x-bmp image/bmp [BITMAP] image/gif [BITMAP] image/jpeg [BITMAP] image/png [BITMAP] -image/svg image/svg+xml [DRAWING] +image/svg+xml [DRAWING] image/tiff [BITMAP] image/vnd.djvu [BITMAP] image/x-portable-pixmap [BITMAP] diff --git a/includes/Parser.php b/includes/Parser.php index 89ab0915b2..7f34fa1e7b 100644 --- a/includes/Parser.php +++ b/includes/Parser.php @@ -4387,8 +4387,8 @@ class Parser * Parse image options text and use it to make an image */ function makeImage( $nt, $options ) { - global $wgUseImageResize, $wgDjvuRenderer; - + # @TODO: let the MediaHandler specify its transform parameters + # # Check if the options text is of the form "options|alt text" # Options are: # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang @@ -4408,6 +4408,7 @@ class Parser # * bottom # * text-bottom + $part = array_map( 'trim', explode( '|', $options) ); $mwAlign = array(); @@ -4422,13 +4423,14 @@ class Parser $mwPage =& MagicWord::get( 'img_page' ); $caption = ''; - $width = $height = $framed = $thumb = false; - $page = null; + $params = array(); + $framed = $thumb = false; $manual_thumb = '' ; $align = $valign = ''; + $sk = $this->mOptions->getSkin(); foreach( $part as $val ) { - if ( $wgUseImageResize && ! is_null( $mwThumb->matchVariableStartToEnd($val) ) ) { + if ( !is_null( $mwThumb->matchVariableStartToEnd($val) ) ) { $thumb=true; } elseif ( ! is_null( $match = $mwManualThumb->matchVariableStartToEnd($val) ) ) { # use manually specified thumbnail @@ -4446,19 +4448,18 @@ class Parser continue 2; } } - if ( isset( $wgDjvuRenderer ) && $wgDjvuRenderer - && ! is_null( $match = $mwPage->matchVariableStartToEnd($val) ) ) { + if ( ! is_null( $match = $mwPage->matchVariableStartToEnd($val) ) ) { # Select a page in a multipage document - $page = $match; - } elseif ( $wgUseImageResize && !$width && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { + $params['page'] = $match; + } elseif ( !isset( $params['width'] ) && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { wfDebug( "img_width match: $match\n" ); # $match is the image width in pixels $m = array(); if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) { - $width = intval( $m[1] ); - $height = intval( $m[2] ); + $params['width'] = intval( $m[1] ); + $params['height'] = intval( $m[2] ); } else { - $width = intval($match); + $params['width'] = intval($match); } } elseif ( ! is_null( $mwFramed->matchVariableStartToEnd($val) ) ) { $framed=true; @@ -4477,8 +4478,7 @@ class Parser $alt = Sanitizer::stripAllTags( $alt ); # Linker does the rest - $sk = $this->mOptions->getSkin(); - return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $width, $height, $framed, $thumb, $manual_thumb, $page, $valign ); + return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $params, $framed, $thumb, $manual_thumb, $valign ); } /** diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php new file mode 100644 index 0000000000..02e665a5b9 --- /dev/null +++ b/includes/media/Bitmap.php @@ -0,0 +1,226 @@ +<?php + +class BitmapHandler extends ImageHandler { + function normaliseParams( $image, &$params ) { + global $wgMaxImageArea; + if ( !parent::normaliseParams( $image, $params ) ) { + return false; + } + + $mimeType = $image->getMimeType(); + $srcWidth = $image->getWidth( $params['page'] ); + $srcHeight = $image->getHeight( $params['page'] ); + + # Don't thumbnail an image so big that it will fill hard drives and send servers into swap + # JPEG has the handy property of allowing thumbnailing without full decompression, so we make + # an exception for it. + if ( $mimeType !== 'image/jpeg' && + $srcWidth * $srcHeight > $wgMaxImageArea ) + { + return false; + } + + # Don't make an image bigger than the source + $params['physicalWidth'] = $params['width']; + $params['physicalHeight'] = $params['height']; + + if ( $params['physicalWidth'] >= $srcWidth ) { + $params['physicalWidth'] = $srcWidth; + $params['physicalHeight'] = $srcHeight; + return true; + } + + return true; + } + + function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { + global $wgUseImageMagick, $wgImageMagickConvertCommand; + global $wgCustomConvertCommand; + global $wgSharpenParameter, $wgSharpenReductionThreshold; + + if ( !$this->normaliseParams( $image, $params ) ) { + return new TransformParameterError( $params ); + } + $physicalWidth = $params['physicalWidth']; + $physicalHeight = $params['physicalHeight']; + $clientWidth = $params['width']; + $clientHeight = $params['height']; + $srcWidth = $image->getWidth(); + $srcHeight = $image->getHeight(); + $mimeType = $image->getMimeType(); + $srcPath = $image->getImagePath(); + $retval = 0; + wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" ); + + if ( $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) { + # normaliseParams (or the user) wants us to return the unscaled image + wfDebug( __METHOD__.": returning unscaled image\n" ); + return new ThumbnailImage( $image->getURL(), $clientWidth, $clientHeight, $srcPath ); + } + + if ( $wgUseImageMagick ) { + $scaler = 'im'; + } elseif ( $wgCustomConvertCommand ) { + $scaler = 'custom'; + } elseif ( function_exists( 'imagecreatetruecolor' ) ) { + $scaler = 'gd'; + } else { + $scaler = 'client'; + } + + if ( $scaler == 'client' ) { + # Client-side image scaling, use the source URL + # Using the destination URL in a TRANSFORM_LATER request would be incorrect + return new ThumbnailImage( $image->getURL(), $clientWidth, $clientHeight, $srcPath ); + } + + if ( $flags & self::TRANSFORM_LATER ) { + return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath ); + } + + if ( !wfMkdirParents( dirname( $dstPath ) ) ) { + return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, + wfMsg( 'thumbnail_dest_directory' ) ); + } + + if ( $scaler == 'im' ) { + # use ImageMagick + + $sharpen = ''; + if ( $mimeType == 'image/jpeg' ) { + $quality = "-quality 80"; // 80% + # Sharpening, see bug 6193 + if ( ( $physicalWidth + $physicalHeight ) / ( $srcWidth + $srcHeight ) < $wgSharpenReductionThreshold ) { + $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter ); + } + } elseif ( $mimeType == 'image/png' ) { + $quality = "-quality 95"; // zlib 9, adaptive filtering + } else { + $quality = ''; // default + } + + # Specify white background color, will be used for transparent images + # in Internet Explorer/Windows instead of default black. + + # Note, we specify "-size {$physicalWidth}" and NOT "-size {$physicalWidth}x{$physicalHeight}". + # It seems that ImageMagick has a bug wherein it produces thumbnails of + # the wrong size in the second case. + + $cmd = wfEscapeShellArg($wgImageMagickConvertCommand) . + " {$quality} -background white -size {$physicalWidth} ". + wfEscapeShellArg($srcPath) . + // Coalesce is needed to scale animated GIFs properly (bug 1017). + ' -coalesce ' . + // For the -resize option a "!" is needed to force exact size, + // or ImageMagick may decide your ratio is wrong and slice off + // a pixel. + " -thumbnail " . wfEscapeShellArg( "{$physicalWidth}x{$physicalHeight}!" ) . + " -depth 8 $sharpen " . + wfEscapeShellArg($dstPath) . " 2>&1"; + wfDebug( __METHOD__.": running ImageMagick: $cmd\n"); + wfProfileIn( 'convert' ); + $err = wfShellExec( $cmd, $retval ); + wfProfileOut( 'convert' ); + } elseif( $scaler == 'custom' ) { + # Use a custom convert command + # Variables: %s %d %w %h + $src = wfEscapeShellArg( $srcPath ); + $dst = wfEscapeShellArg( $dstPath ); + $cmd = $wgCustomConvertCommand; + $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames + $cmd = str_replace( '%h', $physicalHeight, str_replace( '%w', $physicalWidth, $cmd ) ); # Size + wfDebug( __METHOD__.": Running custom convert command $cmd\n" ); + wfProfileIn( 'convert' ); + $err = wfShellExec( $cmd, $retval ); + wfProfileOut( 'convert' ); + } else /* $scaler == 'gd' */ { + # Use PHP's builtin GD library functions. + # + # First find out what kind of file this is, and select the correct + # input routine for this. + + $typemap = array( + 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ), + 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ), + 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ), + 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ), + 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ), + ); + if( !isset( $typemap[$mimeType] ) ) { + $err = 'Image type not supported'; + wfDebug( "$err\n" ); + return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); + } + list( $loader, $colorStyle, $saveType ) = $typemap[$mimeType]; + + if( !function_exists( $loader ) ) { + $err = "Incomplete GD library configuration: missing function $loader"; + wfDebug( "$err\n" ); + return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); + } + + $src_image = call_user_func( $loader, $srcPath ); + $dst_image = imagecreatetruecolor( $physicalWidth, $physicalHeight ); + imagecopyresampled( $dst_image, $src_image, + 0,0,0,0, + $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) ); + call_user_func( $saveType, $dst_image, $dstPath ); + imagedestroy( $dst_image ); + imagedestroy( $src_image ); + $retval = 0; + } + + $removed = $this->removeBadFile( $dstPath, $retval ); + if ( $retval != 0 || $removed ) { + wfDebugLog( 'thumbnail', + sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', + wfHostname(), $retval, trim($err), $cmd ) ); + return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); + } else { + return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath ); + } + } + + static function imageJpegWrapper( $dst_image, $thumbPath ) { + imageinterlace( $dst_image ); + imagejpeg( $dst_image, $thumbPath, 95 ); + } + + + function getMetadata( $image, $filename ) { + global $wgShowEXIF; + if( $wgShowEXIF && file_exists( $filename ) ) { + $exif = new Exif( $filename ); + return serialize( $exif->getFilteredData() ); + } else { + return ''; + } + } + + function getMetadataType( $image ) { + return 'exif'; + } + + function isMetadataValid( $image, $metadata ) { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + # Metadata disabled and so an empty field is expected + return true; + } + if ( $metadata === '0' ) { + # Special value indicating that there is no EXIF data in the file + return true; + } + $exif = @unserialize( $metadata ); + if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) || + $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version() ) + { + # Wrong version + return false; + } + return true; + } + +} + +?> diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php new file mode 100644 index 0000000000..a04de9687a --- /dev/null +++ b/includes/media/DjVu.php @@ -0,0 +1,203 @@ +<?php + +class DjVuHandler extends ImageHandler { + function isEnabled() { + global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML; + if ( !$wgDjvuRenderer || ( !$wgDjvuDump && !$wgDjvuToXML ) ) { + wfDebug( "DjVu is disabled, please set \$wgDjvuRenderer and \$wgDjvuDump\n" ); + return false; + } else { + return true; + } + } + + function mustRender() { return true; } + function isMultiPage() { return true; } + + function validateParam( $name, $value ) { + if ( in_array( $name, array( 'width', 'height', 'page' ) ) ) { + if ( $value <= 0 ) { + return false; + } else { + return true; + } + } else { + return false; + } + } + + function makeParamString( $params ) { + $page = isset( $params['page'] ) ? $params['page'] : 1; + if ( !isset( $params['width'] ) ) { + return false; + } + return "{$params['width']}px-page{$page}"; + } + + function parseParamString( $str ) { + $m = false; + if ( preg_match( '/^(\d+)px-page(\d+)$/', $str, $m ) ) { + return array( 'width' => $m[1], 'page' => $m[2] ); + } else { + return false; + } + } + + function getScriptParams( $params ) { + return array( + 'width' => $params['width'], + 'page' => $params['page'], + ); + } + + function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { + global $wgDjvuRenderer, $wgDjvuPostProcessor; + + // Fetch XML and check it, to give a more informative error message than the one which + // normaliseParams will inevitably give. + $xml = $image->getMetadata(); + if ( !$xml ) { + return new MediaTransformError( 'thumbnail_error', @$params['width'], @$params['height'], + wfMsg( 'djvu_no_xml' ) ); + } + + if ( !$this->normaliseParams( $image, $params ) ) { + return new TransformParameterError( $params ); + } + $width = $params['width']; + $height = $params['height']; + $srcPath = $image->getImagePath(); + $page = $params['page']; + $pageCount = $this->pageCount( $image ); + if ( $page > $this->pageCount( $image ) ) { + return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'djvu_page_error' ) ); + } + + if ( $flags & self::TRANSFORM_LATER ) { + return new ThumbnailImage( $dstUrl, $width, $height, $dstPath ); + } + + if ( !wfMkdirParents( dirname( $dstPath ) ) ) { + return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'thumbnail_dest_directory' ) ); + } + + # Use a subshell (brackets) to aggregate stderr from both pipeline commands + # before redirecting it to the overall stdout. This works in both Linux and Windows XP. + $cmd = '(' . wfEscapeShellArg( $wgDjvuRenderer ) . " -format=ppm -page={$page} -size={$width}x{$height} " . + wfEscapeShellArg( $srcPath ); + if ( $wgDjvuPostProcessor ) { + $cmd .= " | {$wgDjvuPostProcessor}"; + } + $cmd .= ' > ' . wfEscapeShellArg($dstPath) . ') 2>&1'; + wfProfileIn( 'ddjvu' ); + wfDebug( __METHOD__.": $cmd\n" ); + $err = wfShellExec( $cmd, $retval ); + wfProfileOut( 'ddjvu' ); + + $removed = $this->removeBadFile( $dstPath, $retval ); + if ( $retval != 0 || $removed ) { + wfDebugLog( 'thumbnail', + sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', + wfHostname(), $retval, trim($err), $cmd ) ); + return new MediaTransformError( 'thumbnail_error', $width, $height, $err ); + } else { + return new ThumbnailImage( $dstUrl, $width, $height, $dstPath ); + } + } + + /** + * Cache an instance of DjVuImage in an Image object, return that instance + */ + function getDjVuImage( $image, $path ) { + if ( !$image ) { + $deja = new DjVuImage( $path ); + } elseif ( !isset( $image->dejaImage ) ) { + $deja = $image->dejaImage = new DjVuImage( $path ); + } else { + $deja = $image->dejaImage; + } + return $deja; + } + + /** + * Cache a document tree for the DjVu XML metadata + */ + function getMetaTree( $image ) { + if ( isset( $image->dejaMetaTree ) ) { + return $image->dejaMetaTree; + } + + $metadata = $image->getMetadata(); + if ( !$this->isMetadataValid( $image, $metadata ) ) { + wfDebug( "DjVu XML metadata is invalid or missing, should have been fixed in upgradeRow\n" ); + return false; + } + wfProfileIn( __METHOD__ ); + + wfSuppressWarnings(); + try { + $image->dejaMetaTree = new SimpleXMLElement( $metadata ); + } catch( Exception $e ) { + wfDebug( "Bogus multipage XML metadata on '$image->name'\n" ); + // Set to false rather than null to avoid further attempts + $image->dejaMetaTree = false; + } + wfRestoreWarnings(); + wfProfileOut( __METHOD__ ); + return $image->dejaMetaTree; + } + + function getImageSize( $image, $path ) { + return $this->getDjVuImage( $image, $path )->getImageSize(); + } + + function getThumbType( $ext, $mime ) { + global $wgDjvuOutputExtension; + static $mime; + if ( !isset( $mime ) ) { + $magic = MimeMagic::singleton(); + $mime = $magic->guessTypesForExtension( $wgDjvuOutputExtension ); + } + return array( $wgDjvuOutputExtension, $mime ); + } + + function getMetadata( $image, $path ) { + wfDebug( "Getting DjVu metadata for $path\n" ); + return $this->getDjVuImage( $image, $path )->retrieveMetaData(); + } + + function getMetadataType( $image ) { + return 'djvuxml'; + } + + function isMetadataValid( $image, $metadata ) { + return !empty( $metadata ) && $metadata != serialize(array()); + } + + function pageCount( $image ) { + $tree = $this->getMetaTree( $image ); + if ( !$tree ) { + return false; + } + return count( $tree->xpath( '//OBJECT' ) ); + } + + function getPageDimensions( $image, $page ) { + $tree = $this->getMetaTree( $image ); + if ( !$tree ) { + return false; + } + + $o = $tree->BODY[0]->OBJECT[$page-1]; + if ( $o ) { + return array( + 'width' => intval( $o['width'] ), + 'height' => intval( $o['height'] ) + ); + } else { + return false; + } + } +} + +?> diff --git a/includes/media/Generic.php b/includes/media/Generic.php new file mode 100644 index 0000000000..44c08d7cc2 --- /dev/null +++ b/includes/media/Generic.php @@ -0,0 +1,292 @@ +<?php + +/** + * Media-handling base classes and generic functionality + */ + +/** + * Base media handler class + */ +abstract class MediaHandler { + const TRANSFORM_LATER = 1; + + /** + * Instance cache + */ + static $handlers = array(); + + /** + * Get a MediaHandler for a given MIME type from the instance cache + */ + static function getHandler( $type ) { + global $wgMediaHandlers; + if ( !isset( $wgMediaHandlers[$type] ) ) { + return false; + } + $class = $wgMediaHandlers[$type]; + if ( !isset( self::$handlers[$class] ) ) { + self::$handlers[$class] = new $class; + if ( !self::$handlers[$class]->isEnabled() ) { + self::$handlers[$class] = false; + } + } + return self::$handlers[$class]; + } + + /* + * Validate a thumbnail parameter at parse time. + * Return true to accept the parameter, and false to reject it. + * If you return false, the parser will do something quiet and forgiving. + */ + abstract function validateParam( $name, $value ); + + /** + * Merge a parameter array into a string appropriate for inclusion in filenames + */ + abstract function makeParamString( $params ); + + /** + * Parse a param string made with makeParamString back into an array + */ + abstract function parseParamString( $str ); + + /** + * Changes the parameter array as necessary, ready for transformation. + * Should be idempotent. + * Returns false if the parameters are unacceptable and the transform should fail + */ + abstract function normaliseParams( $image, &$params ); + + /** + * Get an image size array like that returned by getimagesize(), or false if it + * can't be determined. + * + * @param Image $image The image object, or false if there isn't one + * @param string $fileName The filename + * @return array + */ + abstract function getImageSize( $image, $path ); + + /** + * Get handler-specific metadata which will be saved in the img_metadata field. + * + * @param Image $image The image object, or false if there isn't one + * @param string $fileName The filename + * @return string + */ + function getMetadata( $image, $path ) { return ''; } + + /** + * Get a string describing the type of metadata, for display purposes. + */ + function getMetadataType( $image ) { return false; } + + /** + * Check if the metadata string is valid for this handler. + * If it returns false, Image will reload the metadata from the file and update the database + */ + function isMetadataValid( $image, $metadata ) { return true; } + + /** + * Get a MediaTransformOutput object representing the transformed output. Does not + * actually do the transform. + * + * @param Image $image The image object + * @param string $dstPath Filesystem destination path + * @param string $dstUrl Destination URL to use in output HTML + * @param array $params Arbitrary set of parameters validated by $this->validateParam() + */ + function getTransform( $image, $dstPath, $dstUrl, $params ) { + return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER ); + } + + /** + * Get a MediaTransformOutput object representing the transformed output. Does the + * transform unless $flags contains self::TRANSFORM_LATER. + * + * @param Image $image The image object + * @param string $dstPath Filesystem destination path + * @param string $dstUrl Destination URL to use in output HTML + * @param array $params Arbitrary set of parameters validated by $this->validateParam() + * @param integer $flags A bitfield, may contain self::TRANSFORM_LATER + */ + abstract function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ); + + /** + * Get the thumbnail extension and MIME type for a given source MIME type + * @return array thumbnail extension and MIME type + */ + function getThumbType( $ext, $mime ) { + return array( $ext, $mime ); + } + + /** + * True if the handled types can be transformed + */ + function canRender() { return true; } + /** + * True if handled types cannot be displayed directly in a browser + * but can be rendered + */ + function mustRender() { return false; } + /** + * True if the type has multi-page capabilities + */ + function isMultiPage() { return false; } + /** + * Page count for a multi-page document, false if unsupported or unknown + */ + function pageCount() { return false; } + /** + * False if the handler is disabled for all files + */ + function isEnabled() { return true; } + + /** + * Get an associative array of page dimensions + * Currently "width" and "height" are understood, but this might be + * expanded in the future. + * Returns false if unknown or if the document is not multi-page. + */ + function getPageDimensions( $image, $page ) { + $gis = $this->getImageSize( $image, $image->getImagePath() ); + return array( + 'width' => $gis[0], + 'height' => $gis[1] + ); + } +} + +/** + * Media handler abstract base class for images + */ +abstract class ImageHandler extends MediaHandler { + function validateParam( $name, $value ) { + if ( in_array( $name, array( 'width', 'height' ) ) ) { + if ( $value <= 0 ) { + return false; + } else { + return true; + } + } else { + return false; + } + } + + function makeParamString( $params ) { + if ( isset( $params['physicalWidth'] ) ) { + $width = $params['physicalWidth']; + } else { + $width = $params['width']; + } + $width = intval( $width ); + return "{$width}px"; + } + + function parseParamString( $str ) { + $m = false; + if ( preg_match( '/^(\d+)px$/', $str, $m ) ) { + return array( 'width' => $m[1] ); + } else { + return false; + } + } + + function getScriptParams( $params ) { + return array( 'width' => $params['width'] ); + } + + function normaliseParams( $image, &$params ) { + $mimeType = $image->getMimeType(); + + if ( !isset( $params['width'] ) ) { + return false; + } + if ( !isset( $params['page'] ) ) { + $params['page'] = 1; + } + $srcWidth = $image->getWidth( $params['page'] ); + $srcHeight = $image->getHeight( $params['page'] ); + if ( isset( $params['height'] ) && $params['height'] != -1 ) { + if ( $params['width'] * $srcHeight > $params['height'] * $srcWidth ) { + $params['width'] = wfFitBoxWidth( $srcWidth, $srcHeight, $params['height'] ); + } + } + $params['height'] = Image::scaleHeight( $srcWidth, $srcHeight, $params['width'] ); + if ( !$this->validateThumbParams( $params['width'], $params['height'], $srcWidth, $srcHeight, $mimeType ) ) { + return false; + } + return true; + } + + /** + * Get a transform output object without actually doing the transform + */ + function getTransform( $image, $dstPath, $dstUrl, $params ) { + return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER ); + } + + /** + * Validate thumbnail parameters and fill in the correct height + * + * @param integer &$width Specified width (input/output) + * @param integer &$height Height (output only) + * @return false to indicate that an error should be returned to the user. + */ + function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) { + $width = intval( $width ); + + # Sanity check $width + if( $width <= 0) { + wfDebug( __METHOD__.": Invalid destination width: $width\n" ); + return false; + } + if ( $srcWidth <= 0 ) { + wfDebug( __METHOD__.": Invalid source width: $srcWidth\n" ); + return false; + } + + $height = Image::scaleHeight( $srcWidth, $srcHeight, $width ); + return true; + } + + function getScriptedTransform( $image, $script, $params ) { + if ( !$this->normaliseParams( $image, $params ) ) { + return false; + } + $url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) ); + return new ThumbnailImage( $url, $params['width'], $params['height'] ); + } + + /** + * Check for zero-sized thumbnails. These can be generated when + * no disk space is available or some other error occurs + * + * @param $dstPath The location of the suspect file + * @param $retval Return value of some shell process, file will be deleted if this is non-zero + * @return true if removed, false otherwise + */ + function removeBadFile( $dstPath, $retval = 0 ) { + $removed = false; + if( file_exists( $dstPath ) ) { + $thumbstat = stat( $dstPath ); + if( $thumbstat['size'] == 0 || $retval != 0 ) { + wfDebugLog( 'thumbnail', + sprintf( 'Removing bad %d-byte thumbnail "%s"', + $thumbstat['size'], $dstPath ) ); + unlink( $dstPath ); + return true; + } + } + return false; + } + + function getImageSize( $image, $path ) { + wfSuppressWarnings(); + $gis = getimagesize( $path ); + wfRestoreWarnings(); + return $gis; + } +} + +?> diff --git a/includes/media/SVG.php b/includes/media/SVG.php new file mode 100644 index 0000000000..407760da23 --- /dev/null +++ b/includes/media/SVG.php @@ -0,0 +1,94 @@ +<?php + +class SvgHandler extends ImageHandler { + function isEnabled() { + global $wgSVGConverters, $wgSVGConverter; + if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) { + wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" ); + return false; + } else { + return true; + } + } + + function mustRender() { + return true; + } + + function normaliseParams( $image, &$params ) { + global $wgSVGMaxSize; + if ( !parent::normaliseParams( $image, $params ) ) { + return false; + } + + # Don't make an image bigger than wgMaxSVGSize + $params['physicalWidth'] = $params['width']; + $params['physicalHeight'] = $params['height']; + if ( $params['physicalWidth'] > $wgSVGMaxSize ) { + $srcWidth = $image->getWidth( $params['page'] ); + $srcHeight = $image->getHeight( $params['page'] ); + $params['physicalWidth'] = $wgSVGMaxSize; + $params['physicalHeight'] = Image::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize ); + } + return true; + } + + function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { + global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath; + + if ( !$this->normaliseParams( $image, $params ) ) { + return new TransformParameterError( $params ); + } + $clientWidth = $params['width']; + $clientHeight = $params['height']; + $physicalWidth = $params['physicalWidth']; + $physicalHeight = $params['physicalHeight']; + $srcWidth = $image->getWidth(); + $srcHeight = $image->getHeight(); + $srcPath = $image->getImagePath(); + + if ( $flags & self::TRANSFORM_LATER ) { + return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight ); + } + + if ( !wfMkdirParents( dirname( $dstPath ) ) ) { + return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, + wfMsg( 'thumbnail_dest_directory' ) ); + } + + $err = false; + if( isset( $wgSVGConverters[$wgSVGConverter] ) ) { + $cmd = str_replace( + array( '$path/', '$width', '$height', '$input', '$output' ), + array( $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "", + intval( $physicalWidth ), + intval( $physicalHeight ), + wfEscapeShellArg( $srcPath ), + wfEscapeShellArg( $dstPath ) ), + $wgSVGConverters[$wgSVGConverter] ) . " 2>&1"; + wfProfileIn( 'rsvg' ); + wfDebug( __METHOD__.": $cmd\n" ); + $err = wfShellExec( $cmd, $retval ); + wfProfileOut( 'rsvg' ); + } + + $removed = $this->removeBadFile( $dstPath, $retval ); + if ( $retval != 0 || $removed ) { + wfDebugLog( 'thumbnail', + sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', + wfHostname(), $retval, trim($err), $cmd ) ); + return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); + } else { + return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight ); + } + } + + function getImageSize( $image, $path ) { + return wfGetSVGsize( $path ); + } + + function getThumbType( $ext, $mime ) { + return array( 'png', 'image/png' ); + } +} +?> diff --git a/includes/mime.info b/includes/mime.info index b2ae92efd1..a960f0233c 100644 --- a/includes/mime.info +++ b/includes/mime.info @@ -19,7 +19,7 @@ image/x-portable-graymap image/x-portable-greymap [BITMAP] image/x-bmp image/bmp application/x-bmp application/bmp [BITMAP] image/x-photoshop image/psd image/x-psd image/photoshop [BITMAP] -image/svg image/svg+xml application/svg+xml application/svg [DRAWING] +image/svg+xml application/svg+xml application/svg image/svg [DRAWING] application/postscript [DRAWING] application/x-latex [DRAWING] application/x-tex [DRAWING] diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index e7be6c0c68..3f88b0f198 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -2139,6 +2139,10 @@ In the latter case you can also use a link, e.g. [[{{ns:Special}}:Export/{{Media 'missingimage' => '<b>Missing image</b><br /><i>$1</i>', 'filemissing' => 'File missing', 'thumbnail_error' => 'Error creating thumbnail: $1', +'djvu_page_error' => 'DjVu page out of range', +'djvu_no_xml' => 'Unable to fetch XML for DjVu file', +'thumbnail_invalid_params' => 'Invalid thumbnail parameters', +'thumbnail_dest_directory' => 'Unable to create destination directory', # Special:Import 'import' => 'Import pages', @@ -2816,8 +2820,8 @@ Please confirm that really want to recreate this page.', * Nederlands|nl", # Multipage image navigation -'imgmultipageprev' => '← previous page', -'imgmultipagenext' => 'next page →', +'imgmultipageprev' => '← previous page', +'imgmultipagenext' => 'next page →', 'imgmultigo' => 'Go!', 'imgmultigotopre' => 'Go to page', 'imgmultigotopost' => '', diff --git a/skins/common/common.css b/skins/common/common.css index cb91c5d475..e39910c329 100644 --- a/skins/common/common.css +++ b/skins/common/common.css @@ -480,4 +480,15 @@ p.mw-ipb-conveniencelinks { #file img, .gallerybox .thumb img { background: url(images/Checker-16x16.png) repeat; } -*/ \ No newline at end of file +*/ +.MediaTransformError { + border: thin solid #777; + background-color: #ccc; + padding: 0.1em; +} +.MediaTransformError td { + text-align: center; + vertical-align: middle; + font-size: 90%; +} + diff --git a/skins/monobook/main.css b/skins/monobook/main.css index bd68c37da8..bd7c77391b 100644 --- a/skins/monobook/main.css +++ b/skins/monobook/main.css @@ -1626,3 +1626,13 @@ p.mw-ipb-conveniencelinks { .texvc { direction: ltr; unicode-bidi: embed; } /* Stop floats from intruding into edit area in previews */ #toolbar, #wpTextbox1 { clear: both; } + +.MediaTransformError { + background-color: #ccc; + padding: 0.1em; +} +.MediaTransformError td { + text-align: center; + vertical-align: middle; + font-size: 90%; +} diff --git a/thumb.php b/thumb.php index 42bc5497a5..c67468b694 100644 --- a/thumb.php +++ b/thumb.php @@ -9,44 +9,49 @@ define( 'MW_NO_OUTPUT_COMPRESSION', 1 ); require_once( './includes/WebStart.php' ); wfProfileIn( 'thumb.php' ); wfProfileIn( 'thumb.php-start' ); -require_once( './includes/GlobalFunctions.php' ); -require_once( './includes/ImageFunctions.php' ); +require_once( "$IP/includes/GlobalFunctions.php" ); +require_once( "$IP/includes/ImageFunctions.php" ); $wgTrivialMimeDetection = true; //don't use fancy mime detection, just check the file extension for jpg/gif/png. -require_once( './includes/Image.php' ); -require_once( './includes/StreamFile.php' ); +require_once( "$IP/includes/StreamFile.php" ); +require_once( "$IP/includes/AutoLoader.php" ); // Get input parameters -$fileName = isset( $_REQUEST['f'] ) ? $_REQUEST['f'] : ''; -$width = isset( $_REQUEST['w'] ) ? intval( $_REQUEST['w'] ) : 0; -$page = isset( $_REQUEST['p'] ) ? intval( $_REQUEST['p'] ) : null; - if ( get_magic_quotes_gpc() ) { - $fileName = stripslashes( $fileName ); + $params = array_map( 'stripslashes', $_REQUEST ); +} else { + $params = $_REQUEST; } -$pre_render= isset($_REQUEST['r']) && $_REQUEST['r']!="0"; +$fileName = isset( $params['f'] ) ? $params['f'] : ''; +unset( $params['f'] ); + +// Backwards compatibility parameters +if ( isset( $params['w'] ) ) { + $params['width'] = $params['w']; + unset( $params['w'] ); +} +if ( isset( $params['p'] ) ) { + $params['page'] = $params['p']; +} +unset( $params['r'] ); // Some basic input validation $fileName = strtr( $fileName, '\\/', '__' ); // Work out paths, carefully avoiding constructing an Image object because that won't work yet +$handler = thumbGetHandler( $fileName ); +if ( $handler ) { + $imagePath = wfImageDir( $fileName ) . '/' . $fileName; + $thumbName = $handler->makeParamString( $params ) . "-$fileName"; + $thumbPath = wfImageThumbDir( $fileName ) . '/' . $thumbName; -$imagePath = wfImageDir( $fileName ) . '/' . $fileName; -$thumbName = "{$width}px-$fileName"; -if ( ! is_null( $page ) ) { - $thumbName = 'page' . $page . '-' . $thumbName; -} -if ( $pre_render ) { - $thumbName .= '.png'; -} -$thumbPath = wfImageThumbDir( $fileName ) . '/' . $thumbName; - -if ( is_file( $thumbPath ) && filemtime( $thumbPath ) >= filemtime( $imagePath ) ) { - wfStreamFile( $thumbPath ); - // Can't log profiling data with no Setup.php - exit; + if ( is_file( $thumbPath ) && filemtime( $thumbPath ) >= filemtime( $imagePath ) ) { + wfStreamFile( $thumbPath ); + // Can't log profiling data with no Setup.php + exit; + } } // OK, no valid thumbnail, time to get out the heavy machinery @@ -57,10 +62,7 @@ wfProfileIn( 'thumb.php-render' ); $img = Image::newFromName( $fileName ); try { if ( $img ) { - if ( ! is_null( $page ) ) { - $img->selectPage( $page ); - } - $thumb = $img->renderThumb( $width, false ); + $thumb = $img->transform( $params, Image::RENDER_NOW ); } else { $thumb = false; } @@ -69,13 +71,33 @@ try { $thumb = false; } -if ( $thumb && $thumb->path ) { - wfStreamFile( $thumb->path ); +if ( $thumb && $thumb->getPath() ) { + wfStreamFile( $thumb->getPath() ); +} elseif ( $img ) { + header( 'Cache-Control: no-cache' ); + header( 'Content-Type: text/html; charset=utf-8' ); + header( 'HTTP/1.1 500 Internal server error' ); + if ( !$thumb ) { + $msg = wfMsgHtml( 'thumbnail_error', 'Image::transform() returned false' ); + } elseif ( $thumb->isError() ) { + $msg = $thumb->toHtml(); + } else { + $msg = wfMsgHtml( 'thumbnail_error', 'No path supplied in thumbnail object' ); + } + echo <<<EOT +<html><head><title>Error generating thumbnail + +$msg + + + +EOT; } else { $badtitle = wfMsg( 'badtitle' ); $badtitletext = wfMsg( 'badtitletext' ); header( 'Cache-Control: no-cache' ); header( 'Content-Type: text/html; charset=utf-8' ); + header( 'HTTP/1.1 500 Internal server error' ); echo " $badtitle @@ -89,4 +111,17 @@ wfProfileOut( 'thumb.php-render' ); wfProfileOut( 'thumb.php' ); wfLogProfilingData(); +//-------------------------------------------------------------------------- + +function thumbGetHandler( $fileName ) { + // Determine type + $magic = MimeMagic::singleton(); + $extPos = strrpos( $fileName, '.' ); + if ( $extPos === false ) { + return false; + } + $mime = $magic->guessTypesForExtension( substr( $fileName, $extPos + 1 ) ); + return MediaHandler::getHandler( $mime ); +} + ?> -- 2.20.1