Bitmap.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <?php
  2. /**
  3. * @file
  4. * @ingroup Media
  5. */
  6. /**
  7. * @ingroup Media
  8. */
  9. class BitmapHandler extends ImageHandler {
  10. function normaliseParams( $image, &$params ) {
  11. global $wgMaxImageArea;
  12. if ( !parent::normaliseParams( $image, $params ) ) {
  13. return false;
  14. }
  15. $mimeType = $image->getMimeType();
  16. $srcWidth = $image->getWidth( $params['page'] );
  17. $srcHeight = $image->getHeight( $params['page'] );
  18. # Don't thumbnail an image so big that it will fill hard drives and send servers into swap
  19. # JPEG has the handy property of allowing thumbnailing without full decompression, so we make
  20. # an exception for it.
  21. if ( $mimeType !== 'image/jpeg' &&
  22. $srcWidth * $srcHeight > $wgMaxImageArea )
  23. {
  24. return false;
  25. }
  26. # Don't make an image bigger than the source
  27. $params['physicalWidth'] = $params['width'];
  28. $params['physicalHeight'] = $params['height'];
  29. if ( $params['physicalWidth'] >= $srcWidth ) {
  30. $params['physicalWidth'] = $srcWidth;
  31. $params['physicalHeight'] = $srcHeight;
  32. return true;
  33. }
  34. return true;
  35. }
  36. function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
  37. global $wgUseImageMagick, $wgImageMagickConvertCommand, $wgImageMagickTempDir;
  38. global $wgCustomConvertCommand, $wgUseImageResize;
  39. global $wgSharpenParameter, $wgSharpenReductionThreshold;
  40. global $wgMaxAnimatedGifArea;
  41. if ( !$this->normaliseParams( $image, $params ) ) {
  42. return new TransformParameterError( $params );
  43. }
  44. $physicalWidth = $params['physicalWidth'];
  45. $physicalHeight = $params['physicalHeight'];
  46. $clientWidth = $params['width'];
  47. $clientHeight = $params['height'];
  48. $srcWidth = $image->getWidth();
  49. $srcHeight = $image->getHeight();
  50. $mimeType = $image->getMimeType();
  51. $srcPath = $image->getPath();
  52. $retval = 0;
  53. wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" );
  54. if ( !$image->mustRender() && $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) {
  55. # normaliseParams (or the user) wants us to return the unscaled image
  56. wfDebug( __METHOD__.": returning unscaled image\n" );
  57. return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
  58. }
  59. if ( !$dstPath ) {
  60. // No output path available, client side scaling only
  61. $scaler = 'client';
  62. } elseif( !$wgUseImageResize ) {
  63. $scaler = 'client';
  64. } elseif ( $wgUseImageMagick ) {
  65. $scaler = 'im';
  66. } elseif ( $wgCustomConvertCommand ) {
  67. $scaler = 'custom';
  68. } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
  69. $scaler = 'gd';
  70. } else {
  71. $scaler = 'client';
  72. }
  73. wfDebug( __METHOD__.": scaler $scaler\n" );
  74. if ( $scaler == 'client' ) {
  75. # Client-side image scaling, use the source URL
  76. # Using the destination URL in a TRANSFORM_LATER request would be incorrect
  77. return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
  78. }
  79. if ( $flags & self::TRANSFORM_LATER ) {
  80. wfDebug( __METHOD__.": Transforming later per flags.\n" );
  81. return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
  82. }
  83. if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
  84. wfDebug( __METHOD__.": Unable to create thumbnail destination directory, falling back to client scaling\n" );
  85. return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
  86. }
  87. if ( $scaler == 'im' ) {
  88. # use ImageMagick
  89. $quality = '';
  90. $sharpen = '';
  91. $frame = '';
  92. $animation = '';
  93. if ( $mimeType == 'image/jpeg' ) {
  94. $quality = "-quality 80"; // 80%
  95. # Sharpening, see bug 6193
  96. if ( ( $physicalWidth + $physicalHeight ) / ( $srcWidth + $srcHeight ) < $wgSharpenReductionThreshold ) {
  97. $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter );
  98. }
  99. } elseif ( $mimeType == 'image/png' ) {
  100. $quality = "-quality 95"; // zlib 9, adaptive filtering
  101. } elseif( $mimeType == 'image/gif' ) {
  102. if( $srcWidth * $srcHeight > $wgMaxAnimatedGifArea ) {
  103. // Extract initial frame only; we're so big it'll
  104. // be a total drag. :P
  105. $frame = '[0]';
  106. } else {
  107. // Coalesce is needed to scale animated GIFs properly (bug 1017).
  108. $animation = ' -coalesce ';
  109. }
  110. }
  111. if ( strval( $wgImageMagickTempDir ) !== '' ) {
  112. $tempEnv = 'MAGICK_TMPDIR=' . wfEscapeShellArg( $wgImageMagickTempDir ) . ' ';
  113. } else {
  114. $tempEnv = '';
  115. }
  116. # Specify white background color, will be used for transparent images
  117. # in Internet Explorer/Windows instead of default black.
  118. # Note, we specify "-size {$physicalWidth}" and NOT "-size {$physicalWidth}x{$physicalHeight}".
  119. # It seems that ImageMagick has a bug wherein it produces thumbnails of
  120. # the wrong size in the second case.
  121. $cmd =
  122. $tempEnv .
  123. wfEscapeShellArg($wgImageMagickConvertCommand) .
  124. " {$quality} -background white -size {$physicalWidth} ".
  125. wfEscapeShellArg($srcPath . $frame) .
  126. $animation .
  127. // For the -resize option a "!" is needed to force exact size,
  128. // or ImageMagick may decide your ratio is wrong and slice off
  129. // a pixel.
  130. " -thumbnail " . wfEscapeShellArg( "{$physicalWidth}x{$physicalHeight}!" ) .
  131. " -depth 8 $sharpen " .
  132. wfEscapeShellArg($dstPath) . " 2>&1";
  133. wfDebug( __METHOD__.": running ImageMagick: $cmd\n");
  134. wfProfileIn( 'convert' );
  135. $err = wfShellExec( $cmd, $retval );
  136. wfProfileOut( 'convert' );
  137. } elseif( $scaler == 'custom' ) {
  138. # Use a custom convert command
  139. # Variables: %s %d %w %h
  140. $src = wfEscapeShellArg( $srcPath );
  141. $dst = wfEscapeShellArg( $dstPath );
  142. $cmd = $wgCustomConvertCommand;
  143. $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
  144. $cmd = str_replace( '%h', $physicalHeight, str_replace( '%w', $physicalWidth, $cmd ) ); # Size
  145. wfDebug( __METHOD__.": Running custom convert command $cmd\n" );
  146. wfProfileIn( 'convert' );
  147. $err = wfShellExec( $cmd, $retval );
  148. wfProfileOut( 'convert' );
  149. } else /* $scaler == 'gd' */ {
  150. # Use PHP's builtin GD library functions.
  151. #
  152. # First find out what kind of file this is, and select the correct
  153. # input routine for this.
  154. $typemap = array(
  155. 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ),
  156. 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ),
  157. 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ),
  158. 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ),
  159. 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ),
  160. );
  161. if( !isset( $typemap[$mimeType] ) ) {
  162. $err = 'Image type not supported';
  163. wfDebug( "$err\n" );
  164. return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
  165. }
  166. list( $loader, $colorStyle, $saveType ) = $typemap[$mimeType];
  167. if( !function_exists( $loader ) ) {
  168. $err = "Incomplete GD library configuration: missing function $loader";
  169. wfDebug( "$err\n" );
  170. return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
  171. }
  172. $src_image = call_user_func( $loader, $srcPath );
  173. $dst_image = imagecreatetruecolor( $physicalWidth, $physicalHeight );
  174. // Initialise the destination image to transparent instead of
  175. // the default solid black, to support PNG and GIF transparency nicely
  176. $background = imagecolorallocate( $dst_image, 0, 0, 0 );
  177. imagecolortransparent( $dst_image, $background );
  178. imagealphablending( $dst_image, false );
  179. if( $colorStyle == 'palette' ) {
  180. // Don't resample for paletted GIF images.
  181. // It may just uglify them, and completely breaks transparency.
  182. imagecopyresized( $dst_image, $src_image,
  183. 0,0,0,0,
  184. $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) );
  185. } else {
  186. imagecopyresampled( $dst_image, $src_image,
  187. 0,0,0,0,
  188. $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) );
  189. }
  190. imagesavealpha( $dst_image, true );
  191. call_user_func( $saveType, $dst_image, $dstPath );
  192. imagedestroy( $dst_image );
  193. imagedestroy( $src_image );
  194. $retval = 0;
  195. }
  196. $removed = $this->removeBadFile( $dstPath, $retval );
  197. if ( $retval != 0 || $removed ) {
  198. wfDebugLog( 'thumbnail',
  199. sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
  200. wfHostname(), $retval, trim($err), $cmd ) );
  201. return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
  202. } else {
  203. return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
  204. }
  205. }
  206. static function imageJpegWrapper( $dst_image, $thumbPath ) {
  207. imageinterlace( $dst_image );
  208. imagejpeg( $dst_image, $thumbPath, 95 );
  209. }
  210. function getMetadata( $image, $filename ) {
  211. global $wgShowEXIF;
  212. if( $wgShowEXIF && file_exists( $filename ) ) {
  213. $exif = new Exif( $filename );
  214. $data = $exif->getFilteredData();
  215. if ( $data ) {
  216. $data['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
  217. return serialize( $data );
  218. } else {
  219. return '0';
  220. }
  221. } else {
  222. return '';
  223. }
  224. }
  225. function getMetadataType( $image ) {
  226. return 'exif';
  227. }
  228. function isMetadataValid( $image, $metadata ) {
  229. global $wgShowEXIF;
  230. if ( !$wgShowEXIF ) {
  231. # Metadata disabled and so an empty field is expected
  232. return true;
  233. }
  234. if ( $metadata === '0' ) {
  235. # Special value indicating that there is no EXIF data in the file
  236. return true;
  237. }
  238. $exif = @unserialize( $metadata );
  239. if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) ||
  240. $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version() )
  241. {
  242. # Wrong version
  243. wfDebug( __METHOD__.": wrong version\n" );
  244. return false;
  245. }
  246. return true;
  247. }
  248. /**
  249. * Get a list of EXIF metadata items which should be displayed when
  250. * the metadata table is collapsed.
  251. *
  252. * @return array of strings
  253. * @access private
  254. */
  255. function visibleMetadataFields() {
  256. $fields = array();
  257. $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
  258. foreach( $lines as $line ) {
  259. $matches = array();
  260. if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
  261. $fields[] = $matches[1];
  262. }
  263. }
  264. $fields = array_map( 'strtolower', $fields );
  265. return $fields;
  266. }
  267. function formatMetadata( $image ) {
  268. $result = array(
  269. 'visible' => array(),
  270. 'collapsed' => array()
  271. );
  272. $metadata = $image->getMetadata();
  273. if ( !$metadata ) {
  274. return false;
  275. }
  276. $exif = unserialize( $metadata );
  277. if ( !$exif ) {
  278. return false;
  279. }
  280. unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
  281. $format = new FormatExif( $exif );
  282. $formatted = $format->getFormattedData();
  283. // Sort fields into visible and collapsed
  284. $visibleFields = $this->visibleMetadataFields();
  285. foreach ( $formatted as $name => $value ) {
  286. $tag = strtolower( $name );
  287. self::addMeta( $result,
  288. in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
  289. 'exif',
  290. $tag,
  291. $value
  292. );
  293. }
  294. return $result;
  295. }
  296. }