diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2011-06-22 11:28:20 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2011-06-22 11:28:20 +0200 |
commit | 9db190c7e736ec8d063187d4241b59feaf7dc2d1 (patch) | |
tree | 46d1a0dee7febef5c2d57a9f7b972be16a163b3d /includes/media | |
parent | 78677c7bbdcc9739f6c10c75935898a20e1acd9e (diff) |
update to MediaWiki 1.17.0
Diffstat (limited to 'includes/media')
-rw-r--r-- | includes/media/BMP.php | 4 | ||||
-rw-r--r-- | includes/media/Bitmap.php | 533 | ||||
-rw-r--r-- | includes/media/Bitmap_ClientOnly.php | 14 | ||||
-rw-r--r-- | includes/media/DjVu.php | 7 | ||||
-rw-r--r-- | includes/media/GIF.php | 43 | ||||
-rw-r--r-- | includes/media/GIFMetadataExtractor.php | 21 | ||||
-rw-r--r-- | includes/media/Generic.php | 51 | ||||
-rw-r--r-- | includes/media/MediaTransformOutput.php | 255 | ||||
-rw-r--r-- | includes/media/PNG.php | 82 | ||||
-rw-r--r-- | includes/media/PNGMetadataExtractor.php | 104 | ||||
-rw-r--r-- | includes/media/SVG.php | 121 | ||||
-rw-r--r-- | includes/media/SVGMetadataExtractor.php | 313 | ||||
-rw-r--r-- | includes/media/Tiff.php | 6 |
13 files changed, 1340 insertions, 214 deletions
diff --git a/includes/media/BMP.php b/includes/media/BMP.php index 39b29744..de836b59 100644 --- a/includes/media/BMP.php +++ b/includes/media/BMP.php @@ -1,5 +1,7 @@ <?php /** + * Handler for Microsoft's bitmap format + * * @file * @ingroup Media */ @@ -17,7 +19,7 @@ class BmpHandler extends BitmapHandler { } // Render files as PNG - function getThumbType( $text, $mime ) { + function getThumbType( $text, $mime, $params = null ) { return array( 'png', 'image/png' ); } diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 870e2126..f5f7ba6d 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -1,10 +1,14 @@ <?php /** + * Generic handler for bitmap images + * * @file * @ingroup Media */ /** + * Generic handler for bitmap images + * * @ingroup Media */ class BitmapHandler extends ImageHandler { @@ -18,15 +22,6 @@ class BitmapHandler extends ImageHandler { $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' && - $this->getImageArea( $image, $srcWidth, $srcHeight ) > $wgMaxImageArea ) - { - return false; - } - # Don't make an image bigger than the source $params['physicalWidth'] = $params['width']; $params['physicalHeight'] = $params['height']; @@ -34,13 +29,25 @@ class BitmapHandler extends ImageHandler { if ( $params['physicalWidth'] >= $srcWidth ) { $params['physicalWidth'] = $srcWidth; $params['physicalHeight'] = $srcHeight; - return true; + # Skip scaling limit checks if no scaling is required + if ( !$image->mustRender() ) + return true; + } + + # 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. + # FIXME: This actually only applies to ImageMagick + if ( $mimeType !== 'image/jpeg' && + $srcWidth * $srcHeight > $wgMaxImageArea ) + { + return false; } return true; } - - + + // 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 ) { @@ -48,36 +55,48 @@ class BitmapHandler extends ImageHandler { } function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { - global $wgUseImageMagick, $wgImageMagickConvertCommand, $wgImageMagickTempDir; + global $wgUseImageMagick; global $wgCustomConvertCommand, $wgUseImageResize; - global $wgSharpenParameter, $wgSharpenReductionThreshold; - global $wgMaxAnimatedGifArea; if ( !$this->normaliseParams( $image, $params ) ) { return new TransformParameterError( $params ); } - $physicalWidth = $params['physicalWidth']; - $physicalHeight = $params['physicalHeight']; - $clientWidth = $params['width']; - $clientHeight = $params['height']; - $comment = isset( $params['descriptionUrl'] ) ? "File source: ". $params['descriptionUrl'] : ''; - $srcWidth = $image->getWidth(); - $srcHeight = $image->getHeight(); - $mimeType = $image->getMimeType(); - $srcPath = $image->getPath(); - $retval = 0; - wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" ); + # Create a parameter array to pass to the scaler + $scalerParams = array( + # The size to which the image will be resized + 'physicalWidth' => $params['physicalWidth'], + 'physicalHeight' => $params['physicalHeight'], + 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}", + # The size of the image on the page + 'clientWidth' => $params['width'], + 'clientHeight' => $params['height'], + # Comment as will be added to the EXIF of the thumbnail + 'comment' => isset( $params['descriptionUrl'] ) ? + "File source: {$params['descriptionUrl']}" : '', + # Properties of the original image + 'srcWidth' => $image->getWidth(), + 'srcHeight' => $image->getHeight(), + 'mimeType' => $image->getMimeType(), + 'srcPath' => $image->getPath(), + 'dstPath' => $dstPath, + ); + + wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath\n" ); + + if ( !$image->mustRender() && + $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] + && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) { - if ( !$image->mustRender() && $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, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); + wfDebug( __METHOD__ . ": returning unscaled image\n" ); + return $this->getClientScalingThumbnailImage( $image, $scalerParams ); } + # Determine scaler type if ( !$dstPath ) { - // No output path available, client side scaling only + # No output path available, client side scaling only $scaler = 'client'; - } elseif( !$wgUseImageResize ) { + } elseif ( !$wgUseImageResize ) { $scaler = 'client'; } elseif ( $wgUseImageMagick ) { $scaler = 'im'; @@ -88,166 +107,289 @@ class BitmapHandler extends ImageHandler { } else { $scaler = 'client'; } - wfDebug( __METHOD__.": scaler $scaler\n" ); + wfDebug( __METHOD__ . ": scaler $scaler\n" ); 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, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); + return $this->getClientScalingThumbnailImage( $image, $scalerParams ); } if ( $flags & self::TRANSFORM_LATER ) { - wfDebug( __METHOD__.": Transforming later per flags.\n" ); - return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); + wfDebug( __METHOD__ . ": Transforming later per flags.\n" ); + return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], + $scalerParams['clientHeight'], $dstPath ); } + # Try to make a target path for the thumbnail if ( !wfMkdirParents( dirname( $dstPath ) ) ) { - wfDebug( __METHOD__.": Unable to create thumbnail destination directory, falling back to client scaling\n" ); - return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); - } - - if ( $scaler == 'im' ) { - # use ImageMagick - - $quality = ''; - $sharpen = ''; - $scene = false; - $animation = ''; - 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 - } elseif( $mimeType == 'image/gif' ) { - if( $srcWidth * $srcHeight > $wgMaxAnimatedGifArea ) { - // Extract initial frame only; we're so big it'll - // be a total drag. :P - $scene = 0; - } else { - // Coalesce is needed to scale animated GIFs properly (bug 1017). - $animation = ' -coalesce '; + wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" ); + return $this->getClientScalingThumbnailImage( $image, $scalerParams ); + } + + switch ( $scaler ) { + case 'im': + $err = $this->transformImageMagick( $image, $scalerParams ); + break; + case 'custom': + $err = $this->transformCustom( $image, $scalerParams ); + break; + case 'gd': + default: + $err = $this->transformGd( $image, $scalerParams ); + break; + } + + # Remove the file if a zero-byte thumbnail was created, or if there was an error + $removed = $this->removeBadFile( $dstPath, (bool)$err ); + if ( $err ) { + # transform returned MediaTransforError + return $err; + } elseif ( $removed ) { + # Thumbnail was zero-byte and had to be removed + return new MediaTransformError( 'thumbnail_error', + $scalerParams['clientWidth'], $scalerParams['clientHeight'] ); + } else { + return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], + $scalerParams['clientHeight'], $dstPath ); + } + } + + /** + * Get a ThumbnailImage that respresents an image that will be scaled + * client side + * + * @param $image File File associated with this thumbnail + * @param $params array Array with scaler params + * @return ThumbnailImage + */ + protected function getClientScalingThumbnailImage( $image, $params ) { + return new ThumbnailImage( $image, $image->getURL(), + $params['clientWidth'], $params['clientHeight'], $params['srcPath'] ); + } + + /** + * Transform an image using ImageMagick + * + * @param $image File File associated with this thumbnail + * @param $params array Array with scaler params + * + * @return MediaTransformError Error object if error occured, false (=no error) otherwise + */ + protected function transformImageMagick( $image, $params ) { + # use ImageMagick + global $wgSharpenReductionThreshold, $wgSharpenParameter, + $wgMaxAnimatedGifArea, + $wgImageMagickTempDir, $wgImageMagickConvertCommand; + + $quality = ''; + $sharpen = ''; + $scene = false; + $animation_pre = ''; + $animation_post = ''; + $decoderHint = ''; + if ( $params['mimeType'] == 'image/jpeg' ) { + $quality = "-quality 80"; // 80% + # Sharpening, see bug 6193 + if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) + / ( $params['srcWidth'] + $params['srcHeight'] ) + < $wgSharpenReductionThreshold ) { + $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter ); + } + // 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 ) { + // Extract initial frame only; we're so big it'll + // be a total drag. :P + $scene = 0; + + } elseif ( $this->isAnimatedImage( $image ) ) { + // Coalesce is needed to scale animated GIFs properly (bug 1017). + $animation_pre = '-coalesce'; + // We optimize the output, but -optimize is broken, + // use optimizeTransparency instead (bug 11822) + if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) { + $animation_post = '-fuzz 5% -layers optimizeTransparency +map'; } } + } - if ( strval( $wgImageMagickTempDir ) !== '' ) { - $tempEnv = 'MAGICK_TMPDIR=' . wfEscapeShellArg( $wgImageMagickTempDir ) . ' '; - } else { - $tempEnv = ''; - } + // Use one thread only, to avoid deadlock bugs on OOM + $env = array( 'OMP_NUM_THREADS' => 1 ); + if ( strval( $wgImageMagickTempDir ) !== '' ) { + $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir; + } - # 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 = - $tempEnv . - wfEscapeShellArg( $wgImageMagickConvertCommand ) . - " {$quality} -background white -size {$physicalWidth} ". - wfEscapeShellArg( $this->escapeMagickInput( $srcPath, $scene ) ) . - $animation . - // 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}!" ) . - // Add the source url as a comment to the thumb. - " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $comment ) ) . - " -depth 8 $sharpen " . - wfEscapeShellArg( $this->escapeMagickOutput( $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" ); - $errMsg = wfMsg ( 'thumbnail_image-type' ); - return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg ); - } - list( $loader, $colorStyle, $saveType ) = $typemap[$mimeType]; + $cmd = + wfEscapeShellArg( $wgImageMagickConvertCommand ) . + // Specify white background color, will be used for transparent images + // in Internet Explorer/Windows instead of default black. + " {$quality} -background white" . + " {$decoderHint} " . + wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) . + " {$animation_pre}" . + // For the -thumbnail option a "!" is needed to force exact size, + // or ImageMagick may decide your ratio is wrong and slice off + // a pixel. + " -thumbnail " . wfEscapeShellArg( "{$params['physicalDimensions']}!" ) . + // Add the source url as a comment to the thumb, but don't add the flag if there's no comment + ( $params['comment'] !== '' + ? " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $params['comment'] ) ) + : '' ) . + " -depth 8 $sharpen" . + " {$animation_post} " . + wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) ) . " 2>&1"; + + wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" ); + wfProfileIn( 'convert' ); + $retval = 0; + $err = wfShellExec( $cmd, $retval, $env ); + wfProfileOut( 'convert' ); - if( !function_exists( $loader ) ) { - $err = "Incomplete GD library configuration: missing function $loader"; - wfDebug( "$err\n" ); - $errMsg = wfMsg ( 'thumbnail_gd-library', $loader ); - return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg ); - } + if ( $retval !== 0 ) { + $this->logErrorForExternalProcess( $retval, $err, $cmd ); + return $this->getMediaTransformError( $params, $err ); + } - if ( !file_exists( $srcPath ) ) { - $err = "File seems to be missing: $srcPath"; - wfDebug( "$err\n" ); - $errMsg = wfMsg ( 'thumbnail_image-missing', $srcPath ); - return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg ); - } + return false; # No error + } - $src_image = call_user_func( $loader, $srcPath ); - $dst_image = imagecreatetruecolor( $physicalWidth, $physicalHeight ); - - // Initialise the destination image to transparent instead of - // the default solid black, to support PNG and GIF transparency nicely - $background = imagecolorallocate( $dst_image, 0, 0, 0 ); - imagecolortransparent( $dst_image, $background ); - imagealphablending( $dst_image, false ); - - if( $colorStyle == 'palette' ) { - // Don't resample for paletted GIF images. - // It may just uglify them, and completely breaks transparency. - imagecopyresized( $dst_image, $src_image, - 0,0,0,0, - $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) ); - } else { - imagecopyresampled( $dst_image, $src_image, - 0,0,0,0, - $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) ); - } + /** + * Transform an image using a custom command + * + * @param $image File File associated with this thumbnail + * @param $params array Array with scaler params + * + * @return MediaTransformError Error object if error occured, false (=no error) otherwise + */ + protected function transformCustom( $image, $params ) { + # Use a custom convert command + global $wgCustomConvertCommand; + + # Variables: %s %d %w %h + $src = wfEscapeShellArg( $params['srcPath'] ); + $dst = wfEscapeShellArg( $params['dstPath'] ); + $cmd = $wgCustomConvertCommand; + $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames + $cmd = str_replace( '%h', $params['physicalHeight'], + str_replace( '%w', $params['physicalWidth'], $cmd ) ); # Size + wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" ); + wfProfileIn( 'convert' ); + $retval = 0; + $err = wfShellExec( $cmd, $retval ); + wfProfileOut( 'convert' ); - imagesavealpha( $dst_image, true ); + if ( $retval !== 0 ) { + $this->logErrorForExternalProcess( $retval, $err, $cmd ); + return $this->getMediaTransformError( $params, $err ); + } + return false; # No error + } - call_user_func( $saveType, $dst_image, $dstPath ); - imagedestroy( $dst_image ); - imagedestroy( $src_image ); - $retval = 0; + /** + * Log an error that occured in an external process + * + * @param $retval int + * @param $err int + * @param $cmd string + */ + protected function logErrorForExternalProcess( $retval, $err, $cmd ) { + wfDebugLog( 'thumbnail', + sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', + wfHostname(), $retval, trim( $err ), $cmd ) ); + } + /** + * Get a MediaTransformError with error 'thumbnail_error' + * + * @param $params array Parameter array as passed to the transform* functions + * @param $errMsg string Error message + * @return MediaTransformError + */ + protected function getMediaTransformError( $params, $errMsg ) { + return new MediaTransformError( 'thumbnail_error', $params['clientWidth'], + $params['clientHeight'], $errMsg ); + } + + /** + * Transform an image using the built in GD library + * + * @param $image File File associated with this thumbnail + * @param $params array Array with scaler params + * + * @return MediaTransformError Error object if error occured, false (=no error) otherwise + */ + protected function transformGd( $image, $params ) { + # 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[$params['mimeType']] ) ) { + $err = 'Image type not supported'; + wfDebug( "$err\n" ); + $errMsg = wfMsg ( 'thumbnail_image-type' ); + return $this->getMediaTransformError( $params, $errMsg ); + } + list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']]; + + if ( !function_exists( $loader ) ) { + $err = "Incomplete GD library configuration: missing function $loader"; + wfDebug( "$err\n" ); + $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'] ); + return $this->getMediaTransformError( $params, $errMsg ); } - $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 ); + $src_image = call_user_func( $loader, $params['srcPath'] ); + $dst_image = imagecreatetruecolor( $params['physicalWidth'], + $params['physicalHeight'] ); + + // Initialise the destination image to transparent instead of + // the default solid black, to support PNG and GIF transparency nicely + $background = imagecolorallocate( $dst_image, 0, 0, 0 ); + imagecolortransparent( $dst_image, $background ); + imagealphablending( $dst_image, false ); + + if ( $colorStyle == 'palette' ) { + // Don't resample for paletted GIF images. + // It may just uglify them, and completely breaks transparency. + imagecopyresized( $dst_image, $src_image, + 0, 0, 0, 0, + $params['physicalWidth'], $params['physicalHeight'], + imagesx( $src_image ), imagesy( $src_image ) ); } else { - return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); + imagecopyresampled( $dst_image, $src_image, + 0, 0, 0, 0, + $params['physicalWidth'], $params['physicalHeight'], + imagesx( $src_image ), imagesy( $src_image ) ); } + + imagesavealpha( $dst_image, true ); + + call_user_func( $saveType, $dst_image, $params['dstPath'] ); + imagedestroy( $dst_image ); + imagedestroy( $src_image ); + + return false; # No error } /** @@ -267,14 +409,14 @@ class BitmapHandler extends ImageHandler { } /** - * Escape a string for ImageMagick's input filenames. See ExpandFilenames() + * Escape a string for ImageMagick's input filenames. See ExpandFilenames() * and GetPathComponent() in magick/utility.c. * * This won't work with an initial ~ or @, so input files should be prefixed - * with the directory name. + * with the directory name. * * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but - * it's broken in a way that doesn't involve trying to convert every file + * it's broken in a way that doesn't involve trying to convert every file * in a directory, so we're better off escaping and waiting for the bugfix * to filter down to users. * @@ -285,7 +427,7 @@ class BitmapHandler extends ImageHandler { # Die on initial metacharacters (caller should prepend path) $firstChar = substr( $path, 0, 1 ); if ( $firstChar === '~' || $firstChar === '@' ) { - throw new MWException( __METHOD__.': cannot escape this path name' ); + throw new MWException( __METHOD__ . ': cannot escape this path name' ); } # Escape glob chars @@ -295,7 +437,7 @@ class BitmapHandler extends ImageHandler { } /** - * Escape a string for ImageMagick's output filename. See + * Escape a string for ImageMagick's output filename. See * InterpretImageFilename() in magick/image.c. */ function escapeMagickOutput( $path, $scene = false ) { @@ -304,7 +446,7 @@ class BitmapHandler extends ImageHandler { } /** - * Armour a string against ImageMagick's GetPathComponent(). This is a + * Armour a string against ImageMagick's GetPathComponent(). This is a * helper function for escapeMagickInput() and escapeMagickOutput(). * * @param $path string The file path @@ -318,11 +460,11 @@ class BitmapHandler extends ImageHandler { // OK, it's a drive letter // ImageMagick has a similar exception, see IsMagickConflict() } else { - throw new MWException( __METHOD__.': unexpected colon character in path name' ); + throw new MWException( __METHOD__ . ': unexpected colon character in path name' ); } } - # If there are square brackets, add a do-nothing scene specification + # If there are square brackets, add a do-nothing scene specification # to force a literal interpretation if ( $scene === false ) { if ( strpos( $path, '[' ) !== false ) { @@ -334,6 +476,33 @@ class BitmapHandler extends ImageHandler { return $path; } + /** + * Retrieve the version of the installed ImageMagick + * You can use PHPs version_compare() to use this value + * Value is cached for one hour. + * @return String representing the IM version. + */ + protected function getMagickVersion() { + global $wgMemc; + + $cache = $wgMemc->get( "imagemagick-version" ); + if ( !$cache ) { + global $wgImageMagickConvertCommand; + $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version'; + wfDebug( __METHOD__ . ": Running convert -version\n" ); + $retval = ''; + $return = wfShellExec( $cmd, $retval ); + $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches ); + if ( $x != 1 ) { + wfDebug( __METHOD__ . ": ImageMagick version check failed\n" ); + return null; + } + $wgMemc->set( "imagemagick-version", $matches[1], 3600 ); + return $matches[1]; + } + return $cache; + } + static function imageJpegWrapper( $dst_image, $thumbPath ) { imageinterlace( $dst_image ); imagejpeg( $dst_image, $thumbPath, 95 ); @@ -342,7 +511,7 @@ class BitmapHandler extends ImageHandler { function getMetadata( $image, $filename ) { global $wgShowEXIF; - if( $wgShowEXIF && file_exists( $filename ) ) { + if ( $wgShowEXIF && file_exists( $filename ) ) { $exif = new Exif( $filename ); $data = $exif->getFilteredData(); if ( $data ) { @@ -370,12 +539,14 @@ class BitmapHandler extends ImageHandler { # Special value indicating that there is no EXIF data in the file return true; } - $exif = @unserialize( $metadata ); + wfSuppressWarnings(); + $exif = unserialize( $metadata ); + wfRestoreWarnings(); if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) || $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version() ) { # Wrong version - wfDebug( __METHOD__.": wrong version\n" ); + wfDebug( __METHOD__ . ": wrong version\n" ); return false; } return true; @@ -391,9 +562,9 @@ class BitmapHandler extends ImageHandler { function visibleMetadataFields() { $fields = array(); $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) ); - foreach( $lines as $line ) { + foreach ( $lines as $line ) { $matches = array(); - if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { + if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { $fields[] = $matches[1]; } } diff --git a/includes/media/Bitmap_ClientOnly.php b/includes/media/Bitmap_ClientOnly.php index 9801f9be..9f6f7b33 100644 --- a/includes/media/Bitmap_ClientOnly.php +++ b/includes/media/Bitmap_ClientOnly.php @@ -1,5 +1,19 @@ <?php +/** + * Handler for bitmap images that will be resized by clients + * + * @file + * @ingroup Media + */ +/** + * Handler for bitmap images that will be resized by clients. + * + * This is not used by default but can be assigned to some image types + * using $wgMediaHandlers. + * + * @ingroup Media + */ class BitmapHandler_ClientOnly extends BitmapHandler { function normaliseParams( $image, &$params ) { return ImageHandler::normaliseParams( $image, $params ); diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index 38c16c21..cc3f1db5 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -1,10 +1,14 @@ <?php /** + * Handler for DjVu images + * * @file * @ingroup Media */ /** + * Handler for DjVu images + * * @ingroup Media */ class DjVuHandler extends ImageHandler { @@ -104,6 +108,7 @@ class DjVuHandler extends ImageHandler { $cmd .= ' > ' . wfEscapeShellArg($dstPath) . ') 2>&1'; wfProfileIn( 'ddjvu' ); wfDebug( __METHOD__.": $cmd\n" ); + $retval = ''; $err = wfShellExec( $cmd, $retval ); wfProfileOut( 'ddjvu' ); @@ -181,7 +186,7 @@ class DjVuHandler extends ImageHandler { return $this->getDjVuImage( $image, $path )->getImageSize(); } - function getThumbType( $ext, $mime ) { + function getThumbType( $ext, $mime, $params = null ) { global $wgDjvuOutputExtension; static $mime; if ( !isset( $mime ) ) { diff --git a/includes/media/GIF.php b/includes/media/GIF.php index dbe5f813..c4ede331 100644 --- a/includes/media/GIF.php +++ b/includes/media/GIF.php @@ -1,5 +1,7 @@ <?php /** + * Handler for GIF images. + * * @file * @ingroup Media */ @@ -12,7 +14,7 @@ class GIFHandler extends BitmapHandler { function getMetadata( $image, $filename ) { - if ( !isset($image->parsedGIFMetadata) ) { + if ( !isset( $image->parsedGIFMetadata ) ) { try { $image->parsedGIFMetadata = GIFMetadataExtractor::getMetadata( $filename ); } catch( Exception $e ) { @@ -22,7 +24,7 @@ class GIFHandler extends BitmapHandler { } } - return serialize($image->parsedGIFMetadata); + return serialize( $image->parsedGIFMetadata ); } @@ -39,22 +41,41 @@ class GIFHandler extends BitmapHandler { return $width * $height; } } + + function isAnimatedImage( $image ) { + $ser = $image->getMetadata(); + if ($ser) { + $metadata = unserialize($ser); + if( $metadata['frameCount'] > 1 ) return true; + } + return false; + } function getMetadataType( $image ) { return 'parsed-gif'; } + function isMetadataValid( $image, $metadata ) { + wfSuppressWarnings(); + $data = unserialize( $metadata ); + wfRestoreWarnings(); + return (boolean) $data; + } + function getLongDesc( $image ) { - global $wgUser, $wgLang; - $sk = $wgUser->getSkin(); - - $metadata = @unserialize($image->getMetadata()); + global $wgLang; + + $original = parent::getLongDesc( $image ); + + wfSuppressWarnings(); + $metadata = unserialize($image->getMetadata()); + wfRestoreWarnings(); - if (!$metadata) return parent::getLongDesc( $image ); + if (!$metadata || $metadata['frameCount'] <= 1) + return $original; $info = array(); - $info[] = $image->getMimeType(); - $info[] = $sk->formatSize( $image->getSize() ); + $info[] = $original; if ($metadata['looped']) $info[] = wfMsgExt( 'file-info-gif-looped', 'parseinline' ); @@ -65,8 +86,6 @@ class GIFHandler extends BitmapHandler { if ($metadata['duration']) $info[] = $wgLang->formatTimePeriod( $metadata['duration'] ); - $infoString = $wgLang->commaList( $info ); - - return "($infoString)"; + return $wgLang->commaList( $info ); } } diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php index fac9012b..bc1a4804 100644 --- a/includes/media/GIFMetadataExtractor.php +++ b/includes/media/GIFMetadataExtractor.php @@ -1,12 +1,21 @@ <?php /** - * GIF frame counter. - * Originally written in Perl by Steve Sanbeg. - * Ported to PHP by Andrew Garrett - * Deliberately not using MWExceptions to avoid external dependencies, encouraging - * redistribution. - */ + * GIF frame counter. + * + * Originally written in Perl by Steve Sanbeg. + * Ported to PHP by Andrew Garrett + * Deliberately not using MWExceptions to avoid external dependencies, encouraging + * redistribution. + * + * @file + * @ingroup Media + */ +/** + * GIF frame counter. + * + * @ingroup Media + */ class GIFMetadataExtractor { static $gif_frame_sep; static $gif_extension_sep; diff --git a/includes/media/Generic.php b/includes/media/Generic.php index 8a4d7054..fa4e731a 100644 --- a/includes/media/Generic.php +++ b/includes/media/Generic.php @@ -1,6 +1,7 @@ <?php /** * Media-handling base classes and generic functionality + * * @file * @ingroup Media */ @@ -72,7 +73,7 @@ abstract class MediaHandler { * can't be determined. * * @param $image File: the image object, or false if there isn't one - * @param $fileName String: the filename + * @param $path String: the filename * @return Array */ abstract function getImageSize( $image, $path ); @@ -80,7 +81,8 @@ 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 + * @param $image File: the image object, or false if there isn't one. + * Warning, File::getPropsFromPath might pass an (object)array() instead (!) * @param $path String: the filename * @return String */ @@ -139,7 +141,7 @@ abstract class MediaHandler { * Get the thumbnail extension and MIME type for a given source MIME type * @return array thumbnail extension and MIME type */ - function getThumbType( $ext, $mime ) { + function getThumbType( $ext, $mime, $params = null ) { return array( $ext, $mime ); } @@ -161,6 +163,10 @@ abstract class MediaHandler { */ function pageCount( $file ) { return false; } /** + * The material is vectorized and thus scaling is lossless + */ + function isVectorized( $file ) { return false; } + /** * False if the handler is disabled for all files */ function isEnabled() { return true; } @@ -235,8 +241,8 @@ abstract class MediaHandler { function getShortDesc( $file ) { global $wgLang; - $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $file->getSize() ) ) . ')'; + $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $file->getSize() ) ); return "$nbytes"; } @@ -250,8 +256,8 @@ abstract class MediaHandler { static function getGeneralShortDesc( $file ) { global $wgLang; - $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $file->getSize() ) ) . ')'; + $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $file->getSize() ) ); return "$nbytes"; } @@ -273,6 +279,20 @@ abstract class MediaHandler { function parserTransformHook( $parser, $file ) {} /** + * File validation hook called on upload. + * + * If the file at the given local path is not valid, or its MIME type does not + * match the handler class, a Status object should be returned containing + * relevant errors. + * + * @param $fileName The local path to the file. + * @return Status object + */ + function verifyUpload( $fileName ) { + return Status::newGood(); + } + + /** * Check for zero-sized thumbnails. These can be generated when * no disk space is available or some other error occurs * @@ -357,9 +377,19 @@ abstract class ImageHandler extends MediaHandler { if ( !isset( $params['width'] ) ) { return false; } + if ( !isset( $params['page'] ) ) { $params['page'] = 1; + } else { + if ( $params['page'] > $image->pageCount() ) { + $params['page'] = $image->pageCount(); + } + + if ( $params['page'] < 1 ) { + $params['page'] = 1; + } } + $srcWidth = $image->getWidth( $params['page'] ); $srcHeight = $image->getHeight( $params['page'] ); if ( isset( $params['height'] ) && $params['height'] != -1 ) { @@ -386,6 +416,9 @@ abstract class ImageHandler extends MediaHandler { * * @param $width Integer: specified width (input/output) * @param $height Integer: height (output only) + * @param $srcWidth Integer: width of the source image + * @param $srcHeight Integer: height of the source image + * @param $mimeType Unused * @return false to indicate that an error should be returned to the user. */ function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) { @@ -424,6 +457,10 @@ abstract class ImageHandler extends MediaHandler { return $gis; } + function isAnimatedImage( $image ) { + return false; + } + function getShortDesc( $file ) { global $wgLang; $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), diff --git a/includes/media/MediaTransformOutput.php b/includes/media/MediaTransformOutput.php new file mode 100644 index 00000000..c441f06c --- /dev/null +++ b/includes/media/MediaTransformOutput.php @@ -0,0 +1,255 @@ +<?php +/** + * Base class for the output of file transformation methods. + * + * @file + * @ingroup Media + */ + +/** + * Base class for the output of MediaHandler::doTransform() and File::transform(). + * + * @ingroup Media + */ +abstract class MediaTransformOutput { + var $file, $width, $height, $url, $page, $path; + + /** + * 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 $options Associative array of options. Boolean options + * should be indicated with a value of true for true, and false or + * absent for false. + * + * alt Alternate text or caption + * desc-link Boolean, show a description link + * file-link Boolean, show a file download link + * custom-url-link Custom URL to link to + * custom-title-link Custom Title object to link to + * valign vertical-align property, if the output is an inline element + * img-class Class applied to the <img> tag, if there is such a tag + * + * For images, desc-link and file-link are implemented as a click-through. For + * sounds and videos, they may be displayed in other ways. + * + * @return string + */ + abstract function toHtml( $options = array() ); + + /** + * 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; + } + } + + function getDescLinkAttribs( $title = null, $params = '' ) { + $query = $this->page ? ( 'page=' . urlencode( $this->page ) ) : ''; + if( $params ) { + $query .= $query ? '&'.$params : $params; + } + $attribs = array( + 'href' => $this->file->getTitle()->getLocalURL( $query ), + 'class' => 'image', + ); + if ( $title ) { + $attribs['title'] = $title; + } + return $attribs; + } +} + + +/** + * Media transform output for images + * + * @ingroup Media + */ +class ThumbnailImage extends MediaTransformOutput { + + /** + * @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 $page Integer: page number, for multipage files + * @private + */ + function __construct( $file, $url, $width, $height, $path = false, $page = false ) { + $this->file = $file; + $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; + $this->page = $page; + } + + /** + * Return HTML <img ... /> tag for the thumbnail, will include + * width and height attributes and a blank alt text (as required). + * + * @param $options Associative array of options. Boolean options + * should be indicated with a value of true for true, and false or + * absent for false. + * + * alt HTML alt attribute + * title HTML title attribute + * desc-link Boolean, show a description link + * file-link Boolean, show a file download link + * valign vertical-align property, if the output is an inline element + * img-class Class applied to the \<img\> tag, if there is such a tag + * desc-query String, description link query params + * custom-url-link Custom URL to link to + * custom-title-link Custom Title object to link to + * custom target-link Value of the target attribute, for custom-target-link + * + * For images, desc-link and file-link are implemented as a click-through. For + * sounds and videos, they may be displayed in other ways. + * + * @return string + */ + function toHtml( $options = array() ) { + if ( count( func_get_args() ) == 2 ) { + throw new MWException( __METHOD__ .' called in the old style' ); + } + + $alt = empty( $options['alt'] ) ? '' : $options['alt']; + + $query = empty( $options['desc-query'] ) ? '' : $options['desc-query']; + + if ( !empty( $options['custom-url-link'] ) ) { + $linkAttribs = array( 'href' => $options['custom-url-link'] ); + if ( !empty( $options['title'] ) ) { + $linkAttribs['title'] = $options['title']; + } + if ( !empty( $options['custom-target-link'] ) ) { + $linkAttribs['target'] = $options['custom-target-link']; + } + } elseif ( !empty( $options['custom-title-link'] ) ) { + $title = $options['custom-title-link']; + $linkAttribs = array( + 'href' => $title->getLinkUrl(), + 'title' => empty( $options['title'] ) ? $title->getFullText() : $options['title'] + ); + } elseif ( !empty( $options['desc-link'] ) ) { + $linkAttribs = $this->getDescLinkAttribs( empty( $options['title'] ) ? null : $options['title'], $query ); + } elseif ( !empty( $options['file-link'] ) ) { + $linkAttribs = array( 'href' => $this->file->getURL() ); + } else { + $linkAttribs = false; + } + + $attribs = array( + 'alt' => $alt, + 'src' => $this->url, + 'width' => $this->width, + 'height' => $this->height, + ); + if ( !empty( $options['valign'] ) ) { + $attribs['style'] = "vertical-align: {$options['valign']}"; + } + if ( !empty( $options['img-class'] ) ) { + $attribs['class'] = $options['img-class']; + } + return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) ); + } + +} + +/** + * Basic media transform error class + * + * @ingroup Media + */ +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( $options = array() ) { + return "<div class=\"MediaTransformError\" style=\"" . + "width: {$this->width}px; height: {$this->height}px; display:inline-block;\">" . + $this->htmlMsg . + "</div>"; + } + + function toText() { + return $this->textMsg; + } + + function getHtmlMsg() { + return $this->htmlMsg; + } + + function isError() { + return true; + } +} + +/** + * Shortcut class for parameter validation errors + * + * @ingroup Media + */ +class TransformParameterError extends MediaTransformError { + function __construct( $params ) { + parent::__construct( 'thumbnail_error', + max( isset( $params['width'] ) ? $params['width'] : 0, 120 ), + max( isset( $params['height'] ) ? $params['height'] : 0, 120 ), + wfMsg( 'thumbnail_invalid_params' ) ); + } +} diff --git a/includes/media/PNG.php b/includes/media/PNG.php new file mode 100644 index 00000000..5197282c --- /dev/null +++ b/includes/media/PNG.php @@ -0,0 +1,82 @@ +<?php +/** + * Handler for PNG images. + * + * @file + * @ingroup Media + */ + +/** + * Handler for PNG images. + * + * @ingroup Media + */ +class PNGHandler extends BitmapHandler { + + function getMetadata( $image, $filename ) { + if ( !isset($image->parsedPNGMetadata) ) { + try { + $image->parsedPNGMetadata = PNGMetadataExtractor::getMetadata( $filename ); + } catch( Exception $e ) { + // Broken file? + wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" ); + return '0'; + } + } + + return serialize($image->parsedPNGMetadata); + + } + + function formatMetadata( $image ) { + return false; + } + + function isAnimatedImage( $image ) { + $ser = $image->getMetadata(); + if ($ser) { + $metadata = unserialize($ser); + if( $metadata['frameCount'] > 1 ) return true; + } + return false; + } + + function getMetadataType( $image ) { + return 'parsed-png'; + } + + function isMetadataValid( $image, $metadata ) { + wfSuppressWarnings(); + $data = unserialize( $metadata ); + wfRestoreWarnings(); + return (boolean) $data; + } + function getLongDesc( $image ) { + global $wgLang; + $original = parent::getLongDesc( $image ); + + wfSuppressWarnings(); + $metadata = unserialize($image->getMetadata()); + wfRestoreWarnings(); + + if( !$metadata || $metadata['frameCount'] <= 0 ) + return $original; + + $info = array(); + $info[] = $original; + + if ($metadata['loopCount'] == 0) + $info[] = wfMsgExt( 'file-info-png-looped', 'parseinline' ); + elseif ($metadata['loopCount'] > 1) + $info[] = wfMsgExt( 'file-info-png-repeat', 'parseinline', $metadata['loopCount'] ); + + if ($metadata['frameCount'] > 0) + $info[] = wfMsgExt( 'file-info-png-frames', 'parseinline', $metadata['frameCount'] ); + + if ($metadata['duration']) + $info[] = $wgLang->formatTimePeriod( $metadata['duration'] ); + + return $wgLang->commaList( $info ); + } + +} diff --git a/includes/media/PNGMetadataExtractor.php b/includes/media/PNGMetadataExtractor.php new file mode 100644 index 00000000..6a931e6c --- /dev/null +++ b/includes/media/PNGMetadataExtractor.php @@ -0,0 +1,104 @@ +<?php +/** + * PNG frame counter. + * Slightly derived from GIFMetadataExtractor.php + * Deliberately not using MWExceptions to avoid external dependencies, encouraging + * redistribution. + * + * @file + * @ingroup Media + */ + +/** + * PNG frame counter. + * + * @ingroup Media + */ +class PNGMetadataExtractor { + static $png_sig; + static $CRC_size; + + static function getMetadata( $filename ) { + self::$png_sig = pack( "C8", 137, 80, 78, 71, 13, 10, 26, 10 ); + self::$CRC_size = 4; + + $frameCount = 0; + $loopCount = 1; + $duration = 0.0; + + if (!$filename) + throw new Exception( __METHOD__ . ": No file name specified" ); + elseif ( !file_exists($filename) || is_dir($filename) ) + throw new Exception( __METHOD__ . ": File $filename does not exist" ); + + $fh = fopen( $filename, 'r' ); + + if (!$fh) { + throw new Exception( __METHOD__ . ": Unable to open file $filename" ); + } + + // Check for the PNG header + $buf = fread( $fh, 8 ); + if ( $buf != self::$png_sig ) { + throw new Exception( __METHOD__ . ": Not a valid PNG file; header: $buf" ); + } + + // Read chunks + while( !feof( $fh ) ) { + $buf = fread( $fh, 4 ); + if( !$buf ) { + throw new Exception( __METHOD__ . ": Read error" ); + } + $chunk_size = unpack( "N", $buf); + $chunk_size = $chunk_size[1]; + + $chunk_type = fread( $fh, 4 ); + if( !$chunk_type ) { + throw new Exception( __METHOD__ . ": Read error" ); + } + + if ( $chunk_type == "acTL" ) { + $buf = fread( $fh, $chunk_size ); + if( !$buf ) { + throw new Exception( __METHOD__ . ": Read error" ); + } + + $actl = unpack( "Nframes/Nplays", $buf ); + $frameCount = $actl['frames']; + $loopCount = $actl['plays']; + } elseif ( $chunk_type == "fcTL" ) { + $buf = fread( $fh, $chunk_size ); + if( !$buf ) { + throw new Exception( __METHOD__ . ": Read error" ); + } + $buf = substr( $buf, 20 ); + + $fctldur = unpack( "ndelay_num/ndelay_den", $buf ); + if( $fctldur['delay_den'] == 0 ) $fctldur['delay_den'] = 100; + if( $fctldur['delay_num'] ) { + $duration += $fctldur['delay_num'] / $fctldur['delay_den']; + } + } elseif ( ( $chunk_type == "IDAT" || $chunk_type == "IEND" ) && $frameCount == 0 ) { + // Not a valid animated image. No point in continuing. + break; + } elseif ( $chunk_type == "IEND" ) { + break; + } else { + fseek( $fh, $chunk_size, SEEK_CUR ); + } + fseek( $fh, self::$CRC_size, SEEK_CUR ); + } + fclose( $fh ); + + if( $loopCount > 1 ) { + $duration *= $loopCount; + } + + return array( + 'frameCount' => $frameCount, + 'loopCount' => $loopCount, + 'duration' => $duration + ); + + } +} diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 4cc66fb1..9a8484f1 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -1,13 +1,19 @@ <?php /** + * Handler for SVG images. + * * @file * @ingroup Media */ /** + * Handler for SVG images. + * * @ingroup Media */ class SvgHandler extends ImageHandler { + const SVG_METADATA_VERSION = 2; + function isEnabled() { global $wgSVGConverters, $wgSVGConverter; if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) { @@ -22,6 +28,22 @@ class SvgHandler extends ImageHandler { return true; } + function isVectorized( $file ) { + return true; + } + + function isAnimatedImage( $file ) { + # TODO: detect animated SVGs + $metadata = $file->getMetadata(); + if ( $metadata ) { + $metadata = $this->unpackMetadata( $metadata ); + if( isset( $metadata['animated'] ) ) { + return $metadata['animated']; + } + } + return false; + } + function normaliseParams( $image, &$params ) { global $wgSVGMaxSize; if ( !parent::normaliseParams( $image, $params ) ) { @@ -57,7 +79,7 @@ class SvgHandler extends ImageHandler { return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, wfMsg( 'thumbnail_dest_directory' ) ); } - + $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight ); if( $status === true ) { return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); @@ -65,7 +87,7 @@ class SvgHandler extends ImageHandler { return $status; // MediaTransformError } } - + /* * Transform an SVG file to PNG * This function can be called outside of thumbnail contexts @@ -78,6 +100,7 @@ class SvgHandler extends ImageHandler { public function rasterize( $srcPath, $dstPath, $width, $height ) { global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath; $err = false; + $retval = ''; if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) { $cmd = str_replace( array( '$path/', '$width', '$height', '$input', '$output' ), @@ -102,11 +125,19 @@ class SvgHandler extends ImageHandler { return true; } - function getImageSize( $image, $path ) { - return wfGetSVGsize( $path ); + function getImageSize( $file, $path, $metadata = false ) { + if ( $metadata === false ) { + $metadata = $file->getMetaData(); + } + $metadata = $this->unpackMetaData( $metadata ); + + if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) { + return array( $metadata['width'], $metadata['height'], 'SVG', + "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" ); + } } - function getThumbType( $ext, $mime ) { + function getThumbType( $ext, $mime, $params = null ) { return array( 'png', 'image/png' ); } @@ -117,4 +148,84 @@ class SvgHandler extends ImageHandler { $wgLang->formatNum( $file->getHeight() ), $wgLang->formatSize( $file->getSize() ) ); } + + function getMetadata( $file, $filename ) { + try { + $metadata = SVGMetadataExtractor::getMetadata( $filename ); + } catch( Exception $e ) { + // Broken file? + wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" ); + return '0'; + } + $metadata['version'] = self::SVG_METADATA_VERSION; + return serialize( $metadata ); + } + + function unpackMetadata( $metadata ) { + $unser = @unserialize( $metadata ); + if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) { + return $unser; + } else { + return false; + } + } + + function getMetadataType( $image ) { + return 'parsed-svg'; + } + + function isMetadataValid( $image, $metadata ) { + return $this->unpackMetadata( $metadata ) !== false; + } + + function visibleMetadataFields() { + $fields = array( 'title', 'description', 'animated' ); + return $fields; + } + + function formatMetadata( $file ) { + $result = array( + 'visible' => array(), + 'collapsed' => array() + ); + $metadata = $file->getMetadata(); + if ( !$metadata ) { + return false; + } + $metadata = $this->unpackMetadata( $metadata ); + if ( !$metadata ) { + return false; + } + unset( $metadata['version'] ); + unset( $metadata['metadata'] ); /* non-formatted XML */ + + /* TODO: add a formatter + $format = new FormatSVG( $metadata ); + $formatted = $format->getFormattedData(); + */ + + // Sort fields into visible and collapsed + $visibleFields = $this->visibleMetadataFields(); + + // Rename fields to be compatible with exif, so that + // the labels for these fields work. + $conversion = array( 'width' => 'imagewidth', + 'height' => 'imagelength', + 'description' => 'imagedescription', + 'title' => 'objectname', + ); + foreach ( $metadata as $name => $value ) { + $tag = strtolower( $name ); + if ( isset( $conversion[$tag] ) ) { + $tag = $conversion[$tag]; + } + self::addMeta( $result, + in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed', + 'exif', + $tag, + $value + ); + } + return $result; + } } diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php new file mode 100644 index 00000000..66ae1edf --- /dev/null +++ b/includes/media/SVGMetadataExtractor.php @@ -0,0 +1,313 @@ +<?php +/** + * SVGMetadataExtractor.php + * + * 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 + * @ingroup Media + * @author Derk-Jan Hartman <hartman _at_ videolan d0t org> + * @author Brion Vibber + * @copyright Copyright © 2010-2010 Brion Vibber, Derk-Jan Hartman + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ + +class SVGMetadataExtractor { + static function getMetadata( $filename ) { + $svg = new SVGReader( $filename ); + return $svg->getMetadata(); + } +} + +class SVGReader { + const DEFAULT_WIDTH = 512; + const DEFAULT_HEIGHT = 512; + const NS_SVG = 'http://www.w3.org/2000/svg'; + + private $reader = null; + private $mDebug = false; + private $metadata = Array(); + + /** + * Constructor + * + * Creates an SVGReader drawing from the source provided + * @param $source String: URI from which to read + */ + function __construct( $source ) { + global $wgSVGMetadataCutoff; + $this->reader = new XMLReader(); + + // Don't use $file->getSize() since file object passed to SVGHandler::getMetadata is bogus. + $size = filesize( $source ); + if ( $size === false ) { + throw new MWException( "Error getting filesize of SVG." ); + } + + if ( $size > $wgSVGMetadataCutoff ) { + $this->debug( "SVG is $size bytes, which is bigger than $wgSVGMetadataCutoff. Truncating." ); + $contents = file_get_contents( $source, false, null, -1, $wgSVGMetadataCutoff ); + if ($contents === false) { + throw new MWException( 'Error reading SVG file.' ); + } + $this->reader->XML( $contents, null, LIBXML_NOERROR | LIBXML_NOWARNING ); + } else { + $this->reader->open( $source, null, LIBXML_NOERROR | LIBXML_NOWARNING ); + } + + $this->metadata['width'] = self::DEFAULT_WIDTH; + $this->metadata['height'] = self::DEFAULT_HEIGHT; + + // Because we cut off the end of the svg making an invalid one. Complicated + // try catch thing to make sure warnings get restored. Seems like there should + // be a better way. + wfSuppressWarnings(); + try { + $this->read(); + } catch( Exception $e ) { + wfRestoreWarnings(); + throw $e; + } + wfRestoreWarnings(); + } + + /* + * @return Array with the known metadata + */ + public function getMetadata() { + return $this->metadata; + } + + /* + * Read the SVG + */ + public function read() { + $keepReading = $this->reader->read(); + + /* Skip until first element */ + while( $keepReading && $this->reader->nodeType != XmlReader::ELEMENT ) { + $keepReading = $this->reader->read(); + } + + if ( $this->reader->localName != 'svg' || $this->reader->namespaceURI != self::NS_SVG ) { + throw new MWException( "Expected <svg> tag, got ". + $this->reader->localName . " in NS " . $this->reader->namespaceURI ); + } + $this->debug( "<svg> tag is correct." ); + $this->handleSVGAttribs(); + + $exitDepth = $this->reader->depth; + $keepReading = $this->reader->read(); + while ( $keepReading ) { + $tag = $this->reader->localName; + $type = $this->reader->nodeType; + $isSVG = ($this->reader->namespaceURI == self::NS_SVG); + + $this->debug( "$tag" ); + + if ( $isSVG && $tag == 'svg' && $type == XmlReader::END_ELEMENT && $this->reader->depth <= $exitDepth ) { + break; + } elseif ( $isSVG && $tag == 'title' ) { + $this->readField( $tag, 'title' ); + } elseif ( $isSVG && $tag == 'desc' ) { + $this->readField( $tag, 'description' ); + } elseif ( $isSVG && $tag == 'metadata' && $type == XmlReader::ELEMENT ) { + $this->readXml( $tag, 'metadata' ); + } elseif ( $tag !== '#text' ) { + $this->debug( "Unhandled top-level XML tag $tag" ); + + if ( !isset( $this->metadata['animated'] ) ) { + // Recurse into children of current tag, looking for animation. + $this->animateFilter( $tag ); + } + } + + // Goto next element, which is sibling of current (Skip children). + $keepReading = $this->reader->next(); + } + + return true; + } + + /* + * Read a textelement from an element + * + * @param String $name of the element that we are reading from + * @param String $metafield that we will fill with the result + */ + private function readField( $name, $metafield=null ) { + $this->debug ( "Read field $metafield" ); + if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) { + return; + } + $keepReading = $this->reader->read(); + while( $keepReading ) { + if( $this->reader->localName == $name && $this->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::END_ELEMENT ) { + break; + } elseif( $this->reader->nodeType == XmlReader::TEXT ){ + $this->metadata[$metafield] = trim( $this->reader->value ); + } + $keepReading = $this->reader->read(); + } + } + + /* + * Read an XML snippet from an element + * + * @param String $metafield that we will fill with the result + */ + private function readXml( $metafield=null ) { + $this->debug ( "Read top level metadata" ); + if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) { + return; + } + // TODO: find and store type of xml snippet. metadata['metadataType'] = "rdf" + $this->metadata[$metafield] = trim( $this->reader->readInnerXML() ); + $this->reader->next(); + } + + /* + * Filter all children, looking for animate elements + * + * @param String $name of the element that we are reading from + */ + private function animateFilter( $name ) { + $this->debug ( "animate filter" ); + if( $this->reader->nodeType != XmlReader::ELEMENT ) { + return; + } + $exitDepth = $this->reader->depth; + $keepReading = $this->reader->read(); + while( $keepReading ) { + if( $this->reader->localName == $name && $this->reader->depth <= $exitDepth + && $this->reader->nodeType == XmlReader::END_ELEMENT ) { + break; + } elseif ( $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::ELEMENT ) { + switch( $this->reader->localName ) { + case 'animate': + case 'set': + case 'animateMotion': + case 'animateColor': + case 'animateTransform': + $this->debug( "HOUSTON WE HAVE ANIMATION" ); + $this->metadata['animated'] = true; + break; + } + } + $keepReading = $this->reader->read(); + } + } + + private function throwXmlError( $err ) { + $this->debug( "FAILURE: $err" ); + wfDebug( "SVGReader XML error: $err\n" ); + } + + private function debug( $data ) { + if( $this->mDebug ) { + wfDebug( "SVGReader: $data\n" ); + } + } + + private function warn( $data ) { + wfDebug( "SVGReader: $data\n" ); + } + + private function notice( $data ) { + wfDebug( "SVGReader WARN: $data\n" ); + } + + /* + * Parse the attributes of an SVG element + * + * The parser has to be in the start element of <svg> + */ + private function handleSVGAttribs( ) { + $defaultWidth = self::DEFAULT_WIDTH; + $defaultHeight = self::DEFAULT_HEIGHT; + $aspect = 1.0; + $width = null; + $height = null; + + if( $this->reader->getAttribute('viewBox') ) { + // min-x min-y width height + $viewBox = preg_split( '/\s+/', trim( $this->reader->getAttribute('viewBox') ) ); + if( count( $viewBox ) == 4 ) { + $viewWidth = $this->scaleSVGUnit( $viewBox[2] ); + $viewHeight = $this->scaleSVGUnit( $viewBox[3] ); + if( $viewWidth > 0 && $viewHeight > 0 ) { + $aspect = $viewWidth / $viewHeight; + $defaultHeight = $defaultWidth / $aspect; + } + } + } + if( $this->reader->getAttribute('width') ) { + $width = $this->scaleSVGUnit( $this->reader->getAttribute('width'), $defaultWidth ); + } + if( $this->reader->getAttribute('height') ) { + $height = $this->scaleSVGUnit( $this->reader->getAttribute('height'), $defaultHeight ); + } + + if( !isset( $width ) && !isset( $height ) ) { + $width = $defaultWidth; + $height = $width / $aspect; + } elseif( isset( $width ) && !isset( $height ) ) { + $height = $width / $aspect; + } elseif( isset( $height ) && !isset( $width ) ) { + $width = $height * $aspect; + } + + if( $width > 0 && $height > 0 ) { + $this->metadata['width'] = intval( round( $width ) ); + $this->metadata['height'] = intval( round( $height ) ); + } + } + + /** + * Return a rounded pixel equivalent for a labeled CSS/SVG length. + * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers + * + * @param $length String: CSS/SVG length. + * @param $viewportSize: Float optional scale for percentage units... + * @return float: length in pixels + */ + static function scaleSVGUnit( $length, $viewportSize=512 ) { + static $unitLength = array( + 'px' => 1.0, + 'pt' => 1.25, + 'pc' => 15.0, + 'mm' => 3.543307, + 'cm' => 35.43307, + 'in' => 90.0, + 'em' => 16.0, // fake it? + 'ex' => 12.0, // fake it? + '' => 1.0, // "User units" pixels by default + ); + $matches = array(); + if( preg_match( '/^\s*(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/', $length, $matches ) ) { + $length = floatval( $matches[1] ); + $unit = $matches[2]; + if( $unit == '%' ) { + return $length * 0.01 * $viewportSize; + } else { + return $length * $unitLength[$unit]; + } + } else { + // Assume pixels + return floatval( $length ); + } + } +} diff --git a/includes/media/Tiff.php b/includes/media/Tiff.php index 9d3fbb78..8773201f 100644 --- a/includes/media/Tiff.php +++ b/includes/media/Tiff.php @@ -1,10 +1,14 @@ <?php /** + * Handler for Tiff images. + * * @file * @ingroup Media */ /** + * Handler for Tiff images. + * * @ingroup Media */ class TiffHandler extends BitmapHandler { @@ -26,7 +30,7 @@ class TiffHandler extends BitmapHandler { return true; } - function getThumbType( $ext, $mime ) { + function getThumbType( $ext, $mime, $params = null ) { global $wgTiffThumbnailType; return $wgTiffThumbnailType; } |