summaryrefslogtreecommitdiff
path: root/includes/media
diff options
context:
space:
mode:
Diffstat (limited to 'includes/media')
-rw-r--r--includes/media/Bitmap.php121
-rw-r--r--includes/media/DjVu.php48
-rw-r--r--includes/media/GIF.php72
-rw-r--r--includes/media/GIFMetadataExtractor.php175
-rw-r--r--includes/media/Generic.php44
-rw-r--r--includes/media/SVG.php2
6 files changed, 426 insertions, 36 deletions
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php
index c2f2458e..870e2126 100644
--- a/includes/media/Bitmap.php
+++ b/includes/media/Bitmap.php
@@ -22,7 +22,7 @@ class BitmapHandler extends ImageHandler {
# JPEG has the handy property of allowing thumbnailing without full decompression, so we make
# an exception for it.
if ( $mimeType !== 'image/jpeg' &&
- $srcWidth * $srcHeight > $wgMaxImageArea )
+ $this->getImageArea( $image, $srcWidth, $srcHeight ) > $wgMaxImageArea )
{
return false;
}
@@ -39,6 +39,13 @@ class BitmapHandler extends ImageHandler {
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 ) {
+ return $width * $height;
+ }
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
global $wgUseImageMagick, $wgImageMagickConvertCommand, $wgImageMagickTempDir;
@@ -53,6 +60,7 @@ class BitmapHandler extends ImageHandler {
$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();
@@ -103,7 +111,7 @@ class BitmapHandler extends ImageHandler {
$quality = '';
$sharpen = '';
- $frame = '';
+ $scene = false;
$animation = '';
if ( $mimeType == 'image/jpeg' ) {
$quality = "-quality 80"; // 80%
@@ -117,7 +125,7 @@ class BitmapHandler extends ImageHandler {
if( $srcWidth * $srcHeight > $wgMaxAnimatedGifArea ) {
// Extract initial frame only; we're so big it'll
// be a total drag. :P
- $frame = '[0]';
+ $scene = 0;
} else {
// Coalesce is needed to scale animated GIFs properly (bug 1017).
$animation = ' -coalesce ';
@@ -139,17 +147,19 @@ class BitmapHandler extends ImageHandler {
$cmd =
$tempEnv .
- wfEscapeShellArg($wgImageMagickConvertCommand) .
+ wfEscapeShellArg( $wgImageMagickConvertCommand ) .
" {$quality} -background white -size {$physicalWidth} ".
- wfEscapeShellArg($srcPath . $frame) .
+ 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($dstPath) . " 2>&1";
- wfDebug( __METHOD__.": running ImageMagick: $cmd\n");
+ wfEscapeShellArg( $this->escapeMagickOutput( $dstPath ) ) . " 2>&1";
+ wfDebug( __METHOD__.": running ImageMagick: $cmd\n" );
wfProfileIn( 'convert' );
$err = wfShellExec( $cmd, $retval );
wfProfileOut( 'convert' );
@@ -181,14 +191,23 @@ class BitmapHandler extends ImageHandler {
if( !isset( $typemap[$mimeType] ) ) {
$err = 'Image type not supported';
wfDebug( "$err\n" );
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
+ $errMsg = wfMsg ( 'thumbnail_image-type' );
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg );
}
list( $loader, $colorStyle, $saveType ) = $typemap[$mimeType];
if( !function_exists( $loader ) ) {
$err = "Incomplete GD library configuration: missing function $loader";
wfDebug( "$err\n" );
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
+ $errMsg = wfMsg ( 'thumbnail_gd-library', $loader );
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg );
+ }
+
+ 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 );
}
$src_image = call_user_func( $loader, $srcPath );
@@ -231,6 +250,90 @@ class BitmapHandler extends ImageHandler {
}
}
+ /**
+ * Escape a string for ImageMagick's property input (e.g. -set -comment)
+ * See InterpretImageProperties() in magick/property.c
+ */
+ function escapeMagickProperty( $s ) {
+ // Double the backslashes
+ $s = str_replace( '\\', '\\\\', $s );
+ // Double the percents
+ $s = str_replace( '%', '%%', $s );
+ // Escape initial - or @
+ if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
+ $s = '\\' . $s;
+ }
+ return $s;
+ }
+
+ /**
+ * 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.
+ *
+ * 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
+ * in a directory, so we're better off escaping and waiting for the bugfix
+ * to filter down to users.
+ *
+ * @param $path string The file path
+ * @param $scene string The scene specification, or false if there is none
+ */
+ function escapeMagickInput( $path, $scene = false ) {
+ # 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' );
+ }
+
+ # Escape glob chars
+ $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
+
+ return $this->escapeMagickPath( $path, $scene );
+ }
+
+ /**
+ * Escape a string for ImageMagick's output filename. See
+ * InterpretImageFilename() in magick/image.c.
+ */
+ function escapeMagickOutput( $path, $scene = false ) {
+ $path = str_replace( '%', '%%', $path );
+ return $this->escapeMagickPath( $path, $scene );
+ }
+
+ /**
+ * Armour a string against ImageMagick's GetPathComponent(). This is a
+ * helper function for escapeMagickInput() and escapeMagickOutput().
+ *
+ * @param $path string The file path
+ * @param $scene string The scene specification, or false if there is none
+ */
+ protected function escapeMagickPath( $path, $scene = false ) {
+ # Die on format specifiers (other than drive letters). The regex is
+ # meant to match all the formats you get from "convert -list format"
+ if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
+ if ( wfIsWindows() && is_dir( $m[0] ) ) {
+ // OK, it's a drive letter
+ // ImageMagick has a similar exception, see IsMagickConflict()
+ } else {
+ throw new MWException( __METHOD__.': unexpected colon character in path name' );
+ }
+ }
+
+ # If there are square brackets, add a do-nothing scene specification
+ # to force a literal interpretation
+ if ( $scene === false ) {
+ if ( strpos( $path, '[' ) !== false ) {
+ $path .= '[0--1]';
+ }
+ } else {
+ $path .= "[$scene]";
+ }
+ return $path;
+ }
+
static function imageJpegWrapper( $dst_image, $thumbPath ) {
imageinterlace( $dst_image );
imagejpeg( $dst_image, $thumbPath, 95 );
diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php
index f0bbcc51..38c16c21 100644
--- a/includes/media/DjVu.php
+++ b/includes/media/DjVu.php
@@ -18,8 +18,8 @@ class DjVuHandler extends ImageHandler {
}
}
- function mustRender() { return true; }
- function isMultiPage() { return true; }
+ function mustRender( $file ) { return true; }
+ function isMultiPage( $file ) { return true; }
function getParamMap() {
return array(
@@ -135,7 +135,7 @@ class DjVuHandler extends ImageHandler {
/**
* Cache a document tree for the DjVu XML metadata
*/
- function getMetaTree( $image ) {
+ function getMetaTree( $image , $gettext = false ) {
if ( isset( $image->dejaMetaTree ) ) {
return $image->dejaMetaTree;
}
@@ -149,15 +149,32 @@ class DjVuHandler extends ImageHandler {
wfSuppressWarnings();
try {
- $image->dejaMetaTree = new SimpleXMLElement( $metadata );
- } catch( Exception $e ) {
- wfDebug( "Bogus multipage XML metadata on '$image->name'\n" );
// Set to false rather than null to avoid further attempts
$image->dejaMetaTree = false;
+ $image->djvuTextTree = false;
+ $tree = new SimpleXMLElement( $metadata );
+ if( $tree->getName() == 'mw-djvu' ) {
+ foreach($tree->children() as $b){
+ if( $b->getName() == 'DjVuTxt' ) {
+ $image->djvuTextTree = $b;
+ }
+ else if ( $b->getName() == 'DjVuXML' ) {
+ $image->dejaMetaTree = $b;
+ }
+ }
+ } else {
+ $image->dejaMetaTree = $tree;
+ }
+ } catch( Exception $e ) {
+ wfDebug( "Bogus multipage XML metadata on '$image->name'\n" );
}
wfRestoreWarnings();
wfProfileOut( __METHOD__ );
- return $image->dejaMetaTree;
+ if( $gettext ) {
+ return $image->djvuTextTree;
+ } else {
+ return $image->dejaMetaTree;
+ }
}
function getImageSize( $image, $path ) {
@@ -211,4 +228,21 @@ class DjVuHandler extends ImageHandler {
return false;
}
}
+
+ function getPageText( $image, $page ){
+ $tree = $this->getMetaTree( $image, true );
+ if ( !$tree ) {
+ return false;
+ }
+
+ $o = $tree->BODY[0]->PAGE[$page-1];
+ if ( $o ) {
+ $txt = $o['value'];
+ return $txt;
+ } else {
+ return false;
+ }
+
+ }
+
}
diff --git a/includes/media/GIF.php b/includes/media/GIF.php
new file mode 100644
index 00000000..dbe5f813
--- /dev/null
+++ b/includes/media/GIF.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for GIF images.
+ *
+ * @ingroup Media
+ */
+class GIFHandler extends BitmapHandler {
+
+ function getMetadata( $image, $filename ) {
+ if ( !isset($image->parsedGIFMetadata) ) {
+ try {
+ $image->parsedGIFMetadata = GIFMetadataExtractor::getMetadata( $filename );
+ } catch( Exception $e ) {
+ // Broken file?
+ wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+ return '0';
+ }
+ }
+
+ return serialize($image->parsedGIFMetadata);
+
+ }
+
+ function formatMetadata( $image ) {
+ return false;
+ }
+
+ function getImageArea( $image, $width, $height ) {
+ $ser = $image->getMetadata();
+ if ($ser) {
+ $metadata = unserialize($ser);
+ return $width * $height * $metadata['frameCount'];
+ } else {
+ return $width * $height;
+ }
+ }
+
+ function getMetadataType( $image ) {
+ return 'parsed-gif';
+ }
+
+ function getLongDesc( $image ) {
+ global $wgUser, $wgLang;
+ $sk = $wgUser->getSkin();
+
+ $metadata = @unserialize($image->getMetadata());
+
+ if (!$metadata) return parent::getLongDesc( $image );
+
+ $info = array();
+ $info[] = $image->getMimeType();
+ $info[] = $sk->formatSize( $image->getSize() );
+
+ if ($metadata['looped'])
+ $info[] = wfMsgExt( 'file-info-gif-looped', 'parseinline' );
+
+ if ($metadata['frameCount'] > 1)
+ $info[] = wfMsgExt( 'file-info-gif-frames', 'parseinline', $metadata['frameCount'] );
+
+ if ($metadata['duration'])
+ $info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
+
+ $infoString = $wgLang->commaList( $info );
+
+ return "($infoString)";
+ }
+}
diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php
new file mode 100644
index 00000000..fac9012b
--- /dev/null
+++ b/includes/media/GIFMetadataExtractor.php
@@ -0,0 +1,175 @@
+<?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.
+ */
+
+class GIFMetadataExtractor {
+ static $gif_frame_sep;
+ static $gif_extension_sep;
+ static $gif_term;
+
+ static function getMetadata( $filename ) {
+ self::$gif_frame_sep = pack( "C", ord("," ) );
+ self::$gif_extension_sep = pack( "C", ord("!" ) );
+ self::$gif_term = pack( "C", ord(";" ) );
+
+ $frameCount = 0;
+ $duration = 0.0;
+ $isLooped = false;
+
+ if (!$filename)
+ throw new Exception( "No file name specified" );
+ elseif ( !file_exists($filename) || is_dir($filename) )
+ throw new Exception( "File $filename does not exist" );
+
+ $fh = fopen( $filename, 'r' );
+
+ if (!$fh)
+ throw new Exception( "Unable to open file $filename" );
+
+ // Check for the GIF header
+ $buf = fread( $fh, 6 );
+ if ( !($buf == 'GIF87a' || $buf == 'GIF89a') ) {
+ throw new Exception( "Not a valid GIF file; header: $buf" );
+ }
+
+ // Skip over width and height.
+ fread( $fh, 4 );
+
+ // Read BPP
+ $buf = fread( $fh, 1 );
+ $bpp = self::decodeBPP( $buf );
+
+ // Skip over background and aspect ratio
+ fread( $fh, 2 );
+
+ // Skip over the GCT
+ self::readGCT( $fh, $bpp );
+
+ while( !feof( $fh ) ) {
+ $buf = fread( $fh, 1 );
+
+ if ($buf == self::$gif_frame_sep) {
+ // Found a frame
+ $frameCount++;
+
+ ## Skip bounding box
+ fread( $fh, 8 );
+
+ ## Read BPP
+ $buf = fread( $fh, 1 );
+ $bpp = self::decodeBPP( $buf );
+
+ ## Read GCT
+ self::readGCT( $fh, $bpp );
+ fread( $fh, 1 );
+ self::skipBlock( $fh );
+ } elseif ( $buf == self::$gif_extension_sep ) {
+ $buf = fread( $fh, 1 );
+ $extension_code = unpack( 'C', $buf );
+ $extension_code = $extension_code[1];
+
+ if ($extension_code == 0xF9) {
+ // Graphics Control Extension.
+ fread( $fh, 1 ); // Block size
+
+ fread( $fh, 1 ); // Transparency, disposal method, user input
+
+ $buf = fread( $fh, 2 ); // Delay, in hundredths of seconds.
+ $delay = unpack( 'v', $buf );
+ $delay = $delay[1];
+ $duration += $delay * 0.01;
+
+ fread( $fh, 1 ); // Transparent colour index
+
+ $term = fread( $fh, 1 ); // Should be a terminator
+ $term = unpack( 'C', $term );
+ $term = $term[1];
+ if ($term != 0 )
+ throw new Exception( "Malformed Graphics Control Extension block" );
+ } elseif ($extension_code == 0xFF) {
+ // Application extension (Netscape info about the animated gif)
+ $blockLength = fread( $fh, 1 );
+ $blockLength = unpack( 'C', $blockLength );
+ $blockLength = $blockLength[1];
+ $data = fread( $fh, $blockLength );
+
+ // NETSCAPE2.0 (application name)
+ if ($blockLength != 11 || $data != 'NETSCAPE2.0') {
+ fseek( $fh, -($blockLength + 1), SEEK_CUR );
+ self::skipBlock( $fh );
+ continue;
+ }
+
+ $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
+
+ if ($data != "\x03\x01") {
+ throw new Exception( "Expected \x03\x01, got $data" );
+ }
+
+ // Unsigned little-endian integer, loop count or zero for "forever"
+ $loopData = fread( $fh, 2 );
+ $loopData = unpack( 'v', $loopData );
+ $loopCount = $loopData[1];
+
+ if ($loopCount != 1) {
+ $isLooped = true;
+ }
+
+ // Read out terminator byte
+ fread( $fh, 1 );
+ } else {
+ self::skipBlock( $fh );
+ }
+ } elseif ( $buf == self::$gif_term ) {
+ break;
+ } else {
+ $byte = unpack( 'C', $buf );
+ $byte = $byte[1];
+ throw new Exception( "At position: ".ftell($fh). ", Unknown byte ".$byte );
+ }
+ }
+
+ return array(
+ 'frameCount' => $frameCount,
+ 'looped' => $isLooped,
+ 'duration' => $duration
+ );
+
+ }
+
+ static function readGCT( $fh, $bpp ) {
+ if ($bpp > 0) {
+ for( $i=1; $i<=pow(2,$bpp); ++$i ) {
+ fread( $fh, 3 );
+ }
+ }
+ }
+
+ static function decodeBPP( $data ) {
+ $buf = unpack( 'C', $data );
+ $buf = $buf[1];
+ $bpp = ( $buf & 7 ) + 1;
+ $buf >>= 7;
+
+ $have_map = $buf & 1;
+
+ return $have_map ? $bpp : 0;
+ }
+
+ static function skipBlock( $fh ) {
+ while ( !feof( $fh ) ) {
+ $buf = fread( $fh, 1 );
+ $block_len = unpack( 'C', $buf );
+ $block_len = $block_len[1];
+ if ($block_len == 0)
+ return;
+ fread( $fh, $block_len );
+ }
+ }
+
+}
diff --git a/includes/media/Generic.php b/includes/media/Generic.php
index a9c681e1..8a4d7054 100644
--- a/includes/media/Generic.php
+++ b/includes/media/Generic.php
@@ -71,18 +71,18 @@ abstract class MediaHandler {
* Get an image size array like that returned by getimagesize(), or false if it
* can't be determined.
*
- * @param Image $image The image object, or false if there isn't one
- * @param string $fileName The filename
- * @return array
+ * @param $image File: the image object, or false if there isn't one
+ * @param $fileName String: the filename
+ * @return Array
*/
abstract function getImageSize( $image, $path );
/**
* Get handler-specific metadata which will be saved in the img_metadata field.
*
- * @param Image $image The image object, or false if there isn't one
- * @param string $fileName The filename
- * @return string
+ * @param $image File: the image object, or false if there isn't one
+ * @param $path String: the filename
+ * @return String
*/
function getMetadata( $image, $path ) { return ''; }
@@ -114,10 +114,10 @@ abstract class MediaHandler {
* Get a MediaTransformOutput object representing the transformed output. Does not
* actually do the transform.
*
- * @param Image $image The image object
- * @param string $dstPath Filesystem destination path
- * @param string $dstUrl Destination URL to use in output HTML
- * @param array $params Arbitrary set of parameters validated by $this->validateParam()
+ * @param $image File: the image object
+ * @param $dstPath String: filesystem destination path
+ * @param $dstUrl String: Destination URL to use in output HTML
+ * @param $params Array: Arbitrary set of parameters validated by $this->validateParam()
*/
function getTransform( $image, $dstPath, $dstUrl, $params ) {
return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER );
@@ -127,11 +127,11 @@ abstract class MediaHandler {
* Get a MediaTransformOutput object representing the transformed output. Does the
* transform unless $flags contains self::TRANSFORM_LATER.
*
- * @param Image $image The image object
- * @param string $dstPath Filesystem destination path
- * @param string $dstUrl Destination URL to use in output HTML
- * @param array $params Arbitrary set of parameters validated by $this->validateParam()
- * @param integer $flags A bitfield, may contain self::TRANSFORM_LATER
+ * @param $image File: the image object
+ * @param $dstPath String: filesystem destination path
+ * @param $dstUrl String: destination URL to use in output HTML
+ * @param $params Array: arbitrary set of parameters validated by $this->validateParam()
+ * @param $flags Integer: a bitfield, may contain self::TRANSFORM_LATER
*/
abstract function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 );
@@ -180,6 +180,14 @@ abstract class MediaHandler {
}
/**
+ * Generic getter for text layer.
+ * Currently overloaded by PDF and DjVu handlers
+ */
+ function getPageText( $image, $page ) {
+ return false;
+ }
+
+ /**
* Get an array structure that looks like this:
*
* array(
@@ -210,7 +218,7 @@ abstract class MediaHandler {
}
/**
- * @fixme document this!
+ * @todo Fixme: document this!
* 'value' thingy goes into a wikitext table; it used to be escaped but
* that was incompatible with previous practice of customized display
* with wikitext formatting via messages such as 'exif-model-value'.
@@ -376,8 +384,8 @@ abstract class ImageHandler extends MediaHandler {
/**
* Validate thumbnail parameters and fill in the correct height
*
- * @param integer &$width Specified width (input/output)
- * @param integer &$height Height (output only)
+ * @param $width Integer: specified width (input/output)
+ * @param $height Integer: height (output only)
* @return false to indicate that an error should be returned to the user.
*/
function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) {
diff --git a/includes/media/SVG.php b/includes/media/SVG.php
index f0519e89..4cc66fb1 100644
--- a/includes/media/SVG.php
+++ b/includes/media/SVG.php
@@ -40,8 +40,6 @@ class SvgHandler extends ImageHandler {
}
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
-
if ( !$this->normaliseParams( $image, $params ) ) {
return new TransformParameterError( $params );
}