Index: trunk/phase3/includes/media/Bitmap.php |
— | — | @@ -37,6 +37,7 @@ |
38 | 38 | # Don't thumbnail an image so big that it will fill hard drives and send servers into swap |
39 | 39 | # JPEG has the handy property of allowing thumbnailing without full decompression, so we make |
40 | 40 | # an exception for it. |
| 41 | + # FIXME: This actually only applies to ImageMagick |
41 | 42 | if ( $mimeType !== 'image/jpeg' && |
42 | 43 | $srcWidth * $srcHeight > $wgMaxImageArea ) |
43 | 44 | { |
— | — | @@ -54,34 +55,46 @@ |
55 | 56 | } |
56 | 57 | |
57 | 58 | function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { |
58 | | - global $wgUseImageMagick, $wgImageMagickConvertCommand, $wgImageMagickTempDir; |
| 59 | + global $wgUseImageMagick; |
59 | 60 | global $wgCustomConvertCommand, $wgUseImageResize; |
60 | | - global $wgSharpenParameter, $wgSharpenReductionThreshold; |
61 | | - global $wgMaxAnimatedGifArea; |
62 | 61 | |
63 | 62 | if ( !$this->normaliseParams( $image, $params ) ) { |
64 | 63 | return new TransformParameterError( $params ); |
65 | 64 | } |
66 | | - $physicalWidth = $params['physicalWidth']; |
67 | | - $physicalHeight = $params['physicalHeight']; |
68 | | - $clientWidth = $params['width']; |
69 | | - $clientHeight = $params['height']; |
70 | | - $comment = isset( $params['descriptionUrl'] ) ? "File source: ". $params['descriptionUrl'] : ''; |
71 | | - $srcWidth = $image->getWidth(); |
72 | | - $srcHeight = $image->getHeight(); |
73 | | - $mimeType = $image->getMimeType(); |
74 | | - $srcPath = $image->getPath(); |
75 | | - $retval = 0; |
76 | | - wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" ); |
| 65 | + # Create a parameter array to pass to the scaler |
| 66 | + $scalerParams = array( |
| 67 | + # The size to which the image will be resized |
| 68 | + 'physicalWidth' => $params['physicalWidth'], |
| 69 | + 'physicalHeight' => $params['physicalHeight'], |
| 70 | + 'physicalSize' => "{$params['physicalWidth']}x{$params['physicalHeight']}", |
| 71 | + # The size of the image on the page |
| 72 | + 'clientWidth' => $params['width'], |
| 73 | + 'clientHeight' => $params['height'], |
| 74 | + # Comment as will be added to the EXIF of the thumbnail |
| 75 | + 'comment' => isset( $params['descriptionUrl'] ) ? |
| 76 | + "File source: {$params['descriptionUrl']}" : '', |
| 77 | + # Properties of the original image |
| 78 | + 'srcWidth' => $image->getWidth(), |
| 79 | + 'srcHeight' => $image->getHeight(), |
| 80 | + 'mimeType' => $image->getMimeType(), |
| 81 | + 'srcPath' => $image->getPath(), |
| 82 | + 'dstPath' => $dstPath, |
| 83 | + ); |
77 | 84 | |
78 | | - if ( !$image->mustRender() && $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) { |
| 85 | + wfDebug( __METHOD__ . ": creating {$scalerParams['physicalSize']} thumbnail at $dstPath\n" ); |
| 86 | + |
| 87 | + if ( !$image->mustRender() && |
| 88 | + $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] |
| 89 | + && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) { |
| 90 | + |
79 | 91 | # normaliseParams (or the user) wants us to return the unscaled image |
80 | | - wfDebug( __METHOD__.": returning unscaled image\n" ); |
81 | | - return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); |
| 92 | + wfDebug( __METHOD__ . ": returning unscaled image\n" ); |
| 93 | + return $this->getClientScalingThumbnailImage( $image, $scalerParams ); |
82 | 94 | } |
83 | 95 | |
| 96 | + # Determine scaler type |
84 | 97 | if ( !$dstPath ) { |
85 | | - // No output path available, client side scaling only |
| 98 | + # No output path available, client side scaling only |
86 | 99 | $scaler = 'client'; |
87 | 100 | } elseif( !$wgUseImageResize ) { |
88 | 101 | $scaler = 'client'; |
— | — | @@ -94,26 +107,80 @@ |
95 | 108 | } else { |
96 | 109 | $scaler = 'client'; |
97 | 110 | } |
98 | | - wfDebug( __METHOD__.": scaler $scaler\n" ); |
| 111 | + wfDebug( __METHOD__ . ": scaler $scaler\n" ); |
99 | 112 | |
100 | 113 | if ( $scaler == 'client' ) { |
101 | 114 | # Client-side image scaling, use the source URL |
102 | 115 | # Using the destination URL in a TRANSFORM_LATER request would be incorrect |
103 | | - return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); |
| 116 | + return $this->getClientScalingThumbnailImage( $image, $scalerParams ); |
104 | 117 | } |
105 | 118 | |
106 | 119 | if ( $flags & self::TRANSFORM_LATER ) { |
107 | | - wfDebug( __METHOD__.": Transforming later per flags.\n" ); |
108 | | - return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); |
| 120 | + wfDebug( __METHOD__ . ": Transforming later per flags.\n" ); |
| 121 | + return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], |
| 122 | + $scalerParams['clientHeight'], $dstPath ); |
109 | 123 | } |
110 | 124 | |
| 125 | + # Try to make a target path for the thumbnail |
111 | 126 | if ( !wfMkdirParents( dirname( $dstPath ) ) ) { |
112 | 127 | wfDebug( __METHOD__.": Unable to create thumbnail destination directory, falling back to client scaling\n" ); |
113 | | - return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); |
| 128 | + return $this->getClientScalingThumbnailImage( $image, $scalerParams ); |
114 | 129 | } |
115 | 130 | |
116 | | - if ( $scaler == 'im' ) { |
| 131 | + switch ( $scaler ) { |
| 132 | + case 'im': |
| 133 | + $err = $this->transformImageMagick( $image, $scalerParams ); |
| 134 | + break; |
| 135 | + case 'custom': |
| 136 | + $err = $this->transformCustom( $image, $scalerParams ); |
| 137 | + break; |
| 138 | + case 'gd': |
| 139 | + default: |
| 140 | + $err = $this->transformGd( $image, $scalerParams ); |
| 141 | + break; |
| 142 | + } |
| 143 | + |
| 144 | + # Remove the file if a zero-byte thumbnail was created, or if there was an error |
| 145 | + $removed = $this->removeBadFile( $dstPath, (bool)$err ); |
| 146 | + if ( $err ) { |
| 147 | + # transform returned MediaTransforError |
| 148 | + return $err; |
| 149 | + } elseif ( $removed ) { |
| 150 | + # Thumbnail was zero-byte and had to be removed |
| 151 | + return new MediaTransformError( 'thumbnail_error', |
| 152 | + $scalerParams['clientWidth'], $scalerParams['clientHeight'] ); |
| 153 | + } else { |
| 154 | + return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], |
| 155 | + $scalerParams['clientHeight'], $dstPath ); |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + /** |
| 160 | + * Get a ThumbnailImage that respresents an image that will be scaled |
| 161 | + * client side |
| 162 | + * |
| 163 | + * @param $image File File associated with this thumbnail |
| 164 | + * @param $params array Array with scaler params |
| 165 | + * @return ThumbnailImage |
| 166 | + */ |
| 167 | + protected function getClientScalingThumbnailImage( $image, $params ) { |
| 168 | + return new ThumbnailImage( $image, $image->getURL(), |
| 169 | + $params['clientWidth'], $params['clientHeight'], $params['srcPath'] ); |
| 170 | + } |
| 171 | + |
| 172 | + /** |
| 173 | + * Transform an image using ImageMagick |
| 174 | + * |
| 175 | + * @param $image File File associated with this thumbnail |
| 176 | + * @param $params array Array with scaler params |
| 177 | + * |
| 178 | + * @return MediaTransformError Error object if error occured, false (=no error) otherwise |
| 179 | + */ |
| 180 | + protected function transformImageMagick( $image, $params ) { |
117 | 181 | # use ImageMagick |
| 182 | + global $wgSharpenReductionThreshold, $wgSharpenParameter, |
| 183 | + $wgMaxAnimatedGifArea, |
| 184 | + $wgImageMagickTempDir, $wgImageMagickConvertCommand; |
118 | 185 | |
119 | 186 | $quality = ''; |
120 | 187 | $sharpen = ''; |
— | — | @@ -121,21 +188,27 @@ |
122 | 189 | $animation_pre = ''; |
123 | 190 | $animation_post = ''; |
124 | 191 | $decoderHint = ''; |
125 | | - if ( $mimeType == 'image/jpeg' ) { |
| 192 | + if ( $params['mimeType'] == 'image/jpeg' ) { |
126 | 193 | $quality = "-quality 80"; // 80% |
127 | 194 | # Sharpening, see bug 6193 |
128 | | - if ( ( $physicalWidth + $physicalHeight ) / ( $srcWidth + $srcHeight ) < $wgSharpenReductionThreshold ) { |
| 195 | + if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) |
| 196 | + / ( $params['srcWidth'] + $params['srcHeight'] ) |
| 197 | + < $wgSharpenReductionThreshold ) { |
129 | 198 | $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter ); |
130 | 199 | } |
131 | 200 | // JPEG decoder hint to reduce memory, available since IM 6.5.6-2 |
132 | | - $decoderHint = "-define jpeg:size={$physicalWidth}x{$physicalHeight}"; |
133 | | - } elseif ( $mimeType == 'image/png' ) { |
| 201 | + $decoderHint = "-define jpeg:size={$params['physicalSize']}"; |
| 202 | + |
| 203 | + } elseif ( $params['mimeType'] == 'image/png' ) { |
134 | 204 | $quality = "-quality 95"; // zlib 9, adaptive filtering |
135 | | - } elseif( $mimeType == 'image/gif' ) { |
136 | | - if( $this->getImageArea( $image, $srcWidth, $srcHeight ) > $wgMaxAnimatedGifArea ) { |
| 205 | + |
| 206 | + } elseif ( $params['mimeType'] == 'image/gif' ) { |
| 207 | + if ( $this->getImageArea( $image, $params['srcWidth'], |
| 208 | + $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) { |
137 | 209 | // Extract initial frame only; we're so big it'll |
138 | 210 | // be a total drag. :P |
139 | 211 | $scene = 0; |
| 212 | + |
140 | 213 | } elseif( $this->isAnimatedImage( $image ) ) { |
141 | 214 | // Coalesce is needed to scale animated GIFs properly (bug 1017). |
142 | 215 | $animation_pre = '-coalesce'; |
— | — | @@ -159,35 +232,93 @@ |
160 | 233 | // in Internet Explorer/Windows instead of default black. |
161 | 234 | " {$quality} -background white". |
162 | 235 | " {$decoderHint} " . |
163 | | - wfEscapeShellArg( $this->escapeMagickInput( $srcPath, $scene ) ) . |
| 236 | + wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) . |
164 | 237 | " {$animation_pre}" . |
165 | 238 | // For the -thumbnail option a "!" is needed to force exact size, |
166 | 239 | // or ImageMagick may decide your ratio is wrong and slice off |
167 | 240 | // a pixel. |
168 | | - " -thumbnail " . wfEscapeShellArg( "{$physicalWidth}x{$physicalHeight}!" ) . |
| 241 | + " -thumbnail " . wfEscapeShellArg( "{$params['physicalSize']}!" ) . |
169 | 242 | // Add the source url as a comment to the thumb. |
170 | | - " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $comment ) ) . |
| 243 | + " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $params['comment'] ) ) . |
171 | 244 | " -depth 8 $sharpen" . |
172 | 245 | " {$animation_post} " . |
173 | | - wfEscapeShellArg( $this->escapeMagickOutput( $dstPath ) ) . " 2>&1"; |
| 246 | + wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) ) . " 2>&1"; |
174 | 247 | |
175 | 248 | wfDebug( __METHOD__.": running ImageMagick: $cmd\n" ); |
176 | 249 | wfProfileIn( 'convert' ); |
| 250 | + $retval = 0; |
177 | 251 | $err = wfShellExec( $cmd, $retval, $env ); |
178 | 252 | wfProfileOut( 'convert' ); |
179 | | - } elseif( $scaler == 'custom' ) { |
| 253 | + |
| 254 | + if ( $retval !== 0 ) { |
| 255 | + $this->logErrorForExternalProcess( $retval, $err, $cmd ); |
| 256 | + return $this->getMediaTransformError( $params, $err ); |
| 257 | + } |
| 258 | + |
| 259 | + return false; # No error |
| 260 | + } |
| 261 | + |
| 262 | + /** |
| 263 | + * Transform an image using a custom command |
| 264 | + * |
| 265 | + * @param $image File File associated with this thumbnail |
| 266 | + * @param $params array Array with scaler params |
| 267 | + * |
| 268 | + * @return MediaTransformError Error object if error occured, false (=no error) otherwise |
| 269 | + */ |
| 270 | + protected function transformCustom( $image, $params ) { |
180 | 271 | # Use a custom convert command |
| 272 | + global $wgCustomConvertCommand; |
| 273 | + |
181 | 274 | # Variables: %s %d %w %h |
182 | | - $src = wfEscapeShellArg( $srcPath ); |
183 | | - $dst = wfEscapeShellArg( $dstPath ); |
| 275 | + $src = wfEscapeShellArg( $params['srcPath'] ); |
| 276 | + $dst = wfEscapeShellArg( $params['dstPath'] ); |
184 | 277 | $cmd = $wgCustomConvertCommand; |
185 | 278 | $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames |
186 | | - $cmd = str_replace( '%h', $physicalHeight, str_replace( '%w', $physicalWidth, $cmd ) ); # Size |
| 279 | + $cmd = str_replace( '%h', $params['physicalHeight'], |
| 280 | + str_replace( '%w', $params['physicalWidth'], $cmd ) ); # Size |
187 | 281 | wfDebug( __METHOD__.": Running custom convert command $cmd\n" ); |
188 | 282 | wfProfileIn( 'convert' ); |
| 283 | + $retval = 0; |
189 | 284 | $err = wfShellExec( $cmd, $retval ); |
190 | 285 | wfProfileOut( 'convert' ); |
191 | | - } else /* $scaler == 'gd' */ { |
| 286 | + |
| 287 | + if ( $retval !== 0 ) { |
| 288 | + $this->logErrorForExternalProcess( $retval, $err, $cmd ); |
| 289 | + return $this->getMediaTransformError( $params, $err ); |
| 290 | + } |
| 291 | + return false; # No error |
| 292 | + } |
| 293 | + |
| 294 | + /** |
| 295 | + * Log an error that occured in an external process |
| 296 | + * |
| 297 | + * @param $retval int |
| 298 | + * @param $err int |
| 299 | + * @param $cmd string |
| 300 | + */ |
| 301 | + protected function logErrorForExternalProcess( $retval, $err, $cmd ) { |
| 302 | + wfDebugLog( 'thumbnail', |
| 303 | + sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', |
| 304 | + wfHostname(), $retval, trim( $err ), $cmd ) ); |
| 305 | + } |
| 306 | + /** |
| 307 | + * |
| 308 | + */ |
| 309 | + protected function getMediaTransformError( $params, $errMsg ) { |
| 310 | + return new MediaTransformError( 'thumbnail_error', $params['clientWidth'], |
| 311 | + $params['clientHeight'], $errMsg ); |
| 312 | + } |
| 313 | + |
| 314 | + /** |
| 315 | + * Transform an image using the built in GD library |
| 316 | + * |
| 317 | + * @param $image File File associated with this thumbnail |
| 318 | + * @param $params array Array with scaler params |
| 319 | + * |
| 320 | + * @return MediaTransformError Error object if error occured, false (=no error) otherwise |
| 321 | + */ |
| 322 | + protected function transformGd( $image, $params ) { |
192 | 323 | # Use PHP's builtin GD library functions. |
193 | 324 | # |
194 | 325 | # First find out what kind of file this is, and select the correct |
— | — | @@ -200,30 +331,31 @@ |
201 | 332 | 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ), |
202 | 333 | 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ), |
203 | 334 | ); |
204 | | - if( !isset( $typemap[$mimeType] ) ) { |
| 335 | + if( !isset( $typemap[$params['mimeType']] ) ) { |
205 | 336 | $err = 'Image type not supported'; |
206 | 337 | wfDebug( "$err\n" ); |
207 | 338 | $errMsg = wfMsg ( 'thumbnail_image-type' ); |
208 | | - return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg ); |
| 339 | + return $this->getMediaTransformError( $params, $errMsg ); |
209 | 340 | } |
210 | | - list( $loader, $colorStyle, $saveType ) = $typemap[$mimeType]; |
| 341 | + list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']]; |
211 | 342 | |
212 | 343 | if( !function_exists( $loader ) ) { |
213 | 344 | $err = "Incomplete GD library configuration: missing function $loader"; |
214 | 345 | wfDebug( "$err\n" ); |
215 | 346 | $errMsg = wfMsg ( 'thumbnail_gd-library', $loader ); |
216 | | - return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg ); |
| 347 | + return $this->getMediaTransformError( $params, $errMsg ); |
217 | 348 | } |
218 | 349 | |
219 | | - if ( !file_exists( $srcPath ) ) { |
220 | | - $err = "File seems to be missing: $srcPath"; |
| 350 | + if ( !file_exists( $params['srcPath'] ) ) { |
| 351 | + $err = "File seems to be missing: {$params['srcPath']}"; |
221 | 352 | wfDebug( "$err\n" ); |
222 | | - $errMsg = wfMsg ( 'thumbnail_image-missing', $srcPath ); |
223 | | - return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg ); |
| 353 | + $errMsg = wfMsg ( 'thumbnail_image-missing', $params['srcPath'] ); |
| 354 | + return $this->getMediaTransformError( $params, $errMsg ); |
224 | 355 | } |
225 | 356 | |
226 | | - $src_image = call_user_func( $loader, $srcPath ); |
227 | | - $dst_image = imagecreatetruecolor( $physicalWidth, $physicalHeight ); |
| 357 | + $src_image = call_user_func( $loader, $params['srcPath'] ); |
| 358 | + $dst_image = imagecreatetruecolor( $params['physicalWidth'], |
| 359 | + $params['physicalHeight'] ); |
228 | 360 | |
229 | 361 | // Initialise the destination image to transparent instead of |
230 | 362 | // the default solid black, to support PNG and GIF transparency nicely |
— | — | @@ -231,35 +363,27 @@ |
232 | 364 | imagecolortransparent( $dst_image, $background ); |
233 | 365 | imagealphablending( $dst_image, false ); |
234 | 366 | |
235 | | - if( $colorStyle == 'palette' ) { |
| 367 | + if ( $colorStyle == 'palette' ) { |
236 | 368 | // Don't resample for paletted GIF images. |
237 | 369 | // It may just uglify them, and completely breaks transparency. |
238 | 370 | imagecopyresized( $dst_image, $src_image, |
239 | 371 | 0,0,0,0, |
240 | | - $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) ); |
| 372 | + $params['physicalWidth'], $params['physicalHeight'], |
| 373 | + imagesx( $src_image ), imagesy( $src_image ) ); |
241 | 374 | } else { |
242 | 375 | imagecopyresampled( $dst_image, $src_image, |
243 | 376 | 0,0,0,0, |
244 | | - $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) ); |
| 377 | + $params['physicalWidth'], $params['physicalHeight'], |
| 378 | + imagesx( $src_image ), imagesy( $src_image ) ); |
245 | 379 | } |
246 | 380 | |
247 | 381 | imagesavealpha( $dst_image, true ); |
248 | 382 | |
249 | | - call_user_func( $saveType, $dst_image, $dstPath ); |
| 383 | + call_user_func( $saveType, $dst_image, $params['dstPath'] ); |
250 | 384 | imagedestroy( $dst_image ); |
251 | 385 | imagedestroy( $src_image ); |
252 | | - $retval = 0; |
253 | | - } |
254 | | - |
255 | | - $removed = $this->removeBadFile( $dstPath, $retval ); |
256 | | - if ( $retval != 0 || $removed ) { |
257 | | - wfDebugLog( 'thumbnail', |
258 | | - sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', |
259 | | - wfHostname(), $retval, trim($err), $cmd ) ); |
260 | | - return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); |
261 | | - } else { |
262 | | - return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); |
263 | | - } |
| 386 | + |
| 387 | + return false; # No error |
264 | 388 | } |
265 | 389 | |
266 | 390 | /** |