Commit f4378075 authored by alexpott's avatar alexpott

Issue #2103621 by mondrake | fietserwin: Move GD logic from ImageInterface to...

Issue #2103621 by mondrake | fietserwin: Move GD logic from ImageInterface to toolkit, tie toolkit instance to Image instance, toolkits should no longer be instantiated separately.
parent 3b3cb6f9
......@@ -608,13 +608,9 @@ services:
image.toolkit.manager:
class: Drupal\Core\ImageToolkit\ImageToolkitManager
arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@config.factory', '@module_handler']
image.toolkit:
class: Drupal\Core\ImageToolkit\ImageToolkitInterface
factory_method: getDefaultToolkit
factory_service: image.toolkit.manager
image.factory:
class: Drupal\Core\Image\ImageFactory
arguments: ['@image.toolkit']
arguments: ['@image.toolkit.manager']
breadcrumb:
class: Drupal\Core\Breadcrumb\BreadcrumbManager
arguments: ['@module_handler']
......
......@@ -33,13 +33,6 @@ class Image implements ImageInterface {
*/
protected $toolkit;
/**
* An image file handle.
*
* @var resource
*/
protected $resource;
/**
* Height, in pixels.
*
......@@ -109,6 +102,14 @@ public function isSupported() {
return in_array($this->getType(), $this->toolkit->supportedTypes());
}
/**
* {@inheritdoc}
*/
public function isExisting() {
$this->processInfo();
return $this->processed;
}
/**
* {@inheritdoc}
*/
......@@ -173,32 +174,6 @@ public function getMimeType() {
return $this->mimeType;
}
/**
* {@inheritdoc}
*/
public function setResource($resource) {
$this->resource = $resource;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasResource() {
return (bool) $this->resource;
}
/**
* {@inheritdoc}
*/
public function getResource() {
if (!$this->hasResource()) {
$this->processInfo();
$this->toolkit->load($this);
}
return $this->resource;
}
/**
* {@inheritdoc}
*/
......@@ -221,6 +196,14 @@ public function getToolkitId() {
return $this->toolkit->getPluginId();
}
/**
* {@inheritdoc}
*/
public function getToolkit() {
$this->processInfo();
return $this->toolkit;
}
/**
* {@inheritdoc}
*/
......@@ -287,22 +270,30 @@ protected function processInfo() {
* image toolkit.
*
* This is a temporary solution to keep patches reviewable. The __call()
* method will be replaced in https://drupal.org/node/2073759 with a new
* method will be replaced in https://drupal.org/node/2110499 with a new
* interface method ImageInterface::apply(). An image operation will be
* performed as in the next example:
* @code
* $image = new Image($path, $toolkit);
* $image->apply('scale', array('width' => 50, 'height' => 100));
* @endcode
* Also in https://drupal.org/node/2073759 operation arguments sent to toolkit
* 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/2073759 in favor of new apply().
* @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 get invoked
// from this class anyway through the magic __call. Will be removed
// through https://drupal.org/node/2110499, when call_user_func_array()
// will be replaced by $this->toolkit->apply($name, $this, $arguments).
if (in_array($method, array('setResource', 'getResource', 'hasResource'))) {
throw new \BadMethodCallException();
}
if (is_callable(array($this->toolkit, $method))) {
// @todo In https://drupal.org/node/2073759, call_user_func_array() will
// @todo In https://drupal.org/node/2110499, call_user_func_array() will
// be replaced by $this->toolkit->apply($name, $this, $arguments).
array_unshift($arguments, $this);
return call_user_func_array(array($this->toolkit, $method), $arguments);
......
......@@ -7,7 +7,7 @@
namespace Drupal\Core\Image;
use Drupal\Core\ImageToolkit\ImageToolkitInterface;
use Drupal\Core\ImageToolkit\ImageToolkitManager;
/**
* Provides a factory for image objects.
......@@ -15,33 +15,40 @@
class ImageFactory {
/**
* The image toolkit to use for this factory.
* The image toolkit plugin manager.
*
* @var \Drupal\Core\ImageToolkit\ImageToolkitInterface
* @var \Drupal\Core\ImageToolkit\ImageToolkitManager
*/
protected $toolkit;
protected $toolkitManager;
/**
* The image toolkit ID to use for this factory.
*
* @var string
*/
protected $toolkitId;
/**
* Constructs a new ImageFactory object.
*
* @param \Drupal\Core\ImageToolkit\ImageToolkitInterface $toolkit
* The image toolkit to use for this image factory.
* @param \Drupal\Core\ImageToolkit\ImageToolkitManager $toolkit_manager
* The image toolkit plugin manager.
*/
public function __construct(ImageToolkitInterface $toolkit) {
$this->toolkit = $toolkit;
public function __construct(ImageToolkitManager $toolkit_manager) {
$this->toolkitManager = $toolkit_manager;
}
/**
* Sets a custom image toolkit.
* Sets this factory image toolkit ID.
*
* @param \Drupal\Core\ImageToolkit\ImageToolkitInterface $toolkit
* The image toolkit to use for this image factory.
* @param string $toolkit_id
* The image toolkit ID to use for this image factory.
*
* @return self
* Returns this image.
*/
public function setToolkit(ImageToolkitInterface $toolkit) {
$this->toolkit = $toolkit;
public function setToolkitId($toolkit_id) {
$this->toolkitId = $toolkit_id;
return $this;
}
......@@ -55,7 +62,10 @@ public function setToolkit(ImageToolkitInterface $toolkit) {
* The new Image object.
*/
public function get($source) {
return new Image($source, $this->toolkit);
if (!$this->toolkitId) {
$this->toolkitId = $this->toolkitManager->getDefaultToolkitId();
}
return new Image($source, $this->toolkitManager->createInstance($this->toolkitId));
}
}
......@@ -20,6 +20,14 @@ interface ImageInterface {
*/
public function isSupported();
/**
* Checks if the image is existing.
*
* @return bool
* TRUE if the image exists and is a valid image, FALSE otherwise.
*/
public function isExisting();
/**
* Returns the extension of the image file.
*
......@@ -89,33 +97,6 @@ public function getType();
*/
public function getMimeType();
/**
* Sets the image file resource.
*
* @param resource $resource
* The image file handle.
*
* @return self
* Returns this image file.
*/
public function setResource($resource);
/**
* Determines if this image file has a resource set.
*
* @return bool
* TRUE if this image file has a resource set, FALSE otherwise.
*/
public function hasResource();
/**
* Retrieves the image file resource.
*
* @return resource
* The image file handle.
*/
public function getResource();
/**
* Sets the source path of the image file.
*
......@@ -135,6 +116,14 @@ public function setSource($source);
*/
public function getSource();
/**
* Returns the image toolkit used for this image file.
*
* @return string
* The image toolkit.
*/
public function getToolkit();
/**
* Returns the ID of the image toolkit used for this image file.
*
......
......@@ -131,17 +131,6 @@ public function crop(ImageInterface $image, $x, $y, $width, $height);
*/
public function desaturate(ImageInterface $image);
/**
* Creates an image resource from a file.
*
* @param \Drupal\Core\Image\ImageInterface $image
* An image object. The $image->resource value will populated by this call.
*
* @return bool
* TRUE or FALSE, based on success.
*/
public function load(ImageInterface $image);
/**
* Writes an image resource to a destination file.
*
......
......@@ -48,12 +48,12 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
}
/**
* Gets the default image toolkit.
* Gets the default image toolkit ID.
*
* @return \Drupal\Core\ImageToolkit\ImageToolkitInterface
* Object of the default toolkit, or FALSE on error.
* @return string|bool
* ID of the default toolkit, or FALSE on error.
*/
public function getDefaultToolkit() {
public function getDefaultToolkitId() {
$toolkit_id = $this->configFactory->get('system.image')->get('toolkit');
$toolkits = $this->getAvailableToolkits();
......@@ -64,14 +64,20 @@ public function getDefaultToolkit() {
$toolkit_id = key($toolkits);
}
if ($toolkit_id) {
$toolkit = $this->createInstance($toolkit_id);
}
else {
$toolkit = FALSE;
}
return $toolkit_id;
}
return $toolkit;
/**
* Gets the default image toolkit.
*
* @return \Drupal\Core\ImageToolkit\ImageToolkitInterface
* Object of the default toolkit, or FALSE on error.
*/
public function getDefaultToolkit() {
if ($toolkit_id = $this->getDefaultToolkitId()) {
return $this->createInstance($toolkit_id);
}
return FALSE;
}
/**
......
......@@ -406,7 +406,7 @@ function file_validate_is_image(File $file) {
$image = \Drupal::service('image.factory')->get($file->getFileUri());
if (!$image->isSupported()) {
$toolkit = \Drupal::service('image.toolkit');
$toolkit = \Drupal::service('image.toolkit.manager')->getDefaultToolkit();
$extensions = array();
foreach ($toolkit->supportedTypes() as $image_type) {
$extensions[] = Unicode::strtoupper(image_type_to_extension($image_type));
......@@ -453,7 +453,7 @@ function file_validate_image_resolution(File $file, $maximum_dimensions = 0, $mi
if ($image->getWidth() > $width || $image->getHeight() > $height) {
// Try to resize the image to fit the dimensions.
$image = $image_factory->get($file->getFileUri());
if ($image->getResource()) {
if ($image->isExisting()) {
$image->scale($width, $height);
$image->save();
$file->filesize = $image->getFileSize();
......
......@@ -79,7 +79,7 @@ function testFileValidateImageResolution() {
$this->assertEqual(count($errors), 1, 'Small images report an error.', 'File');
// Maximum size.
if ($this->container->has('image.toolkit')) {
if ($this->container->has('image.toolkit.manager')) {
// Copy the image so that the original doesn't get resized.
copy('core/misc/druplicon.png', 'temporary://druplicon.png');
$this->image->setFileUri('temporary://druplicon.png');
......
......@@ -278,7 +278,7 @@ public function createDerivative($original_uri, $derivative_uri) {
}
$image = \Drupal::service('image.factory')->get($original_uri);
if (!$image->getResource()) {
if (!$image->isExisting()) {
return FALSE;
}
......
......@@ -21,6 +21,37 @@
*/
class GDToolkit extends ImageToolkitBase {
/**
* A GD image resource.
*
* @var resource
*/
protected $resource;
/**
* Sets the GD image resource.
*
* @param resource $resource
* The GD image resource.
*
* @return self
* Returns this toolkit object.
*/
public function setResource($resource) {
$this->resource = $resource;
return $this;
}
/**
* Retrieves the GD image resource.
*
* @return resource
* The GD image resource.
*/
public function getResource() {
return $this->resource;
}
/**
* {@inheritdoc}
*/
......@@ -55,16 +86,16 @@ public function resize(ImageInterface $image, $width, $height) {
$width = (int) round($width);
$height = (int) round($height);
$res = $this->createTmp($image, $width, $height);
$res = $this->createTmp($image->getType(), $width, $height);
if (!imagecopyresampled($res, $image->getResource(), 0, 0, 0, 0, $width, $height, $image->getWidth(), $image->getHeight())) {
if (!imagecopyresampled($res, $this->getResource(), 0, 0, 0, 0, $width, $height, $image->getWidth(), $image->getHeight())) {
return FALSE;
}
imagedestroy($image->getResource());
imagedestroy($this->getResource());
// Update image object.
$this->setResource($res);
$image
->setResource($res)
->setWidth($width)
->setHeight($height);
return TRUE;
......@@ -86,39 +117,39 @@ public function rotate(ImageInterface $image, $degrees, $background = NULL) {
for ($i = 16; $i >= 0; $i -= 8) {
$rgb[] = (($background >> $i) & 0xFF);
}
$background = imagecolorallocatealpha($image->getResource(), $rgb[0], $rgb[1], $rgb[2], 0);
$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($image->getResource());
$background = imagecolortransparent($this->getResource());
// If no transparent colors, use white.
if ($background == 0) {
$background = imagecolorallocatealpha($image->getResource(), 255, 255, 255, 0);
$background = imagecolorallocatealpha($this->getResource(), 255, 255, 255, 0);
}
}
// Images are assigned a new color palette when rotating, removing any
// transparency flags. For GIF images, keep a record of the transparent color.
if ($image->getType() == IMAGETYPE_GIF) {
$transparent_index = imagecolortransparent($image->getResource());
$transparent_index = imagecolortransparent($this->getResource());
if ($transparent_index != 0) {
$transparent_gif_color = imagecolorsforindex($image->getResource(), $transparent_index);
$transparent_gif_color = imagecolorsforindex($this->getResource(), $transparent_index);
}
}
$image->setResource(imagerotate($image->getResource(), 360 - $degrees, $background));
$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($image->getResource(), $transparent_gif_color['red'], $transparent_gif_color['green'], $transparent_gif_color['blue'], $transparent_gif_color['alpha']);
imagecolortransparent($image->getResource(), $background);
$background = imagecolorexactalpha($this->getResource(), $transparent_gif_color['red'], $transparent_gif_color['green'], $transparent_gif_color['blue'], $transparent_gif_color['alpha']);
imagecolortransparent($this->getResource(), $background);
}
$image
->setWidth(imagesx($image->getResource()))
->setHeight(imagesy($image->getResource()));
->setWidth(imagesx($this->getResource()))
->setHeight(imagesy($this->getResource()));
return TRUE;
}
......@@ -134,16 +165,16 @@ public function crop(ImageInterface $image, $x, $y, $width, $height) {
$width = (int) round($width);
$height = (int) round($height);
$res = $this->createTmp($image, $width, $height);
$res = $this->createTmp($image->getType(), $width, $height);
if (!imagecopyresampled($res, $image->getResource(), 0, 0, $x, $y, $width, $height, $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($image->getResource());
imagedestroy($this->getResource());
$this->setResource($res);
$image
->setResource($res)
->setWidth($width)
->setHeight($height);
return TRUE;
......@@ -159,7 +190,7 @@ public function desaturate(ImageInterface $image) {
return FALSE;
}
return imagefilter($image->getResource(), IMG_FILTER_GRAYSCALE);
return imagefilter($this->getResource(), IMG_FILTER_GRAYSCALE);
}
/**
......@@ -199,21 +230,29 @@ public function scaleAndCrop(ImageInterface $image, $width, $height) {
}
/**
* {@inheritdoc}
* Creates a resource from a file.
*
* @param string $source
* String specifying the path of the image file.
* @param array $details
* An array of image details.
*
* @return bool
* TRUE or FALSE, based on success.
*/
public function load(ImageInterface $image) {
$function = 'imagecreatefrom' . image_type_to_extension($image->getType(), FALSE);
if (function_exists($function) && $resource = $function($image->getSource())) {
$image->setResource($resource);
protected function load($source, array $details) {
$function = 'imagecreatefrom' . image_type_to_extension($details['type'], FALSE);
if (function_exists($function) && $resource = $function($source)) {
$this->setResource($resource);
if (!imageistruecolor($resource)) {
// Convert indexed images to true color, so that filters work
// correctly and don't result in unnecessary dither.
$new_image = $this->createTmp($image, $image->getWidth(), $image->getHeight());
imagecopy($new_image, $resource, 0, 0, 0, 0, $image->getWidth(), $image->getHeight());
$new_image = $this->createTmp($details['type'], $details['width'], $details['height']);
imagecopy($new_image, $resource, 0, 0, 0, 0, $details['width'], $details['height']);
imagedestroy($resource);
$image->setResource($new_image);
$this->setResource($new_image);
}
return (bool) $image->getResource();
return (bool) $this->getResource();
}
return FALSE;
......@@ -241,15 +280,15 @@ public function save(ImageInterface $image, $destination) {
return FALSE;
}
if ($image->getType() == IMAGETYPE_JPEG) {
$success = $function($image->getResource(), $destination, \Drupal::config('system.image.gd')->get('jpeg_quality'));
$success = $function($this->getResource(), $destination, \Drupal::config('system.image.gd')->get('jpeg_quality'));
}
else {
// Always save PNG images with full transparency.
if ($image->getType() == IMAGETYPE_PNG) {
imagealphablending($image->getResource(), FALSE);
imagesavealpha($image->getResource(), TRUE);
imagealphablending($this->getResource(), FALSE);
imagesavealpha($this->getResource(), TRUE);
}
$success = $function($image->getResource(), $destination);
$success = $function($this->getResource(), $destination);
}
// Move temporary local file to remote destination.
if (isset($permanent_destination) && $success) {
......@@ -265,7 +304,7 @@ public function getInfo(ImageInterface $image) {
$details = FALSE;
$data = getimagesize($image->getSource());
if (isset($data) && is_array($data)) {
if (isset($data) && is_array($data) && in_array($data[2], $this->supportedTypes())) {
$details = array(
'width' => $data[0],
'height' => $data[1],
......@@ -274,14 +313,18 @@ public function getInfo(ImageInterface $image) {
);
}
if ($details) {
$this->load($image->getSource(), $details);
}
return $details;
}
/**
* Creates a truecolor image preserving transparency from a provided image.
*
* @param \Drupal\Core\Image\ImageInterface $image
* An image object.
* @param int $type
* An image type represented by a PHP IMAGETYPE_* constant (e.g.
* IMAGETYPE_JPEG, IMAGETYPE_PNG, etc.).
* @param int $width
* The new width of the new image, in pixels.
* @param int $height
......@@ -290,16 +333,16 @@ public function getInfo(ImageInterface $image) {
* @return resource
* A GD image handle.
*/
public function createTmp(ImageInterface $image, $width, $height) {
public function createTmp($type, $width, $height) {
$res = imagecreatetruecolor($width, $height);
if ($image->getType() == IMAGETYPE_GIF) {
if ($type == IMAGETYPE_GIF) {
// Grab transparent color index from image resource.