summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwebchick2014-10-09 18:46:03 (GMT)
committerwebchick2014-10-10 15:26:40 (GMT)
commit0366f4be9161dafd096d534a4be612d7d5feec01 (patch)
treecf7769d494a27e690c2cae951c93dbc3be2e2cbf
parent257b73e1ba4dbf47844fd202354f096c7c733f5e (diff)
Issue #2330899 by attiks, Jelle_S, fietserwin: Allow image effects to change the MIME type + extension, add a convert image effect.
-rw-r--r--core/lib/Drupal/Core/Image/Image.php7
-rw-r--r--core/lib/Drupal/Core/Image/ImageInterface.php15
-rw-r--r--core/modules/image/src/Controller/ImageStyleDownloadController.php16
-rw-r--r--core/modules/image/src/Entity/ImageStyle.php132
-rw-r--r--core/modules/image/src/ImageEffectBase.php10
-rw-r--r--core/modules/image/src/ImageEffectInterface.php12
-rw-r--r--core/modules/image/src/ImageStyleInterface.php12
-rw-r--r--core/modules/image/src/Plugin/ImageEffect/ConvertImageEffect.php98
-rw-r--r--core/modules/image/src/Tests/ImageEffectsTest.php15
-rw-r--r--core/modules/image/src/Tests/ImageStyleTest.php242
-rw-r--r--core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php23
-rw-r--r--core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Convert.php65
-rw-r--r--core/modules/system/src/Tests/Image/ToolkitGdTest.php40
-rw-r--r--core/modules/system/src/Tests/Image/ToolkitTestBase.php2
-rw-r--r--core/tests/Drupal/Tests/Core/Image/ImageTest.php13
15 files changed, 688 insertions, 14 deletions
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 @@ class Image implements ImageInterface {
/**
* {@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..3094c9c 100644
--- a/core/lib/Drupal/Core/Image/ImageInterface.php
+++ b/core/lib/Drupal/Core/Image/ImageInterface.php
@@ -149,6 +149,21 @@ interface ImageInterface {
public function scaleAndCrop($width, $height);
/**
+ * Instructs the toolkit to save the image in the format specified by the
+ * extension.
+ *
+ * @param string $extension
+ * The extension to convert to (e.g. 'jpeg' or 'png'). Allowed values depend
+ * on the current image toolkit.
+ *
+ * @return bool
+ * TRUE on success, FALSE on failure.
+ *
+ * @see \Drupal\Core\ImageToolkit\ImageToolkitInterface::getSupportedExtensions()
+ */
+ 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 @@ class ImageStyleDownloadController extends FileDownloadController {
// 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..0aea62d 100644
--- a/core/modules/image/src/Entity/ImageStyle.php
+++ b/core/modules/image/src/Entity/ImageStyle.php
@@ -172,15 +172,15 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface, Entity
* {@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,19 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface, Entity
/**
* {@inheritdoc}
*/
+ public function getDerivativeExtension($extension) {
+ foreach ($this->getEffects() as $effect) {
+ $extension = $effect->getDerivativeExtension($extension);
+ }
+ return $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 +346,7 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface, Entity
*/
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 +390,114 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface, Entity
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 while still allowing us 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) {
+ $original_extension = pathinfo($path, PATHINFO_EXTENSION);
+ $extension = $this->getDerivativeExtension($original_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 string
+ * '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..3b3bbb7 100644
--- a/core/modules/image/src/ImageEffectBase.php
+++ b/core/modules/image/src/ImageEffectBase.php
@@ -77,6 +77,16 @@ abstract class ImageEffectBase extends PluginBase implements ImageEffectInterfac
/**
* {@inheritdoc}
*/
+ public function getDerivativeExtension($extension) {
+ // Most image effects will not change the extension. This base
+ // implementation represents this behavior. Override this method if your
+ // image effect does change the extension.
+ return $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..790d7b2 100644
--- a/core/modules/image/src/ImageEffectInterface.php
+++ b/core/modules/image/src/ImageEffectInterface.php
@@ -44,6 +44,18 @@ interface ImageEffectInterface extends PluginInspectionInterface, ConfigurablePl
public function transformDimensions(array &$dimensions);
/**
+ * Returns the extension the derivative would have have after applying this
+ * image effect.
+ *
+ * @param string $extension
+ * The file extension the derivative has before applying.
+ *
+ * @return string
+ * The file extension after applying.
+ */
+ 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..94ef73c 100644
--- a/core/modules/image/src/ImageStyleInterface.php
+++ b/core/modules/image/src/ImageStyleInterface.php
@@ -132,6 +132,18 @@ interface ImageStyleInterface extends ConfigEntityInterface, ThirdPartySettingsI
public function transformDimensions(array &$dimensions);
/**
+ * Determines the extension of the derivative without generating it.
+ *
+ * @param string $extension
+ * The file extension of the original image.
+ *
+ * @return string
+ * The extension the derivative image will have, given the extension of the
+ * original.
+ */
+ 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..d88712c
--- /dev/null
+++ b/core/modules/image/src/Plugin/ImageEffect/ConvertImageEffect.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Plugin\ImageEffect\ConvertImageEffect.
+ */
+
+namespace Drupal\image\Plugin\ImageEffect;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Image\ImageInterface;
+use Drupal\image\ConfigurableImageEffectBase;
+
+/**
+ * Converts an image resource.
+ *
+ * @ImageEffect(
+ * id = "image_convert",
+ * label = @Translation("Convert"),
+ * description = @Translation("Converts an image between extensions (e.g. from PNG to JPEG).")
+ * )
+ */
+class ConvertImageEffect extends ConfigurableImageEffectBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transformDimensions(array &$dimensions) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyEffect(ImageInterface $image) {
+ if (!$image->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 getDerivativeExtension($extension) {
+ return $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..0f19f8b 100644
--- a/core/modules/image/src/Tests/ImageEffectsTest.php
+++ b/core/modules/image/src/Tests/ImageEffectsTest.php
@@ -89,6 +89,21 @@ class ImageEffectsTest extends ToolkitTestBase {
}
/**
+ * Tests the ConvertImageEffect plugin.
+ */
+ function testConvertEffect() {
+ // Test jpeg.
+ $this->assertImageEffect('image_convert', array(
+ 'extension' => 'jpeg',
+ ));
+ $this->assertToolkitOperationsCalled(array('convert'));
+
+ // Check the parameters.
+ $calls = $this->imageTestGetAllCalls();
+ $this->assertEqual($calls['convert'][0][0], 'jpeg', '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..97095ba
--- /dev/null
+++ b/core/modules/image/src/Tests/ImageStyleTest.php
@@ -0,0 +1,242 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\Tests\ImageStyleTest.
+ */
+
+namespace Drupal\image\Tests;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\Component\Utility\Crypt;
+
+/**
+ * @coversDefaultClass \Drupal\image\Entity\ImageStyle
+ *
+ * @group Image
+ */
+class ImageStyleTest extends UnitTestCase {
+
+ /**
+ * The entity type used for testing.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $entityType;
+
+ /**
+ * The entity manager used for testing.
+ *
+ * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $entityManager;
+
+ /**
+ * The ID of the type of the entity under test.
+ *
+ * @var string
+ */
+ protected $entityTypeId;
+
+ /**
+ * Gets a mocked image style for testing.
+ *
+ * @param string $image_effect_id
+ * The image effect ID.
+ * @param \Drupal\image\ImageEffectInterface|\PHPUnit_Framework_MockObject_MockObject $image_effect
+ * The image effect used for testing.
+ *
+ * @return \Drupal\image\ImageStyleInterface|\Drupal\image\ImageStyleInterface
+ * The mocked image style.
+ */
+ protected function getImageStyleMock($image_effect_id, $image_effect, $stubs = array()) {
+ $effectManager = $this->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 ::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->returnValue('png'));
+
+ $image_style = $this->getImageStyleMock($image_effect_id, $image_effect);
+
+ $extensions = array('jpeg', 'gif', 'png');
+ foreach ($extensions as $extension) {
+ $extensionReturned = $image_style->getDerivativeExtension($extension);
+ $this->assertEquals($extensionReturned, '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->returnValue('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->returnArgument(0));
+
+ $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->returnValue('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->returnArgument(0));
+
+ $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..473972f 100644
--- a/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
+++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php
@@ -369,6 +369,29 @@ class GDToolkit extends ImageToolkitBase {
}
/**
+ * Returns the IMAGETYPE_xxx constant for the given extension.
+ *
+ * This is the reverse of the image_type_to_extension() function.
+ *
+ * @param string $extension
+ * The extension to get the IMAGETYPE_xxx constant for.
+ *
+ * @return int
+ * The IMAGETYPE_xxx constant for the given extension, or IMAGETYPE_UNKNOWN
+ * for unsupported extensions.
+ *
+ * @see image_type_to_extension()
+ */
+ public function extensionToImageType($extension) {
+ foreach ($this->supportedTypes() as $type) {
+ if (image_type_to_extension($type, FALSE) === $extension) {
+ return $type;
+ }
+ }
+ return IMAGETYPE_UNKNOWN;
+ }
+
+ /**
* Returns a list of image types supported by the toolkit.
*
* @return array
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..49f7fbe
--- /dev/null
+++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Convert.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\Convert.
+ */
+
+namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
+
+use Drupal\Component\Utility\String;
+
+/**
+ * Defines GD2 convert operation.
+ *
+ * @ImageToolkitOperation(
+ * id = "gd_convert",
+ * toolkit = "gd",
+ * operation = "convert",
+ * label = @Translation("Convert"),
+ * description = @Translation("Instructs the toolkit to save the image with a specified extension.")
+ * )
+ */
+class Convert extends GDImageToolkitOperationBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function arguments() {
+ return array(
+ 'extension' => 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) {
+ $type = $this->getToolkit()->extensionToImageType($arguments['extension']);
+
+ $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..4639e42 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 @@ use Drupal\Component\Utility\String;
*
* @group Image
*/
-class ToolkitGdTest extends DrupalUnitTestBase {
+class ToolkitGdTest extends KernelTestBase {
/**
* The image factory service.
@@ -174,6 +174,27 @@ class ToolkitGdTest extends DrupalUnitTestBase {
'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.
@@ -240,6 +261,7 @@ class ToolkitGdTest extends DrupalUnitTestBase {
$this->fail(String::format('Could not load image %file.', array('%file' => $file)));
continue 2;
}
+ $image_original_type = $image->getToolkit()->getType();
// All images should be converted to truecolor when loaded.
$image_truecolor = imageistruecolor($toolkit->getResource());
@@ -291,8 +313,9 @@ class ToolkitGdTest extends DrupalUnitTestBase {
$this->assertTrue($correct_dimensions_real, String::format('Image %file after %action action has proper dimensions.', array('%file' => $file, '%action' => $op)));
$this->assertTrue($correct_dimensions_object, String::format('Image %file object after %action action is reporting the proper height and width values.', array('%file' => $file, '%action' => $op)));
- // JPEG colors will always be messed up due to compression.
- if ($image->getToolkit()->getType() != IMAGETYPE_JPEG) {
+ // JPEG colors will always be messed up due to compression. So we skip
+ // these tests if the original or the result is in jpeg format.
+ if ($image->getToolkit()->getType() != IMAGETYPE_JPEG && $image_original_type != IMAGETYPE_JPEG) {
// Now check each of the corners to ensure color correctness.
foreach ($values['corners'] as $key => $corner) {
// The test gif that does not have transparency has yellow where the
@@ -320,8 +343,13 @@ class ToolkitGdTest extends DrupalUnitTestBase {
break;
}
$color = $this->getPixelColor($image, $x, $y);
- $correct_colors = $this->colorsAreEqual($color, $corner);
- $this->assertTrue($correct_colors, String::format('Image %file object after %action action has the correct color placement at corner %corner.', array('%file' => $file, '%action' => $op, '%corner' => $key)));
+ // We also skip the color test for transparency for gif <-> png
+ // conversion. The convert operation cannot handle that correctly.
+ if ($image->getToolkit()->getType() == $image_original_type || $corner != $this->transparent) {
+ $correct_colors = $this->colorsAreEqual($color, $corner);
+ $this->assertTrue($correct_colors, String::format('Image %file object after %action action has the correct color placement at corner %corner.',
+ array('%file' => $file, '%action' => $op, '%corner' => $key)));
+ }
}
}
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 @@ abstract class ToolkitTestBase extends WebTestBase {
'scale',
'scale_and_crop',
'my_operation',
+ 'convert',
);
if (count(array_intersect($expected, $operations)) > 0 && !in_array('apply', $expected)) {
$expected[] = 'apply';
@@ -136,6 +137,7 @@ abstract class ToolkitTestBase extends WebTestBase {
'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..e81bb76 100644
--- a/core/tests/Drupal/Tests/Core/Image/ImageTest.php
+++ b/core/tests/Drupal/Tests/Core/Image/ImageTest.php
@@ -389,6 +389,19 @@ class ImageTest extends UnitTestCase {
}
/**
+ * 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['extension']);
+ }
+
+ /**
* Tests \Drupal\Core\Image\Image::resize().
*/
public function testResize() {