diff --git a/core/lib/Drupal/Core/Image/Image.php b/core/lib/Drupal/Core/Image/Image.php index e170991..aff5d81 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($extension) { + return $this->apply('convert', array('extension' => $extension)); + } + + /** + * {@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..ae07fe2 100644 --- a/core/lib/Drupal/Core/Image/ImageInterface.php +++ b/core/lib/Drupal/Core/Image/ImageInterface.php @@ -149,6 +149,20 @@ public function scale($width, $height = NULL, $upscale = FALSE); public function scaleAndCrop($width, $height); /** + * Instructs the toolkit to save the image with the specified extension. + * + * @param string $extension + * The extension to convert to (e.g. 'jpeg', 'png'). Allowed values depend + * on the implementation of the convert operation of the image toolkit. + * For a working example, see + * \Drupal\system\Plugin\ImageToolkit\Operation\gd\Convert. + * + * @return bool + * TRUE on success, FALSE on failure. + */ + public function convert($extension); + + /** * 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..2b2d89f 100644 --- a/core/modules/image/src/Controller/ImageStyleDownloadController.php +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php @@ -130,8 +130,20 @@ 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); + // If the image style converted the extension, it has been added to the + // original file, resulting in filenames like image.png.jpeg. So to find + // the actual source image, we remove the extension and check if that + // image exists. + $path_info = pathinfo($image_uri); + $converted_image_uri = $path_info['dirname'] . DIRECTORY_SEPARATOR . $path_info['filename']; + if (!file_exists($converted_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); + } + else { + // The converted file does exist, use it as the source. + $image_uri = $converted_image_uri; + } } // 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 752dc1e..88cd42b 100644 --- a/core/modules/image/src/Entity/ImageStyle.php +++ b/core/modules/image/src/Entity/ImageStyle.php @@ -172,15 +172,15 @@ protected static function replaceImageStyle(ImageStyleInterface $style) { * {@inheritdoc} */ public function buildUri($uri) { - $scheme = file_uri_scheme($uri); + $scheme = $this->fileUriScheme($uri); if ($scheme) { - $path = file_uri_target($uri); + $path = $this->fileUriTarget($uri); } else { $path = $uri; - $scheme = file_default_scheme(); + $scheme = $this->fileDefaultScheme(); } - return $scheme . '://styles/' . $this->id() . '/' . $scheme . '/' . $path; + return $scheme . '://styles/' . $this->id() . '/' . $scheme . '/' . $this->addExtension($path); } /** @@ -310,9 +310,27 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ + 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->getDerivativeExtension($extension); + } + } + + /** + * {@inheritdoc} + */ public function getPathToken($uri) { // Return the first 8 characters. - return substr(Crypt::hmacBase64($this->id() . ':' . $uri, \Drupal::service('private_key')->get() . Settings::getHashSalt()), 0, 8); + return substr(Crypt::hmacBase64($this->id() . ':' . $this->addExtension($uri), $this->getPrivateKey() . $this->getHashSalt()), 0, 8); } /** @@ -336,7 +354,7 @@ public function getEffect($effect) { */ public function getEffects() { if (!$this->effectsBag) { - $this->effectsBag = new ImageEffectBag(\Drupal::service('plugin.manager.image.effect'), $this->effects); + $this->effectsBag = new ImageEffectBag($this->getImageEffectPluginManager(), $this->effects); $this->effectsBag->sort(); } return $this->effectsBag; @@ -380,4 +398,115 @@ public function setName($name) { return $this; } + /** + * Returns the image effect plugin manager. + * + * @return \Drupal\Component\Plugin\PluginManagerInterface + * The image effect plugin manager. + */ + protected function getImageEffectPluginManager() { + return \Drupal::service('plugin.manager.image.effect'); + } + + /** + * Gets the Drupal private key. + * + * @return string + * The Drupal private key. + */ + protected function getPrivateKey() { + return \Drupal::service('private_key')->get(); + } + + /** + * Gets a salt useful for hardening against SQL injection. + * + * @return string + * A salt based on information in settings.php, not in the database. + * + * @throws \RuntimeException + */ + protected function getHashSalt() { + return Settings::getHashSalt(); + } + + /** + * Adds an extension to a path. + * + * If this image style changes the extension of the derivative, this method + * adds the new extension to the given path. This way we avoid filename + * clashes and make it easy to find the source image. + * + * @param string $path + * The path to add the extension to. + * + * @return string + * The given path if this image style doesn't change its extension, or the + * path with the added extension if it does. + */ + protected function addExtension($path) { + $extension = pathinfo($path, PATHINFO_EXTENSION); + $original_extension = $extension; + $this->getDerivativeExtension($extension); + if ($original_extension !== $extension) { + $path .= '.' . $extension; + } + return $path; + } + + /** + * Provides a wrapper for file_uri_scheme() to allow unit testing. + * + * Returns the scheme of a URI (e.g. a stream). + * + * @param string $uri + * A stream, referenced as "scheme://target" or "data:target". + * + * @see file_uri_target() + * + * @todo: Remove when https://www.drupal.org/node/2050759 is in. + * + * @return string + * A string containing the name of the scheme, or FALSE if none. For example, + * the URI "public://example.txt" would return "public". + */ + protected function fileUriScheme($uri) { + return file_uri_scheme($uri); + } + + /** + * Provides a wrapper for file_uri_target() to allow unit testing. + * + * Returns the part of a URI after the schema. + * + * @param string $uri + * A stream, referenced as "scheme://target" or "data:target". + * + * @see file_uri_scheme() + * + * @todo: Convert file_uri_target() into a proper injectable service. + * + * @return string|bool + * A string containing the target (path), or FALSE if none. + * For example, the URI "public://sample/test.txt" would return + * "sample/test.txt". + */ + protected function fileUriTarget($uri) { + return file_uri_target($uri); + } + + /** + * Provides a wrapper for file_default_scheme() to allow unit testing. + * + * Gets the default file stream implementation. + * + * @todo: Convert file_default_scheme() into a proper injectable service. + * + * @return + * 'public', 'private' or any other file scheme defined as the default. + */ + protected function fileDefaultScheme() { + return file_default_scheme(); + } + } diff --git a/core/modules/image/src/ImageEffectBase.php b/core/modules/image/src/ImageEffectBase.php index 6714dd8..559a5a6 100644 --- a/core/modules/image/src/ImageEffectBase.php +++ b/core/modules/image/src/ImageEffectBase.php @@ -77,6 +77,18 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ + public function getDerivativeMimeType(&$mime_type) { + } + + /** + * {@inheritdoc} + */ + public function getDerivativeExtension(&$extension) { + } + + /** + * {@inheritdoc} + */ public function getSummary() { return array( '#markup' => '', diff --git a/core/modules/image/src/ImageEffectInterface.php b/core/modules/image/src/ImageEffectInterface.php index b0ea2ea..5b272f9 100644 --- a/core/modules/image/src/ImageEffectInterface.php +++ b/core/modules/image/src/ImageEffectInterface.php @@ -44,6 +44,22 @@ public function applyEffect(ImageInterface $image); public function transformDimensions(array &$dimensions); /** + * Determines the MIME type of the derivative without generating it. + * + * @param string $mime_type + * The MIME type to be set to the derivative's MIME type. + */ + public function getDerivativeMimeType(&$mime_type); + + /** + * Determines the extension of the derivative without generating it. + * + * @param string $extension + * The file extension to be set to the derivative's file extension. + */ + public function getDerivativeExtension(&$extension); + + /** * Returns a render array summarizing the configuration of the image effect. * * @return array diff --git a/core/modules/image/src/ImageStyleInterface.php b/core/modules/image/src/ImageStyleInterface.php index 7475b03..91a2e9c 100644 --- a/core/modules/image/src/ImageStyleInterface.php +++ b/core/modules/image/src/ImageStyleInterface.php @@ -132,6 +132,22 @@ public function createDerivative($original_uri, $derivative_uri); public function transformDimensions(array &$dimensions); /** + * Determines the MIME type of the derivative without generating it. + * + * @param string $mime_type + * The MIME type to be set to the derivative's MIME type. + */ + public function getDerivativeMimeType(&$mime_type); + + /** + * Determines the extension of the derivative without generating it. + * + * @param string $extension + * The file extension to be set to the derivative's file extension. + */ + public function getDerivativeExtension(&$extension); + + /** * Returns a specific image effect. * * @param string $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..50ceed3 --- /dev/null +++ b/core/modules/image/src/Plugin/ImageEffect/ConvertImageEffect.php @@ -0,0 +1,107 @@ +convert($this->configuration['extension'])) { + $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) { + // The guess method on the MIME type guesser needs a full filename. It does + // not work with just an extension. + $mime_type = \Drupal::service('file.mime_type.guesser.extension')->guess('test.' . $this->configuration['extension']); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeExtension(&$extension) { + $extension = $this->configuration['extension']; + } + + /** + * {@inheritdoc} + */ + public function getSummary() { + $summary = array( + '#markup' => Unicode::strtoupper($this->configuration['extension']), + ); + $summary += parent::getSummary(); + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array( + 'extension' => 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['extension'] = array( + '#type' => 'select', + '#title' => t('Extension'), + '#default_value' => $this->configuration['extension'], + '#required' => TRUE, + '#options' => $options, + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['extension'] = $form_state->getValue('extension'); + } + +} diff --git a/core/modules/image/src/Tests/ImageEffectsTest.php b/core/modules/image/src/Tests/ImageEffectsTest.php index ad5c5d8..1549d5b 100644 --- a/core/modules/image/src/Tests/ImageEffectsTest.php +++ b/core/modules/image/src/Tests/ImageEffectsTest.php @@ -89,6 +89,41 @@ function testCropEffect() { } /** + * Test the image_convert_effect() function. + */ + function testConvertEffect() { + // Test jpg. + $this->assertImageEffect('image_convert', array( + 'extension' => 'jpg', + )); + $this->assertToolkitOperationsCalled(array('convert')); + + // Check the parameters. + $calls = $this->imageTestGetAllCalls(); + $this->assertEqual($calls['convert'][0][0], 'jpg', 'Extension was passed correctly'); + + // Test gif. + $this->assertImageEffect('image_convert', array( + 'extension' => 'gif', + )); + $this->assertToolkitOperationsCalled(array('convert')); + + // Check the parameters. + $calls = $this->imageTestGetAllCalls(); + $this->assertEqual($calls['convert'][0][0], 'gif', 'Extension was passed correctly'); + + // Test png. + $this->assertImageEffect('image_convert', array( + 'extension' => 'png', + )); + $this->assertToolkitOperationsCalled(array('convert')); + + // Check the parameters. + $calls = $this->imageTestGetAllCalls(); + $this->assertEqual($calls['convert'][0][0], 'png', 'Extension 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 new file mode 100644 index 0000000..0a005cb --- /dev/null +++ b/core/modules/image/src/Tests/ImageStyleTest.php @@ -0,0 +1,264 @@ +getMockBuilder('\Drupal\image\ImageEffectManager') + ->disableOriginalConstructor() + ->getMock(); + $effectManager->expects($this->any()) + ->method('createInstance') + ->with($image_effect_id) + ->will($this->returnValue($image_effect)); + $default_stubs = array( + 'getImageEffectPluginManager', + 'fileUriScheme', + 'fileUriTarget', + 'fileDefaultScheme', + ); + $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_merge($default_stubs, $stubs)) + ->getMock(); + + $image_style->expects($this->any()) + ->method('getImageEffectPluginManager') + ->will($this->returnValue($effectManager)); + $image_style->expects($this->any()) + ->method('fileUriScheme') + ->will($this->returnCallback(array($this, 'fileUriScheme'))); + $image_style->expects($this->any()) + ->method('fileUriTarget') + ->will($this->returnCallback(array($this, 'fileUriTarget'))); + $image_style->expects($this->any()) + ->method('fileDefaultScheme') + ->will($this->returnCallback(array($this, 'fileDefaultScheme'))); + + return $image_style; + } + + /** + * {@inheritdoc} + */ + public function setUp() { + $this->entityTypeId = $this->randomMachineName(); + $this->provider = $this->randomMachineName(); + $this->entityType = $this->getMock('\Drupal\Core\Entity\EntityTypeInterface'); + $this->entityType->expects($this->any()) + ->method('getProvider') + ->will($this->returnValue($this->provider)); + $this->entityManager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface'); + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->with($this->entityTypeId) + ->will($this->returnValue($this->entityType)); + } + + /** + * @covers ::getDerivativeMimeType + */ + public function testGetDerivativeMimeType() { + $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('getDerivativeMimeType') + ->will($this->returnCallback(function (&$mime_type) { $mime_type = 'image/webp';})); + + $image_style = $this->getImageStyleMock($image_effect_id, $image_effect); + + $mime_types = array('image/jpeg', 'image/gif', 'image/png'); + foreach ($mime_types as $mime_type) { + $image_style->getDerivativeMimeType($mime_type); + $this->assertEquals($mime_type, 'image/webp'); + } + } + + /** + * @covers ::getDerivativeExtension + */ + public function testGetDerivativeExtension() { + $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';})); + + $image_style = $this->getImageStyleMock($image_effect_id, $image_effect); + + $extensions = array('jpeg', 'gif', 'png'); + foreach ($extensions as $extension) { + $image_style->getDerivativeExtension($extension); + $this->assertEquals($extension, 'png'); + } + } + + /** + * @covers ::buildUri + */ + public function testBuildUri() { + // Image style that changes the extension. + $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';})); + + $image_style = $this->getImageStyleMock($image_effect_id, $image_effect); + $this->assertEquals($image_style->buildUri('public://test.jpeg'), 'public://styles/' . $image_style->id() . '/public/test.jpeg.png'); + + // Image style that doesn't change the extension. + $image_effect_id = $this->randomMachineName(); + $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) {})); + + $image_style = $this->getImageStyleMock($image_effect_id, $image_effect); + $this->assertEquals($image_style->buildUri('public://test.jpeg'), 'public://styles/' . $image_style->id() . '/public/test.jpeg'); + } + + /** + * @covers ::getPathToken + */ + public function testGetPathToken() { + $logger = $this->getMockBuilder('\Psr\Log\LoggerInterface')->getMock(); + $private_key = $this->randomMachineName(); + $hash_salt = $this->randomMachineName(); + + // Image style that changes the extension. + $image_effect_id = $this->randomMachineName(); + $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';})); + + $image_style = $this->getImageStyleMock($image_effect_id, $image_effect, array('getPrivateKey', 'getHashSalt')); + $image_style->expects($this->any()) + ->method('getPrivateKey') + ->will($this->returnValue($private_key)); + $image_style->expects($this->any()) + ->method('getHashSalt') + ->will($this->returnValue($hash_salt)); + + // Assert the extension has been added to the URI before creating the token. + $this->assertEquals($image_style->getPathToken('public://test.jpeg.png'), $image_style->getPathToken('public://test.jpeg')); + $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg')); + $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg')); + + // Image style that doesn't change the extension. + $image_effect_id = $this->randomMachineName(); + $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) {})); + + $image_style = $this->getImageStyleMock($image_effect_id, $image_effect, array('getPrivateKey', 'getHashSalt')); + $image_style->expects($this->any()) + ->method('getPrivateKey') + ->will($this->returnValue($private_key)); + $image_style->expects($this->any()) + ->method('getHashSalt') + ->will($this->returnValue($hash_salt)); + // Assert no extension has been added to the uri before creating the token. + $this->assertNotEquals($image_style->getPathToken('public://test.jpeg.png'), $image_style->getPathToken('public://test.jpeg')); + $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg')); + $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg')); + } + + /** + * Mock function for ImageStyle::fileUriScheme(). + */ + public function fileUriScheme($uri) { + if (preg_match('/^([\w\-]+):\/\/|^(data):/', $uri, $matches)) { + // The scheme will always be the last element in the matches array. + return array_pop($matches); + } + + return FALSE; + } + + /** + * Mock function for ImageStyle::fileUriTarget(). + */ + public function fileUriTarget($uri) { + // Remove the scheme from the URI and remove erroneous leading or trailing, + // forward-slashes and backslashes. + $target = trim(preg_replace('/^[\w\-]+:\/\/|^data:/', '', $uri), '\/'); + + // If nothing was replaced, the URI doesn't have a valid scheme. + return $target !== $uri ? $target : FALSE; + } + + /** + * Mock function for ImageStyle::fileDefaultScheme(). + */ + public function fileDefaultScheme() { + return 'public'; + } + +} diff --git a/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php index 98ae863..14ee446 100644 --- a/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php +++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php @@ -375,7 +375,7 @@ public static function getSupportedExtensions() { * An array of available image types. An image type is represented by a PHP * IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG, IMAGETYPE_PNG, etc.). */ - protected static function supportedTypes() { + public static function supportedTypes() { return array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF); } } 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..d9c591e --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Convert.php @@ -0,0 +1,72 @@ + array( + 'description' => 'The new extension of the converted image', + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function validateArguments(array $arguments) { + if (!in_array($arguments['extension'], $this->getToolkit()->getSupportedExtensions())) { + throw new \InvalidArgumentException(String::format("Invalid extension (@value) specified for the image 'convert' operation", array('@value' => $arguments['extension']))); + } + return $arguments; + } + + /** + * {@inheritdoc} + */ + protected function execute(array $arguments = array()) { + // Make sure we have an image type. + $type = FALSE; + foreach ($this->getToolkit()->supportedTypes() as $type) { + if (image_type_to_extension($type) === $arguments['extension']) { + break; + } + } + + $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()); + + // Update the image object. + $this->getToolkit()->setType($type); + $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 c3f8e1c..40a8691 100644 --- a/core/modules/system/src/Tests/Image/ToolkitGdTest.php +++ b/core/modules/system/src/Tests/Image/ToolkitGdTest.php @@ -8,7 +8,7 @@ namespace Drupal\system\Tests\Image; use Drupal\Core\Image\ImageInterface; -use Drupal\simpletest\DrupalUnitTestBase; +use \Drupal\simpletest\KernelTestBase; use Drupal\Component\Utility\String; /** @@ -17,7 +17,7 @@ * * @group Image */ -class ToolkitGdTest extends DrupalUnitTestBase { +class ToolkitGdTest extends KernelTestBase { /** * The image factory service. @@ -174,6 +174,27 @@ function testManipulations() { 'height' => 8, 'corners' => array_fill(0, 4, $this->black), ), + 'convert_jpg' => array( + 'function' => 'convert', + 'width' => 40, + 'height' => 20, + 'arguments' => array('extension' => 'jpeg'), + 'corners' => $default_corners, + ), + 'convert_gif' => array( + 'function' => 'convert', + 'width' => 40, + 'height' => 20, + 'arguments' => array('extension' => 'gif'), + 'corners' => $default_corners, + ), + 'convert_png' => array( + 'function' => 'convert', + 'width' => 40, + 'height' => 20, + 'arguments' => array('extension' => 'png'), + 'corners' => $default_corners, + ), ); // 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/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/TestToolkit.php b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/TestToolkit.php index f72eaaa..b1b31a7 100644 --- a/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/TestToolkit.php +++ b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/TestToolkit.php @@ -255,7 +255,7 @@ public static function getSupportedExtensions() { * An array of available image types. An image type is represented by a PHP * IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG, IMAGETYPE_PNG, etc.). */ - protected static function supportedTypes() { + public static function supportedTypes() { return array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF); } diff --git a/core/tests/Drupal/Tests/Core/Image/ImageTest.php b/core/tests/Drupal/Tests/Core/Image/ImageTest.php index fa91f28..2f25038 100644 --- a/core/tests/Drupal/Tests/Core/Image/ImageTest.php +++ b/core/tests/Drupal/Tests/Core/Image/ImageTest.php @@ -389,6 +389,38 @@ public function testCrop() { } /** + * Tests \Drupal\Core\Image\Image::convert(). + */ + public function testConvert() { + // Test png. + $this->getTestImageForOperation('Convert'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->will($this->returnArgument(0)); + + $ret = $this->image->convert('png'); + $this->assertEquals('png', $ret['extension']); + + // Test jpg. + $this->getTestImageForOperation('Convert'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->will($this->returnArgument(0)); + + $ret = $this->image->convert('jpg'); + $this->assertEquals('jpg', $ret['extension']); + + // Test gif. + $this->getTestImageForOperation('Convert'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->will($this->returnArgument(0)); + + $ret = $this->image->convert('gif'); + $this->assertEquals('gif', $ret['extension']); + } + + /** * Tests \Drupal\Core\Image\Image::resize(). */ public function testResize() {