summaryrefslogtreecommitdiff
path: root/includes/media
diff options
context:
space:
mode:
Diffstat (limited to 'includes/media')
-rw-r--r--includes/media/BMP.php4
-rw-r--r--includes/media/Bitmap.php533
-rw-r--r--includes/media/Bitmap_ClientOnly.php14
-rw-r--r--includes/media/DjVu.php7
-rw-r--r--includes/media/GIF.php43
-rw-r--r--includes/media/GIFMetadataExtractor.php21
-rw-r--r--includes/media/Generic.php51
-rw-r--r--includes/media/MediaTransformOutput.php255
-rw-r--r--includes/media/PNG.php82
-rw-r--r--includes/media/PNGMetadataExtractor.php104
-rw-r--r--includes/media/SVG.php121
-rw-r--r--includes/media/SVGMetadataExtractor.php313
-rw-r--r--includes/media/Tiff.php6
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;
}