2007-04-20 12:31:36 +00:00
|
|
|
<?php
|
WARNING: HUGE COMMIT
Doxygen documentation update:
* Changed alls @addtogroup to @ingroup. @addtogroup adds the comment to the group description, but doesn't add the file, class, function, ... to the group like @ingroup does. See for example http://svn.wikimedia.org/doc/group__SpecialPage.html where it's impossible to see related files, classes, ... that should belong to that group.
* Added @file to file description, it seems that it should be explicitely decalred for file descriptions, otherwise doxygen will think that the comment document the first class, variabled, function, ... that is in that file.
* Removed some empty comments
* Removed some ?>
Added following groups:
* ExternalStorage
* JobQueue
* MaintenanceLanguage
One more thing: there are still a lot of warnings when generating the doc.
2008-05-20 17:13:28 +00:00
|
|
|
/**
|
2010-08-15 17:27:41 +00:00
|
|
|
* Generic handler for bitmap images
|
|
|
|
|
*
|
WARNING: HUGE COMMIT
Doxygen documentation update:
* Changed alls @addtogroup to @ingroup. @addtogroup adds the comment to the group description, but doesn't add the file, class, function, ... to the group like @ingroup does. See for example http://svn.wikimedia.org/doc/group__SpecialPage.html where it's impossible to see related files, classes, ... that should belong to that group.
* Added @file to file description, it seems that it should be explicitely decalred for file descriptions, otherwise doxygen will think that the comment document the first class, variabled, function, ... that is in that file.
* Removed some empty comments
* Removed some ?>
Added following groups:
* ExternalStorage
* JobQueue
* MaintenanceLanguage
One more thing: there are still a lot of warnings when generating the doc.
2008-05-20 17:13:28 +00:00
|
|
|
* @file
|
|
|
|
|
* @ingroup Media
|
|
|
|
|
*/
|
2007-04-20 12:31:36 +00:00
|
|
|
|
2007-04-24 06:53:31 +00:00
|
|
|
/**
|
2010-08-15 17:27:41 +00:00
|
|
|
* Generic handler for bitmap images
|
|
|
|
|
*
|
WARNING: HUGE COMMIT
Doxygen documentation update:
* Changed alls @addtogroup to @ingroup. @addtogroup adds the comment to the group description, but doesn't add the file, class, function, ... to the group like @ingroup does. See for example http://svn.wikimedia.org/doc/group__SpecialPage.html where it's impossible to see related files, classes, ... that should belong to that group.
* Added @file to file description, it seems that it should be explicitely decalred for file descriptions, otherwise doxygen will think that the comment document the first class, variabled, function, ... that is in that file.
* Removed some empty comments
* Removed some ?>
Added following groups:
* ExternalStorage
* JobQueue
* MaintenanceLanguage
One more thing: there are still a lot of warnings when generating the doc.
2008-05-20 17:13:28 +00:00
|
|
|
* @ingroup Media
|
2007-04-24 06:53:31 +00:00
|
|
|
*/
|
2007-04-20 12:31:36 +00:00
|
|
|
class BitmapHandler extends ImageHandler {
|
2009-08-19 02:07:00 +00:00
|
|
|
function normaliseParams( $image, &$params ) {
|
2007-04-20 12:31:36 +00:00
|
|
|
global $wgMaxImageArea;
|
|
|
|
|
if ( !parent::normaliseParams( $image, $params ) ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$mimeType = $image->getMimeType();
|
|
|
|
|
$srcWidth = $image->getWidth( $params['page'] );
|
|
|
|
|
$srcHeight = $image->getHeight( $params['page'] );
|
|
|
|
|
|
|
|
|
|
# Don't make an image bigger than the source
|
|
|
|
|
$params['physicalWidth'] = $params['width'];
|
|
|
|
|
$params['physicalHeight'] = $params['height'];
|
2008-04-14 07:45:50 +00:00
|
|
|
|
2007-04-20 12:31:36 +00:00
|
|
|
if ( $params['physicalWidth'] >= $srcWidth ) {
|
|
|
|
|
$params['physicalWidth'] = $srcWidth;
|
|
|
|
|
$params['physicalHeight'] = $srcHeight;
|
2010-04-07 05:17:19 +00:00
|
|
|
# Skip scaling limit checks if no scaling is required
|
2010-10-30 19:11:30 +00:00
|
|
|
if ( !$image->mustRender() )
|
2010-04-07 05:17:19 +00:00
|
|
|
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.
|
2010-10-30 19:04:26 +00:00
|
|
|
# FIXME: This actually only applies to ImageMagick
|
2010-04-07 05:17:19 +00:00
|
|
|
if ( $mimeType !== 'image/jpeg' &&
|
2010-04-07 05:41:44 +00:00
|
|
|
$srcWidth * $srcHeight > $wgMaxImageArea )
|
2010-04-07 05:17:19 +00:00
|
|
|
{
|
|
|
|
|
return false;
|
2007-04-20 12:31:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2010-10-30 19:11:30 +00:00
|
|
|
|
|
|
|
|
|
2009-08-19 02:07:00 +00:00
|
|
|
// 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 ) {
|
2009-08-03 15:01:51 +00:00
|
|
|
return $width * $height;
|
|
|
|
|
}
|
2008-04-14 07:45:50 +00:00
|
|
|
|
2009-08-19 02:07:00 +00:00
|
|
|
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
|
2010-10-30 19:04:26 +00:00
|
|
|
global $wgUseImageMagick;
|
2009-03-25 06:00:36 +00:00
|
|
|
global $wgCustomConvertCommand, $wgUseImageResize;
|
2007-04-20 12:31:36 +00:00
|
|
|
|
|
|
|
|
if ( !$this->normaliseParams( $image, $params ) ) {
|
|
|
|
|
return new TransformParameterError( $params );
|
|
|
|
|
}
|
2010-10-30 19:04:26 +00:00
|
|
|
# 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'],
|
2010-10-30 20:02:53 +00:00
|
|
|
'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
|
2010-10-30 19:04:26 +00:00
|
|
|
# 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
|
2010-10-30 19:11:30 +00:00
|
|
|
'comment' => isset( $params['descriptionUrl'] ) ?
|
2010-10-30 19:04:26 +00:00
|
|
|
"File source: {$params['descriptionUrl']}" : '',
|
|
|
|
|
# Properties of the original image
|
|
|
|
|
'srcWidth' => $image->getWidth(),
|
|
|
|
|
'srcHeight' => $image->getHeight(),
|
|
|
|
|
'mimeType' => $image->getMimeType(),
|
|
|
|
|
'srcPath' => $image->getPath(),
|
|
|
|
|
'dstPath' => $dstPath,
|
|
|
|
|
);
|
|
|
|
|
|
2010-10-30 20:02:53 +00:00
|
|
|
wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath\n" );
|
2007-04-20 12:31:36 +00:00
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
if ( !$image->mustRender() &&
|
2010-10-30 19:04:26 +00:00
|
|
|
$scalerParams['physicalWidth'] == $scalerParams['srcWidth']
|
|
|
|
|
&& $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) {
|
2010-10-30 19:11:30 +00:00
|
|
|
|
2007-04-20 12:31:36 +00:00
|
|
|
# normaliseParams (or the user) wants us to return the unscaled image
|
2010-10-30 19:04:26 +00:00
|
|
|
wfDebug( __METHOD__ . ": returning unscaled image\n" );
|
|
|
|
|
return $this->getClientScalingThumbnailImage( $image, $scalerParams );
|
2007-04-20 12:31:36 +00:00
|
|
|
}
|
|
|
|
|
|
2010-10-30 19:04:26 +00:00
|
|
|
# Determine scaler type
|
2007-05-30 21:02:32 +00:00
|
|
|
if ( !$dstPath ) {
|
2010-10-30 19:04:26 +00:00
|
|
|
# No output path available, client side scaling only
|
2007-05-30 21:02:32 +00:00
|
|
|
$scaler = 'client';
|
2010-10-30 19:11:30 +00:00
|
|
|
} elseif ( !$wgUseImageResize ) {
|
2009-03-25 06:00:36 +00:00
|
|
|
$scaler = 'client';
|
2007-05-30 21:02:32 +00:00
|
|
|
} elseif ( $wgUseImageMagick ) {
|
2007-04-20 12:31:36 +00:00
|
|
|
$scaler = 'im';
|
|
|
|
|
} elseif ( $wgCustomConvertCommand ) {
|
|
|
|
|
$scaler = 'custom';
|
|
|
|
|
} elseif ( function_exists( 'imagecreatetruecolor' ) ) {
|
|
|
|
|
$scaler = 'gd';
|
|
|
|
|
} else {
|
|
|
|
|
$scaler = 'client';
|
|
|
|
|
}
|
2010-10-30 19:04:26 +00:00
|
|
|
wfDebug( __METHOD__ . ": scaler $scaler\n" );
|
2007-04-20 12:31:36 +00:00
|
|
|
|
|
|
|
|
if ( $scaler == 'client' ) {
|
|
|
|
|
# Client-side image scaling, use the source URL
|
|
|
|
|
# Using the destination URL in a TRANSFORM_LATER request would be incorrect
|
2010-10-30 19:04:26 +00:00
|
|
|
return $this->getClientScalingThumbnailImage( $image, $scalerParams );
|
2007-04-20 12:31:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $flags & self::TRANSFORM_LATER ) {
|
2010-10-30 19:04:26 +00:00
|
|
|
wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
|
2010-10-30 19:11:30 +00:00
|
|
|
return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
|
2010-10-30 19:04:26 +00:00
|
|
|
$scalerParams['clientHeight'], $dstPath );
|
2007-04-20 12:31:36 +00:00
|
|
|
}
|
|
|
|
|
|
2010-10-30 19:04:26 +00:00
|
|
|
# Try to make a target path for the thumbnail
|
2007-04-20 12:31:36 +00:00
|
|
|
if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
|
2010-10-30 19:11:30 +00:00
|
|
|
wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" );
|
2010-10-30 19:04:26 +00:00
|
|
|
return $this->getClientScalingThumbnailImage( $image, $scalerParams );
|
2007-04-20 12:31:36 +00:00
|
|
|
}
|
|
|
|
|
|
2010-10-30 19:04:26 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2010-10-30 19:11:30 +00:00
|
|
|
|
2010-10-30 19:04:26 +00:00
|
|
|
# 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
|
2010-10-30 19:11:30 +00:00
|
|
|
return new MediaTransformError( 'thumbnail_error',
|
2010-10-30 19:04:26 +00:00
|
|
|
$scalerParams['clientWidth'], $scalerParams['clientHeight'] );
|
|
|
|
|
} else {
|
2010-10-30 19:11:30 +00:00
|
|
|
return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
|
2010-10-30 19:04:26 +00:00
|
|
|
$scalerParams['clientHeight'], $dstPath );
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-10-30 19:11:30 +00:00
|
|
|
|
2010-10-30 19:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* Get a ThumbnailImage that respresents an image that will be scaled
|
|
|
|
|
* client side
|
2010-10-30 19:11:30 +00:00
|
|
|
*
|
2010-10-30 19:04:26 +00:00
|
|
|
* @param $image File File associated with this thumbnail
|
|
|
|
|
* @param $params array Array with scaler params
|
|
|
|
|
* @return ThumbnailImage
|
|
|
|
|
*/
|
|
|
|
|
protected function getClientScalingThumbnailImage( $image, $params ) {
|
2010-10-30 19:11:30 +00:00
|
|
|
return new ThumbnailImage( $image, $image->getURL(),
|
2010-10-30 19:04:26 +00:00
|
|
|
$params['clientWidth'], $params['clientHeight'], $params['srcPath'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Transform an image using ImageMagick
|
2010-10-30 19:11:30 +00:00
|
|
|
*
|
2010-10-30 19:04:26 +00:00
|
|
|
* @param $image File File associated with this thumbnail
|
|
|
|
|
* @param $params array Array with scaler params
|
2010-10-30 19:11:30 +00:00
|
|
|
*
|
2010-10-30 19:04:26 +00:00
|
|
|
* @return MediaTransformError Error object if error occured, false (=no error) otherwise
|
|
|
|
|
*/
|
|
|
|
|
protected function transformImageMagick( $image, $params ) {
|
2010-10-30 19:11:30 +00:00
|
|
|
# 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
|
2010-10-30 20:02:53 +00:00
|
|
|
$decoderHint = "-define jpeg:size={$params['physicalDimensions']}";
|
2010-10-30 19:11:30 +00:00
|
|
|
|
|
|
|
|
} 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';
|
2008-10-13 21:50:16 +00:00
|
|
|
}
|
2007-04-20 12:31:36 +00:00
|
|
|
}
|
2010-10-30 19:11:30 +00:00
|
|
|
}
|
2007-04-20 12:31:36 +00:00
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
// Use one thread only, to avoid deadlock bugs on OOM
|
|
|
|
|
$env = array( 'OMP_NUM_THREADS' => 1 );
|
|
|
|
|
if ( strval( $wgImageMagickTempDir ) !== '' ) {
|
|
|
|
|
$env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
|
|
|
|
|
}
|
2008-11-02 16:45:52 +00:00
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
$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.
|
2010-10-30 20:02:53 +00:00
|
|
|
" -thumbnail " . wfEscapeShellArg( "{$params['physicalDimensions']}!" ) .
|
2010-10-30 19:11:30 +00:00
|
|
|
// Add the source url as a comment to the thumb.
|
|
|
|
|
" -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 ( $retval !== 0 ) {
|
|
|
|
|
$this->logErrorForExternalProcess( $retval, $err, $cmd );
|
|
|
|
|
return $this->getMediaTransformError( $params, $err );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false; # No error
|
2010-10-30 19:04:26 +00:00
|
|
|
}
|
2010-10-30 19:11:30 +00:00
|
|
|
|
2010-10-30 19:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* Transform an image using a custom command
|
2010-10-30 19:11:30 +00:00
|
|
|
*
|
2010-10-30 19:04:26 +00:00
|
|
|
* @param $image File File associated with this thumbnail
|
|
|
|
|
* @param $params array Array with scaler params
|
2010-10-30 19:11:30 +00:00
|
|
|
*
|
2010-10-30 19:04:26 +00:00
|
|
|
* @return MediaTransformError Error object if error occured, false (=no error) otherwise
|
|
|
|
|
*/
|
|
|
|
|
protected function transformCustom( $image, $params ) {
|
2010-10-30 19:11:30 +00:00
|
|
|
# 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' );
|
|
|
|
|
|
|
|
|
|
if ( $retval !== 0 ) {
|
|
|
|
|
$this->logErrorForExternalProcess( $retval, $err, $cmd );
|
|
|
|
|
return $this->getMediaTransformError( $params, $err );
|
|
|
|
|
}
|
|
|
|
|
return false; # No error
|
2010-10-30 19:04:26 +00:00
|
|
|
}
|
2010-10-30 19:11:30 +00:00
|
|
|
|
2010-10-30 19:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* Log an error that occured in an external process
|
2010-10-30 19:11:30 +00:00
|
|
|
*
|
2010-10-30 19:04:26 +00:00
|
|
|
* @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 ) );
|
|
|
|
|
}
|
|
|
|
|
/**
|
2010-10-30 20:02:53 +00:00
|
|
|
* Get a MediaTransformError with error 'thumbnail_error'
|
|
|
|
|
*
|
|
|
|
|
* @param $params array Paramter array as passed to the transform* functions
|
|
|
|
|
* @param $errMsg string Error message
|
|
|
|
|
* @return MediaTransformError
|
2010-10-30 19:04:26 +00:00
|
|
|
*/
|
|
|
|
|
protected function getMediaTransformError( $params, $errMsg ) {
|
2010-10-30 19:11:30 +00:00
|
|
|
return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
|
2010-10-30 19:04:26 +00:00
|
|
|
$params['clientHeight'], $errMsg );
|
|
|
|
|
}
|
2010-10-30 19:11:30 +00:00
|
|
|
|
2010-10-30 19:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* Transform an image using the built in GD library
|
2010-10-30 19:11:30 +00:00
|
|
|
*
|
2010-10-30 19:04:26 +00:00
|
|
|
* @param $image File File associated with this thumbnail
|
|
|
|
|
* @param $params array Array with scaler params
|
2010-10-30 19:11:30 +00:00
|
|
|
*
|
2010-10-30 19:04:26 +00:00
|
|
|
* @return MediaTransformError Error object if error occured, false (=no error) otherwise
|
|
|
|
|
*/
|
|
|
|
|
protected function transformGd( $image, $params ) {
|
2010-10-30 19:11:30 +00:00
|
|
|
# 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']];
|
2007-04-20 12:31:36 +00:00
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
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 );
|
|
|
|
|
}
|
2007-04-20 12:31:36 +00:00
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
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 );
|
|
|
|
|
}
|
2009-04-13 15:05:47 +00:00
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
$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 {
|
|
|
|
|
imagecopyresampled( $dst_image, $src_image,
|
|
|
|
|
0, 0, 0, 0,
|
|
|
|
|
$params['physicalWidth'], $params['physicalHeight'],
|
|
|
|
|
imagesx( $src_image ), imagesy( $src_image ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imagesavealpha( $dst_image, true );
|
2007-08-22 00:30:16 +00:00
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
call_user_func( $saveType, $dst_image, $params['dstPath'] );
|
|
|
|
|
imagedestroy( $dst_image );
|
|
|
|
|
imagedestroy( $src_image );
|
2008-04-14 07:45:50 +00:00
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
return false; # No error
|
2007-04-20 12:31:36 +00:00
|
|
|
}
|
|
|
|
|
|
2010-04-23 16:24:54 +00:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2010-10-30 19:11:30 +00:00
|
|
|
* Escape a string for ImageMagick's input filenames. See ExpandFilenames()
|
2010-04-23 16:24:54 +00:00
|
|
|
* and GetPathComponent() in magick/utility.c.
|
|
|
|
|
*
|
|
|
|
|
* This won't work with an initial ~ or @, so input files should be prefixed
|
2010-10-30 19:11:30 +00:00
|
|
|
* with the directory name.
|
2010-04-23 16:24:54 +00:00
|
|
|
*
|
|
|
|
|
* Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
|
2010-10-30 19:11:30 +00:00
|
|
|
* it's broken in a way that doesn't involve trying to convert every file
|
2010-04-23 16:24:54 +00:00
|
|
|
* 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 === '@' ) {
|
2010-10-30 19:11:30 +00:00
|
|
|
throw new MWException( __METHOD__ . ': cannot escape this path name' );
|
2010-04-23 16:24:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Escape glob chars
|
|
|
|
|
$path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
|
|
|
|
|
|
|
|
|
|
return $this->escapeMagickPath( $path, $scene );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2010-10-30 19:11:30 +00:00
|
|
|
* Escape a string for ImageMagick's output filename. See
|
2010-04-23 16:24:54 +00:00
|
|
|
* InterpretImageFilename() in magick/image.c.
|
|
|
|
|
*/
|
|
|
|
|
function escapeMagickOutput( $path, $scene = false ) {
|
|
|
|
|
$path = str_replace( '%', '%%', $path );
|
|
|
|
|
return $this->escapeMagickPath( $path, $scene );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2010-10-30 19:11:30 +00:00
|
|
|
* Armour a string against ImageMagick's GetPathComponent(). This is a
|
2010-04-23 16:24:54 +00:00
|
|
|
* 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 {
|
2010-10-30 19:11:30 +00:00
|
|
|
throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
|
2010-04-23 16:24:54 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2010-10-30 19:11:30 +00:00
|
|
|
# If there are square brackets, add a do-nothing scene specification
|
2010-04-23 16:24:54 +00:00
|
|
|
# to force a literal interpretation
|
|
|
|
|
if ( $scene === false ) {
|
|
|
|
|
if ( strpos( $path, '[' ) !== false ) {
|
|
|
|
|
$path .= '[0--1]';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$path .= "[$scene]";
|
|
|
|
|
}
|
|
|
|
|
return $path;
|
|
|
|
|
}
|
|
|
|
|
|
2010-08-20 13:22:02 +00:00
|
|
|
/**
|
|
|
|
|
* 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" );
|
2010-10-30 19:11:30 +00:00
|
|
|
if ( !$cache ) {
|
2010-08-20 13:22:02 +00:00
|
|
|
global $wgImageMagickConvertCommand;
|
|
|
|
|
$cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
|
2010-10-30 19:11:30 +00:00
|
|
|
wfDebug( __METHOD__ . ": Running convert -version\n" );
|
2010-09-02 22:15:20 +00:00
|
|
|
$retval = '';
|
2010-09-03 16:00:58 +00:00
|
|
|
$return = wfShellExec( $cmd, $retval );
|
2010-10-30 19:11:30 +00:00
|
|
|
$x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
|
|
|
|
|
if ( $x != 1 ) {
|
|
|
|
|
wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
|
2010-08-20 13:22:02 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
$wgMemc->set( "imagemagick-version", $matches[1], 3600 );
|
|
|
|
|
return $matches[1];
|
|
|
|
|
}
|
|
|
|
|
return $cache;
|
|
|
|
|
}
|
|
|
|
|
|
2009-08-19 02:07:00 +00:00
|
|
|
static function imageJpegWrapper( $dst_image, $thumbPath ) {
|
2007-04-20 12:31:36 +00:00
|
|
|
imageinterlace( $dst_image );
|
|
|
|
|
imagejpeg( $dst_image, $thumbPath, 95 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2009-08-19 02:07:00 +00:00
|
|
|
function getMetadata( $image, $filename ) {
|
2007-04-20 12:31:36 +00:00
|
|
|
global $wgShowEXIF;
|
2010-10-30 19:11:30 +00:00
|
|
|
if ( $wgShowEXIF && file_exists( $filename ) ) {
|
2007-04-20 12:31:36 +00:00
|
|
|
$exif = new Exif( $filename );
|
2007-04-20 21:35:27 +00:00
|
|
|
$data = $exif->getFilteredData();
|
|
|
|
|
if ( $data ) {
|
|
|
|
|
$data['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
|
|
|
|
|
return serialize( $data );
|
|
|
|
|
} else {
|
|
|
|
|
return '0';
|
|
|
|
|
}
|
2007-04-20 12:31:36 +00:00
|
|
|
} else {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2009-08-19 02:07:00 +00:00
|
|
|
function getMetadataType( $image ) {
|
2007-04-20 12:31:36 +00:00
|
|
|
return 'exif';
|
|
|
|
|
}
|
|
|
|
|
|
2009-08-19 02:07:00 +00:00
|
|
|
function isMetadataValid( $image, $metadata ) {
|
2007-04-20 12:31:36 +00:00
|
|
|
global $wgShowEXIF;
|
|
|
|
|
if ( !$wgShowEXIF ) {
|
|
|
|
|
# Metadata disabled and so an empty field is expected
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if ( $metadata === '0' ) {
|
|
|
|
|
# Special value indicating that there is no EXIF data in the file
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2010-06-09 10:15:19 +00:00
|
|
|
wfSuppressWarnings();
|
|
|
|
|
$exif = unserialize( $metadata );
|
|
|
|
|
wfRestoreWarnings();
|
2007-04-20 12:31:36 +00:00
|
|
|
if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) ||
|
|
|
|
|
$exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version() )
|
|
|
|
|
{
|
|
|
|
|
# Wrong version
|
2010-10-30 19:11:30 +00:00
|
|
|
wfDebug( __METHOD__ . ": wrong version\n" );
|
2007-04-20 12:31:36 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2007-07-28 01:15:35 +00:00
|
|
|
/**
|
|
|
|
|
* Get a list of EXIF metadata items which should be displayed when
|
|
|
|
|
* the metadata table is collapsed.
|
|
|
|
|
*
|
|
|
|
|
* @return array of strings
|
|
|
|
|
* @access private
|
|
|
|
|
*/
|
2009-08-19 02:07:00 +00:00
|
|
|
function visibleMetadataFields() {
|
2007-07-28 01:15:35 +00:00
|
|
|
$fields = array();
|
|
|
|
|
$lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
|
2010-10-30 19:11:30 +00:00
|
|
|
foreach ( $lines as $line ) {
|
2007-07-28 01:15:35 +00:00
|
|
|
$matches = array();
|
2010-10-30 19:11:30 +00:00
|
|
|
if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
|
2007-07-28 01:15:35 +00:00
|
|
|
$fields[] = $matches[1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$fields = array_map( 'strtolower', $fields );
|
|
|
|
|
return $fields;
|
|
|
|
|
}
|
|
|
|
|
|
2009-08-19 02:07:00 +00:00
|
|
|
function formatMetadata( $image ) {
|
2007-07-28 01:15:35 +00:00
|
|
|
$result = array(
|
|
|
|
|
'visible' => array(),
|
|
|
|
|
'collapsed' => array()
|
|
|
|
|
);
|
|
|
|
|
$metadata = $image->getMetadata();
|
|
|
|
|
if ( !$metadata ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
$exif = unserialize( $metadata );
|
|
|
|
|
if ( !$exif ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
|
|
|
|
|
$format = new FormatExif( $exif );
|
|
|
|
|
|
|
|
|
|
$formatted = $format->getFormattedData();
|
|
|
|
|
// Sort fields into visible and collapsed
|
|
|
|
|
$visibleFields = $this->visibleMetadataFields();
|
2009-06-06 15:48:06 +00:00
|
|
|
foreach ( $formatted as $name => $value ) {
|
|
|
|
|
$tag = strtolower( $name );
|
|
|
|
|
self::addMeta( $result,
|
|
|
|
|
in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
|
|
|
|
|
'exif',
|
|
|
|
|
$tag,
|
|
|
|
|
$value
|
|
|
|
|
);
|
2007-07-28 01:15:35 +00:00
|
|
|
}
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
2007-04-20 12:31:36 +00:00
|
|
|
}
|