diff --git a/core/core.services.yml b/core/core.services.yml index ac24e17..dd16fbd 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -607,7 +607,10 @@ services: - { name: event_subscriber } image.toolkit.manager: class: Drupal\Core\ImageToolkit\ImageToolkitManager - arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@config.factory', '@module_handler'] + arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@config.factory', '@module_handler', '@image.toolkit.operation.manager'] + image.toolkit.operation.manager: + class: Drupal\Core\ImageToolkit\ImageToolkitOperationManager + parent: default_plugin_manager image.factory: class: Drupal\Core\Image\ImageFactory arguments: ['@image.toolkit.manager'] diff --git a/core/lib/Drupal/Core/Image/Image.php b/core/lib/Drupal/Core/Image/Image.php index f7a5cd7..deff4b5 100644 --- a/core/lib/Drupal/Core/Image/Image.php +++ b/core/lib/Drupal/Core/Image/Image.php @@ -266,39 +266,10 @@ protected function processInfo() { } /** - * Passes through calls that represent image toolkit operations onto the - * image toolkit. - * - * This is a temporary solution to keep patches reviewable. The __call() - * method will be replaced in https://drupal.org/node/2110499 with a new - * interface method ImageInterface::apply(). An image operation will be - * performed as in the next example: - * @code - * $image = new Image($path, $toolkit); - * $image->apply('scale', array('width' => 50, 'height' => 100)); - * @endcode - * Also in https://drupal.org/node/2110499 operation arguments sent to toolkit - * will be moved to a keyed array, unifying the interface of toolkit - * operations. - * - * @todo Drop this in https://drupal.org/node/2110499 in favor of new apply(). + * {@inheritdoc} */ - public function __call($method, $arguments) { - // @todo Temporary to avoid that legacy GD setResource(), getResource(), - // hasResource() methods moved to GD toolkit in #2103621 get invoked - // from this class anyway through the magic __call. Will be removed - // through https://drupal.org/node/2110499, when call_user_func_array() - // will be replaced by $this->toolkit->apply($name, $this, $arguments). - if (in_array($method, array('setResource', 'getResource', 'hasResource'))) { - throw new \BadMethodCallException(); - } - if (is_callable(array($this->toolkit, $method))) { - // @todo In https://drupal.org/node/2110499, call_user_func_array() will - // be replaced by $this->toolkit->apply($name, $this, $arguments). - array_unshift($arguments, $this); - return call_user_func_array(array($this->toolkit, $method), $arguments); - } - throw new \BadMethodCallException(); + public function apply($operation, array $arguments = array()) { + return $this->toolkit->apply($operation, $this, $arguments); } /** diff --git a/core/lib/Drupal/Core/Image/ImageInterface.php b/core/lib/Drupal/Core/Image/ImageInterface.php index 151bef1..92694a6 100644 --- a/core/lib/Drupal/Core/Image/ImageInterface.php +++ b/core/lib/Drupal/Core/Image/ImageInterface.php @@ -133,6 +133,22 @@ public function getToolkit(); public function getToolkitId(); /** + * Applies a toolkit operation to the image. + * + * The operation is deferred to the active toolkit. + * + * @param string $operation + * The operation to be performed against the image. + * @param array $arguments + * An associative array with required arguments to be passed to the toolkit + * operation. E.g. array('width' => 50, 'height' => 100, 'upscale' => TRUE). + * + * @return bool + * TRUE on success, FALSE on failure. + */ + public function apply($operation, array $arguments = array()); + + /** * Closes the image and saves the changes to a file. * * @param string|null $destination diff --git a/core/lib/Drupal/Core/ImageToolkit/Annotation/ImageToolkitOperation.php b/core/lib/Drupal/Core/ImageToolkit/Annotation/ImageToolkitOperation.php new file mode 100644 index 0000000..0db12e8 --- /dev/null +++ b/core/lib/Drupal/Core/ImageToolkit/Annotation/ImageToolkitOperation.php @@ -0,0 +1,78 @@ +operationManager = $operation_manager; + } + + /** * {@inheritdoc} */ public function getRequirements() { return array(); } + /** + * Return a toolkit operation plugin instance. + * + * @param string $operation + * The toolkit operation requested. + * + * @return \Drupal\Core\ImageToolkit\ImageToolkitOperationInterface + * An instance of the requested toolkit operation plugin. + */ + protected function getToolkitOperation($operation) { + $plugin_id = $this->operationManager->getToolkitOperationPluginId($this->getPluginId(), $operation); + return $this->operationManager->createInstance($plugin_id, array(), $this); + } + + /** + * @inheritdoc + */ + public function apply($operation, ImageInterface $image, array $arguments = array()) { + $plugin = $this->getToolkitOperation($operation); + + // Validate and prepare arguments before applying the operation. The + // operation might be cancelled if prepare() decides that the final image + // will not change. + if ($plugin->prepare($image, $arguments) !== FALSE) { + return $plugin->apply($image, $arguments); + } + + // No operation is required because the target image will be the same as the + // original but the result is still successful. + return TRUE; + } + } diff --git a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitInterface.php b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitInterface.php index 61c6ece..7926302 100644 --- a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitInterface.php +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitInterface.php @@ -60,78 +60,6 @@ public function settingsForm(); public function settingsFormSubmit($form, &$form_state); /** - * Scales an image to the specified size. - * - * @param \Drupal\Core\Image\ImageInterface $image - * An image object. The $image->resource, $image->info['width'], and - * $image->info['height'] values will be modified by this call. - * @param int $width - * The new width of the resized image, in pixels. - * @param int $height - * The new height of the resized image, in pixels. - * - * @return bool - * TRUE or FALSE, based on success. - */ - public function resize(ImageInterface $image, $width, $height); - - /** - * Rotates an image the given number of degrees. - * - * @param \Drupal\Core\Image\ImageInterface $image - * An image object. The $image->resource, $image->info['width'], and - * $image->info['height'] values will be modified by this call. - * @param int $degrees - * The number of (clockwise) degrees to rotate the image. - * @param string $background - * (optional) An hexadecimal integer specifying the background color to use - * for the uncovered area of the image after the rotation. E.g. 0x000000 for - * black, 0xff00ff for magenta, and 0xffffff for white. For images that - * support transparency, this will default to transparent. Otherwise it will - * be white. - * - * @return bool - * TRUE or FALSE, based on success. - */ - public function rotate(ImageInterface $image, $degrees, $background = NULL); - - /** - * Crops an image. - * - * @param \Drupal\Core\Image\ImageInterface $image - * An image object. The $image->resource, $image->info['width'], and - * $image->info['height'] values will be modified by this call. - * @param int $x - * The starting x offset at which to start the crop, in pixels. - * @param int $y - * The starting y offset at which to start the crop, in pixels. - * @param int $width - * The width of the cropped area, in pixels. - * @param int $height - * The height of the cropped area, in pixels. - * - * @return bool - * TRUE or FALSE, based on success. - * - * @see image_crop() - */ - public function crop(ImageInterface $image, $x, $y, $width, $height); - - /** - * Converts an image resource to grayscale. - * - * Note that transparent GIFs loose transparency when desaturated. - * - * @param \Drupal\Core\Image\ImageInterface $image - * An image object. The $image->resource value will be modified by this - * call. - * - * @return bool - * TRUE or FALSE, based on success. - */ - public function desaturate(ImageInterface $image); - - /** * Writes an image resource to a destination file. * * @param \Drupal\Core\Image\ImageInterface $image @@ -145,49 +73,6 @@ public function desaturate(ImageInterface $image); public function save(ImageInterface $image, $destination); /** - * Scales an image while maintaining aspect ratio. - * - * The resulting image can be smaller for one or both target dimensions. - * - * @param \Drupal\Core\Image\ImageInterface $image - * An image object. - * @param int $width - * (optional) The target width, in pixels. This value is omitted then the - * scaling will based only on the height value. - * @param int $height - * (optional) The target height, in pixels. This value is omitted then the - * scaling will based only on the width value. - * @param bool $upscale - * (optional) Boolean indicating that files smaller than the dimensions will - * be scaled up. This generally results in a low quality image. - * - * @return bool - * TRUE on success, FALSE on failure. - */ - public function scale(ImageInterface $image, $width = NULL, $height = NULL, $upscale = FALSE); - - /** - * Scales an image to the exact width and height given. - * - * This function achieves the target aspect ratio by cropping the original - * image equally on both sides, or equally on the top and bottom. This - * function is useful to create uniform sized avatars from larger images. - * - * The resulting image always has the exact target dimensions. - * - * @param \Drupal\Core\Image\ImageInterface $image - * An image object. - * @param int $width - * The target width, in pixels. - * @param int $height - * The target height, in pixels. - * - * @return bool - * TRUE on success, FALSE on failure. - */ - public function scaleAndCrop(ImageInterface $image, $width, $height); - - /** * Gets details about an image. * * @param \Drupal\Core\Image\ImageInterface $image @@ -236,4 +121,20 @@ public static function isAvailable(); */ public static function supportedTypes(); + /** + * Applies a toolkit specific operation to an image. + * + * @param string $operation + * The toolkit operation to be processed. + * @param \Drupal\Core\Image\ImageInterface $image + * An image object. + * @param array $arguments + * An associative array with required arguments to be passed to the toolkit + * operation. E.g. array('width' => 50, 'height' => 100, 'upscale' => TRUE). + * + * @return mixed + * TRUE if the operation was performed successfully, FALSE otherwise. + */ + public function apply($operation, ImageInterface $image, array $arguments = array()); + } diff --git a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitManager.php b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitManager.php index e503558..4671042 100644 --- a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitManager.php +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitManager.php @@ -10,8 +10,10 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface; use Drupal\Core\Language\LanguageManager; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Component\Plugin\Factory\DefaultFactory; /** * Manages toolkit plugins. @@ -26,6 +28,13 @@ class ImageToolkitManager extends DefaultPluginManager { protected $configFactory; /** + * The image toolkit operation manager. + * + * @var \Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface + */ + protected $operationManager; + + /** * Constructs the ImageToolkitManager object. * * @param \Traversable $namespaces @@ -39,12 +48,15 @@ class ImageToolkitManager extends DefaultPluginManager { * The config factory. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. + * @param \Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface $operation_manager + * The toolkit operation manager. */ - public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler) { + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ImageToolkitOperationManagerInterface $operation_manager) { parent::__construct('Plugin/ImageToolkit', $namespaces, $module_handler, 'Drupal\Core\ImageToolkit\Annotation\ImageToolkit'); $this->setCacheBackend($cache_backend, $language_manager, 'image_toolkit_plugins'); $this->configFactory = $config_factory; + $this->operationManager = $operation_manager; } /** @@ -100,4 +112,14 @@ public function getAvailableToolkits() { return $output; } + + /** + * {@inheritdoc} + */ + public function createInstance($plugin_id, array $configuration = array()) { + $plugin_definition = $this->getDefinition($plugin_id); + $plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition); + return new $plugin_class($configuration, $plugin_id, $plugin_definition, $this->operationManager); + } + } diff --git a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationBase.php b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationBase.php new file mode 100644 index 0000000..81b103b --- /dev/null +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationBase.php @@ -0,0 +1,106 @@ +toolkit = $toolkit; + } + + /** + * Validates and normalizes arguments. + * + * @param array $arguments + * An associative array of arguments to be used by the toolkit operation. + * + * @throws \InvalidArgumentException. + */ + protected function validateArguments(array $arguments) { + $manager = \Drupal::typedDataManager(); + + // Get arguments definition. + $definition = $this->getPluginDefinition(); + $definition = $definition['arguments']; + + foreach ($definition as $id => $argument) { + + // The defined argument has been provided. + if (array_key_exists($id, $arguments)) { + $data_definition = DataDefinition::create($argument['type']); + $typed_data = $manager->create($data_definition, $arguments[$id]); + $violations = $typed_data->validate(); + + // There are violations against defined TypedData. + if ($violations->count()) { + $fail = TRUE; + + // One more try: Check if casted value passes. + if (method_exists($typed_data, 'getCastedValue')) { + $casted = $typed_data->getCastedValue(); + $typed_data->setValue($casted); + $fail = (bool) $typed_data->validate()->count(); + } + + if ($fail) { + $message_arguments = array('@value' => $arguments[$id], '@plugin' => $this->getPluginId(), '@arg' => $id, '@type' => $argument['type']); + $message = String::format("The value '@value' provided to '@plugin' plugin in '@arg' argument cannot be casted as '@type'.", $message_arguments); + throw new \InvalidArgumentException($message); + } + } + } + // A defined argument hasn't been passed. + else { + if ($argument['required']) { + // If the argument is required throw an exception. + $message = String::format("Argument '@arg' expected by plugin '@plugin' but not passed.", array('@arg' => $id, '@plugin' => $this->getPluginId())); + throw new \InvalidArgumentException($message); + } + else { + // Optional argument? Assign the default value. + $arguments[$id] = $argument['default']; + } + } + } + + return $arguments; + } + + /** + * {@inheritdoc} + */ + public function prepare(ImageInterface $image, array &$arguments) { + $arguments = $this->validateArguments($arguments); + } + +} diff --git a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationInterface.php b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationInterface.php new file mode 100644 index 0000000..e0b9714 --- /dev/null +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationInterface.php @@ -0,0 +1,52 @@ +alterInfo('image_toolkit_operation'); + $this->setCacheBackend($cache_backend, $language_manager, 'image_toolkit_operation_plugins'); + } + + /** + * {@inheritdoc} + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + // Get the toolkit ID from the image operation plugin class namespace. Each + // plugin is placed under Plugin\ImageToolkit\{plugin_id}. + $path = explode('\\', $definition['class']); + $definition['toolkit'] = $path[count($path) - 2]; + + foreach ($definition['arguments'] as $id => &$argument) { + // Set argument 'required' as TRUE if hasn't been specified in annotation. + $argument['required'] = isset($argument['required']) ? $argument['required'] : TRUE; + + // If the argument is optional a 'default' should be provided. + if (!$argument['required'] && !array_key_exists('default', $argument)) { + $message = String::format("Argument '@arg' in plugin '@plugin' is optional but doesn't provide a default value.", array('@arg' => $id, '@plugin' => $definition['id'])); + throw new PluginException($message); + } + } + } + + /** + * {@inheritdoc} + */ + public function getToolkitOperationPluginId($toolkit, $operation) { + $definitions = $this->getDefinitions(); + + $definitions = array_filter($definitions, + function ($definition) use ($toolkit, $operation) { + return $definition['toolkit'] == $toolkit && $definition['operation'] == $operation; + } + ); + + if (!$definitions) { + $message = String::format("No image operation plugin for '@toolkit' toolkit and '@operation' operation.", array('@toolkit' => $toolkit, '@operation' => $operation)); + throw new UnknownPluginException('', $message); + } + else { + // Pickup the first plugin found. + // @todo In https://drupal.org/node/2110591 we'll return here the UI + // selected plugin or the first found if missed. + $definition = reset($definitions); + return $definition['id']; + } + } + + /** + * {@inheritdoc} + */ + public function createInstance($plugin_id, array $configuration = array(), ImageToolkitInterface $toolkit = NULL, TypedDataManager $typed_data_manager = NULL) { + $plugin_definition = $this->getDefinition($plugin_id); + $plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition); + return new $plugin_class($configuration, $plugin_id, $plugin_definition, $toolkit); + } + +} diff --git a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationManagerInterface.php b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationManagerInterface.php new file mode 100644 index 0000000..57f5a14 --- /dev/null +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationManagerInterface.php @@ -0,0 +1,30 @@ +get($file->getFileUri()); if ($image->isExisting()) { - $image->scale($width, $height); - $image->save(); - $file->filesize = $image->getFileSize(); - drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions))); + if ($image->apply('scale', array('width' => $width, 'height' => $height))) { + $image->save(); + $file->filesize = $image->getFileSize(); + drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions))); + } + else { + $errors[] = t('The image exceeds the maximum allowed dimensions and an attempt to resize it failed.'); + } } else { $errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions)); diff --git a/core/modules/file/lib/Drupal/file/Tests/ValidatorTest.php b/core/modules/file/lib/Drupal/file/Tests/ValidatorTest.php index f054602..17da383 100644 --- a/core/modules/file/lib/Drupal/file/Tests/ValidatorTest.php +++ b/core/modules/file/lib/Drupal/file/Tests/ValidatorTest.php @@ -91,6 +91,13 @@ function testFileValidateImageResolution() { $this->assertTrue($image->getWidth() <= 10, 'Image scaled to correct width.', 'File'); $this->assertTrue($image->getHeight() <= 5, 'Image scaled to correct height.', 'File'); + // Once again, now with negative width and height to force an error. + copy('core/misc/druplicon.png', 'temporary://druplicon.png'); + $this->image->setFileUri('temporary://druplicon.png'); + + $errors = file_validate_image_resolution($this->image, '-10x-5'); + $this->assertEqual(count($errors), 1, 'An error reported for an oversized image that can not be scaled down.', 'File'); + drupal_unlink('temporary://druplicon.png'); } else { diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/CropImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/CropImageEffect.php index 8b1857b..40cd0de 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/CropImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/CropImageEffect.php @@ -27,7 +27,8 @@ public function applyEffect(ImageInterface $image) { list($x, $y) = explode('-', $this->configuration['anchor']); $x = image_filter_keyword($x, $image->getWidth(), $this->configuration['width']); $y = image_filter_keyword($y, $image->getHeight(), $this->configuration['height']); - if (!$image->crop($x, $y, $this->configuration['width'], $this->configuration['height'])) { + $data = array('x' => $x, 'y' => $y, 'width' => $this->configuration['width'], 'height' => $this->configuration['height']); + if (!$image->apply('crop', $data)) { watchdog('image', 'Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php index 4d899c6..62311f3 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php @@ -31,7 +31,7 @@ public function transformDimensions(array &$dimensions) { * {@inheritdoc} */ public function applyEffect(ImageInterface $image) { - if (!$image->desaturate()) { + if (!$image->apply('desaturate')) { watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php index da9b176..6c02ee1 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php @@ -26,7 +26,8 @@ class ResizeImageEffect extends ImageEffectBase implements ConfigurableImageEffe * {@inheritdoc} */ public function applyEffect(ImageInterface $image) { - if (!$image->resize($this->configuration['width'], $this->configuration['height'])) { + $data = array('width' => $this->configuration['width'], 'height' => $this->configuration['height']); + if (!$image->apply('resize', $data)) { watchdog('image', 'Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php index b0e6bf3..d14d260 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php @@ -45,7 +45,8 @@ public function applyEffect(ImageInterface $image) { $this->configuration['degrees'] = rand(-1 * $degrees, $degrees); } - if (!$image->rotate($this->configuration['degrees'], $this->configuration['bgcolor'])) { + $data = array('degrees' => $this->configuration['degrees'], 'background' => $this->configuration['bgcolor']); + if (!$image->apply('rotate', $data)) { watchdog('image', 'Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleAndCropImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleAndCropImageEffect.php index 7fed9b1..4aa77f4 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleAndCropImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleAndCropImageEffect.php @@ -24,7 +24,8 @@ class ScaleAndCropImageEffect extends ResizeImageEffect { * {@inheritdoc} */ public function applyEffect(ImageInterface $image) { - if (!$image->scaleAndCrop($this->configuration['width'], $this->configuration['height'])) { + $data = array('width' => $this->configuration['width'], 'height' => $this->configuration['height']); + if (!$image->apply('scale_and_crop', $data)) { watchdog('image', 'Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php index 4316bd0..a8b0565 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php @@ -25,7 +25,8 @@ class ScaleImageEffect extends ResizeImageEffect { * {@inheritdoc} */ public function applyEffect(ImageInterface $image) { - if (!$image->scale($this->configuration['width'], $this->configuration['height'], $this->configuration['upscale'])) { + $data = array('width' => $this->configuration['width'], 'height' => $this->configuration['height'], 'upscale' => $this->configuration['upscale']); + if (!$image->apply('scale', $data)) { watchdog('image', 'Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php index 8afcb22..4c692fe 100644 --- a/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php +++ b/core/modules/image/lib/Drupal/image/Tests/ImageEffectsTest.php @@ -103,12 +103,12 @@ function testScaleAndCropEffect() { 'width' => 5, 'height' => 10, )); - $this->assertToolkitOperationsCalled(array('scaleAndCrop')); + $this->assertToolkitOperationsCalled(array('scale_and_crop')); // Check the parameters. $calls = $this->imageTestGetAllCalls(); - $this->assertEqual($calls['scaleAndCrop'][0][1], 5, 'Width was computed and passed correctly'); - $this->assertEqual($calls['scaleAndCrop'][0][2], 10, 'Height was computed and passed correctly'); + $this->assertEqual($calls['scale_and_crop'][0][1], 5, 'Width was computed and passed correctly'); + $this->assertEqual($calls['scale_and_crop'][0][2], 10, 'Height was computed and passed correctly'); } /** diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/GDToolkit.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/GDToolkit.php index 1bd1d36..742bbec 100644 --- a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/GDToolkit.php +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/GDToolkit.php @@ -2,14 +2,13 @@ /** * @file - * Contains \Drupal\system\Plugin\ImageToolkit\GDToolkit;. + * Contains \Drupal\system\Plugin\ImageToolkit\GDToolkit. */ namespace Drupal\system\Plugin\ImageToolkit; use Drupal\Core\Image\ImageInterface; use Drupal\Core\ImageToolkit\ImageToolkitBase; -use Drupal\Component\Utility\Image as ImageUtility; /** * Defines the GD2 toolkit for image manipulation within Drupal. @@ -78,158 +77,6 @@ public function settingsFormSubmit($form, &$form_state) { } /** - * {@inheritdoc} - */ - public function resize(ImageInterface $image, $width, $height) { - // @todo Dimensions computation will be moved into a dedicated functionality - // in https://drupal.org/node/2108307. - $width = (int) round($width); - $height = (int) round($height); - - $res = $this->createTmp($image->getType(), $width, $height); - - if (!imagecopyresampled($res, $this->getResource(), 0, 0, 0, 0, $width, $height, $image->getWidth(), $image->getHeight())) { - return FALSE; - } - - imagedestroy($this->getResource()); - // Update image object. - $this->setResource($res); - $image - ->setWidth($width) - ->setHeight($height); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function rotate(ImageInterface $image, $degrees, $background = NULL) { - // PHP installations using non-bundled GD do not have imagerotate. - if (!function_exists('imagerotate')) { - watchdog('image', 'The image %file could not be rotated because the imagerotate() function is not available in this PHP installation.', array('%file' => $image->getSource())); - return FALSE; - } - - // Convert the hexadecimal background value to a color index value. - if (isset($background)) { - $rgb = array(); - for ($i = 16; $i >= 0; $i -= 8) { - $rgb[] = (($background >> $i) & 0xFF); - } - $background = imagecolorallocatealpha($this->getResource(), $rgb[0], $rgb[1], $rgb[2], 0); - } - // Set the background color as transparent if $background is NULL. - else { - // Get the current transparent color. - $background = imagecolortransparent($this->getResource()); - - // If no transparent colors, use white. - if ($background == 0) { - $background = imagecolorallocatealpha($this->getResource(), 255, 255, 255, 0); - } - } - - // Images are assigned a new color palette when rotating, removing any - // transparency flags. For GIF images, keep a record of the transparent color. - if ($image->getType() == IMAGETYPE_GIF) { - $transparent_index = imagecolortransparent($this->getResource()); - if ($transparent_index != 0) { - $transparent_gif_color = imagecolorsforindex($this->getResource(), $transparent_index); - } - } - - $this->setResource(imagerotate($this->getResource(), 360 - $degrees, $background)); - - // GIFs need to reassign the transparent color after performing the rotate. - if (isset($transparent_gif_color)) { - $background = imagecolorexactalpha($this->getResource(), $transparent_gif_color['red'], $transparent_gif_color['green'], $transparent_gif_color['blue'], $transparent_gif_color['alpha']); - imagecolortransparent($this->getResource(), $background); - } - - $image - ->setWidth(imagesx($this->getResource())) - ->setHeight(imagesy($this->getResource())); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function crop(ImageInterface $image, $x, $y, $width, $height) { - // @todo Dimensions computation will be moved into a dedicated functionality - // in https://drupal.org/node/2108307. - $aspect = $image->getHeight() / $image->getWidth(); - $height = empty($height) ? $width * $aspect : $height; - $width = empty($width) ? $height / $aspect : $width; - $width = (int) round($width); - $height = (int) round($height); - - $res = $this->createTmp($image->getType(), $width, $height); - - if (!imagecopyresampled($res, $this->getResource(), 0, 0, $x, $y, $width, $height, $width, $height)) { - return FALSE; - } - - // Destroy the original image and return the modified image. - imagedestroy($this->getResource()); - $this->setResource($res); - $image - ->setWidth($width) - ->setHeight($height); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function desaturate(ImageInterface $image) { - // PHP installations using non-bundled GD do not have imagefilter. - if (!function_exists('imagefilter')) { - watchdog('image', 'The image %file could not be desaturated because the imagefilter() function is not available in this PHP installation.', array('%file' => $image->getSource())); - return FALSE; - } - - return imagefilter($this->getResource(), IMG_FILTER_GRAYSCALE); - } - - /** - * {@inheritdoc} - */ - public function scale(ImageInterface $image, $width = NULL, $height = NULL, $upscale = FALSE) { - // @todo Dimensions computation will be moved into a dedicated functionality - // in https://drupal.org/node/2108307. - $dimensions = array( - 'width' => $image->getWidth(), - 'height' => $image->getHeight(), - ); - - // Scale the dimensions - if they don't change then just return success. - if (!ImageUtility::scaleDimensions($dimensions, $width, $height, $upscale)) { - return TRUE; - } - - return $this->resize($image, $dimensions['width'], $dimensions['height']); - } - - /** - * {@inheritdoc} - */ - public function scaleAndCrop(ImageInterface $image, $width, $height) { - // @todo Dimensions computation will be moved into a dedicated functionality - // in https://drupal.org/node/2108307. - $scale = max($width / $image->getWidth(), $height / $image->getHeight()); - $x = ($image->getWidth() * $scale - $width) / 2; - $y = ($image->getHeight() * $scale - $height) / 2; - - if ($this->resize($image, $image->getWidth() * $scale, $image->getHeight() * $scale)) { - return $this->crop($image, $x, $y, $width, $height); - } - - return FALSE; - } - - /** * Creates a resource from a file. * * @param string $source diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Crop.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Crop.php new file mode 100644 index 0000000..fa03ecf --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Crop.php @@ -0,0 +1,79 @@ +getHeight() / $image->getWidth(); + $arguments['height'] = empty($arguments['height']) ? $arguments['width'] * $aspect : $arguments['height']; + $arguments['width'] = empty($arguments['width']) ? $arguments['height'] / $aspect : $arguments['width']; + + // Assure integers for all arguments. + foreach (array('x', 'y', 'width', 'height') as $key) { + $arguments[$key] = (int) round($arguments[$key]); + } + } + + /** + * {@inheritdoc} + */ + public function apply(ImageInterface $image, array $arguments = array()) { + $res = $this->toolkit->createTmp($image->getType(), $arguments['width'], $arguments['height']); + + if (!imagecopyresampled($res, $image->getToolkit()->getResource(), 0, 0, $arguments['x'], $arguments['y'], $arguments['width'], $arguments['height'], $arguments['width'], $arguments['height'])) { + return FALSE; + } + + // Destroy the original image and return the modified image. + imagedestroy($image->getToolkit()->getResource()); + $image->getToolkit()->setResource($res); + $image + ->setWidth($arguments['width']) + ->setHeight($arguments['height']); + return TRUE; + } + +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Desaturate.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Desaturate.php new file mode 100644 index 0000000..4a51a6e --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Desaturate.php @@ -0,0 +1,39 @@ + $image->getSource())); + return FALSE; + } + + return imagefilter($image->getToolkit()->getResource(), IMG_FILTER_GRAYSCALE); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Resize.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Resize.php new file mode 100644 index 0000000..663e4e3 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Resize.php @@ -0,0 +1,70 @@ +toolkit->createTmp($image->getType(), $arguments['width'], $arguments['height']); + + if (!imagecopyresampled($res, $image->getToolkit()->getResource(), 0, 0, 0, 0, $arguments['width'], $arguments['height'], $image->getWidth(), $image->getHeight())) { + return FALSE; + } + + imagedestroy($image->getToolkit()->getResource()); + // Update image object. + $image->getToolkit()->setResource($res); + $image + ->setWidth($arguments['width']) + ->setHeight($arguments['height']); + return TRUE; + } + +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Rotate.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Rotate.php new file mode 100644 index 0000000..b90e4f2 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Rotate.php @@ -0,0 +1,89 @@ + $image->getSource())); + return FALSE; + } + + // Convert the hexadecimal background value to a color index value. + if (!empty($arguments['background'])) { + $rgb = array(); + for ($i = 16; $i >= 0; $i -= 8) { + $rgb[] = (($arguments['background'] >> $i) & 0xFF); + } + $arguments['background'] = imagecolorallocatealpha($image->getToolkit()->getResource(), $rgb[0], $rgb[1], $rgb[2], 0); + } + // Set background color as transparent if $arguments['background'] is NULL. + else { + // Get the current transparent color. + $arguments['background'] = imagecolortransparent($image->getToolkit()->getResource()); + + // If no transparent colors, use white. + if ($arguments['background'] == 0) { + $arguments['background'] = imagecolorallocatealpha($image->getToolkit()->getResource(), 255, 255, 255, 0); + } + } + + // Images are assigned a new color palette when rotating, removing any + // transparency flags. For GIF images, keep a record of the transparent color. + if ($image->getType() == IMAGETYPE_GIF) { + $transparent_index = imagecolortransparent($image->getToolkit()->getResource()); + if ($transparent_index != 0) { + $transparent_gif_color = imagecolorsforindex($image->getToolkit()->getResource(), $transparent_index); + } + } + + $image->getToolkit()->setResource(imagerotate($image->getToolkit()->getResource(), 360 - $arguments['degrees'], $arguments['background'])); + + // GIFs need to reassign the transparent color after performing the rotate. + if (isset($transparent_gif_color)) { + $arguments['background'] = imagecolorexactalpha($image->getToolkit()->getResource(), $transparent_gif_color['red'], $transparent_gif_color['green'], $transparent_gif_color['blue'], $transparent_gif_color['alpha']); + imagecolortransparent($image->getToolkit()->getResource(), $arguments['background']); + } + + $image + ->setWidth(imagesx($image->getToolkit()->getResource())) + ->setHeight(imagesy($image->getToolkit()->getResource())); + return TRUE; + } + +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Scale.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Scale.php new file mode 100644 index 0000000..fe414b1 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/Scale.php @@ -0,0 +1,80 @@ + $this->getPluginId())); + throw new \InvalidArgumentException($message); + } + + // Calculate one of the dimensions from the other target dimension, + // ensuring the same aspect ratio as the source dimensions. If one of the + // target dimensions is missing, that is the one that is calculated. If both + // are specified then the dimension calculated is the one that would not be + // calculated to be bigger than its target. + $scale = $image->getHeight() / $image->getWidth(); + if (($width && !$height) || ($width && $height && $scale < $height / $width)) { + $height = (int) round($width * $scale); + } + else { + $width = (int) round($height / $scale); + } + + // Call parent validation + parent::prepare($image, $arguments); + + // Don't upscale if the option isn't enabled. + if (!$arguments['upscale'] && ($width >= $image->getWidth() || $height >= $image->getHeight())) { + return FALSE; + } + } + +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/ScaleAndCrop.php b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/ScaleAndCrop.php new file mode 100644 index 0000000..7ce04e9 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/ImageToolkit/Operation/gd/ScaleAndCrop.php @@ -0,0 +1,65 @@ +getWidth(); + $height = $image->getHeight(); + + $scale = max($arguments['width'] / $width, $arguments['height'] / $height); + + $arguments['x'] = (int) round(($width * $scale - $arguments['width']) / 2); + $arguments['y'] = (int) round(($height * $scale - $arguments['height']) / 2); + $arguments['resize'] = array( + 'width' => (int) round($width * $scale), + 'height' => (int) round($height * $scale), + ); + } + + /** + * {@inheritdoc} + */ + public function apply(ImageInterface $image, array $arguments = array()) { + if ($this->toolkit->apply('resize', $image, $arguments['resize'])) { + return $this->toolkit->apply('crop', $image, $arguments); + } + + return FALSE; + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitGdTest.php b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitGdTest.php index d683b70..aca8b08 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitGdTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitGdTest.php @@ -107,49 +107,49 @@ function testManipulations() { $operations = array( 'resize' => array( 'function' => 'resize', - 'arguments' => array(20, 10), + 'arguments' => array('width' => 20, 'height' => 10), 'width' => 20, 'height' => 10, 'corners' => $default_corners, ), 'scale_x' => array( 'function' => 'scale', - 'arguments' => array(20, NULL), + 'arguments' => array('width' => 20), 'width' => 20, 'height' => 10, 'corners' => $default_corners, ), 'scale_y' => array( 'function' => 'scale', - 'arguments' => array(NULL, 10), + 'arguments' => array('height' => 10), 'width' => 20, 'height' => 10, 'corners' => $default_corners, ), 'upscale_x' => array( 'function' => 'scale', - 'arguments' => array(80, NULL, TRUE), + 'arguments' => array('width' => 80, 'upscale' => TRUE), 'width' => 80, 'height' => 40, 'corners' => $default_corners, ), 'upscale_y' => array( 'function' => 'scale', - 'arguments' => array(NULL, 40, TRUE), + 'arguments' => array('height' => 40, 'upscale' => TRUE), 'width' => 80, 'height' => 40, 'corners' => $default_corners, ), 'crop' => array( 'function' => 'crop', - 'arguments' => array(12, 4, 16, 12), + 'arguments' => array('x' => 12, 'y' => 4, 'width' => 16, 'height' => 12), 'width' => 16, 'height' => 12, 'corners' => array_fill(0, 4, $this->white), ), 'scale_and_crop' => array( - 'function' => 'scaleAndCrop', - 'arguments' => array(10, 8), + 'function' => 'scale_and_crop', + 'arguments' => array('width' => 10, 'height' => 8), 'width' => 10, 'height' => 8, 'corners' => array_fill(0, 4, $this->black), @@ -161,28 +161,28 @@ function testManipulations() { $operations += array( 'rotate_5' => array( 'function' => 'rotate', - 'arguments' => array(5, 0xFF00FF), // Fuchsia background. + 'arguments' => array('degrees' => 5, 'background' => 0xFF00FF), // Fuchsia background. 'width' => 42, 'height' => 24, 'corners' => array_fill(0, 4, $this->fuchsia), ), 'rotate_90' => array( 'function' => 'rotate', - 'arguments' => array(90, 0xFF00FF), // Fuchsia background. + 'arguments' => array('degrees' => 90, 'background' => 0xFF00FF), // Fuchsia background. 'width' => 20, 'height' => 40, 'corners' => array($this->transparent, $this->red, $this->green, $this->blue), ), 'rotate_transparent_5' => array( 'function' => 'rotate', - 'arguments' => array(5), + 'arguments' => array('degrees' => 5), 'width' => 42, 'height' => 24, 'corners' => array_fill(0, 4, $this->transparent), ), 'rotate_transparent_90' => array( 'function' => 'rotate', - 'arguments' => array(90), + 'arguments' => array('degrees' => 90), 'width' => 20, 'height' => 40, 'corners' => array($this->transparent, $this->red, $this->green, $this->blue), @@ -234,7 +234,7 @@ function testManipulations() { } // Perform our operation. - call_user_func_array(array($image, $values['function']), $values['arguments']); + $image->apply($values['function'], $values['arguments']); // To keep from flooding the test with assert values, make a general // value for whether each group of values fail. diff --git a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTest.php b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTest.php index 34376ba..ab98be1 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTest.php @@ -53,7 +53,8 @@ function testSave() { * Test the image_resize() function. */ function testResize() { - $this->assertTrue($this->image->resize(1, 2), 'Function returned the expected value.'); + $data = array('width' => 1, 'height' => 2); + $this->assertTrue($this->image->apply('resize', $data), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('resize')); // Check the parameters. @@ -67,7 +68,8 @@ function testResize() { */ function testScale() { // TODO: need to test upscaling - $this->assertTrue($this->image->scale(10, 10), 'Function returned the expected value.'); + $data = array('width' => 10, 'height' => 10); + $this->assertTrue($this->image->apply('scale', $data), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('scale')); // Check the parameters. @@ -80,21 +82,23 @@ function testScale() { * Test the image_scale_and_crop() function. */ function testScaleAndCrop() { - $this->assertTrue($this->image->scaleAndCrop(5, 10), 'Function returned the expected value.'); - $this->assertToolkitOperationsCalled(array('scaleAndCrop')); + $data = array('width' => 5, 'height' => 10); + $this->assertTrue($this->image->apply('scale_and_crop', $data), 'Function returned the expected value.'); + $this->assertToolkitOperationsCalled(array('scale_and_crop')); // Check the parameters. $calls = $this->imageTestGetAllCalls(); - $this->assertEqual($calls['scaleAndCrop'][0][1], 5, 'Width was computed and passed correctly'); - $this->assertEqual($calls['scaleAndCrop'][0][2], 10, 'Height was computed and passed correctly'); + $this->assertEqual($calls['scale_and_crop'][0][1], 5, 'Width was computed and passed correctly'); + $this->assertEqual($calls['scale_and_crop'][0][2], 10, 'Height was computed and passed correctly'); } /** * Test the image_rotate() function. */ function testRotate() { - $this->assertTrue($this->image->rotate(90, 1), 'Function returned the expected value.'); + $data = array('degrees' => 90, 'background' => 1); + $this->assertTrue($this->image->apply('rotate', $data), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('rotate')); // Check the parameters. @@ -107,7 +111,8 @@ function testRotate() { * Test the image_crop() function. */ function testCrop() { - $this->assertTrue($this->image->crop(1, 2, 3, 4), 'Function returned the expected value.'); + $data = array('x' => 1, 'y' => 2, 'width' => 3, 'height' => 4); + $this->assertTrue($this->image->apply('crop', $data), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('crop')); // Check the parameters. @@ -122,7 +127,7 @@ function testCrop() { * Test the image_desaturate() function. */ function testDesaturate() { - $this->assertTrue($this->image->desaturate(), 'Function returned the expected value.'); + $this->assertTrue($this->image->apply('desaturate'), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('desaturate')); // Check the parameters. diff --git a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTestBase.php index 5c89169..61a0268 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Image/ToolkitTestBase.php @@ -108,6 +108,8 @@ function imageTestReset() { 'rotate' => array(), 'crop' => array(), 'desaturate' => array(), + 'scale' => array(), + 'scale_and_crop' => array(), ); \Drupal::state()->set('image_test.results', $results); } diff --git a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Crop.php b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Crop.php new file mode 100644 index 0000000..6e87309 --- /dev/null +++ b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Crop.php @@ -0,0 +1,51 @@ +toolkit->logCall('crop', array($image, $arguments['x'], $arguments['y'], $arguments['width'], $arguments['height'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Desaturate.php b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Desaturate.php new file mode 100644 index 0000000..e101965 --- /dev/null +++ b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Desaturate.php @@ -0,0 +1,34 @@ +toolkit->logCall('desaturate', array($image)); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Resize.php b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Resize.php new file mode 100644 index 0000000..4c56480 --- /dev/null +++ b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Resize.php @@ -0,0 +1,43 @@ +toolkit->logCall('resize', array($image, $arguments['width'], $arguments['height'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Rotate.php b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Rotate.php new file mode 100644 index 0000000..e2fb41a --- /dev/null +++ b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Rotate.php @@ -0,0 +1,45 @@ +toolkit->logCall('rotate', array($image, $arguments['degrees'], $arguments['background'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Scale.php b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Scale.php new file mode 100644 index 0000000..f6cd0c1 --- /dev/null +++ b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/Scale.php @@ -0,0 +1,52 @@ +toolkit->logCall('scale', array($image, $arguments['width'], $arguments['height'], $arguments['upscale'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/ScaleAndCrop.php b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/ScaleAndCrop.php new file mode 100644 index 0000000..f0c9a6c --- /dev/null +++ b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/Operation/test/ScaleAndCrop.php @@ -0,0 +1,42 @@ +toolkit->logCall('scale_and_crop', array($image, $arguments['width'], $arguments['height'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/TestToolkit.php b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/TestToolkit.php index d85d813..56c3f49 100644 --- a/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/TestToolkit.php +++ b/core/modules/system/tests/modules/image_test/lib/Drupal/image_test/Plugin/ImageToolkit/TestToolkit.php @@ -96,54 +96,6 @@ public function save(ImageInterface $image, $destination) { } /** - * {@inheritdoc} - */ - public function crop(ImageInterface $image, $x, $y, $width, $height) { - $this->logCall('crop', array($image, $x, $y, $width, $height)); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function resize(ImageInterface $image, $width, $height) { - $this->logCall('resize', array($image, $width, $height)); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function rotate(ImageInterface $image, $degrees, $background = NULL) { - $this->logCall('rotate', array($image, $degrees, $background)); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function desaturate(ImageInterface $image) { - $this->logCall('desaturate', array($image)); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function scale(ImageInterface $image, $width = NULL, $height = NULL, $upscale = FALSE) { - $this->logCall('scale', array($image, $width, $height, $upscale)); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function scaleAndCrop(ImageInterface $image, $width, $height) { - $this->logCall('scaleAndCrop', array($image, $width, $height)); - return TRUE; - } - - /** * Stores the values passed to a toolkit call. * * @param string $op @@ -155,7 +107,7 @@ public function scaleAndCrop(ImageInterface $image, $width, $height) { * @see \Drupal\system\Tests\Image\ToolkitTestBase::imageTestReset() * @see \Drupal\system\Tests\Image\ToolkitTestBase::imageTestGetAllCalls() */ - protected function logCall($op, $args) { + public function logCall($op, $args) { $results = \Drupal::state()->get('image_test.results') ?: array(); $results[$op][] = $args; \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 d2c5fd7..367f7d9 100644 --- a/core/tests/Drupal/Tests/Core/Image/ImageTest.php +++ b/core/tests/Drupal/Tests/Core/Image/ImageTest.php @@ -16,6 +16,13 @@ class ImageTest extends UnitTestCase { /** + * Image source path. + * + * @var string + */ + protected $source; + + /** * Image object. * * @var \Drupal\Core\Image\Image @@ -23,12 +30,22 @@ class ImageTest extends UnitTestCase { protected $image; /** - * Image toolkit. + * Mocked image toolkit. * * @var \Drupal\Core\ImageToolkit\ImageToolkitInterface */ protected $toolkit; + /** + * Mocked image toolkit operation. + * + * @var \Drupal\Core\ImageToolkit\ImageToolkitOperationInterface + */ + protected $toolkitOperation; + + /** + * @inheritdoc + */ public static function getInfo() { return array( 'name' => 'Image class functionality', @@ -37,9 +54,55 @@ public static function getInfo() { ); } + /** + * @inheritdoc + */ protected function setUp() { // Use the Druplicon image. $this->source = __DIR__ . '/../../../../../misc/druplicon.png'; + } + + /** + * Mocks a toolkit. + * + * @param array $stubs + * (optional) Array containing methods to be replaced with stubs. + * + * @return PHPUnit_Framework_MockObject_MockObject + */ + protected function getToolkitMock(array $stubs = array()) { + $mock_builder = $this->getMockBuilder('Drupal\system\Plugin\ImageToolkit\GDToolkit'); + if ($stubs && is_array($stubs)) { + $mock_builder->setMethods($stubs); + } + return $mock_builder + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * Mocks a toolkit operation. + * + * @param string $class_name + * The name of the GD toolkit operation class to be mocked. + * + * @return PHPUnit_Framework_MockObject_MockObject + */ + protected function getToolkitOperationMock($class_name) { + $mock_builder = $this->getMockBuilder('Drupal\\system\\Plugin\\ImageToolkit\\Operation\\gd\\' . $class_name); + return $mock_builder + ->setMethods(array('apply', 'validateArguments')) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * Get an image with a mocked toolkit, for testing. + * + * @param array $stubs + * (optional) Array containing toolkit methods to be replaced with stubs. + */ + protected function getTestImage(array $stubs = array()) { $this->toolkit = $this->getToolkitMock(); $this->toolkit->expects($this->any()) @@ -60,87 +123,113 @@ protected function setUp() { } /** - * Mocks a toolkit. - * - * @param array $stubs - * (optional) Array containing methods to be replaced with stubs. + * Get an image with mocked toolkit and operation, for operation testing. * - * @return PHPUnit_Framework_MockObject_MockObject + * @param string $class_name + * The name of the GD toolkit operation class to be mocked. */ - protected function getToolkitMock(array $stubs = array()) { - $mock_builder = $this->getMockBuilder('Drupal\system\Plugin\ImageToolkit\GDToolkit'); - if ($stubs && is_array($stubs)) { - $mock_builder->setMethods($stubs); - } - return $mock_builder - ->disableOriginalConstructor() - ->getMock(); + protected function getTestImageForOperation($class_name) { + $this->toolkit = $this->getToolkitMock(array('getToolkitOperation', 'getPluginId', 'getInfo')); + $this->toolkitOperation = $this->getToolkitOperationMock($class_name); + + $this->toolkit->expects($this->any()) + ->method('getPluginId') + ->will($this->returnValue('gd')); + + $this->toolkit->expects($this->any()) + ->method('getInfo') + ->will($this->returnValue(array( + 'width' => 88, + 'height' => 100, + 'extension' => 'png', + 'type' => IMAGETYPE_PNG, + 'mime_type' => 'image/png', + ))); + + $this->toolkit->expects($this->any()) + ->method('getToolkitOperation') + ->will($this->returnValue($this->toolkitOperation)); + + $this->toolkitOperation->expects($this->any()) + ->method('validateArguments') + ->will($this->returnArgument(0)); + + $this->image = new Image($this->source, $this->toolkit); } /** * Tests \Drupal\Core\Image\Image::getExtension(). */ public function testGetExtension() { - $this->assertEquals($this->image->getExtension(), 'png'); + $this->getTestImage(); + $this->assertEquals('png', $this->image->getExtension()); } /** * Tests \Drupal\Core\Image\Image::getHeight(). */ public function testGetHeight() { - $this->assertEquals($this->image->getHeight(), 100); + $this->getTestImage(); + $this->assertEquals(100, $this->image->getHeight()); } /** * Tests \Drupal\Core\Image\Image::setHeight(). */ public function testSetHeight() { + $this->getTestImage(); $this->image->getHeight(); $this->image->setHeight(400); - $this->assertEquals($this->image->getHeight(), 400); + $this->assertEquals(400, $this->image->getHeight()); } /** * Tests \Drupal\Core\Image\Image::getWidth(). */ public function testGetWidth() { - $this->assertEquals($this->image->getWidth(), 88); + $this->getTestImage(); + $this->assertEquals(88, $this->image->getWidth()); } /** * Tests \Drupal\Core\Image\Image::setWidth(). */ public function testSetWidth() { + $this->getTestImage(); $this->image->getHeight(); $this->image->setWidth(337); - $this->assertEquals($this->image->getWidth(), 337); + $this->assertEquals(337, $this->image->getWidth()); } /** * Tests \Drupal\Core\Image\Image::getFileSize */ public function testGetFileSize() { - $this->assertEquals($this->image->getFileSize(), 3905); + $this->getTestImage(); + $this->assertEquals(3905, $this->image->getFileSize()); } /** * Tests \Drupal\Core\Image\Image::getType(). */ public function testGetType() { - $this->assertEquals($this->image->getType(), IMAGETYPE_PNG); + $this->getTestImage(); + $this->assertEquals(IMAGETYPE_PNG, $this->image->getType()); } /** * Tests \Drupal\Core\Image\Image::getMimeType(). */ public function testGetMimeType() { - $this->assertEquals($this->image->getMimeType(), 'image/png'); + $this->getTestImage(); + $this->assertEquals('image/png', $this->image->getMimeType()); } /** * Tests \Drupal\Core\Image\Image::isExisting(). */ public function testIsExisting() { + $this->getTestImage(); $this->assertTrue($this->image->isExisting()); $this->assertTrue(is_readable($this->image->getSource())); } @@ -149,22 +238,25 @@ public function testIsExisting() { * Tests \Drupal\Core\Image\Image::setSource(). */ public function testSetSource() { + $this->getTestImage(); $source = __DIR__ . '/../../../../../misc/grippie.png'; $this->image->setSource($source); - $this->assertEquals($this->image->getSource(), $source); + $this->assertEquals($source, $this->image->getSource()); } /** * Tests \Drupal\Core\Image\Image::getToolkitId(). */ public function testGetToolkitId() { - $this->assertEquals($this->image->getToolkitId(), 'gd'); + $this->getTestImage(); + $this->assertEquals('gd', $this->image->getToolkitId()); } /** * Tests \Drupal\Core\Image\Image::save(). */ public function testSave() { + $this->getTestImage(); // This will fail if save() method isn't called on the toolkit. $this->toolkit->expects($this->once()) ->method('save') @@ -182,6 +274,7 @@ public function testSave() { * Tests \Drupal\Core\Image\Image::save(). */ public function testSaveFails() { + $this->getTestImage(); // This will fail if save() method isn't called on the toolkit. $this->toolkit->expects($this->once()) ->method('save') @@ -194,6 +287,7 @@ public function testSaveFails() { * Tests \Drupal\Core\Image\Image::save(). */ public function testChmodFails() { + $this->getTestImage(); // This will fail if save() method isn't called on the toolkit. $this->toolkit->expects($this->once()) ->method('save') @@ -211,6 +305,7 @@ public function testChmodFails() { * Tests \Drupal\Core\Image\Image::save(). */ public function testProcessInfoFails() { + $this->getTestImage(); $this->image->setSource('magic-foobars.png'); $this->assertFalse((bool) $this->image->getWidth()); } @@ -219,157 +314,176 @@ public function testProcessInfoFails() { * Tests \Drupal\Core\Image\Image::scale(). */ public function testScaleWidth() { - $toolkit = $this->getToolkitMock(array('resize')); - $image = new Image($this->source, $toolkit); + $this->getTestImageForOperation('Scale'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); - $toolkit->expects($this->any()) - ->method('resize') - ->will($this->returnArgument(2)); - $height = $image->scale(44); - $this->assertEquals($height, 50); + $ret = $this->image->apply('scale', array('width' => 44, 'upscale' => FALSE)); + $this->assertEquals(50, $ret['height']); } /** * Tests \Drupal\Core\Image\Image::scale(). */ public function testScaleHeight() { - $toolkit = $this->getToolkitMock(array('resize')); - $image = new Image($this->source, $toolkit); - - $toolkit->expects($this->any()) - ->method('resize') + $this->getTestImageForOperation('Scale'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') ->will($this->returnArgument(1)); - $width = $image->scale(NULL, 50); - $this->assertEquals($width, 44); + + $ret = $this->image->apply('scale', array('height' => 50, 'upscale' => FALSE)); + $this->assertEquals(44, $ret['width']); } /** * Tests \Drupal\Core\Image\Image::scale(). */ public function testScaleSame() { - $toolkit = $this->getToolkitMock(array('resize')); - $image = new Image($this->source, $toolkit); - + $this->getTestImageForOperation('Scale'); // Dimensions are the same, resize should not be called. - $toolkit->expects($this->never()) - ->method('resize') - ->will($this->returnArgument(1)); + $this->toolkitOperation->expects($this->never()) + ->method('apply'); - $width = $image->scale(88, 100); - $this->assertEquals($width, 88); + $ret = $this->image->apply('scale', array ('width' => 88, 'height' => 100, 'upscale' => FALSE)); } /** * Tests \Drupal\Core\Image\Image::scaleAndCrop(). */ public function testScaleAndCropWidth() { - $toolkit = $this->getToolkitMock(array('resize', 'crop')); - $image = new Image($this->source, $toolkit); - - $toolkit->expects($this->once()) - ->method('resize') - ->will($this->returnValue(TRUE)); - - $toolkit->expects($this->once()) - ->method('crop') + $this->getTestImageForOperation('ScaleAndCrop'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') ->will($this->returnArgument(1)); - $x = $image->scaleAndCrop(34, 50); - $this->assertEquals($x, 5); + $ret = $this->image->apply('scale_and_crop', array ('width' => 34, 'height' => 50, 'upscale' => FALSE)); + $this->assertEquals(5, $ret['x']); } /** * Tests \Drupal\Core\Image\Image::scaleAndCrop(). */ public function testScaleAndCropHeight() { - $toolkit = $this->getToolkitMock(array('resize', 'crop')); - $image = new Image($this->source, $toolkit); - - $toolkit->expects($this->once()) - ->method('resize') - ->will($this->returnValue(TRUE)); - - $toolkit->expects($this->once()) - ->method('crop') - ->will($this->returnArgument(2)); + $this->getTestImageForOperation('ScaleAndCrop'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); - $y = $image->scaleAndCrop(44, 40); - $this->assertEquals($y, 5); + $ret = $this->image->apply('scale_and_crop', array ('width' => 44, 'height' => 40)); + $this->assertEquals(5, $ret['y']); } /** * Tests \Drupal\Core\Image\Image::scaleAndCrop(). */ public function testScaleAndCropFails() { - $toolkit = $this->getToolkitMock(array('resize', 'crop')); - $image = new Image($this->source, $toolkit); - - $toolkit->expects($this->once()) - ->method('resize') - ->will($this->returnValue(FALSE)); + $this->getTestImageForOperation('ScaleAndCrop'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); - $toolkit->expects($this->never()) - ->method('crop'); - $image->scaleAndCrop(44, 40); + $ret = $this->image->apply('scale_and_crop', array('width' => 44, 'height' => 40)); + $this->assertEquals(0, $ret['x']); + $this->assertEquals(5, $ret['y']); + $this->assertEquals(44, $ret['resize']['width']); + $this->assertEquals(50, $ret['resize']['height']); } /** * Tests \Drupal\Core\Image\Image::crop(). - * - * @todo Because \Drupal\Tests\Core\Image\ImageTest::testCropWidth() tests - * image geometry conversions (like dimensions, coordinates, etc) and has - * lost its scope in https://drupal.org/node/2103635, it was temporarily - * removed. The test will be added back when implementing the dedicated - * functionality from https://drupal.org/node/2108307. */ + public function testCropWidth() { + $this->getTestImageForOperation('Crop'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); + + // Cropping with width only should preserve the aspect ratio. + $ret = $this->image->apply('crop', array ('x' => 0, 'y' => 0, 'width' => 44)); + $this->assertEquals(50, $ret['height']); + } /** * Tests \Drupal\Core\Image\Image::crop(). - * - * @todo Because \Drupal\Tests\Core\Image\ImageTest::testCropHeight() tests - * image geometry conversions (like dimensions, coordinates, etc) and has - * lost its scope in https://drupal.org/node/2103635, it was temporarily - * removed. The test will be added back when implementing the dedicated - * functionality from https://drupal.org/node/2108307. */ + public function testCropHeight() { + $this->getTestImageForOperation('Crop'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); + + // Cropping with height only should preserve the aspect ratio. + $ret = $this->image->apply('crop', array ('x' => 0, 'y' => 0, 'height' => 50)); + $this->assertEquals(44, $ret['width']); + } /** * Tests \Drupal\Core\Image\Image::crop(). */ public function testCrop() { - $this->toolkit->expects($this->once()) - ->method('crop') - ->will($this->returnArgument(3)); - $width = $this->image->crop(0, 0, 44, 50); - $this->assertEquals($width, 44); + $this->getTestImageForOperation('Crop'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); + + $ret = $this->image->apply('crop', array ('x' => 0, 'y' => 0, 'width' => 44, 'height' => 50)); + $this->assertEquals(44, $ret['width']); + } + + /** + * Tests \Drupal\Core\Image\Image::resize(). + */ + public function testResize() { + $this->getTestImageForOperation('Resize'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); + + // Resize with integer for width and height. + $ret = $this->image->apply('resize', array('width' => 30, 'height' => 40)); + $this->assertEquals(30, $ret['width']); + $this->assertEquals(40, $ret['height']); } /** * Tests \Drupal\Core\Image\Image::resize(). - * - * @todo Because \Drupal\Tests\Core\Image\ImageTest::testResize() tests image - * geometry conversions (like dimensions, coordinates, etc) and has lost its - * scope in https://drupal.org/node/2103635, it was temporarily removed. The - * test will be added back when implementing the dedicated functionality - * from https://drupal.org/node/2108307. */ + public function testFloatResize() { + $this->getTestImageForOperation('Resize'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); + + // Pass a float for width. + $ret = $this->image->apply('resize', array('width' => 30.4, 'height' => 40)); + // Ensure that the float was rounded to an integer first. + $this->assertEquals(30, $ret['width']); + } /** * Tests \Drupal\Core\Image\Image::desaturate(). */ public function testDesaturate() { - $this->toolkit->expects($this->once()) - ->method('desaturate'); - $this->image->desaturate(); + $this->getTestImageForOperation('Desaturate'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); + + $this->image->apply('desaturate'); } /** * Tests \Drupal\Core\Image\Image::rotate(). */ public function testRotate() { - $this->toolkit->expects($this->once()) - ->method('rotate'); - $this->image->rotate(90); + $this->getTestImageForOperation('Rotate'); + $this->toolkitOperation->expects($this->once()) + ->method('apply') + ->will($this->returnArgument(1)); + + $ret = $this->image->apply('rotate', array('degrees' => 90)); + $this->assertEquals(90, $ret['degrees']); } }