diff --git a/core/lib/Drupal/Core/Image/Image.php b/core/lib/Drupal/Core/Image/Image.php index e170991..173321a 100644 --- a/core/lib/Drupal/Core/Image/Image.php +++ b/core/lib/Drupal/Core/Image/Image.php @@ -151,6 +151,13 @@ public function apply($operation, array $arguments = array()) { /** * {@inheritdoc} */ + public function convert($format) { + return $this->apply('convert', array('format' => $format)); + } + + /** + * {@inheritdoc} + */ public function crop($x, $y, $width, $height = NULL) { return $this->apply('crop', array('x' => $x, 'y' => $y, 'width' => $width, 'height' => $height)); } diff --git a/core/lib/Drupal/Core/Image/ImageInterface.php b/core/lib/Drupal/Core/Image/ImageInterface.php index 02db478..1e40e90 100644 --- a/core/lib/Drupal/Core/Image/ImageInterface.php +++ b/core/lib/Drupal/Core/Image/ImageInterface.php @@ -149,6 +149,14 @@ public function scale($width, $height = NULL, $upscale = FALSE); public function scaleAndCrop($width, $height); /** + * Converts an image to a specified format. + * + * @param string $format + * The format to convert to (e.g. 'jpeg', 'png'). + */ + public function convert($format); + + /** * Crops an image to a rectangle specified by the given dimensions. * * @param int $x diff --git a/core/modules/image/src/Controller/ImageStyleDownloadController.php b/core/modules/image/src/Controller/ImageStyleDownloadController.php index a18c677..7d154b5 100644 --- a/core/modules/image/src/Controller/ImageStyleDownloadController.php +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php @@ -130,8 +130,13 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st // Don't try to generate file if source is missing. if (!file_exists($image_uri)) { - $this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri)); - return new Response($this->t('Error generating image, missing source file.'), 404); + // Remove the extension to check if the image style added one. + $path_info = pathinfo($image_uri); + $image_uri = $path_info['dirname'] . DIRECTORY_SEPARATOR . $path_info['filename']; + if (!file_exists($image_uri)) { + $this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri)); + return new Response($this->t('Error generating image, missing source file.'), 404); + } } // Don't start generating the image if the derivative already exists or if diff --git a/core/modules/image/src/Entity/ImageStyle.php b/core/modules/image/src/Entity/ImageStyle.php index fca017f..81ae5f0 100644 --- a/core/modules/image/src/Entity/ImageStyle.php +++ b/core/modules/image/src/Entity/ImageStyle.php @@ -180,6 +180,12 @@ public function buildUri($uri) { $path = $uri; $scheme = file_default_scheme(); } + $extension = pathinfo($path, PATHINFO_EXTENSION); + $original_extension = $extension; + $this->getDerivativeExtension($extension); + if ($original_extension != $extension) { + $path .= '.' . $extension; + } return $scheme . '://styles/' . $this->id() . '/' . $scheme . '/' . $path; } @@ -310,9 +316,18 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ - public function transformMimeType(&$mime_type) { + public function getDerivativeMimeType(&$mime_type) { + foreach ($this->getEffects() as $effect) { + $effect->getDerivativeMimeType($mime_type); + } + } + + /** + * {@inheritdoc} + */ + public function getDerivativeExtension(&$extension) { foreach ($this->getEffects() as $effect) { - $effect->transformMimeType($mime_type); + $effect->getDerivativeExtension($extension); } } diff --git a/core/modules/image/src/ImageEffectBase.php b/core/modules/image/src/ImageEffectBase.php index 329f332..559a5a6 100644 --- a/core/modules/image/src/ImageEffectBase.php +++ b/core/modules/image/src/ImageEffectBase.php @@ -77,7 +77,13 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ - public function transformMimeType(&$mime_type) { + public function getDerivativeMimeType(&$mime_type) { + } + + /** + * {@inheritdoc} + */ + public function getDerivativeExtension(&$extension) { } /** diff --git a/core/modules/image/src/ImageEffectInterface.php b/core/modules/image/src/ImageEffectInterface.php index 36b789b..eddcf62 100644 --- a/core/modules/image/src/ImageEffectInterface.php +++ b/core/modules/image/src/ImageEffectInterface.php @@ -44,12 +44,20 @@ public function applyEffect(ImageInterface $image); public function transformDimensions(array &$dimensions); /** - * Determines the MIME type of the image effect. + * Determines the MIME type of the derivative without generating it. * * @param string $mime_type * The MIME type to be modified. */ - public function transformMimeType(&$mime_type); + public function getDerivativeMimeType(&$mime_type); + + /** + * Determines the extension of the derivative without generating it. + * + * @param string $extension + * Extension to be modified. + */ + public function getDerivativeExtension(&$extension); /** * Returns a render array summarizing the configuration of the image effect. diff --git a/core/modules/image/src/ImageStyleInterface.php b/core/modules/image/src/ImageStyleInterface.php index cf5c6c1..46ae24c 100644 --- a/core/modules/image/src/ImageStyleInterface.php +++ b/core/modules/image/src/ImageStyleInterface.php @@ -132,12 +132,23 @@ public function createDerivative($original_uri, $derivative_uri); public function transformDimensions(array &$dimensions); /** - * Determines the MIME type of the image style. + * Determines the MIME type of the derivative without generating it. * * @param string $mime_type * The MIME type to be modified. */ - public function transformMimeType(&$mime_type); + public function getDerivativeMimeType(&$mime_type); + + /** + * Determines the extension of the derivative without generating it. + * + * Stores the extension of this image style into $extension. + * + * @param string $extension + * String passed by reference. Implementations have to store the + * resulting extension, without the leading dot. + */ + public function getDerivativeExtension(&$extension); /** * Returns a specific image effect. diff --git a/core/modules/image/src/Plugin/ImageEffect/ConvertImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/ConvertImageEffect.php new file mode 100644 index 0000000..88e0179 --- /dev/null +++ b/core/modules/image/src/Plugin/ImageEffect/ConvertImageEffect.php @@ -0,0 +1,99 @@ +convert($this->configuration['format'])) { + $this->logger->error('Image convert failed using the %toolkit toolkit on %path (%mimetype)', array('%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType())); + return FALSE; + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getDerivativeMimeType(&$mime_type) { + $mime_type = \Drupal::service('file.mime_type.guesser.extension')->guess($this->configuration['format']); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeExtension(&$extension) { + $extension = $this->configuration['format']; + } + + /** + * {@inheritdoc} + */ + public function getSummary() { + $summary = array( + '#markup' => Unicode::strtoupper($this->configuration['format']), + ); + $summary += parent::getSummary(); + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array( + 'format' => NULL, + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $extensions = \Drupal::service('image.toolkit.manager')->getDefaultToolkit()->getSupportedExtensions(); + $options = array_combine( + $extensions, + array_map(array('\Drupal\Component\Utility\Unicode', 'strtoupper'), $extensions) + ); + $form['format'] = array( + '#type' => 'select', + '#title' => t('Format'), + '#default_value' => $this->configuration['format'], + '#required' => TRUE, + '#options' => $options, + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['format'] = $form_state->getValue('format'); + } + +} diff --git a/core/modules/image/src/Tests/ImageEffectsTest.php b/core/modules/image/src/Tests/ImageEffectsTest.php index 95fd952..5c1f8ef 100644 --- a/core/modules/image/src/Tests/ImageEffectsTest.php +++ b/core/modules/image/src/Tests/ImageEffectsTest.php @@ -89,6 +89,20 @@ function testCropEffect() { } /** + * Test the image_convert_effect() function. + */ + function testConvertEffect() { + $this->assertImageEffect('image_convert', array( + 'format' => 'png', + )); + $this->assertToolkitOperationsCalled(array('convert')); + + // Check the parameters. + $calls = $this->imageTestGetAllCalls(); + $this->assertEqual($calls['convert'][0][0], 'png', 'Format was passed correctly'); + } + + /** * Test the image_scale_and_crop_effect() function. */ function testScaleAndCropEffect() { diff --git a/core/modules/image/src/Tests/ImageStyleTest.php b/core/modules/image/src/Tests/ImageStyleTest.php index 78d9b7c..ac54a7d 100644 --- a/core/modules/image/src/Tests/ImageStyleTest.php +++ b/core/modules/image/src/Tests/ImageStyleTest.php @@ -73,7 +73,7 @@ public function setUp() { } /** - * @covers ::transformMimeType + * @covers ::getDerivativeMimeType */ public function testTransformMimeType() { $image_effect_id = $this->randomMachineName(); @@ -82,7 +82,7 @@ public function testTransformMimeType() { ->setConstructorArgs(array(array(), $image_effect_id, array(), $logger)) ->getMock(); $image_effect->expects($this->any()) - ->method('transformMimeType') + ->method('getDerivativeMimeType') ->will($this->returnCallback(function (&$mime_type) { $mime_type = 'image/webp';})); $this->effectManager->expects($this->any()) @@ -103,9 +103,45 @@ public function testTransformMimeType() { $mime_types = array('image/jpeg', 'image/gif', 'image/png'); foreach ($mime_types as $mime_type) { - $image_style->transformMimeType($mime_type); + $image_style->getDerivativeMimeType($mime_type); $this->assertEquals($mime_type, 'image/webp'); } } + /** + * @covers ::getDerivativeExtension + */ + public function testTransformExtension() { + $image_effect_id = $this->randomMachineName(); + $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); + $image_effect = $this->getMockBuilder('\Drupal\image\ImageEffectBase') + ->setConstructorArgs(array(array(), $image_effect_id, array(), $logger)) + ->getMock(); + $image_effect->expects($this->any()) + ->method('getDerivativeExtension') + ->will($this->returnCallback(function (&$extension) { $extension = 'png';})); + + $this->effectManager->expects($this->any()) + ->method('createInstance') + ->with($image_effect_id) + ->will($this->returnValue($image_effect)); + + $image_style = $this->getMockBuilder('\Drupal\image\Entity\ImageStyle') + ->setConstructorArgs(array( + array('effects' => array($image_effect_id => array('id' => $image_effect_id))), + $this->entityTypeId, + )) + ->setMethods(array('getImageEffectPluginManager')) + ->getMock(); + $image_style->expects($this->any()) + ->method('getImageEffectPluginManager') + ->will($this->returnValue($this->effectManager)); + + $extensions = array('jpeg', 'gif', 'png'); + foreach ($extensions as $extension) { + $image_style->getDerivativeExtension($extension); + $this->assertEquals($extension, 'png'); + } + } + } diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Convert.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Convert.php new file mode 100644 index 0000000..38dfff8 --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Convert.php @@ -0,0 +1,84 @@ + array( + 'description' => 'The new format of the converted image', + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function validateArguments(array $arguments) { + if (!in_array($arguments['format'], $this->getToolkit()->getSupportedExtensions())) { + throw new \InvalidArgumentException(String::format("Invalid format (@value) specified for the image 'convert' operation", array('@value' => $arguments['format']))); + } + return $arguments; + } + + /** + * {@inheritdoc} + */ + protected function execute(array $arguments = array()) { + $type = FALSE; + switch ($arguments['format']) { + case 'jpeg': + case 'jpg': + case 'jpe': + $type = IMAGETYPE_JPEG; + break; + + default: + $constant = 'IMAGETYPE_' . \Drupal\Component\Utility\Unicode::strtoupper($arguments['format']); + if (defined($constant)) { + $type = constant($constant); + } + break; + } + + if (!$type) { + return FALSE; + } + + $res = $this->getToolkit()->createTmp($type, $this->getToolkit()->getWidth(), $this->getToolkit()->getHeight()); + if (!imagecopyresampled($res, $this->getToolkit()->getResource(), 0, 0, 0, 0, $this->getToolkit()->getWidth(), $this->getToolkit()->getHeight(), $this->getToolkit()->getWidth(), $this->getToolkit()->getHeight())) { + return FALSE; + } + + imagedestroy($this->getToolkit()->getResource()); + + $this->getToolkit()->setType($type); + // Update image object. + $this->getToolkit()->setResource($res); + + return TRUE; + } + +} diff --git a/core/modules/system/src/Tests/Image/ToolkitGdTest.php b/core/modules/system/src/Tests/Image/ToolkitGdTest.php index 9288f5d..8ed1368 100644 --- a/core/modules/system/src/Tests/Image/ToolkitGdTest.php +++ b/core/modules/system/src/Tests/Image/ToolkitGdTest.php @@ -171,6 +171,12 @@ function testManipulations() { 'height' => 8, 'corners' => array_fill(0, 4, $this->black), ), + 'convert' => array( + 'function' => 'convert', + 'width' => 40, + 'height' => 20, + 'arguments' => array('format' => 'jpeg'), + ), ); // Systems using non-bundled GD2 don't have imagerotate. Test if available. diff --git a/core/modules/system/src/Tests/Image/ToolkitTestBase.php b/core/modules/system/src/Tests/Image/ToolkitTestBase.php index a04dbb3..9b295f7 100644 --- a/core/modules/system/src/Tests/Image/ToolkitTestBase.php +++ b/core/modules/system/src/Tests/Image/ToolkitTestBase.php @@ -91,6 +91,7 @@ function assertToolkitOperationsCalled(array $expected) { 'scale', 'scale_and_crop', 'my_operation', + 'convert', ); if (count(array_intersect($expected, $operations)) > 0 && !in_array('apply', $expected)) { $expected[] = 'apply'; @@ -136,6 +137,7 @@ function imageTestReset() { 'desaturate' => array(), 'scale' => array(), 'scale_and_crop' => array(), + 'convert' => array(), ); \Drupal::state()->set('image_test.results', $results); } diff --git a/core/tests/Drupal/Tests/Core/Image/ImageTest.php b/core/tests/Drupal/Tests/Core/Image/ImageTest.php index fa91f28..691f25a 100644 --- a/core/tests/Drupal/Tests/Core/Image/ImageTest.php +++ b/core/tests/Drupal/Tests/Core/Image/ImageTest.php @@ -389,6 +389,19 @@ public function testCrop() { } /** + * Tests \Drupal\Core\Image\Image::convert(). + */ + public function testConvert() { + $this->getTestImageForOperation('Convert'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->will($this->returnArgument(0)); + + $ret = $this->image->convert('png'); + $this->assertEquals('png', $ret['format']); + } + + /** * Tests \Drupal\Core\Image\Image::resize(). */ public function testResize() {