diff options
Diffstat (limited to 'includes/media')
-rw-r--r-- | includes/media/Bitmap.php | 85 | ||||
-rw-r--r-- | includes/media/BitmapMetadataHandler.php | 19 | ||||
-rw-r--r-- | includes/media/Bitmap_ClientOnly.php | 2 | ||||
-rw-r--r-- | includes/media/DjVu.php | 9 | ||||
-rw-r--r-- | includes/media/DjVuImage.php | 379 | ||||
-rw-r--r-- | includes/media/Exif.php | 5 | ||||
-rw-r--r-- | includes/media/ExifBitmap.php | 10 | ||||
-rw-r--r-- | includes/media/FormatMetadata.php | 11 | ||||
-rw-r--r-- | includes/media/GIF.php | 23 | ||||
-rw-r--r-- | includes/media/Generic.php | 122 | ||||
-rw-r--r-- | includes/media/JpegMetadataExtractor.php | 10 | ||||
-rw-r--r-- | includes/media/MediaTransformOutput.php | 86 | ||||
-rw-r--r-- | includes/media/SVG.php | 12 | ||||
-rw-r--r-- | includes/media/SVGMetadataExtractor.php | 8 | ||||
-rw-r--r-- | includes/media/XCF.php | 137 | ||||
-rw-r--r-- | includes/media/XMP.php | 6 | ||||
-rw-r--r-- | includes/media/XMPInfo.php | 23 | ||||
-rw-r--r-- | includes/media/XMPValidate.php | 53 |
18 files changed, 836 insertions, 164 deletions
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 3a66d8c9..619485cc 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -12,7 +12,6 @@ * @ingroup Media */ class BitmapHandler extends ImageHandler { - /** * @param $image File * @param $params array Transform parameters. Entries with the keys 'width' @@ -21,12 +20,10 @@ class BitmapHandler extends ImageHandler { * @return bool */ function normaliseParams( $image, &$params ) { - global $wgMaxImageArea; if ( !parent::normaliseParams( $image, $params ) ) { return false; } - $mimeType = $image->getMimeType(); # Obtain the source, pre-rotation dimensions $srcWidth = $image->getWidth( $params['page'] ); $srcHeight = $image->getHeight( $params['page'] ); @@ -43,19 +40,27 @@ class BitmapHandler extends ImageHandler { } } - # 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. - # @todo FIXME: This actually only applies to ImageMagick - if ( $mimeType !== 'image/jpeg' && - $srcWidth * $srcHeight > $wgMaxImageArea ) - { - return false; + # Check if the file is smaller than the maximum image area for thumbnailing + $checkImageAreaHookResult = null; + wfRunHooks( 'BitmapHandlerCheckImageArea', array( $image, &$params, &$checkImageAreaHookResult ) ); + if ( is_null( $checkImageAreaHookResult ) ) { + global $wgMaxImageArea; + + if ( $srcWidth * $srcHeight > $wgMaxImageArea && + !( $image->getMimeType() == 'image/jpeg' && + self::getScalerType( false, false ) == 'im' ) ) { + # Only ImageMagick can efficiently downsize jpg images without loading + # the entire file in memory + return false; + } + } else { + return $checkImageAreaHookResult; } return true; } + /** * Extracts the width/height if the image will be scaled before rotating * @@ -81,10 +86,15 @@ class BitmapHandler extends ImageHandler { } - // Function that returns the number of pixels to be thumbnailed. - // Intended for animated GIFs to multiply by the number of frames. - function getImageArea( $image, $width, $height ) { - return $width * $height; + /** + * Function that returns the number of pixels to be thumbnailed. + * Intended for animated GIFs to multiply by the number of frames. + * + * @param File $image + * @return int + */ + function getImageArea( $image ) { + return $image->getWidth() * $image->getHeight(); } /** @@ -115,12 +125,14 @@ class BitmapHandler extends ImageHandler { 'srcWidth' => $image->getWidth(), 'srcHeight' => $image->getHeight(), 'mimeType' => $image->getMimeType(), - 'srcPath' => $image->getPath(), 'dstPath' => $dstPath, 'dstUrl' => $dstUrl, ); - wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath\n" ); + # Determine scaler type + $scaler = self::getScalerType( $dstPath ); + + wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath using scaler $scaler\n" ); if ( !$image->mustRender() && $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] @@ -131,9 +143,6 @@ class BitmapHandler extends ImageHandler { return $this->getClientScalingThumbnailImage( $image, $scalerParams ); } - # Determine scaler type - $scaler = self::getScalerType( $dstPath ); - wfDebug( __METHOD__ . ": scaler $scaler\n" ); if ( $scaler == 'client' ) { # Client-side image scaling, use the source URL @@ -144,15 +153,18 @@ class BitmapHandler extends ImageHandler { if ( $flags & self::TRANSFORM_LATER ) { wfDebug( __METHOD__ . ": Transforming later per flags.\n" ); return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], - $scalerParams['clientHeight'], $dstPath ); + $scalerParams['clientHeight'], false ); } # Try to make a target path for the thumbnail - if ( !wfMkdirParents( dirname( $dstPath ) ) ) { + if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" ); return $this->getClientScalingThumbnailImage( $image, $scalerParams ); } + # Transform functions and binaries need a FS source file + $scalerParams['srcPath'] = $image->getLocalRefPath(); + # Try a hook $mto = null; wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) ); @@ -223,13 +235,6 @@ class BitmapHandler extends ImageHandler { } else { $scaler = 'client'; } - - if ( $scaler != 'client' && $dstPath ) { - if ( !wfMkdirParents( dirname( $dstPath ) ) ) { - # Unable to create a path for the thumbnail - return 'client'; - } - } return $scaler; } @@ -245,7 +250,7 @@ class BitmapHandler extends ImageHandler { */ protected function getClientScalingThumbnailImage( $image, $params ) { return new ThumbnailImage( $image, $image->getURL(), - $params['clientWidth'], $params['clientHeight'], $params['srcPath'] ); + $params['clientWidth'], $params['clientHeight'], null ); } /** @@ -276,15 +281,16 @@ class BitmapHandler extends ImageHandler { < $wgSharpenReductionThreshold ) { $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter ); } - // JPEG decoder hint to reduce memory, available since IM 6.5.6-2 - $decoderHint = "-define jpeg:size={$params['physicalDimensions']}"; + if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) { + // JPEG decoder hint to reduce memory, available since IM 6.5.6-2 + $decoderHint = "-define jpeg:size={$params['physicalDimensions']}"; + } } elseif ( $params['mimeType'] == 'image/png' ) { $quality = "-quality 95"; // zlib 9, adaptive filtering } elseif ( $params['mimeType'] == 'image/gif' ) { - if ( $this->getImageArea( $image, $params['srcWidth'], - $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) { + if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) { // Extract initial frame only; we're so big it'll // be a total drag. :P $scene = 0; @@ -298,6 +304,8 @@ class BitmapHandler extends ImageHandler { $animation_post = '-fuzz 5% -layers optimizeTransparency'; } } + } elseif ( $params['mimeType'] == 'image/x-xcf' ) { + $animation_post = '-layers merge'; } // Use one thread only, to avoid deadlock bugs on OOM @@ -372,8 +380,7 @@ class BitmapHandler extends ImageHandler { } elseif( $params['mimeType'] == 'image/png' ) { $im->setCompressionQuality( 95 ); } elseif ( $params['mimeType'] == 'image/gif' ) { - if ( $this->getImageArea( $image, $params['srcWidth'], - $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) { + if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) { // Extract initial frame only; we're so big it'll // be a total drag. :P $im->setImageScene( 0 ); @@ -502,7 +509,7 @@ class BitmapHandler extends ImageHandler { if ( !isset( $typemap[$params['mimeType']] ) ) { $err = 'Image type not supported'; wfDebug( "$err\n" ); - $errMsg = wfMsg ( 'thumbnail_image-type' ); + $errMsg = wfMsg( 'thumbnail_image-type' ); return $this->getMediaTransformError( $params, $errMsg ); } list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']]; @@ -510,14 +517,14 @@ class BitmapHandler extends ImageHandler { if ( !function_exists( $loader ) ) { $err = "Incomplete GD library configuration: missing function $loader"; wfDebug( "$err\n" ); - $errMsg = wfMsg ( 'thumbnail_gd-library', $loader ); + $errMsg = wfMsg( 'thumbnail_gd-library', $loader ); return $this->getMediaTransformError( $params, $errMsg ); } if ( !file_exists( $params['srcPath'] ) ) { $err = "File seems to be missing: {$params['srcPath']}"; wfDebug( "$err\n" ); - $errMsg = wfMsg ( 'thumbnail_image-missing', $params['srcPath'] ); + $errMsg = wfMsg( 'thumbnail_image-missing', $params['srcPath'] ); return $this->getMediaTransformError( $params, $errMsg ); } diff --git a/includes/media/BitmapMetadataHandler.php b/includes/media/BitmapMetadataHandler.php index d1caa67a..746dddda 100644 --- a/includes/media/BitmapMetadataHandler.php +++ b/includes/media/BitmapMetadataHandler.php @@ -32,7 +32,15 @@ class BitmapMetadataHandler { * @param String $app13 String containing app13 block from jpeg file */ private function doApp13 ( $app13 ) { - $this->iptcType = JpegMetadataExtractor::doPSIR( $app13 ); + try { + $this->iptcType = JpegMetadataExtractor::doPSIR( $app13 ); + } catch ( MWException $e ) { + // Error reading the iptc hash information. + // This probably means the App13 segment is something other than what we expect. + // However, still try to read it, and treat it as if the hash didn't exist. + wfDebug( "Error parsing iptc data of file: " . $e->getMessage() . "\n" ); + $this->iptcType = 'iptc-no-hash'; + } $iptc = IPTC::parse( $app13 ); $this->addMetadata( $iptc, $this->iptcType ); @@ -44,7 +52,10 @@ class BitmapMetadataHandler { * Basically what used to be in BitmapHandler::getMetadata(). * Just calls stuff in the Exif class. * + * Parameters are passed to the Exif class. + * * @param $filename string + * @param $byteOrder string */ function getExif ( $filename, $byteOrder ) { global $wgShowEXIF; @@ -122,8 +133,10 @@ class BitmapMetadataHandler { if ( isset( $seg['COM'] ) && isset( $seg['COM'][0] ) ) { $meta->addMetadata( Array( 'JPEGFileComment' => $seg['COM'] ), 'native' ); } - if ( isset( $seg['PSIR'] ) ) { - $meta->doApp13( $seg['PSIR'] ); + if ( isset( $seg['PSIR'] ) && count( $seg['PSIR'] ) > 0 ) { + foreach( $seg['PSIR'] as $curPSIRValue ) { + $meta->doApp13( $curPSIRValue ); + } } if ( isset( $seg['XMP'] ) && $showXMP ) { $xmp = new XMPReader(); diff --git a/includes/media/Bitmap_ClientOnly.php b/includes/media/Bitmap_ClientOnly.php index 50679229..3c5d9738 100644 --- a/includes/media/Bitmap_ClientOnly.php +++ b/includes/media/Bitmap_ClientOnly.php @@ -38,6 +38,6 @@ class BitmapHandler_ClientOnly extends BitmapHandler { return new TransformParameterError( $params ); } return new ThumbnailImage( $image, $image->getURL(), $params['width'], - $params['height'], $image->getPath() ); + $params['height'], $image->getLocalRefPath() ); } } diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index 2833f683..dedbee0d 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -131,7 +131,7 @@ class DjVuHandler extends ImageHandler { } $width = $params['width']; $height = $params['height']; - $srcPath = $image->getPath(); + $srcPath = $image->getLocalRefPath(); $page = $params['page']; if ( $page > $this->pageCount( $image ) ) { return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'djvu_page_error' ) ); @@ -141,13 +141,13 @@ class DjVuHandler extends ImageHandler { return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page ); } - if ( !wfMkdirParents( dirname( $dstPath ) ) ) { + if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { 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}" . + $cmd = '(' . wfEscapeShellArg( $wgDjvuRenderer ) . " -format=ppm -page={$page}" . " -size={$params['physicalWidth']}x{$params['physicalHeight']} " . wfEscapeShellArg( $srcPath ); if ( $wgDjvuPostProcessor ) { @@ -190,6 +190,7 @@ class DjVuHandler extends ImageHandler { /** * Cache a document tree for the DjVu XML metadata * @param $image File + * @param $gettext Boolean: DOCUMENT (Default: false) */ function getMetaTree( $image , $gettext = false ) { if ( isset( $image->dejaMetaTree ) ) { @@ -222,7 +223,7 @@ class DjVuHandler extends ImageHandler { $image->dejaMetaTree = $tree; } } catch( Exception $e ) { - wfDebug( "Bogus multipage XML metadata on '$image->name'\n" ); + wfDebug( "Bogus multipage XML metadata on '{$image->getName()}'\n" ); } wfRestoreWarnings(); wfProfileOut( __METHOD__ ); diff --git a/includes/media/DjVuImage.php b/includes/media/DjVuImage.php new file mode 100644 index 00000000..80b7408c --- /dev/null +++ b/includes/media/DjVuImage.php @@ -0,0 +1,379 @@ +<?php +/** + * DjVu image handler + * + * Copyright © 2006 Brion Vibber <brion@pobox.com> + * http://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Support for detecting/validating DjVu image files and getting + * some basic file metadata (resolution etc) + * + * File format docs are available in source package for DjVuLibre: + * http://djvulibre.djvuzone.org/ + * + * @ingroup Media + */ +class DjVuImage { + function __construct( $filename ) { + $this->mFilename = $filename; + } + + /** + * Check if the given file is indeed a valid DjVu image file + * @return bool + */ + public function isValid() { + $info = $this->getInfo(); + return $info !== false; + } + + + /** + * Return data in the style of getimagesize() + * @return array or false on failure + */ + public function getImageSize() { + $data = $this->getInfo(); + + if( $data !== false ) { + $width = $data['width']; + $height = $data['height']; + + return array( $width, $height, 'DjVu', + "width=\"$width\" height=\"$height\"" ); + } + return false; + } + + // --------- + + /** + * For debugging; dump the IFF chunk structure + */ + function dump() { + $file = fopen( $this->mFilename, 'rb' ); + $header = fread( $file, 12 ); + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + extract( unpack( 'a4magic/a4chunk/NchunkLength', $header ) ); + echo "$chunk $chunkLength\n"; + $this->dumpForm( $file, $chunkLength, 1 ); + fclose( $file ); + } + + private function dumpForm( $file, $length, $indent ) { + $start = ftell( $file ); + $secondary = fread( $file, 4 ); + echo str_repeat( ' ', $indent * 4 ) . "($secondary)\n"; + while( ftell( $file ) - $start < $length ) { + $chunkHeader = fread( $file, 8 ); + if( $chunkHeader == '' ) { + break; + } + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) ); + echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n"; + + if( $chunk == 'FORM' ) { + $this->dumpForm( $file, $chunkLength, $indent + 1 ); + } else { + fseek( $file, $chunkLength, SEEK_CUR ); + if( $chunkLength & 1 == 1 ) { + // Padding byte between chunks + fseek( $file, 1, SEEK_CUR ); + } + } + } + } + + function getInfo() { + wfSuppressWarnings(); + $file = fopen( $this->mFilename, 'rb' ); + wfRestoreWarnings(); + if( $file === false ) { + wfDebug( __METHOD__ . ": missing or failed file read\n" ); + return false; + } + + $header = fread( $file, 16 ); + $info = false; + + if( strlen( $header ) < 16 ) { + wfDebug( __METHOD__ . ": too short file header\n" ); + } else { + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) ); + + if( $magic != 'AT&T' ) { + wfDebug( __METHOD__ . ": not a DjVu file\n" ); + } elseif( $subtype == 'DJVU' ) { + // Single-page document + $info = $this->getPageInfo( $file, $formLength ); + } elseif( $subtype == 'DJVM' ) { + // Multi-page document + $info = $this->getMultiPageInfo( $file, $formLength ); + } else { + wfDebug( __METHOD__ . ": unrecognized DJVU file type '$formType'\n" ); + } + } + fclose( $file ); + return $info; + } + + private function readChunk( $file ) { + $header = fread( $file, 8 ); + if( strlen( $header ) < 8 ) { + return array( false, 0 ); + } else { + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + extract( unpack( 'a4chunk/Nlength', $header ) ); + return array( $chunk, $length ); + } + } + + private function skipChunk( $file, $chunkLength ) { + fseek( $file, $chunkLength, SEEK_CUR ); + + if( $chunkLength & 0x01 == 1 && !feof( $file ) ) { + // padding byte + fseek( $file, 1, SEEK_CUR ); + } + } + + private function getMultiPageInfo( $file, $formLength ) { + // For now, we'll just look for the first page in the file + // and report its information, hoping others are the same size. + $start = ftell( $file ); + do { + list( $chunk, $length ) = $this->readChunk( $file ); + if( !$chunk ) { + break; + } + + if( $chunk == 'FORM' ) { + $subtype = fread( $file, 4 ); + if( $subtype == 'DJVU' ) { + wfDebug( __METHOD__ . ": found first subpage\n" ); + return $this->getPageInfo( $file, $length ); + } + $this->skipChunk( $file, $length - 4 ); + } else { + wfDebug( __METHOD__ . ": skipping '$chunk' chunk\n" ); + $this->skipChunk( $file, $length ); + } + } while( $length != 0 && !feof( $file ) && ftell( $file ) - $start < $formLength ); + + wfDebug( __METHOD__ . ": multi-page DJVU file contained no pages\n" ); + return false; + } + + private function getPageInfo( $file, $formLength ) { + list( $chunk, $length ) = $this->readChunk( $file ); + if( $chunk != 'INFO' ) { + wfDebug( __METHOD__ . ": expected INFO chunk, got '$chunk'\n" ); + return false; + } + + if( $length < 9 ) { + wfDebug( __METHOD__ . ": INFO should be 9 or 10 bytes, found $length\n" ); + return false; + } + $data = fread( $file, $length ); + if( strlen( $data ) < $length ) { + wfDebug( __METHOD__ . ": INFO chunk cut off\n" ); + return false; + } + + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + extract( unpack( + 'nwidth/' . + 'nheight/' . + 'Cminor/' . + 'Cmajor/' . + 'vresolution/' . + 'Cgamma', $data ) ); + # Newer files have rotation info in byte 10, but we don't use it yet. + + return array( + 'width' => $width, + 'height' => $height, + 'version' => "$major.$minor", + 'resolution' => $resolution, + 'gamma' => $gamma / 10.0 ); + } + + /** + * Return an XML string describing the DjVu image + * @return string + */ + function retrieveMetaData() { + global $wgDjvuToXML, $wgDjvuDump, $wgDjvuTxt; + wfProfileIn( __METHOD__ ); + + 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; + } + # Text layer + if ( isset( $wgDjvuTxt ) ) { + wfProfileIn( 'djvutxt' ); + $cmd = wfEscapeShellArg( $wgDjvuTxt ) . ' --detail=page ' . wfEscapeShellArg( $this->mFilename ) ; + wfDebug( __METHOD__.": $cmd\n" ); + $retval = ''; + $txt = wfShellExec( $cmd, $retval ); + wfProfileOut( 'djvutxt' ); + if( $retval == 0) { + # Strip some control characters + $txt = preg_replace( "/[\013\035\037]/", "", $txt ); + $reg = <<<EOR + /\(page\s[\d-]*\s[\d-]*\s[\d-]*\s[\d-]*\s*" + ((?> # Text to match is composed of atoms of either: + \\\\. # - any escaped character + | # - any character different from " and \ + [^"\\\\]+ + )*?) + "\s*\) + | # Or page can be empty ; in this case, djvutxt dumps () + \(\s*()\)/sx +EOR; + $txt = preg_replace_callback( $reg, array( $this, 'pageTextCallback' ), $txt ); + $txt = "<DjVuTxt>\n<HEAD></HEAD>\n<BODY>\n" . $txt . "</BODY>\n</DjVuTxt>\n"; + $xml = preg_replace( "/<DjVuXML>/", "<mw-djvu><DjVuXML>", $xml, 1 ); + $xml = $xml . $txt. '</mw-djvu>' ; + } + } + wfProfileOut( __METHOD__ ); + return $xml; + } + + function pageTextCallback( $matches ) { + # Get rid of invalid UTF-8, strip control characters + return '<PAGE value="' . htmlspecialchars( UtfNormal::cleanUp( $matches[1] ) ) . '" />'; + } + + /** + * 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/media/Exif.php b/includes/media/Exif.php index 345a6f19..a4acdfe0 100644 --- a/includes/media/Exif.php +++ b/includes/media/Exif.php @@ -101,6 +101,7 @@ class Exif { * Constructor * * @param $file String: filename. + * @param $byteOrder String Type of byte ordering either 'BE' (Big Endian) or 'LE' (Little Endian). Default ''. * @todo FIXME: The following are broke: * SubjectArea. Need to test the more obscure tags. * @@ -537,7 +538,7 @@ class Exif { * @deprecated since 1.18 */ function makeFormattedData( ) { - wfDeprecated( __METHOD__ ); + wfDeprecated( __METHOD__, '1.18' ); $this->mFormattedExifData = FormatMetadata::getFormattedData( $this->mFilteredExifData ); } @@ -569,7 +570,7 @@ class Exif { * @deprecated since 1.18 */ function getFormattedData() { - wfDeprecated( __METHOD__ ); + wfDeprecated( __METHOD__, '1.18' ); if (!$this->mFormattedExifData) { $this->makeFormattedData(); } diff --git a/includes/media/ExifBitmap.php b/includes/media/ExifBitmap.php index 05ce161b..7b9867f7 100644 --- a/includes/media/ExifBitmap.php +++ b/includes/media/ExifBitmap.php @@ -34,8 +34,8 @@ class ExifBitmapHandler extends BitmapHandler { // Treat Software as a special case because in can contain // an array of (SoftwareName, Version). - if (isset( $metadata['Software'] ) - && is_array( $metadata['Software'] ) + if (isset( $metadata['Software'] ) + && is_array( $metadata['Software'] ) && is_array( $metadata['Software'][0]) && isset( $metadata['Software'][0][0] ) && isset( $metadata['Software'][0][1]) @@ -136,8 +136,8 @@ class ExifBitmapHandler extends BitmapHandler { function getImageSize( $image, $path ) { global $wgEnableAutoRotation; $gis = parent::getImageSize( $image, $path ); - - // Don't just call $image->getMetadata(); File::getPropsFromPath() calls us with a bogus object. + + // Don't just call $image->getMetadata(); FSFile::getPropsFromPath() calls us with a bogus object. // This may mean we read EXIF data twice on initial upload. if ( $wgEnableAutoRotation ) { $meta = $this->getMetadata( $image, $path ); @@ -171,7 +171,7 @@ class ExifBitmapHandler extends BitmapHandler { if ( !$wgEnableAutoRotation ) { return 0; } - + $data = $file->getMetadata(); return $this->getRotationForExif( $data ); } diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php index 47fc1adc..91cb6914 100644 --- a/includes/media/FormatMetadata.php +++ b/includes/media/FormatMetadata.php @@ -233,10 +233,19 @@ class FormatMetadata { if ( $val == '0000:00:00 00:00:00' || $val == ' : : : : ' ) { $val = wfMsg( 'exif-unknowndate' ); } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', $val ) ) { + // Full date. $time = wfTimestamp( TS_MW, $val ); if ( $time && intval( $time ) > 0 ) { $val = $wgLang->timeanddate( $time ); } + } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) { + // No second field. Still format the same + // since timeanddate doesn't include seconds anyways, + // but second still available in api + $time = wfTimestamp( TS_MW, $val . ':00' ); + if ( $time && intval( $time ) > 0 ) { + $val = $wgLang->timeanddate( $time ); + } } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) { // If only the date but not the time is filled in. $time = wfTimestamp( TS_MW, substr( $val, 0, 4 ) @@ -1174,7 +1183,7 @@ class FormatMetadata { * Format a coordinate value, convert numbers from floating point * into degree minute second representation. * - * @param $coords Array: degrees, minutes and seconds + * @param $coord Array: degrees, minutes and seconds * @param $type String: latitude or longitude (for if its a NWS or E) * @return mixed A floating point number or whatever we were fed */ diff --git a/includes/media/GIF.php b/includes/media/GIF.php index 3bfa45a1..32618e94 100644 --- a/includes/media/GIF.php +++ b/includes/media/GIF.php @@ -14,7 +14,7 @@ class GIFHandler extends BitmapHandler { const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata. - + function getMetadata( $image, $filename ) { try { $parsedGIFMetadata = BitmapMetadataHandler::GIF( $filename ); @@ -50,17 +50,16 @@ class GIFHandler extends BitmapHandler { /** * @param $image File - * @param $width - * @param $height - * @return + * @todo unittests + * @return bool */ - function getImageArea( $image, $width, $height ) { + function getImageArea( $image ) { $ser = $image->getMetadata(); if ( $ser ) { $metadata = unserialize( $ser ); - return $width * $height * $metadata['frameCount']; + return $image->getWidth() * $image->getHeight() * $metadata['frameCount']; } else { - return $width * $height; + return $image->getWidth() * $image->getHeight(); } } @@ -118,7 +117,7 @@ class GIFHandler extends BitmapHandler { wfSuppressWarnings(); $metadata = unserialize($image->getMetadata()); wfRestoreWarnings(); - + if (!$metadata || $metadata['frameCount'] <= 1) { return $original; } @@ -126,19 +125,19 @@ class GIFHandler extends BitmapHandler { /* Preserve original image info string, but strip the last char ')' so we can add even more */ $info = array(); $info[] = $original; - + if ( $metadata['looped'] ) { $info[] = wfMsgExt( 'file-info-gif-looped', 'parseinline' ); } - + if ( $metadata['frameCount'] > 1 ) { $info[] = wfMsgExt( 'file-info-gif-frames', 'parseinline', $metadata['frameCount'] ); } - + if ( $metadata['duration'] ) { $info[] = $wgLang->formatTimePeriod( $metadata['duration'] ); } - + return $wgLang->commaList( $info ); } } diff --git a/includes/media/Generic.php b/includes/media/Generic.php index 6ef21e1e..271d3a8d 100644 --- a/includes/media/Generic.php +++ b/includes/media/Generic.php @@ -89,7 +89,7 @@ abstract class MediaHandler { * * @param $image File: the image object, or false if there isn't one * @param $path String: the filename - * @return Array + * @return Array Follow the format of PHP getimagesize() internal function. See http://www.php.net/getimagesize */ abstract function getImageSize( $image, $path ); @@ -97,7 +97,7 @@ abstract class MediaHandler { * Get handler-specific metadata which will be saved in the img_metadata field. * * @param $image File: the image object, or false if there isn't one. - * Warning, File::getPropsFromPath might pass an (object)array() instead (!) + * Warning, FSFile::getPropsFromPath might pass an (object)array() instead (!) * @param $path String: the filename * @return String */ @@ -187,7 +187,7 @@ abstract class MediaHandler { * @param $dstUrl String: Destination URL to use in output HTML * @param $params Array: Arbitrary set of parameters validated by $this->validateParam() */ - function getTransform( $image, $dstPath, $dstUrl, $params ) { + final function getTransform( $image, $dstPath, $dstUrl, $params ) { return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER ); } @@ -200,6 +200,8 @@ abstract class MediaHandler { * @param $dstUrl String: destination URL to use in output HTML * @param $params Array: arbitrary set of parameters validated by $this->validateParam() * @param $flags Integer: a bitfield, may contain self::TRANSFORM_LATER + * + * @return MediaTransformOutput */ abstract function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ); @@ -258,7 +260,7 @@ abstract class MediaHandler { * @param $image File */ function getPageDimensions( $image, $page ) { - $gis = $this->getImageSize( $image, $image->getPath() ); + $gis = $this->getImageSize( $image, $image->getLocalRefPath() ); return array( 'width' => $gis[0], 'height' => $gis[1] @@ -362,7 +364,7 @@ abstract class MediaHandler { * @param &$array Array An array containing elements for each type of visibility * and each of those elements being an array of metadata items. This function adds * a value to that array. - * @param $visbility string ('visible' or 'collapsed') if this value is hidden + * @param $visibility string ('visible' or 'collapsed') if this value is hidden * by default. * @param $type String type of metadata tag (currently always 'exif') * @param $id String the name of the metadata tag (like 'artist' for example). @@ -378,8 +380,10 @@ abstract class MediaHandler { * Note, everything here is passed through the parser later on (!) */ protected static function addMeta( &$array, $visibility, $type, $id, $value, $param = false ) { - $msgName = "$type-$id"; - if ( wfEmptyMsg( $msgName ) ) { + $msg = wfMessage( "$type-$id", $param ); + if ( $msg->exists() ) { + $name = $msg->text(); + } else { // This is for future compatibility when using instant commons. // So as to not display as ugly a name if a new metadata // property is defined that we don't know about @@ -387,8 +391,6 @@ abstract class MediaHandler { // by default). wfDebug( __METHOD__ . ' Unknown metadata name: ' . $id . "\n" ); $name = wfEscapeWikiText( $id ); - } else { - $name = wfMsg( $msgName, $param ); } $array[$visibility][] = array( 'id' => "$type-$id", @@ -403,9 +405,7 @@ abstract class MediaHandler { */ function getShortDesc( $file ) { global $wgLang; - $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $file->getSize() ) ); - return "$nbytes"; + return htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); } /** @@ -414,9 +414,8 @@ abstract class MediaHandler { */ function getLongDesc( $file ) { global $wgLang; - return wfMsgExt( 'file-info', 'parseinline', - $wgLang->formatSize( $file->getSize() ), - $file->getMimeType() ); + return wfMessage( 'file-info', htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ), + $file->getMimeType() )->parse(); } /** @@ -425,9 +424,7 @@ abstract class MediaHandler { */ static function getGeneralShortDesc( $file ) { global $wgLang; - $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $file->getSize() ) ); - return "$nbytes"; + return $wgLang->formatSize( $file->getSize() ); } /** @@ -436,9 +433,26 @@ abstract class MediaHandler { */ static function getGeneralLongDesc( $file ) { global $wgLang; - return wfMsgExt( 'file-info', 'parseinline', - $wgLang->formatSize( $file->getSize() ), - $file->getMimeType() ); + return wfMessage( 'file-info', $wgLang->formatSize( $file->getSize() ), + $file->getMimeType() )->parse(); + } + + /** + * Calculate the largest thumbnail width for a given original file size + * such that the thumbnail's height is at most $maxHeight. + * @param $boxWidth Integer Width of the thumbnail box. + * @param $boxHeight Integer Height of the thumbnail box. + * @param $maxHeight Integer Maximum height expected for the thumbnail. + * @return Integer. + */ + public static function fitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) { + $idealWidth = $boxWidth * $maxHeight / $boxHeight; + $roundedUp = ceil( $idealWidth ); + if( round( $roundedUp * $boxHeight / $boxWidth ) > $maxHeight ) { + return floor( $idealWidth ); + } else { + return $roundedUp; + } } function getDimensionsString( $file ) { @@ -476,15 +490,32 @@ abstract class MediaHandler { 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 ); + $result = unlink( $dstPath ); + + if ( $result ) { + wfDebugLog( 'thumbnail', + sprintf( 'Removing bad %d-byte thumbnail "%s". unlink() succeeded', + $thumbstat['size'], $dstPath ) ); + } else { + wfDebugLog( 'thumbnail', + sprintf( 'Removing bad %d-byte thumbnail "%s". unlink() failed', + $thumbstat['size'], $dstPath ) ); + } return true; } } return false; } + + /** + * Remove files from the purge list + * + * @param array $files + * @param array $options + */ + public function filterThumbnailPurgeList( &$files, $options ) { + // Do nothing + } } /** @@ -575,7 +606,7 @@ abstract class ImageHandler extends MediaHandler { # Height & width were both set if ( $params['width'] * $srcHeight > $params['height'] * $srcWidth ) { # Height is the relative smaller dimension, so scale width accordingly - $params['width'] = wfFitBoxWidth( $srcWidth, $srcHeight, $params['height'] ); + $params['width'] = self::fitBoxWidth( $srcWidth, $srcHeight, $params['height'] ); if ( $params['width'] == 0 ) { # Very small image, so we need to rely on client side scaling :( @@ -614,13 +645,6 @@ abstract class ImageHandler extends MediaHandler { } /** - * 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 $width Integer: specified width (input/output) @@ -686,9 +710,8 @@ abstract class ImageHandler extends MediaHandler { */ function getShortDesc( $file ) { global $wgLang; - $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $file->getSize() ) ); - $widthheight = wfMsgHtml( 'widthheight', $wgLang->formatNum( $file->getWidth() ) ,$wgLang->formatNum( $file->getHeight() ) ); + $nbytes = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); + $widthheight = wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->escaped(); return "$widthheight ($nbytes)"; } @@ -700,19 +723,15 @@ abstract class ImageHandler extends MediaHandler { function getLongDesc( $file ) { global $wgLang; $pages = $file->pageCount(); + $size = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); if ( $pages === false || $pages <= 1 ) { - $msg = wfMsgExt('file-info-size', 'parseinline', - $wgLang->formatNum( $file->getWidth() ), - $wgLang->formatNum( $file->getHeight() ), - $wgLang->formatSize( $file->getSize() ), - $file->getMimeType() ); + $msg = wfMessage( 'file-info-size' )->numParams( $file->getWidth(), + $file->getHeight() )->params( $size, + $file->getMimeType() )->parse(); } else { - $msg = wfMsgExt('file-info-size-pages', 'parseinline', - $wgLang->formatNum( $file->getWidth() ), - $wgLang->formatNum( $file->getHeight() ), - $wgLang->formatSize( $file->getSize() ), - $file->getMimeType(), - $wgLang->formatNum( $pages ) ); + $msg = wfMessage( 'file-info-size-pages' )->numParams( $file->getWidth(), + $file->getHeight() )->params( $size, + $file->getMimeType() )->numParams( $pages )->parse(); } return $msg; } @@ -722,16 +741,11 @@ abstract class ImageHandler extends MediaHandler { * @return string */ function getDimensionsString( $file ) { - global $wgLang; $pages = $file->pageCount(); - $width = $wgLang->formatNum( $file->getWidth() ); - $height = $wgLang->formatNum( $file->getHeight() ); - $pagesFmt = $wgLang->formatNum( $pages ); - if ( $pages > 1 ) { - return wfMsgExt( 'widthheightpage', 'parsemag', $width, $height, $pagesFmt ); + return wfMessage( 'widthheightpage' )->numParams( $file->getWidth(), $file->getHeight(), $pages )->text(); } else { - return wfMsg( 'widthheight', $width, $height ); + return wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->text(); } } } diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php index 4769bf8e..224b4a2b 100644 --- a/includes/media/JpegMetadataExtractor.php +++ b/includes/media/JpegMetadataExtractor.php @@ -31,6 +31,7 @@ class JpegMetadataExtractor { $segments = array( 'XMP_ext' => array(), 'COM' => array(), + 'PSIR' => array(), ); if ( !$filename ) { @@ -122,7 +123,7 @@ class JpegMetadataExtractor { // APP13 - PSIR. IPTC and some photoshop stuff $temp = self::jpegExtractMarker( $fh ); if ( substr( $temp, 0, 14 ) === "Photoshop 3.0\x00" ) { - $segments["PSIR"] = $temp; + $segments["PSIR"][] = $temp; } } elseif ( $buffer === "\xD9" || $buffer === "\xDA" ) { // EOI - end of image or SOS - start of scan. either way we're past any interesting segments @@ -162,11 +163,12 @@ class JpegMetadataExtractor { * This should generally be called by BitmapMetadataHandler::doApp13() * * @param String $app13 photoshop psir app13 block from jpg. + * @throws MWException (It gets caught next level up though) * @return String if the iptc hash is good or not. */ public static function doPSIR ( $app13 ) { if ( !$app13 ) { - return; + throw new MWException( "No App13 segment given" ); } // First compare hash with real thing // 0x404 contains IPTC, 0x425 has hash @@ -218,8 +220,8 @@ class JpegMetadataExtractor { // this should not happen, but check. if ( $lenData['len'] + $offset > $appLen ) { - wfDebug( __METHOD__ . " PSIR data too long.\n" ); - return 'iptc-no-hash'; + throw new MWException( "PSIR data too long. (item length=" . $lenData['len'] + . "; offset=$offset; total length=$appLen)" ); } if ( $valid ) { diff --git a/includes/media/MediaTransformOutput.php b/includes/media/MediaTransformOutput.php index f170bb9d..fcfb2f45 100644 --- a/includes/media/MediaTransformOutput.php +++ b/includes/media/MediaTransformOutput.php @@ -18,33 +18,42 @@ abstract class MediaTransformOutput { var $file; var $width, $height, $url, $page, $path; + protected $storagePath = false; /** * Get the width of the output box */ - function getWidth() { + public function getWidth() { return $this->width; } /** * Get the height of the output box */ - function getHeight() { + public function getHeight() { return $this->height; } /** * @return string The thumbnail URL */ - function getUrl() { + public function getUrl() { return $this->url; } /** - * @return String: destination file path (local filesystem) + * @return string|false The permanent thumbnail storage path */ - function getPath() { - return $this->path; + public function getStoragePath() { + return $this->storagePath; + } + + /** + * @param $storagePath string The permanent storage path + * @return void + */ + public function setStoragePath( $storagePath ) { + $this->storagePath = $storagePath; } /** @@ -67,16 +76,66 @@ abstract class MediaTransformOutput { * * @return string */ - abstract function toHtml( $options = array() ); + abstract public function toHtml( $options = array() ); /** * This will be overridden to return true in error classes */ - function isError() { + public function isError() { return false; } /** + * Check if an output thumbnail file actually exists. + * This will return false if there was an error, the + * thumbnail is to be handled client-side only, or if + * transformation was deferred via TRANSFORM_LATER. + * + * @return Bool + */ + public function hasFile() { + // If TRANSFORM_LATER, $this->path will be false. + // Note: a null path means "use the source file". + return ( !$this->isError() && ( $this->path || $this->path === null ) ); + } + + /** + * Check if the output thumbnail is the same as the source. + * This can occur if the requested width was bigger than the source. + * + * @return Bool + */ + public function fileIsSource() { + return ( !$this->isError() && $this->path === null ); + } + + /** + * Get the path of a file system copy of the thumbnail. + * Callers should never write to this path. + * + * @return string|false Returns false if there isn't one + */ + public function getLocalCopyPath() { + if ( $this->isError() ) { + return false; + } elseif ( $this->path === null ) { + return $this->file->getLocalRefPath(); + } else { + return $this->path; // may return false + } + } + + /** + * Stream the file if there were no errors + * + * @param $headers Array Additional HTTP headers to send on success + * @return Bool success + */ + public function streamFile( $headers = array() ) { + return $this->path && StreamFile::stream( $this->getLocalCopyPath(), $headers ); + } + + /** * Wrap some XHTML text in an anchor tag with the given attributes * * @param $linkAttribs array @@ -97,7 +156,7 @@ abstract class MediaTransformOutput { * @param $params array * @return array */ - function getDescLinkAttribs( $title = null, $params = '' ) { + public function getDescLinkAttribs( $title = null, $params = '' ) { $query = $this->page ? ( 'page=' . urlencode( $this->page ) ) : ''; if( $params ) { $query .= $query ? '&'.$params : $params; @@ -119,13 +178,16 @@ abstract class MediaTransformOutput { * @ingroup Media */ class ThumbnailImage extends MediaTransformOutput { - /** + * Get a thumbnail object from a file and parameters. + * If $path is set to null, the output file is treated as a source copy. + * If $path is set to false, no output file will be created. + * * @param $file File object * @param $url String: URL path to the thumb * @param $width Integer: file's width * @param $height Integer: file's height - * @param $path String: filesystem path to the thumb + * @param $path String|false|null: filesystem path to the thumb * @param $page Integer: page number, for multipage files * @private */ @@ -185,7 +247,7 @@ class ThumbnailImage extends MediaTransformOutput { } elseif ( !empty( $options['custom-title-link'] ) ) { $title = $options['custom-title-link']; $linkAttribs = array( - 'href' => $title->getLinkUrl(), + 'href' => $title->getLinkURL(), 'title' => empty( $options['title'] ) ? $title->getFullText() : $options['title'] ); } elseif ( !empty( $options['desc-link'] ) ) { diff --git a/includes/media/SVG.php b/includes/media/SVG.php index ceffd7c3..aac838e1 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -93,13 +93,13 @@ class SvgHandler extends ImageHandler { $clientHeight = $params['height']; $physicalWidth = $params['physicalWidth']; $physicalHeight = $params['physicalHeight']; - $srcPath = $image->getPath(); + $srcPath = $image->getLocalRefPath(); if ( $flags & self::TRANSFORM_LATER ) { return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); } - if ( !wfMkdirParents( dirname( $dstPath ) ) ) { + if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, wfMsg( 'thumbnail_dest_directory' ) ); } @@ -119,7 +119,7 @@ class SvgHandler extends ImageHandler { * @param string $dstPath * @param string $width * @param string $height - * @returns TRUE/MediaTransformError + * @return true|MediaTransformError */ public function rasterize( $srcPath, $dstPath, $width, $height ) { global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath; @@ -129,7 +129,7 @@ class SvgHandler extends ImageHandler { if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) { // This is a PHP callable $func = $wgSVGConverters[$wgSVGConverter][0]; - $args = array_merge( array( $srcPath, $dstPath, $width, $height ), + $args = array_merge( array( $srcPath, $dstPath, $width, $height ), array_slice( $wgSVGConverters[$wgSVGConverter], 1 ) ); if ( !is_callable( $func ) ) { throw new MWException( "$func is not callable" ); @@ -161,13 +161,13 @@ class SvgHandler extends ImageHandler { } return true; } - + public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) { $im = new Imagick( $srcPath ); $im->setImageFormat( 'png' ); $im->setBackgroundColor( 'transparent' ); $im->setImageDepth( 8 ); - + if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) { return 'Could not resize image'; } diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php index 22ef8e61..db9f05fd 100644 --- a/includes/media/SVGMetadataExtractor.php +++ b/includes/media/SVGMetadataExtractor.php @@ -68,6 +68,12 @@ class SVGReader { $this->reader->open( $source, null, LIBXML_NOERROR | LIBXML_NOWARNING ); } + // Expand entities, since Adobe Illustrator uses them for xmlns + // attributes (bug 31719). Note that libxml2 has some protection + // against large recursive entity expansions so this is not as + // insecure as it might appear to be. + $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES, true ); + $this->metadata['width'] = self::DEFAULT_WIDTH; $this->metadata['height'] = self::DEFAULT_HEIGHT; @@ -166,7 +172,7 @@ class SVGReader { } } - /* + /** * Read an XML snippet from an element * * @param String $metafield that we will fill with the result diff --git a/includes/media/XCF.php b/includes/media/XCF.php new file mode 100644 index 00000000..806db73c --- /dev/null +++ b/includes/media/XCF.php @@ -0,0 +1,137 @@ +<?php +/** + * Handler for the Gimp's native file format (XCF) + * + * Overview: + * http://en.wikipedia.org/wiki/XCF_(file_format) + * Specification in Gnome repository: + * http://svn.gnome.org/viewvc/gimp/trunk/devel-docs/xcf.txt?view=markup + * + * @file + * @ingroup Media + */ + +/** + * Handler for the Gimp's native file format; getimagesize() doesn't + * support these files + * + * @ingroup Media + */ +class XCFHandler extends BitmapHandler { + + /** + * @param $file + * @return bool + */ + function mustRender( $file ) { + return true; + } + + /** + * Render files as PNG + * + * @param $ext + * @param $mime + * @param $params + * @return array + */ + function getThumbType( $ext, $mime, $params = null ) { + return array( 'png', 'image/png' ); + } + + /** + * Get width and height from the XCF header. + * + * @param $image + * @param $filename + * @return array + */ + function getImageSize( $image, $filename ) { + return self::getXCFMetaData( $filename ); + } + + /** + * Metadata for a given XCF file + * + * Will return false if file magic signature is not recognized + * @author Hexmode + * @author Hashar + * + * @param $filename String Full path to a XCF file + * @return false|metadata array just like PHP getimagesize() + */ + static function getXCFMetaData( $filename ) { + # Decode master structure + $f = fopen( $filename, 'rb' ); + if( !$f ) { + return false; + } + # The image structure always starts at offset 0 in the XCF file. + # So we just read it :-) + $binaryHeader = fread( $f, 26 ); + fclose($f); + + # Master image structure: + # + # byte[9] "gimp xcf " File type magic + # byte[4] version XCF version + # "file" - version 0 + # "v001" - version 1 + # "v002" - version 2 + # byte 0 Zero-terminator for version tag + # uint32 width With of canvas + # uint32 height Height of canvas + # uint32 base_type Color mode of the image; one of + # 0: RGB color + # 1: Grayscale + # 2: Indexed color + # (enum GimpImageBaseType in libgimpbase/gimpbaseenums.h) + try { + $header = wfUnpack( + "A9magic" # A: space padded + . "/a5version" # a: zero padded + . "/Nwidth" # \ + . "/Nheight" # N: unsigned long 32bit big endian + . "/Nbase_type" # / + , $binaryHeader + ); + } catch( MWException $mwe ) { + return false; + } + + # Check values + if( $header['magic'] !== 'gimp xcf' ) { + wfDebug( __METHOD__ . " '$filename' has invalid magic signature.\n" ); + return false; + } + # TODO: we might want to check for sane values of width and height + + wfDebug( __METHOD__ . ": canvas size of '$filename' is {$header['width']} x {$header['height']} px\n" ); + + # Forge a return array containing metadata information just like getimagesize() + # See PHP documentation at: http://www.php.net/getimagesize + $metadata = array(); + $metadata[0] = $header['width']; + $metadata[1] = $header['height']; + $metadata[2] = null; # IMAGETYPE constant, none exist for XCF. + $metadata[3] = sprintf( + 'height="%s" width="%s"', $header['height'], $header['width'] + ); + $metadata['mime'] = 'image/x-xcf'; + $metadata['channels'] = null; + $metadata['bits'] = 8; # Always 8-bits per color + + assert( '7 == count($metadata); # return array must contains 7 elements just like getimagesize() return' ); + + return $metadata; + } + + /** + * Must use "im" for XCF + * + * @return string + */ + protected static function getScalerType( $dstPath, $checkDstPath = true ) { + return "im"; + } +} diff --git a/includes/media/XMP.php b/includes/media/XMP.php index 1e578582..0dbf5632 100644 --- a/includes/media/XMP.php +++ b/includes/media/XMP.php @@ -210,9 +210,9 @@ class XMPReader { * Also catches any errors during processing, writes them to * debug log, blanks result array and returns false. * - * @param String: $content XMP data - * @param Boolean: $allOfIt If this is all the data (true) or if its split up (false). Default true - * @param Boolean: $reset - does xml parser need to be reset. Default false + * @param $content String: XMP data + * @param $allOfIt Boolean: If this is all the data (true) or if its split up (false). Default true + * @param $reset Boolean: does xml parser need to be reset. Default false * @return Boolean success. */ public function parse( $content, $allOfIt = true, $reset = false ) { diff --git a/includes/media/XMPInfo.php b/includes/media/XMPInfo.php index 1d580ff7..156d9b50 100644 --- a/includes/media/XMPInfo.php +++ b/includes/media/XMPInfo.php @@ -631,12 +631,23 @@ class XMPInfo { 'validate' => 'validateClosed', 'choices' => array( '1' => true, '2' => true ), ), - 'YCbCrSubSampling' => array( - 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateClosed', - 'choices' => array( '1' => true, '2' => true ), - ), + /******** + * Disable extracting this property (bug 31944) + * Several files have a string instead of a Seq + * for this property. XMPReader doesn't handle + * mismatched types very gracefully (it marks + * the entire file as invalid, instead of just + * the relavent prop). Since this prop + * doesn't communicate all that useful information + * just disable this prop for now, until such + * XMPReader is more graceful (bug 32172) + * 'YCbCrSubSampling' => array( + * 'map_group' => 'exif', + * 'mode' => XMPReader::MODE_SEQ, + * 'validate' => 'validateClosed', + * 'choices' => array( '1' => true, '2' => true ), + * ), + */ ), 'http://ns.adobe.com/exif/1.0/aux/' => array( 'Lens' => array( diff --git a/includes/media/XMPValidate.php b/includes/media/XMPValidate.php index 0f1d375c..600d99de 100644 --- a/includes/media/XMPValidate.php +++ b/includes/media/XMPValidate.php @@ -201,10 +201,20 @@ class XMPValidate { } /** - * function to validate date properties, and convert to Exif format. + * function to validate date properties, and convert to (partial) Exif format. + * + * Dates can be one of the following formats: + * YYYY + * YYYY-MM + * YYYY-MM-DD + * YYYY-MM-DDThh:mmTZD + * YYYY-MM-DDThh:mm:ssTZD + * YYYY-MM-DDThh:mm:ss.sTZD * * @param $info Array information about current property * @param &$val Mixed current value to validate. Converts to TS_EXIF as a side-effect. + * in cases where there's only a partial date, it will give things like + * 2011:04. * @param $standalone Boolean if this is a simple property or array */ public static function validateDate( $info, &$val, $standalone ) { @@ -240,25 +250,41 @@ class XMPValidate { $val = null; return; } - //if month, etc unspecified, full out as 01. - $res[2] = isset( $res[2] ) ? $res[2] : '01'; //month - $res[3] = isset( $res[3] ) ? $res[3] : '01'; //day + if ( !isset( $res[4] ) ) { //hour - //just have the year month day - $val = $res[1] . ':' . $res[2] . ':' . $res[3]; + //just have the year month day (if that) + $val = $res[1]; + if ( isset( $res[2] ) ) { + $val .= ':' . $res[2]; + } + if ( isset( $res[3] ) ) { + $val .= ':' . $res[3]; + } return; } - //if hour is set, so is minute or regex above will fail. - //Extra check for empty string necessary due to TZ but no second case. - $res[6] = isset( $res[6] ) && $res[6] != '' ? $res[6] : '00'; if ( !isset( $res[7] ) || $res[7] === 'Z' ) { + //if hour is set, then minute must also be or regex above will fail. $val = $res[1] . ':' . $res[2] . ':' . $res[3] - . ' ' . $res[4] . ':' . $res[5] . ':' . $res[6]; + . ' ' . $res[4] . ':' . $res[5]; + if ( isset( $res[6] ) && $res[6] !== '' ) { + $val .= ':' . $res[6]; + } return; } - //do timezone processing. We've already done the case that tz = Z. + + // Extra check for empty string necessary due to TZ but no second case. + $stripSeconds = false; + if ( !isset( $res[6] ) || $res[6] === '' ) { + $res[6] = '00'; + $stripSeconds = true; + } + + // Do timezone processing. We've already done the case that tz = Z. + + // We know that if we got to this step, year, month day hour and min must be set + // by virtue of regex not failing. $unix = wfTimestamp( TS_UNIX, $res[1] . $res[2] . $res[3] . $res[4] . $res[5] . $res[6] ); $offset = intval( substr( $res[7], 1, 2 ) ) * 60 * 60; @@ -267,6 +293,11 @@ class XMPValidate { $offset = -$offset; } $val = wfTimestamp( TS_EXIF, $unix + $offset ); + + if ( $stripSeconds ) { + // If seconds weren't specified, remove the trailing ':00'. + $val = substr( $val, 0, -3 ); + } } } |