diff --git a/core/core.services.yml b/core/core.services.yml index a8313a6..9205116 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -695,7 +695,10 @@ services: - { name: event_subscriber } image.toolkit.manager: class: Drupal\Core\ImageToolkit\ImageToolkitManager - arguments: ['@container.namespaces', '@cache.discovery', '@config.factory', '@module_handler'] + arguments: ['@container.namespaces', '@cache.discovery', '@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 5b1b505..47ee5aa 100644 --- a/core/lib/Drupal/Core/Image/Image.php +++ b/core/lib/Drupal/Core/Image/Image.php @@ -163,42 +163,10 @@ protected function parseFile() { } /** - * 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($toolkit, $path); - * $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(). - */ - public function __call($method, $arguments) { - // @todo Temporary to avoid that legacy GD setResource(), getResource(), - // hasResource() methods moved to GD toolkit in #2103621, setWidth(), - // setHeight() methods moved to ImageToolkitInterface in #2196067, - // getType() method moved to GDToolkit in #2211227 get - // invoked from this class anyway through the magic __call. Will be - // removed through https://drupal.org/node/2073759, when - // call_user_func_array() will be replaced by - // $this->toolkit->apply($name, $this, $arguments). - if (in_array($method, array('setResource', 'getResource', 'hasResource', 'setWidth', 'setHeight', 'getType'))) { - throw new \BadMethodCallException($method); - } - if (is_callable(array($this->toolkit, $method))) { - // @todo In https://drupal.org/node/2073759, 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($method); + * {@inheritdoc} + */ + 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 af1b31c..ed33313 100644 --- a/core/lib/Drupal/Core/Image/ImageInterface.php +++ b/core/lib/Drupal/Core/Image/ImageInterface.php @@ -79,6 +79,23 @@ 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 of 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..1e9b9d8 --- /dev/null +++ b/core/lib/Drupal/Core/ImageToolkit/Annotation/ImageToolkitOperation.php @@ -0,0 +1,69 @@ +operationManager = $operation_manager; + } + + /** * {@inheritdoc} */ public function getRequirements() { return array(); } + /** + * Gets 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) { + return $this->operationManager->getToolkitOperation($this, $operation); + } + + /** + * {@inheritdoc} + */ + public function apply($operation, ImageInterface $image, array $arguments = array()) { + try { + // Get the plugin to use for the operation and apply the operation. + return $this->getToolkitOperation($operation)->apply($image, $arguments); + } + catch (PluginNotFoundException $e) { + \Drupal::logger('image')->error('The selected image handling toolkit %toolkit can not process operation %operation.', array('%toolkit' => $this->getPluginId(), '%operation' => $operation)); + return FALSE; + } + catch (\InvalidArgumentException $e) { + \Drupal::logger('image')->warning($e->getMessage(), array()); + return FALSE; + } + } + } diff --git a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitInterface.php b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitInterface.php index b7f4c8c..04d32f3 100644 --- a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitInterface.php +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitInterface.php @@ -60,75 +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. - * @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. - * @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. - * @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 @@ -142,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); - - /** * Determines if a file contains a valid image. * * @param \Drupal\Core\Image\ImageInterface $image @@ -259,4 +147,21 @@ public static function isAvailable(); */ public static function getSupportedExtensions(); + /** + * Applies a toolkit 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 of arguments to be passed to the toolkit + * operation, e.g. array('width' => 50, 'height' => 100, + * 'upscale' => TRUE). + * + * @return bool + * 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 a780630..789ef5a 100644 --- a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitManager.php +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitManager.php @@ -11,6 +11,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Component\Plugin\Factory\DefaultFactory; /** * Manages toolkit plugins. @@ -25,6 +26,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 @@ -36,12 +44,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, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler) { + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, 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, 'image_toolkit_plugins'); $this->configFactory = $config_factory; + $this->operationManager = $operation_manager; } /** @@ -97,4 +108,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..79f9310 --- /dev/null +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationBase.php @@ -0,0 +1,183 @@ +toolkit = $toolkit; + } + + /** + * Returns the image toolkit instance for this operation. + * + * Image toolkit implementers should provide a trait that overrides this + * method to correctly document the return type of this getter. This provides + * better DX (code checking and code completion) for image toolkit operation + * developers. + * + * @return \Drupal\Core\ImageToolkit\ImageToolkitInterface + */ + protected function getToolkit() { + return $this->toolkit; + } + + /** + * Returns the definition of the operation arguments. + * + * Image toolkit operation implementers must implement this method to + * "document" their operation, thus also if no arguments are expected. + * + * @return array + * An array whose keys are the names of the arguments (e.g. "width", + * "degrees") and each value is an associative array having the following + * keys: + * - description: A string with the argument description. This is used only + * internally for documentation purposes, so it does not need to be + * translatable. + * - required: (optional) A boolean indicating if this argument should be + * provided or not. Defaults to TRUE. + * - default: (optional) When the argument is set to "required" = FALSE, + * this must be set to a default value. Ignored for "required" = TRUE + * arguments. + */ + abstract protected function arguments(); + + /** + * Checks if required arguments are passed in and adds defaults for non passed + * in optional arguments. + * + * Image toolkit operation implementers should not normally need to override + * this method as they should place their own validation in validateArguments. + * + * @param array $arguments + * An associative array of arguments to be used by the toolkit operation. + * + * @return array + * The prepared arguments array. + * + * @throws \InvalidArgumentException. + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException. + */ + protected function prepareArguments(array $arguments) { + foreach ($this->arguments() as $id => $argument) { + $argument += array('required' => TRUE); + // Check if the argument is required and, if so, has been provided. + if ($argument['required']) { + if (!array_key_exists($id, $arguments)) { + // If the argument is required throw an exception. + throw new \InvalidArgumentException(String::format("Argument '!argument' expected by plugin '!plugin' but not passed.", array('!argument' => $id, '!plugin' => $this->getPluginId()))); + } + } + else { + // Optional arguments require a 'default' value. + // We check this even if the argument is provided by the caller, as we + // want to fail fast here, i.e. at development time. + if (!array_key_exists('default', $argument)) { + // The plugin did not define a default, so throw a plugin exception, + // not an invalid argument exception. + throw new InvalidPluginDefinitionException(String::format("Default for argument '!argument' expected by plugin '!plugin' but not defined.", array('!argument' => $id, '!plugin' => $this->getPluginId()))); + } + + // Use the default value if the argument is not passed in. + if (!array_key_exists($id, $arguments)) { + $arguments[$id] = $argument['default']; + } + } + } + return $arguments; + } + + /** + * Validates the arguments. + * + * Image toolkit operation implementers should place any argument validation + * in this method, throwing an InvalidArgumentException when an error is + * encountered. + * + * Validation typically includes things like: + * - Checking that width and height are not negative. + * - Checking that a color value is indeed a color. + * + * But validation may also include correcting the arguments, e.g: + * - Casting arguments to the correct type. + * - Rounding pixel values to an integer. + * + * This base implementation just returns the array of arguments and thus does + * not need to be called by overriding methods. + * + * @param \Drupal\Core\Image\ImageInterface $image + * An image object. + * @param array $arguments + * An associative array of arguments to be used by the toolkit operation. + * + * @return array + * The validated and corrected arguments array. + * + * @throws \InvalidArgumentException. + * If one or more of the arguments are not valid. + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException. + * If the plugin does not define a default for an optional argument. + */ + protected function validateArguments(ImageInterface $image, array $arguments) { + return $arguments; + } + + /** + * {@inheritdoc} + */ + public final function apply(ImageInterface $image, array $arguments) { + $arguments = $this->prepareArguments($arguments); + $arguments = $this->validateArguments($image, $arguments); + return $this->execute($image, $arguments); + } + + /** + * Performs the actual manipulation on the image. + * + * Image toolkit operation implementers must implement this method. This + * method is responsible for actually performing the operation on the image. + * When this method gets called, the implementer may assume all arguments, + * also the optional ones, to be available, validated and corrected. + * + * @param \Drupal\Core\Image\ImageInterface $image + * An image object. + * @param array $arguments + * An associative array of arguments to be used by the toolkit operation. + * + * @return bool + * TRUE if the operation was performed successfully, FALSE otherwise. + */ + abstract protected function execute(ImageInterface $image, array $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..d0ee39d --- /dev/null +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationInterface.php @@ -0,0 +1,38 @@ +alterInfo('image_toolkit_operation'); + $this->setCacheBackend($cache_backend, '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]; + } + + /** + * Returns the plugin ID for a given toolkit and operation. + * + * @param string $toolkit_id + * The toolkit plugin ID. + * @param string $operation + * The operation (e.g. "crop"). + * + * @return string + * The plugin ID. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * When no plugin is available. + */ + protected function getToolkitOperationPluginId($toolkit_id, $operation) { + $definitions = $this->getDefinitions(); + + $definitions = array_filter($definitions, + function ($definition) use ($toolkit_id, $operation) { + return $definition['toolkit'] == $toolkit_id && $definition['operation'] == $operation; + } + ); + + if (!$definitions) { + $message = String::format("No image operation plugin for '@toolkit' toolkit and '@operation' operation.", array('@toolkit' => $toolkit_id, '@operation' => $operation)); + throw new PluginNotFoundException($toolkit_id . '.' . $operation, $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) { + $plugin_definition = $this->getDefinition($plugin_id); + $plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition); + return new $plugin_class($configuration, $plugin_id, $plugin_definition, $toolkit); + } + + /** + * {@inheritdoc} + */ + public function getToolkitOperation(ImageToolkitInterface $toolkit, $operation) { + $plugin_id = $this->getToolkitOperationPluginId($toolkit->getPluginId(), $operation); + return $this->createInstance($plugin_id, array(), $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..e632adc --- /dev/null +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationManagerInterface.php @@ -0,0 +1,31 @@ +getWidth() > $width || $image->getHeight() > $height) { // Try to resize the image to fit the dimensions. - if ($image->scale($width, $height)) { + 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))); diff --git a/core/modules/image/src/Plugin/ImageEffect/CropImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/CropImageEffect.php index 42f6561..b242d4c 100644 --- a/core/modules/image/src/Plugin/ImageEffect/CropImageEffect.php +++ b/core/modules/image/src/Plugin/ImageEffect/CropImageEffect.php @@ -27,8 +27,9 @@ 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'])) { - 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); + $data = array('x' => $x, 'y' => $y, 'width' => $this->configuration['width'], 'height' => $this->configuration['height']); + if (!$image->apply('crop', $data)) { + \Drupal::logger('image')->error('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())); return FALSE; } return TRUE; diff --git a/core/modules/image/src/Plugin/ImageEffect/DesaturateImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/DesaturateImageEffect.php index 4d899c6..8d323b9 100644 --- a/core/modules/image/src/Plugin/ImageEffect/DesaturateImageEffect.php +++ b/core/modules/image/src/Plugin/ImageEffect/DesaturateImageEffect.php @@ -31,8 +31,8 @@ public function transformDimensions(array &$dimensions) { * {@inheritdoc} */ public function applyEffect(ImageInterface $image) { - if (!$image->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); + if (!$image->apply('desaturate')) { + \Drupal::logger('image')->error('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())); return FALSE; } return TRUE; diff --git a/core/modules/image/src/Plugin/ImageEffect/ResizeImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/ResizeImageEffect.php index f7d495d..e39f609 100644 --- a/core/modules/image/src/Plugin/ImageEffect/ResizeImageEffect.php +++ b/core/modules/image/src/Plugin/ImageEffect/ResizeImageEffect.php @@ -25,8 +25,9 @@ class ResizeImageEffect extends ConfigurableImageEffectBase { * {@inheritdoc} */ public function applyEffect(ImageInterface $image) { - if (!$image->resize($this->configuration['width'], $this->configuration['height'])) { - 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); + $data = array('width' => $this->configuration['width'], 'height' => $this->configuration['height']); + if (!$image->apply('resize', $data)) { + \Drupal::logger('image')->error('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())); return FALSE; } return TRUE; diff --git a/core/modules/image/src/Plugin/ImageEffect/RotateImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/RotateImageEffect.php index 49529cd..6538303 100644 --- a/core/modules/image/src/Plugin/ImageEffect/RotateImageEffect.php +++ b/core/modules/image/src/Plugin/ImageEffect/RotateImageEffect.php @@ -45,8 +45,9 @@ public function applyEffect(ImageInterface $image) { $this->configuration['degrees'] = rand(-1 * $degrees, $degrees); } - if (!$image->rotate($this->configuration['degrees'], $this->configuration['bgcolor'])) { - 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); + $data = array('degrees' => $this->configuration['degrees'], 'background' => $this->configuration['bgcolor']); + if (!$image->apply('rotate', $data)) { + \Drupal::logger('image')->error('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())); return FALSE; } return TRUE; diff --git a/core/modules/image/src/Plugin/ImageEffect/ScaleAndCropImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/ScaleAndCropImageEffect.php index 7fed9b1..ab24c5d 100644 --- a/core/modules/image/src/Plugin/ImageEffect/ScaleAndCropImageEffect.php +++ b/core/modules/image/src/Plugin/ImageEffect/ScaleAndCropImageEffect.php @@ -24,8 +24,9 @@ class ScaleAndCropImageEffect extends ResizeImageEffect { * {@inheritdoc} */ public function applyEffect(ImageInterface $image) { - if (!$image->scaleAndCrop($this->configuration['width'], $this->configuration['height'])) { - 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); + $data = array('width' => $this->configuration['width'], 'height' => $this->configuration['height']); + if (!$image->apply('scale_and_crop', $data)) { + \Drupal::logger('image')->error('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())); return FALSE; } return TRUE; diff --git a/core/modules/image/src/Plugin/ImageEffect/ScaleImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/ScaleImageEffect.php index e1d8ec5..325c2a9 100644 --- a/core/modules/image/src/Plugin/ImageEffect/ScaleImageEffect.php +++ b/core/modules/image/src/Plugin/ImageEffect/ScaleImageEffect.php @@ -25,8 +25,9 @@ class ScaleImageEffect extends ResizeImageEffect { * {@inheritdoc} */ public function applyEffect(ImageInterface $image) { - if (!$image->scale($this->configuration['width'], $this->configuration['height'], $this->configuration['upscale'])) { - 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); + $data = array('width' => $this->configuration['width'], 'height' => $this->configuration['height'], 'upscale' => $this->configuration['upscale']); + if (!$image->apply('scale', $data)) { + \Drupal::logger('image')->error('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())); return FALSE; } return TRUE; diff --git a/core/modules/image/src/Tests/ImageEffectsTest.php b/core/modules/image/src/Tests/ImageEffectsTest.php index 8afcb22..4c692fe 100644 --- a/core/modules/image/src/Tests/ImageEffectsTest.php +++ b/core/modules/image/src/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/src/Plugin/ImageToolkit/GDToolkit.php b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php index 2adc45a..7fbca7e 100644 --- a/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php +++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\system\Plugin\ImageToolkit\GDToolkit;. + * Contains \Drupal\system\Plugin\ImageToolkit\GDToolkit. */ namespace Drupal\system\Plugin\ImageToolkit; @@ -10,7 +10,6 @@ use Drupal\Component\Utility\Unicode; 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. @@ -85,157 +84,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); - - if ($width <= 0 || $height <= 0) { - return FALSE; - } - - $res = $this->createTmp($this->getType(), $width, $height); - - if (!imagecopyresampled($res, $this->getResource(), 0, 0, 0, 0, $width, $height, $this->getWidth($image), $this->getHeight($image))) { - return FALSE; - } - - imagedestroy($this->getResource()); - // Update image object. - $this->setResource($res); - 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 ($this->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); - } - - 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 = $this->getHeight($image) / $this->getWidth($image); - $height = empty($height) ? $width * $aspect : $height; - $width = empty($width) ? $height / $aspect : $width; - $width = (int) round($width); - $height = (int) round($height); - - if ($width <= 0 || $height <= 0) { - return FALSE; - } - - $res = $this->createTmp($this->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); - 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' => $this->getWidth($image), - 'height' => $this->getHeight($image), - ); - - // 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 / $this->getWidth($image), $height / $this->getHeight($image)); - $x = ($this->getWidth($image) * $scale - $width) / 2; - $y = ($this->getHeight($image) * $scale - $height) / 2; - - if ($this->resize($image, $this->getWidth($image) * $scale, $this->getHeight($image) * $scale)) { - return $this->crop($image, $x, $y, $width, $height); - } - - return FALSE; - } - - /** * Loads a GD resource from a file. * * @param \Drupal\Core\Image\ImageInterface $image diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Crop.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Crop.php new file mode 100644 index 0000000..a9b494b --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Crop.php @@ -0,0 +1,95 @@ + array( + 'description' => 'The starting x offset at which to start the crop, in pixels', + ), + 'y' => array( + 'description' => 'The starting y offset at which to start the crop, in pixels', + ), + 'width' => array( + 'description' => 'The width of the cropped area, in pixels', + 'required' => FALSE, + 'default' => NULL, + ), + 'height' => array( + 'description' => 'The height of the cropped area, in pixels', + 'required' => FALSE, + 'default' => NULL, + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function validateArguments(ImageInterface $image, array $arguments) { + // Assure at least one dimension. + if (empty($arguments['width']) && empty($arguments['height'])) { + throw new \InvalidArgumentException("At least one dimension ('width' or 'height') must be provided to the image 'crop' operation."); + } + + // Preserve aspect. + $aspect = $this->getToolkit()->getHeight($image) / $this->getToolkit()->getWidth($image); + $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]); + } + + // Fail when width or height are 0 or negative. + if ($arguments['width'] <= 0) { + throw new \InvalidArgumentException("Invalid width specified for the image 'crop' operation."); + } + if ($arguments['height'] <= 0) { + throw new \InvalidArgumentException("Invalid height specified for the image 'crop' operation."); + } + + return $arguments; + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments) { + $res = $this->getToolkit()->createTmp($this->getToolkit()->getType(), $arguments['width'], $arguments['height']); + + if (!imagecopyresampled($res, $this->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($this->getToolkit()->getResource()); + $this->getToolkit()->setResource($res); + + return TRUE; + } + +} diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Desaturate.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Desaturate.php new file mode 100644 index 0000000..cd17f5f --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Desaturate.php @@ -0,0 +1,45 @@ +notice('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->getToolkit()->getResource(), IMG_FILTER_GRAYSCALE); + } + +} diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/GDImageToolkitOperationBase.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/GDImageToolkitOperationBase.php new file mode 100644 index 0000000..b1a9b79 --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/GDImageToolkitOperationBase.php @@ -0,0 +1,23 @@ + array( + 'description' => 'The new width of the resized image, in pixels', + ), + 'height' => array( + 'description' => 'The new height of the resized image, in pixels', + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function validateArguments(ImageInterface $image, array $arguments) { + // Assure integers for all arguments. + $arguments['width'] = (int) round($arguments['width']); + $arguments['height'] = (int) round($arguments['height']); + + // Fail when width or height are 0 or negative. + if ($arguments['width'] <= 0) { + throw new \InvalidArgumentException("Invalid width specified for the image 'resize' operation."); + } + if ($arguments['height'] <= 0) { + throw new \InvalidArgumentException("Invalid height specified for the image 'resize' operation."); + } + + return $arguments; + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments = array()) { + $res = $this->getToolkit()->createTmp($this->getToolkit()->getType(), $arguments['width'], $arguments['height']); + + if (!imagecopyresampled($res, $this->getToolkit()->getResource(), 0, 0, 0, 0, $arguments['width'], $arguments['height'], $this->getToolkit()->getWidth($image), $this->getToolkit()->getHeight($image))) { + return FALSE; + } + + imagedestroy($this->getToolkit()->getResource()); + // Update image object. + $this->getToolkit()->setResource($res); + + return TRUE; + } + +} diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Rotate.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Rotate.php new file mode 100644 index 0000000..f9b51d1 --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Rotate.php @@ -0,0 +1,89 @@ + array( + 'description' => 'The number of (clockwise) degrees to rotate the image', + ), + 'background' => array( + 'description' => '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', + 'required' => FALSE, + 'default' => NULL, + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments) { + // PHP installations using non-bundled GD do not have imagerotate. + if (!function_exists('imagerotate')) { + \Drupal::logger('image')->notice('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 (!empty($arguments['background'])) { + $rgb = array(); + for ($i = 16; $i >= 0; $i -= 8) { + $rgb[] = (($arguments['background'] >> $i) & 0xFF); + } + $arguments['background'] = imagecolorallocatealpha($this->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($this->getToolkit()->getResource()); + + // If no transparent colors, use white. + if ($arguments['background'] == 0) { + $arguments['background'] = imagecolorallocatealpha($this->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 ($this->getToolkit()->getType() == IMAGETYPE_GIF) { + $transparent_index = imagecolortransparent($this->getToolkit()->getResource()); + if ($transparent_index != 0) { + $transparent_gif_color = imagecolorsforindex($this->getToolkit()->getResource(), $transparent_index); + } + } + + $this->getToolkit()->setResource(imagerotate($this->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($this->getToolkit()->getResource(), $transparent_gif_color['red'], $transparent_gif_color['green'], $transparent_gif_color['blue'], $transparent_gif_color['alpha']); + imagecolortransparent($this->getToolkit()->getResource(), $arguments['background']); + } + + return TRUE; + } + +} diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Scale.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Scale.php new file mode 100644 index 0000000..4d7f7f3 --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Scale.php @@ -0,0 +1,98 @@ + array( + 'description' => 'The target width, in pixels. This value is omitted then the scaling will based only on the height value', + 'required' => FALSE, + 'default' => NULL, + ), + 'height' => array( + 'description' => 'The target height, in pixels. This value is omitted then the scaling will based only on the width value', + 'required' => FALSE, + 'default' => NULL, + ), + 'upscale' => array( + 'description' => 'Boolean indicating that files smaller than the dimensions will be scaled up. This generally results in a low quality image', + 'required' => FALSE, + 'default' => FALSE, + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function validateArguments(ImageInterface $image, array $arguments) { + // Assure at least one dimension. + if (empty($arguments['width']) && empty($arguments['height'])) { + throw new \InvalidArgumentException("At least one dimension ('width' or 'height') must be provided to the image 'scale' operation."); + } + + // 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. + $aspect = $this->getToolkit()->getHeight($image) / $this->getToolkit()->getWidth($image); + if (($arguments['width'] && !$arguments['height']) || ($arguments['width'] && $arguments['height'] && $aspect < $arguments['height'] / $arguments['width'])) { + $arguments['height'] = (int) round($arguments['width'] * $aspect); + } + else { + $arguments['width'] = (int) round($arguments['height'] / $aspect); + } + + // Assure integers for all arguments. + $arguments['width'] = (int) round($arguments['width']); + $arguments['height'] = (int) round($arguments['height']); + + // Fail when width or height are 0 or negative. + if ($arguments['width'] <= 0) { + throw new \InvalidArgumentException("Invalid width specified for the image 'scale' operation."); + } + if ($arguments['height'] <= 0) { + throw new \InvalidArgumentException("Invalid height specified for the image 'scale' operation."); + } + + return $arguments; + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments = array()) { + // Don't scale if we don't change the dimensions at all. + if ($arguments['width'] !== $this->getToolkit()->getWidth($image) || $arguments['height'] !== $this->getToolkit()->getHeight($image)) { + // Don't upscale if the option isn't enabled. + if ($arguments['upscale'] || ($arguments['width'] <= $this->getToolkit()->getWidth($image) && $arguments['height'] <= $this->getToolkit()->getHeight($image))) { + return parent::execute($image, $arguments); + } + } + return TRUE; + } + +} diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/ScaleAndCrop.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/ScaleAndCrop.php new file mode 100644 index 0000000..14df6b9 --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/ScaleAndCrop.php @@ -0,0 +1,65 @@ + array( + 'description' => 'The target width, in pixels', + ), + 'height' => array( + 'description' => 'The target height, in pixels', + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function validateArguments(ImageInterface $image, array $arguments) { + $actualWidth = $this->getToolkit()->getWidth($image); + $actualHeight = $this->getToolkit()->getHeight($image); + + $scaleFactor = max($arguments['width'] / $actualWidth, $arguments['height'] / $actualHeight); + + $arguments['x'] = (int) round(($actualWidth * $scaleFactor - $arguments['width']) / 2); + $arguments['y'] = (int) round(($actualHeight * $scaleFactor - $arguments['height']) / 2); + $arguments['resize'] = array( + 'width' => (int) round($actualWidth * $scaleFactor), + 'height' => (int) round($actualHeight * $scaleFactor), + ); + + return $arguments; + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments = array()) { + return $this->getToolkit()->apply('resize', $image, $arguments['resize']) + && $this->getToolkit()->apply('crop', $image, $arguments); + } + +} diff --git a/core/modules/system/src/Tests/Image/ToolkitGdTest.php b/core/modules/system/src/Tests/Image/ToolkitGdTest.php index 1e0f192..1ba8c19 100644 --- a/core/modules/system/src/Tests/Image/ToolkitGdTest.php +++ b/core/modules/system/src/Tests/Image/ToolkitGdTest.php @@ -129,49 +129,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), @@ -183,28 +183,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), @@ -238,7 +238,7 @@ function testManipulations() { // Load up a fresh image. $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file); $toolkit = $image->getToolkit(); - if (!$image) { + if (!$image->isValid()) { $this->fail(String::format('Could not load image %file.', array('%file' => $file))); continue 2; } @@ -255,7 +255,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. @@ -315,4 +315,25 @@ function testManipulations() { } } + /** + * Tests calling a missing image operation plugin. + */ + function testMissingOperation() { + + // Test that the image factory is set to use the GD toolkit. + $this->assertEqual($this->imageFactory->getToolkitId(), 'gd', 'The image factory is set to use the \'gd\' image toolkit.'); + + // An image file that will be tested. + $file = 'image-test.png'; + + // Load up a fresh image. + $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file); + if (!$image->isValid()) { + $this->fail(String::format('Could not load image %file.', array('%file' => $file))); + } + + // Try perform a missing toolkit operation. + $this->assertFalse($image->apply('missing_op', array()), 'Calling a missing image toolkit operation plugin fails.'); + } + } diff --git a/core/modules/system/src/Tests/Image/ToolkitTest.php b/core/modules/system/src/Tests/Image/ToolkitTest.php index dc60eca..5588c96 100644 --- a/core/modules/system/src/Tests/Image/ToolkitTest.php +++ b/core/modules/system/src/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/src/Tests/Image/ToolkitTestBase.php b/core/modules/system/src/Tests/Image/ToolkitTestBase.php index c8b04e1..eabc88d 100644 --- a/core/modules/system/src/Tests/Image/ToolkitTestBase.php +++ b/core/modules/system/src/Tests/Image/ToolkitTestBase.php @@ -116,6 +116,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/src/Plugin/ImageToolkit/Operation/test/Crop.php b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Crop.php new file mode 100644 index 0000000..3928ff5 --- /dev/null +++ b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Crop.php @@ -0,0 +1,57 @@ + array( + 'description' => 'The starting x offset at which to start the crop, in pixels', + ), + 'y' => array( + 'description' => 'The starting y offset at which to start the crop, in pixels', + ), + 'width' => array( + 'description' => 'The width of the cropped area, in pixels', + 'required' => FALSE, + 'default' => NULL, + ), + 'height' => array( + 'description' => 'The height of the cropped area, in pixels', + 'required' => FALSE, + 'default' => NULL, + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments) { + $this->getToolkit()->logCall('crop', array($image, $arguments['x'], $arguments['y'], $arguments['width'], $arguments['height'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Desaturate.php b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Desaturate.php new file mode 100644 index 0000000..1c87c5c --- /dev/null +++ b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Desaturate.php @@ -0,0 +1,41 @@ +getToolkit()->logCall('desaturate', array($image)); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Resize.php b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Resize.php new file mode 100644 index 0000000..6d2441e --- /dev/null +++ b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Resize.php @@ -0,0 +1,47 @@ + array( + 'description' => 'The new width of the resized image, in pixels', + ), + 'height' => array( + 'description' => 'The new height of the resized image, in pixels', + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments) { + $this->getToolkit()->logCall('resize', array($image, $arguments['width'], $arguments['height'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Rotate.php b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Rotate.php new file mode 100644 index 0000000..7d75121 --- /dev/null +++ b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Rotate.php @@ -0,0 +1,49 @@ + array( + 'description' => 'The number of (clockwise) degrees to rotate the image', + ), + 'background' => array( + 'description' => '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', + 'required' => FALSE, + 'default' => NULL, + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments) { + $this->getToolkit()->logCall('rotate', array($image, $arguments['degrees'], $arguments['background'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Scale.php b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Scale.php new file mode 100644 index 0000000..198a4ea --- /dev/null +++ b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/Scale.php @@ -0,0 +1,55 @@ + array( + 'description' => 'The target width, in pixels. This value is omitted then the scaling will based only on the height value', + 'required' => FALSE, + 'default' => NULL, + ), + 'height' => array( + 'description' => 'The target height, in pixels. This value is omitted then the scaling will based only on the width value', + 'required' => FALSE, + 'default' => NULL, + ), + 'upscale' => array( + 'description' => 'Boolean indicating that files smaller than the dimensions will be scaled up. This generally results in a low quality image', + 'required' => FALSE, + 'default' => FALSE, + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments) { + $this->getToolkit()->logCall('scale', array($image, $arguments['width'], $arguments['height'], $arguments['upscale'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/ScaleAndCrop.php b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/ScaleAndCrop.php new file mode 100644 index 0000000..f857e69 --- /dev/null +++ b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/Operation/test/ScaleAndCrop.php @@ -0,0 +1,46 @@ + array( + 'description' => 'The target width, in pixels', + ), + 'height' => array( + 'description' => 'The target height, in pixels', + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(ImageInterface $image, array $arguments) { + $this->getToolkit()->logCall('scale_and_crop', array($image, $arguments['width'], $arguments['height'])); + return TRUE; + } + +} diff --git a/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/TestToolkit.php b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/TestToolkit.php index 452087a..7c24947 100644 --- a/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/TestToolkit.php +++ b/core/modules/system/tests/modules/image_test/src/Plugin/ImageToolkit/TestToolkit.php @@ -93,54 +93,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 @@ -152,7 +104,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 e2542ed..058e222 100644 --- a/core/tests/Drupal/Tests/Core/Image/ImageTest.php +++ b/core/tests/Drupal/Tests/Core/Image/ImageTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\Image; use Drupal\Core\Image\Image; +use Drupal\Core\ImageToolkit\ImageToolkitInterface; use Drupal\Tests\UnitTestCase; /** @@ -16,6 +17,13 @@ class ImageTest extends UnitTestCase { /** + * Image source path. + * + * @var string + */ + protected $source; + + /** * Image object. * * @var \Drupal\Core\Image\Image @@ -23,12 +31,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,16 +55,12 @@ public static function getInfo() { ); } + /** + * @inheritdoc + */ protected function setUp() { // Use the Druplicon image. $this->source = __DIR__ . '/../../../../../misc/druplicon.png'; - $this->toolkit = $this->getToolkitMock(); - - $this->toolkit->expects($this->any()) - ->method('getPluginId') - ->will($this->returnValue('gd')); - - $this->image = new Image($this->toolkit, $this->source); } /** @@ -67,44 +81,111 @@ protected function getToolkitMock(array $stubs = array()) { } /** + * Mocks a toolkit operation. + * + * @param string $class_name + * The name of the GD toolkit operation class to be mocked. + * @param ImageToolkitInterface $toolkit + * The image toolkit object. + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getToolkitOperationMock($class_name, ImageToolkitInterface $toolkit) { + $mock_builder = $this->getMockBuilder('Drupal\system\Plugin\ImageToolkit\Operation\gd\\' . $class_name); + return $mock_builder + ->setMethods(array('execute')) + ->setConstructorArgs(array(array(), '', array(), $toolkit)) + ->getMock(); + } + + /** + * Get an image with a mocked toolkit, for testing. + * + * @param array $stubs + * (optional) Array containing toolkit methods to be replaced with stubs. + * + * @return \Drupal\Core\Image\Image + * An image object. + */ + protected function getTestImage(array $stubs = array()) { + $this->toolkit = $this->getToolkitMock(); + + $this->toolkit->expects($this->any()) + ->method('getPluginId') + ->will($this->returnValue('gd')); + + $this->image = new Image($this->toolkit, $this->source); + } + + /** + * Get an image with mocked toolkit and operation, for operation testing. + * + * @param string $class_name + * The name of the GD toolkit operation class to be mocked. + * + * @return \Drupal\Core\Image\Image + * An image object. + */ + protected function getTestImageForOperation($class_name) { + $this->toolkit = $this->getToolkitMock(array('getToolkitOperation', 'getPluginId')); + $this->toolkitOperation = $this->getToolkitOperationMock($class_name, $this->toolkit); + + $this->toolkit->expects($this->any()) + ->method('getPluginId') + ->will($this->returnValue('gd')); + + $this->toolkit->expects($this->any()) + ->method('getToolkitOperation') + ->will($this->returnValue($this->toolkitOperation)); + + $this->image = new Image($this->toolkit, $this->source); + } + + /** * 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::getWidth(). */ public function testGetWidth() { - $this->assertEquals($this->image->getWidth(), 88); + $this->getTestImage(); + $this->assertEquals(88, $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::getToolkit()->getType(). */ public function testGetType() { - $this->assertEquals($this->image->getToolkit()->getType(), IMAGETYPE_PNG); + $this->getTestImage(); + $this->assertEquals(IMAGETYPE_PNG, $this->image->getToolkit()->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::isValid(). */ public function testIsValid() { + $this->getTestImage(); $this->assertTrue($this->image->isValid()); $this->assertTrue(is_readable($this->image->getSource())); } @@ -113,13 +194,15 @@ public function testIsValid() { * 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') @@ -137,6 +220,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') @@ -149,6 +233,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') @@ -177,166 +262,179 @@ public function testParseFileFails() { * Tests \Drupal\Core\Image\Image::scale(). */ public function testScaleWidth() { - $toolkit = $this->getToolkitMock(array('resize')); - $image = new Image($toolkit, $this->source); - - $toolkit->expects($this->any()) - ->method('resize') - ->will($this->returnArgument(2)); - $height = $image->scale(44); - $this->assertEquals($height, 50); + $this->getTestImageForOperation('Scale'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->will($this->returnArgument(1)); + + $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($toolkit, $this->source); - - $toolkit->expects($this->any()) - ->method('resize') + $this->getTestImageForOperation('Scale'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') ->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($toolkit, $this->source); - + $this->getTestImageForOperation('Scale'); // Dimensions are the same, resize should not be called. - $toolkit->expects($this->never()) - ->method('resize') + $this->toolkitOperation->expects($this->once()) + ->method('execute') ->will($this->returnArgument(1)); - $width = $image->scale(88, 100); - $this->assertEquals($width, 88); + $ret = $this->image->apply('scale', array ('width' => 88, 'height' => 100, 'upscale' => FALSE)); + $this->assertEquals(88, $ret['width']); + $this->assertEquals(100, $ret['height']); } /** * Tests \Drupal\Core\Image\Image::scaleAndCrop(). */ public function testScaleAndCropWidth() { - $toolkit = $this->getToolkitMock(array('resize', 'crop')); - $image = new Image($toolkit, $this->source); - - $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('execute') ->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($toolkit, $this->source); - - $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('execute') + ->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($toolkit, $this->source); - - $toolkit->expects($this->once()) - ->method('resize') - ->will($this->returnValue(FALSE)); + $this->getTestImageForOperation('ScaleAndCrop'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->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('execute') + ->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('execute') + ->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() { - $toolkit = $this->getToolkitMock(array('crop')); - $image = new Image($toolkit, $this->source); - - $toolkit->expects($this->once()) - ->method('crop') - ->will($this->returnArgument(3)); - $width = $image->crop(0, 0, 44, 50); - $this->assertEquals($width, 44); + $this->getTestImageForOperation('Crop'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->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(). - * - * @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 testResize() { + $this->getTestImageForOperation('Resize'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->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(). + */ + public function testFloatResize() { + $this->getTestImageForOperation('Resize'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->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() { - $toolkit = $this->getToolkitMock(array('desaturate')); - $image = new Image($toolkit, $this->source); + $this->getTestImageForOperation('Desaturate'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->will($this->returnArgument(1)); - $toolkit->expects($this->once()) - ->method('desaturate'); - $image->desaturate(); + $this->image->apply('desaturate'); } /** * Tests \Drupal\Core\Image\Image::rotate(). */ public function testRotate() { - $toolkit = $this->getToolkitMock(array('rotate')); - $image = new Image($toolkit, $this->source); + $this->getTestImageForOperation('Rotate'); + $this->toolkitOperation->expects($this->once()) + ->method('execute') + ->will($this->returnArgument(1)); - $toolkit->expects($this->once()) - ->method('rotate'); - $image->rotate(90); + $ret = $this->image->apply('rotate', array('degrees' => 90)); + $this->assertEquals(90, $ret['degrees']); } }