Commit 697991fa authored by alexpott's avatar alexpott

Issue #2073759 by mondrake, fietserwin, claudiu.cristea: Convert toolkit operations to plugins.

parent a780b52e
......@@ -734,7 +734,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']
......
......@@ -58,7 +58,7 @@ class Image implements ImageInterface {
*/
public function __construct(ImageToolkitInterface $toolkit, $source = NULL) {
$this->toolkit = $toolkit;
$this->toolkit->setImage($this);
$this->getToolkit()->setImage($this);
if ($source) {
$this->source = $source;
$this->parseFile();
......@@ -76,14 +76,14 @@ public function isValid() {
* {@inheritdoc}
*/
public function getHeight() {
return $this->toolkit->getHeight();
return $this->getToolkit()->getHeight();
}
/**
* {@inheritdoc}
*/
public function getWidth() {
return $this->toolkit->getWidth();
return $this->getToolkit()->getWidth();
}
/**
......@@ -97,7 +97,7 @@ public function getFileSize() {
* {@inheritdoc}
*/
public function getMimeType() {
return $this->toolkit->getMimeType();
return $this->getToolkit()->getMimeType();
}
/**
......@@ -111,7 +111,7 @@ public function getSource() {
* {@inheritdoc}
*/
public function getToolkitId() {
return $this->toolkit->getPluginId();
return $this->getToolkit()->getPluginId();
}
/**
......@@ -131,7 +131,7 @@ public function save($destination = NULL) {
}
$destination = $destination ?: $this->getSource();
if ($return = $this->toolkit->save($destination)) {
if ($return = $this->getToolkit()->save($destination)) {
// Clear the cached file size and refresh the image information.
clearstatcache(TRUE, $destination);
$this->fileSize = filesize($destination);
......@@ -157,48 +157,59 @@ public function save($destination = NULL) {
* image information is populated.
*/
protected function parseFile() {
if ($this->valid = $this->toolkit->parseFile()) {
if ($this->valid = $this->getToolkit()->parseFile()) {
$this->fileSize = filesize($this->source);
}
return $this->valid;
}
/**
* 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', 'setImage'))) {
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, $arguments).
return call_user_func_array(array($this->toolkit, $method), $arguments);
}
throw new \BadMethodCallException($method);
* {@inheritdoc}
*/
public function apply($operation, array $arguments = array()) {
return $this->getToolkit()->apply($operation, $arguments);
}
/**
* {@inheritdoc}
*/
public function crop($x, $y, $width, $height = NULL) {
return $this->apply('crop', array('x' => $x, 'y' => $y, 'width' => $width, 'height' => $height));
}
/**
* {@inheritdoc}
*/
public function desaturate() {
return $this->apply('desaturate', array());
}
/**
* {@inheritdoc}
*/
public function resize($width, $height) {
return $this->apply('resize', array('width' => $width, 'height' => $height));
}
/**
* {@inheritdoc}
*/
public function rotate($degrees, $background = NULL) {
return $this->apply('rotate', array('degrees' => $degrees, 'background' => $background));
}
/**
* {@inheritdoc}
*/
public function scaleAndCrop($width, $height) {
return $this->apply('scale_and_crop', array('width' => $width, 'height' => $height));
}
/**
* {@inheritdoc}
*/
public function scale($width, $height = NULL, $upscale = FALSE) {
return $this->apply('scale', array('width' => $width, 'height' => $height, 'upscale' => $upscale));
}
/**
......
......@@ -78,6 +78,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.
*
......@@ -92,4 +109,97 @@ public function getToolkitId();
*/
public function save($destination = NULL);
/**
* Scales an image while maintaining aspect ratio.
*
* The resulting image can be smaller for one or both target dimensions.
*
* @param int|null $width
* The target width, in pixels. If this value is null then the scaling will
* be based only on the height value.
* @param int|null $height
* (optional) The target height, in pixels. If this value is null then the
* scaling will be 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($width, $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 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($width, $height);
/**
* Crops an image to a rectangle specified by the given dimensions.
*
* @param int $x
* The top left coordinate, in pixels, of the crop area (x axis value).
* @param int $y
* The top left coordinate, in pixels, of the crop area (y axis value).
* @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 crop($x, $y, $width, $height = NULL);
/**
* Resizes an image to the given dimensions (ignoring aspect ratio).
*
* @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 resize($width, $height);
/**
* Converts an image to grayscale.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
public function desaturate();
/**
* Rotates an image by the given number of degrees.
*
* @param float $degrees
* The number of (clockwise) degrees to rotate the image.
* @param string|null $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 on success, FALSE on failure.
*/
public function rotate($degrees, $background = NULL);
}
<?php
/**
* @file
* Contains \Drupal\Core\ImageToolkit\Annotation\ImageToolkitOperation.
*/
namespace Drupal\Core\ImageToolkit\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Plugin annotation object for the image toolkit operation plugin.
*
* @Annotation
*
* @see \Drupal\Core\ImageToolkit\ImageToolkitOperationManager
*/
class ImageToolkitOperation extends Plugin {
/**
* The plugin ID.
*
* There are no strict requirements as to the string to be used to identify
* the plugin, since discovery of the appropriate operation plugin to be
* used to apply an operation is based on the values of the 'toolkit' and
* the 'operation' annotation values.
*
* However, it is recommended that the following patterns be used:
* - '{toolkit}_{operation}' for the first implementation of an operation
* by a toolkit.
* - '{module}_{toolkit}_{operation}' for overrides of existing
* implementations supplied by an alternative module, and for new
* module-supplied operations.
*
* @var string
*/
public $id;
/**
* The id of the image toolkit plugin for which the operation is implemented.
*
* @var string
*/
public $toolkit;
/**
* The machine name of the image toolkit operation implemented (e.g. "crop").
*
* @var string
*/
public $operation;
/**
* The human-readable name of the image toolkit operation.
*
* The string should be wrapped in a @Translation().
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The description of the image toolkit operation.
*
* The string should be wrapped in a @Translation().
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\ImageToolkit;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\Plugin\PluginBase;
......@@ -19,12 +20,36 @@ abstract class ImageToolkitBase extends PluginBase implements ImageToolkitInterf
*/
protected $image;
/**
* The image toolkit operation manager.
*
* @var \Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface
*/
protected $operationManager;
/**
* Constructs an ImageToolkitBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface $operation_manager
* The toolkit operation manager.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, ImageToolkitOperationManagerInterface $operation_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->operationManager = $operation_manager;
}
/**
* {@inheritdoc}
*/
public function setImage(ImageInterface $image) {
if ($this->image) {
throw new \BadMethodCallException(__METHOD__ . '() may only be called once.');
throw new \BadMethodCallException(__METHOD__ . '() may only be called once');
}
$this->image = $image;
}
......@@ -43,4 +68,35 @@ 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, array $arguments = array()) {
try {
// Get the plugin to use for the operation and apply the operation.
return $this->getToolkitOperation($operation)->apply($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;
}
}
}
......@@ -62,11 +62,11 @@ public function settingsFormSubmit($form, &$form_state);
/**
* Sets the image object that this toolkit instance is tied to.
*
* @throws \BadMethodCallException
* When called twice.
*
* @param \Drupal\Core\Image\ImageInterface $image
* The image that this toolkit instance will be tied to.
*
* @throws \BadMethodCallException
* When called twice.
*/
public function setImage(ImageInterface $image);
......@@ -78,65 +78,6 @@ public function setImage(ImageInterface $image);
*/
public function getImage();
/**
* Scales an image to the specified size.
*
* @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 on success, FALSE on failure.
*/
public function resize($width, $height);
/**
* Rotates an image the given number of degrees.
*
* @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 on success, FALSE on failure.
*/
public function rotate($degrees, $background = NULL);
/**
* Crops an image.
*
* @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 on success, FALSE on failure.
*
* @see image_crop()
*/
public function crop($x, $y, $width, $height);
/**
* Converts an image resource to grayscale.
*
* Note that transparent GIFs loose transparency when desaturated.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
public function desaturate();
/**
* Writes an image resource to a destination file.
*
......@@ -148,45 +89,6 @@ public function desaturate();
*/
public function save($destination);
/**
* Scales an image while maintaining aspect ratio.
*
* The resulting image can be smaller for one or both target dimensions.
*
* @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($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 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($width, $height);
/**
* Determines if a file contains a valid image.
*
......@@ -250,4 +152,19 @@ 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 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, array $arguments = array());
}
......@@ -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.
......@@ -24,6 +25,13 @@ class ImageToolkitManager extends DefaultPluginManager {
*/
protected $configFactory;
/**
* The image toolkit operation manager.
*
* @var \Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface
*/
protected $operationManager;
/**
* Constructs the ImageToolkitManager object.
*
......@@ -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);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\ImageToolkit\ImageToolkitOperationBase.
*/
namespace Drupal\Core\ImageToolkit;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Utility\String;
use Drupal\Core\Plugin\PluginBase;
abstract class ImageToolkitOperationBase extends PluginBase implements ImageToolkitOperationInterface {
/**
* The image toolkit.
*
* @var \Drupal\Core\ImageToolkit\ImageToolkitInterface
*/
protected $toolkit;
/**
* Constructs an image toolkit operation plugin.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\ImageToolkit\ImageToolkitInterface $toolkit
* The image toolkit.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, ImageToolkitInterface $toolkit) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->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.
*