PageRenderTime 24ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/VipsScaler/VipsScaler_body.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 357 lines | 207 code | 44 blank | 106 comment | 54 complexity | 3c98fcbdeddd3ed3040e1b5cefdeaed4 MD5 | raw file
  1. <?php
  2. class VipsScaler {
  3. /**
  4. * Hook to BitmapHandlerTransform. Transforms the file using VIPS if it
  5. * matches a condition in $wgVipsConditions
  6. *
  7. * @param BitmapHandler $handler
  8. * @param File $file
  9. * @param array $params
  10. * @param MediaTransformOutput $mto
  11. */
  12. public static function onTransform( $handler, $file, &$params, &$mto ) {
  13. # Check $wgVipsConditions
  14. $options = self::getHandlerOptions( $handler, $file, $params );
  15. if ( !$options ) {
  16. return true;
  17. }
  18. $vipsCommands = self::makeCommands( $handler, $file, $params, $options );
  19. if ( count( $vipsCommands ) == 0 ) {
  20. return true;
  21. }
  22. # Execute the commands
  23. foreach ( $vipsCommands as $i => $command ) {
  24. # Set input/output files
  25. if ( $i == 0 && count( $vipsCommands ) == 1 ) {
  26. # Single command, so output directly to dstPath
  27. $command->setIO( $params['srcPath'], $params['dstPath'] );
  28. } elseif ( $i == 0 ) {
  29. # First command, input from srcPath, output to temp
  30. $command->setIO( $params['srcPath'], 'v', VipsCommand::TEMP_OUTPUT );
  31. } elseif ( $i + 1 == count( $vipsCommands ) ) {
  32. # Last command, output to dstPath
  33. $command->setIO( $vipsCommands[$i - 1], $params['dstPath'] );
  34. } else {
  35. $command->setIO( $vipsCommands[$i - 1], 'v', VipsCommand::TEMP_OUTPUT );
  36. }
  37. $retval = $command->execute();
  38. if ( $retval != 0 ) {
  39. wfDebug( __METHOD__ . ": vips command failed!\n" );
  40. $mto = $handler->getMediaTransformError( $params, $command->getErrorString() );
  41. return false;
  42. }
  43. }
  44. # Set the output variable
  45. $mto = new ThumbnailImage( $file, $params['dstUrl'],
  46. $params['clientWidth'], $params['clientHeight'], $params['dstPath'] );
  47. # Stop processing
  48. return false;
  49. }
  50. public static function makeCommands( $handler, $file, $params, $options ) {
  51. global $wgVipsCommand;
  52. $commands = array();
  53. # Get the proper im_XXX2vips handler
  54. $vipsHandler = self::getVipsHandler( $file );
  55. if ( !$vipsHandler ) {
  56. return array();
  57. }
  58. # Check if we need to convert to a .v file first
  59. if ( !empty( $options['preconvert'] ) ) {
  60. $commands[] = new VipsCommand( $wgVipsCommand, array( $vipsHandler ) );
  61. }
  62. # Do the resizing
  63. $rotation = 360 - $handler->getRotation( $file );
  64. if ( empty( $options['bilinear'] ) ) {
  65. # Calculate shrink factors. Offsetting by 0.5 pixels is required
  66. # because of rounding down of the target size by VIPS. See 25990#c7
  67. if ( $rotation % 180 == 90 ) {
  68. # Rotated 90 degrees, so width = height and vice versa
  69. $rx = $params['srcWidth'] / ($params['physicalHeight'] + 0.5);
  70. $ry = $params['srcHeight'] / ($params['physicalWidth'] + 0.5);
  71. } else {
  72. $rx = $params['srcWidth'] / ($params['physicalWidth'] + 0.5);
  73. $ry = $params['srcHeight'] / ($params['physicalHeight'] + 0.5);
  74. }
  75. $commands[] = new VipsCommand( $wgVipsCommand, array( 'im_shrink', $rx, $ry ) );
  76. } else {
  77. if ( $rotation % 180 == 90 ) {
  78. $dstWidth = $params['physicalHeight'];
  79. $dstHeight = $params['physicalWidth'];
  80. } else {
  81. $dstWidth = $params['physicalWidth'];
  82. $dstHeight = $params['physicalHeight'];
  83. }
  84. $commands[] = new VipsCommand( $wgVipsCommand,
  85. array( 'im_resize_linear', $dstWidth, $dstHeight ) );
  86. }
  87. if ( !empty( $options['sharpen'] ) ) {
  88. $options['convolution'] = self::makeSharpenMatrix( $options['sharpen'] );
  89. }
  90. if ( !empty( $options['convolution'] ) ) {
  91. $commands[] = new VipsConvolution( $wgVipsCommand,
  92. array( 'im_convf', $options['convolution'] ) );
  93. }
  94. # Rotation
  95. if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
  96. $commands[] = new VipsCommand( $wgVipsCommand, array( "im_rot{$rotation}" ) );
  97. }
  98. return $commands;
  99. }
  100. /**
  101. * Create a sharpening matrix suitable for im_convf. Uses the ImageMagick
  102. * sharpening algorithm from SharpenImage() in magick/effect.c
  103. *
  104. * @param mixed $params
  105. * @return array
  106. */
  107. public static function makeSharpenMatrix( $params ) {
  108. $sigma = $params['sigma'];
  109. $radius = empty( $params['radius'] ) ?
  110. # After 3 sigma there should be no significant values anymore
  111. intval( round( $sigma * 3 ) ) : $params['radius'];
  112. $norm = 0;
  113. $conv = array();
  114. # Fill the matrix with a negative Gaussian distribution
  115. $variance = $sigma * $sigma;
  116. for ( $x = -$radius; $x <= $radius; $x++ ) {
  117. $row = array();
  118. for ( $y = -$radius; $y <= $radius; $y++ ) {
  119. $z = -exp( -( $x*$x + $y*$y ) / ( 2 * $variance ) ) /
  120. ( 2 * pi() * $variance );
  121. $row[] = $z;
  122. $norm += $z;
  123. }
  124. $conv[] = $row;
  125. }
  126. # Calculate the scaling parameter to ensure that the mean of the
  127. # matrix is zero
  128. $scale = - $conv[$radius][$radius] - $norm;
  129. # Set the center pixel to obtain a sharpening matrix
  130. $conv[$radius][$radius] = -$norm * 2;
  131. # Add the matrix descriptor
  132. array_unshift( $conv, array( $radius * 2 + 1, $radius * 2 + 1, $scale, 0 ) );
  133. return $conv;
  134. }
  135. /**
  136. * Check the file and params against $wgVipsOptions
  137. *
  138. * @param BitmapHandler $handler
  139. * @param File $file
  140. * @param array $params
  141. * @return bool
  142. */
  143. protected static function getHandlerOptions( $handler, $file, $params ) {
  144. global $wgVipsOptions;
  145. # Iterate over conditions
  146. foreach ( $wgVipsOptions as $option ) {
  147. if ( isset( $option['conditions'] ) ) {
  148. $condition = $option['conditions'];
  149. } else {
  150. # Unconditionally pass
  151. return $option;
  152. }
  153. if ( isset( $condition['mimeType'] ) &&
  154. $file->getMimeType() != $condition['mimeType'] ) {
  155. continue;
  156. }
  157. $area = $handler->getImageArea( $file, $params['srcWidth'], $params['srcHeight'] );
  158. if ( isset( $condition['minArea'] ) && $area < $condition['minArea'] ) {
  159. continue;
  160. }
  161. if ( isset( $condition['maxArea'] ) && $area >= $condition['maxArea'] ) {
  162. continue;
  163. }
  164. $shrinkFactor = $params['srcWidth'] / (
  165. ( ( $handler->getRotation( $file ) % 180 ) == 90 ) ?
  166. $params['physicalHeight'] : $params['physicalWidth'] );
  167. if ( isset( $condition['minShrinkFactor'] ) &&
  168. $shrinkFactor < $condition['minShrinkFactor'] ) {
  169. continue;
  170. }
  171. if ( isset( $condition['maxShrinkFactor'] ) &&
  172. $shrinkFactor >= $condition['maxShrinkFactor'] ) {
  173. continue;
  174. }
  175. # This condition passed
  176. return $option;
  177. }
  178. # All conditions failed
  179. return false;
  180. }
  181. /**
  182. * Return the appropriate im_XXX2vips handler for this file
  183. * @param File $file
  184. * @return mixed String or false
  185. */
  186. protected static function getVipsHandler( $file ) {
  187. list( $major, $minor ) = File::splitMime( $file->getMimeType() );
  188. if ( $major == 'image' && in_array( $minor, array( 'jpeg', 'png', 'tiff' ) ) ) {
  189. return "im_{$minor}2vips";
  190. } else {
  191. return false;
  192. }
  193. }
  194. }
  195. /**
  196. * Wrapper class around the vips command, useful to chain multiple commands
  197. * with intermediate .v files
  198. */
  199. class VipsCommand {
  200. /** Flag to indicate that the output file should be a temporary .v file */
  201. const TEMP_OUTPUT = true;
  202. /**
  203. * Constructor
  204. *
  205. * @param string $vips Path to binary
  206. * @param array $args Array or arguments
  207. */
  208. public function __construct( $vips, $args ) {
  209. $this->vips = $vips;
  210. $this->args = $args;
  211. }
  212. /**
  213. * Set the input and output file of this command
  214. *
  215. * @param mixed $input Input file name or an VipsCommand object to use the
  216. * output of that command
  217. * @param string $output Output file name or extension of the temporary file
  218. * @param bool $tempOutput Output to a temporary file
  219. */
  220. public function setIO( $input, $output, $tempOutput = false ) {
  221. if ( $input instanceof VipsCommand ) {
  222. $this->input = $input->getOutput();
  223. $this->removeInput = true;
  224. } else {
  225. $this->input = $input;
  226. $this->removeInput = false;
  227. }
  228. if ( $tempOutput ) {
  229. $this->output = self::makeTemp( $output );
  230. } else {
  231. $this->output = $output;
  232. }
  233. }
  234. /**
  235. * Returns the output filename
  236. * @return string
  237. */
  238. public function getOutput() {
  239. return $this->output;
  240. }
  241. /**
  242. * Return the output of the command
  243. * @return string
  244. */
  245. public function getErrorString() {
  246. return $this->err;
  247. }
  248. /**
  249. * Call the vips binary with varargs and returns the return value.
  250. *
  251. * @return int Return value
  252. */
  253. public function execute() {
  254. # Build and escape the command string
  255. $cmd = wfEscapeShellArg( $this->vips,
  256. array_shift( $this->args ),
  257. $this->input, $this->output );
  258. foreach ( $this->args as $arg ) {
  259. $cmd .= ' ' . wfEscapeShellArg( $arg );
  260. }
  261. $cmd .= ' 2>&1';
  262. # Execute
  263. $retval = 0;
  264. $this->err = wfShellExec( $cmd, $retval );
  265. # Cleanup temp file
  266. if ( $this->removeInput ) {
  267. unlink( $this->input );
  268. }
  269. return $retval;
  270. }
  271. /**
  272. * Generate a random, non-existent temporary file with a specified
  273. * extension.
  274. *
  275. * @param string Extension
  276. * @return string
  277. */
  278. protected static function makeTemp( $extension ) {
  279. do {
  280. # Generate a random file
  281. $fileName = wfTempDir() . DIRECTORY_SEPARATOR .
  282. dechex( mt_rand() ) . dechex( mt_rand() ) .
  283. '.' . $extension;
  284. } while ( file_exists( $fileName ) );
  285. # Create the file
  286. touch( $fileName );
  287. return $fileName;
  288. }
  289. }
  290. /**
  291. * A wrapper class around im_conv because that command expects a a convolution
  292. * matrix file as its last argument
  293. */
  294. class VipsConvolution extends VipsCommand {
  295. public function execute() {
  296. # Convert a 2D array into a space/newline separated matrix
  297. $convolutionMatrix = array_pop( $this->args );
  298. $convolutionString = '';
  299. foreach ( $convolutionMatrix as $i=>$row ) {
  300. $convolutionString .= implode( ' ', $row ) . "\n";
  301. }
  302. # Save the matrix in a tempfile
  303. $convolutionFile = self::makeTemp( 'conv' );
  304. file_put_contents( $convolutionFile, $convolutionString );
  305. array_push( $this->args, $convolutionFile );
  306. wfDebug( __METHOD__ . ": Convolving image [\n" . $convolutionString . "] \n" );
  307. # Call the parent to actually execute the command
  308. $retval = parent::execute();
  309. # Remove the temporary matrix file
  310. unlink( $convolutionFile );
  311. return $retval;
  312. }
  313. }