Commit ae8eb106 authored by alexpott's avatar alexpott

Issue #2211227 by mondrake, xjm | fietserwin: Refactor image and imagetoolkit:...

Issue #2211227 by mondrake, xjm | fietserwin: Refactor image and imagetoolkit: isExisting, isSupported, supportedTypes, getMimeType.
parent 22fc66d5
......@@ -20,11 +20,11 @@
class Image implements ImageInterface {
/**
* String specifying the path of the image file.
* Path of the image file.
*
* @var string
*/
protected $source;
protected $source = '';
/**
* An image toolkit object.
......@@ -33,26 +33,19 @@ class Image implements ImageInterface {
*/
protected $toolkit;
/**
* Image type represented by a PHP IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG).
*
* @var int
*/
protected $type;
/**
* File size in bytes.
*
* @var int
*/
protected $fileSize = 0;
protected $fileSize;
/**
* If this image file has been processed.
* If this image object is valid.
*
* @var bool
*/
protected $processed = FALSE;
protected $valid = FALSE;
/**
* Constructs a new Image object.
......@@ -67,30 +60,21 @@ public function __construct(ImageToolkitInterface $toolkit, $source = NULL) {
$this->toolkit = $toolkit;
if ($source) {
$this->source = $source;
$this->processInfo();
$this->parseFile();
}
}
/**
* {@inheritdoc}
*/
public function isSupported() {
return in_array($this->getType(), $this->toolkit->supportedTypes());
}
/**
* {@inheritdoc}
*/
public function isExisting() {
$this->processInfo();
return $this->processed;
public function isValid() {
return $this->valid;
}
/**
* {@inheritdoc}
*/
public function getHeight() {
$this->processInfo();
return $this->toolkit->getHeight($this);
}
......@@ -98,7 +82,6 @@ public function getHeight() {
* {@inheritdoc}
*/
public function getWidth() {
$this->processInfo();
return $this->toolkit->getWidth($this);
}
......@@ -106,32 +89,14 @@ public function getWidth() {
* {@inheritdoc}
*/
public function getFileSize() {
$this->processInfo();
return $this->fileSize;
}
/**
* {@inheritdoc}
*/
public function getType() {
$this->processInfo();
return $this->type;
}
/**
* {@inheritdoc}
*/
public function getMimeType() {
$this->processInfo();
return $this->type ? image_type_to_mime_type($this->type) : '';
}
/**
* {@inheritdoc}
*/
public function setSource($source) {
$this->source = $source;
return $this;
return $this->toolkit->getMimeType($this);
}
/**
......@@ -152,7 +117,6 @@ public function getToolkitId() {
* {@inheritdoc}
*/
public function getToolkit() {
$this->processInfo();
return $this->toolkit;
}
......@@ -160,14 +124,17 @@ public function getToolkit() {
* {@inheritdoc}
*/
public function save($destination = NULL) {
if (empty($destination)) {
$destination = $this->getSource();
// Return immediately if the image is not valid.
if (!$this->isValid()) {
return FALSE;
}
$destination = $destination ?: $this->getSource();
if ($return = $this->toolkit->save($this, $destination)) {
// Clear the cached file size and refresh the image information.
clearstatcache(TRUE, $destination);
$this->setSource($destination);
$this->processInfo();
$this->fileSize = filesize($destination);
$this->source = $destination;
// @todo Use File utility when https://drupal.org/node/2050759 is in.
if ($this->chmod($destination)) {
......@@ -178,7 +145,7 @@ public function save($destination = NULL) {
}
/**
* Prepares the image information.
* Determines if a file contains a valid image.
*
* Drupal supports GIF, JPG and PNG file formats when used with the GD
* toolkit, and may support others, depending on which toolkits are
......@@ -188,23 +155,11 @@ public function save($destination = NULL) {
* FALSE, if the file could not be found or is not an image. Otherwise, the
* image information is populated.
*/
protected function processInfo() {
if ($this->processed) {
return TRUE;
}
$destination = $this->getSource();
if (!is_file($destination) && !is_uploaded_file($destination)) {
return FALSE;
}
if ($details = $this->toolkit->getInfo($this)) {
$this->type = $details['type'];
$this->fileSize = filesize($destination);
$this->processed = TRUE;
protected function parseFile() {
if ($this->valid = $this->toolkit->parseFile($this)) {
$this->fileSize = filesize($this->source);
}
return TRUE;
return $this->valid;
}
/**
......@@ -227,22 +182,23 @@ protected function processInfo() {
*/
public function __call($method, $arguments) {
// @todo Temporary to avoid that legacy GD setResource(), getResource(),
// hasResource() methods moved to GD toolkit in #2103621, and setWidth(),
// setHeight() methods moved to ImageToolkitInterface in #2196067 get
// 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/2110499, when
// 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'))) {
throw new \BadMethodCallException();
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/2110499, call_user_func_array() will
// @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();
throw new \BadMethodCallException($method);
}
/**
......
......@@ -90,4 +90,23 @@ public function get($source = NULL, $toolkit_id = NULL) {
$toolkit_id = $toolkit_id ?: $this->toolkitId;
return new Image($this->toolkitManager->createInstance($toolkit_id), $source);
}
/**
* Returns the image file extensions supported by the toolkit.
*
* @param string|null $toolkit_id
* (optional) The ID of the image toolkit to use for checking, or NULL
* to use the current toolkit.
*
* @return array
* An array of supported image file extensions (e.g. png/jpeg/gif).
*
* @see \Drupal\Core\ImageToolkit\ImageToolkitInterface::getSupportedExtensions()
*/
public function getSupportedExtensions($toolkit_id = NULL) {
$toolkit_id = $toolkit_id ?: $this->toolkitId;
$definition = $this->toolkitManager->getDefinition($toolkit_id);
return call_user_func($definition['class'] . '::getSupportedExtensions');
}
}
......@@ -13,20 +13,12 @@
interface ImageInterface {
/**
* Checks if the image format is supported.
* Checks if the image is valid.
*
* @return bool
* Returns TRUE if the image format is supported by the toolkit.
* TRUE if the image object contains a valid image, FALSE otherwise.
*/
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();
public function isValid();
/**
* Returns the height of the image.
......@@ -47,20 +39,11 @@ public function getWidth();
/**
* Returns the size of the image file.
*
* @return int
* The size of the file in bytes, or 0 if the file is invalid.
* @return int|null
* The size of the file in bytes, or NULL if the image is invalid.
*/
public function getFileSize();
/**
* Returns the type of the image.
*
* @return int
* The image type represented by a PHP IMAGETYPE_* constant (e.g.
* IMAGETYPE_JPEG).
*/
public function getType();
/**
* Returns the MIME type of the image file.
*
......@@ -70,22 +53,12 @@ public function getType();
*/
public function getMimeType();
/**
* Sets the source path of the image file.
*
* @param string $source
* A string specifying the path of the image file.
*
* @return self
* Returns this image file.
*/
public function setSource($source);
/**
* Retrieves the source path of the image file.
*
* @return string
* The source path of the image file.
* The source path of the image file. An empty string if the source is
* not set.
*/
public function getSource();
......
......@@ -185,19 +185,15 @@ public function scale(ImageInterface $image, $width = NULL, $height = NULL, $ups
public function scaleAndCrop(ImageInterface $image, $width, $height);
/**
* Gets details about an image.
* Determines if a file contains a valid image.
*
* @param \Drupal\Core\Image\ImageInterface $image
* An image object.
*
* @return array
* If the file could not be found or is not an image, an empty array;
* otherwise, a keyed array containing information about the image:
* - "type": Image type represented as an IMAGETYPE_* constant.
*
* @see \Drupal\Core\Image\ImageInterface::processInfo()
* @return bool
* TRUE if the file could be found and is an image, FALSE otherwise.
*/
public function getInfo(ImageInterface $image);
public function parseFile(ImageInterface $image);
/**
* Returns the height of the image.
......@@ -221,6 +217,18 @@ public function getHeight(ImageInterface $image);
*/
public function getWidth(ImageInterface $image);
/**
* Returns the MIME type of the image file.
*
* @param \Drupal\Core\Image\ImageInterface $image
* An image object.
*
* @return string
* The MIME type of the image file, or an empty string if the image is
* invalid.
*/
public function getMimeType(ImageInterface $image);
/**
* Gets toolkit requirements in a format suitable for hook_requirements().
*
......@@ -244,12 +252,11 @@ public function getRequirements();
public static function isAvailable();
/**
* Returns a list of image types supported by the toolkit.
* Returns a list of image file extensions supported by the toolkit.
*
* @return array
* An array of available image types. An image type is represented by a PHP
* IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG, IMAGETYPE_PNG, etc.).
* An array of supported image file extensions (e.g. png/jpeg/gif).
*/
public static function supportedTypes();
public static function getSupportedExtensions();
}
......@@ -410,7 +410,7 @@ function file_validate_size(File $file, $file_limit = 0, $user_limit = 0) {
}
/**
* Checks that the file is recognized by Image::getInfo() as an image.
* Checks that the file is recognized as a valid image.
*
* @param \Drupal\file\File $file
* A file entity.
......@@ -423,13 +423,11 @@ function file_validate_size(File $file, $file_limit = 0, $user_limit = 0) {
function file_validate_is_image(File $file) {
$errors = array();
$image = \Drupal::service('image.factory')->get($file->getFileUri());
if (!$image->isSupported()) {
$extensions = array();
foreach ($image->getToolkit()->supportedTypes() as $image_type) {
$extensions[] = Unicode::strtoupper(image_type_to_extension($image_type));
}
$errors[] = t('Image type not supported. Allowed types: @types.', array('@types' => implode(', ', $extensions)));
$image_factory = \Drupal::service('image.factory');
$image = $image_factory->get($file->getFileUri());
if (!$image->isValid()) {
$supported_extensions = $image_factory->getSupportedExtensions();
$errors[] = t('Image type not supported. Allowed types: %types', array('%types' => implode(' ', $supported_extensions)));
}
return $errors;
......@@ -464,21 +462,19 @@ function file_validate_image_resolution(File $file, $maximum_dimensions = 0, $mi
// Check first that the file is an image.
$image_factory = \Drupal::service('image.factory');
$image = $image_factory->get($file->getFileUri());
if ($image->isSupported()) {
if ($image->isValid()) {
if ($maximum_dimensions) {
// Check that it is smaller than the given dimensions.
list($width, $height) = explode('x', $maximum_dimensions);
if ($image->getWidth() > $width || $image->getHeight() > $height) {
// Try to resize the image to fit the dimensions.
$image = $image_factory->get($file->getFileUri());
if ($image->isExisting()) {
$image->scale($width, $height);
if ($image->scale($width, $height)) {
$image->save();
$file->filesize = $image->getFileSize();
drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions)));
}
else {
$errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions));
$errors[] = t('The image exceeds the maximum allowed dimensions and an attempt to resize it failed.');
}
}
}
......
......@@ -91,6 +91,12 @@ function testFileValidateImageResolution() {
$this->assertTrue($image->getWidth() <= 10, 'Image scaled to correct width.', 'File');
$this->assertTrue($image->getHeight() <= 5, 'Image scaled to correct height.', 'File');
// Once again, now with negative width and height to force an error.
copy('core/misc/druplicon.png', 'temporary://druplicon.png');
$this->image->setFileUri('temporary://druplicon.png');
$errors = file_validate_image_resolution($this->image, '-10x-5');
$this->assertEqual(count($errors), 1, 'An error reported for an oversized image that can not be scaled down.', 'File');
drupal_unlink('temporary://druplicon.png');
}
else {
......
......@@ -184,7 +184,7 @@ function image_file_download($uri) {
// Check that the file exists and is an image.
$image = \Drupal::service('image.factory')->get($uri);
if ($image->isSupported()) {
if ($image->isValid()) {
// Check the permissions of the original to grant access to this image.
$headers = \Drupal::moduleHandler()->invokeAll('file_download', array($original_uri));
// Confirm there's at least one module granting access and none denying access.
......
......@@ -276,7 +276,7 @@ public function createDerivative($original_uri, $derivative_uri) {
}
$image = \Drupal::service('image.factory')->get($original_uri);
if (!$image->isExisting()) {
if (!$image->isValid()) {
return FALSE;
}
......
......@@ -303,7 +303,7 @@ public function preSave() {
// Determine the dimensions if necessary.
if (empty($width) || empty($height)) {
$image = \Drupal::service('image.factory')->get($this->entity->getFileUri());
if ($image->isSupported()) {
if ($image->isValid()) {
$this->width = $image->getWidth();
$this->height =$image->getHeight();
}
......
......@@ -166,7 +166,7 @@ public static function process($element, &$form_state, $form) {
}
else {
$image = \Drupal::service('image.factory')->get($file->getFileUri());
if ($image->isExisting()) {
if ($image->isValid()) {
$variables['width'] = $image->getWidth();
$variables['height'] = $image->getHeight();
}
......
......@@ -7,6 +7,7 @@
namespace Drupal\system\Plugin\ImageToolkit;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\ImageToolkit\ImageToolkitBase;
use Drupal\Component\Utility\Image as ImageUtility;
......@@ -28,6 +29,13 @@ class GDToolkit extends ImageToolkitBase {
*/
protected $resource;
/**
* Image type represented by a PHP IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG).
*
* @var int
*/
protected $type;
/**
* Sets the GD image resource.
*
......@@ -86,7 +94,11 @@ public function resize(ImageInterface $image, $width, $height) {
$width = (int) round($width);
$height = (int) round($height);
$res = $this->createTmp($image->getType(), $width, $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;
......@@ -129,7 +141,7 @@ public function rotate(ImageInterface $image, $degrees, $background = NULL) {
// 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) {
if ($this->getType() == IMAGETYPE_GIF) {
$transparent_index = imagecolortransparent($this->getResource());
if ($transparent_index != 0) {
$transparent_gif_color = imagecolorsforindex($this->getResource(), $transparent_index);
......@@ -159,7 +171,11 @@ public function crop(ImageInterface $image, $x, $y, $width, $height) {
$width = (int) round($width);
$height = (int) round($height);
$res = $this->createTmp($image->getType(), $width, $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;
......@@ -221,31 +237,28 @@ public function scaleAndCrop(ImageInterface $image, $width, $height) {
}
/**
* Creates a resource from a file.
* Loads a GD resource from a file.
*
* @param string $source
* String specifying the path of the image file.
* @param array $details
* An array of image details.
* @param \Drupal\Core\Image\ImageInterface $image
* An image object.
*
* @return bool
* TRUE or FALSE, based on success.
*/
protected function load($source, array $details) {
$function = 'imagecreatefrom' . image_type_to_extension($details['type'], FALSE);
if (function_exists($function) && $resource = $function($source)) {
protected function load($image) {
$function = 'imagecreatefrom' . image_type_to_extension($this->getType(), FALSE);
if (function_exists($function) && $resource = $function($image->getSource())) {
$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($details['type'], imagesx($resource), imagesy($resource));
$new_image = $this->createTmp($this->getType(), imagesx($resource), imagesy($resource));
imagecopy($new_image, $resource, 0, 0, 0, 0, imagesx($resource), imagesy($resource));
imagedestroy($resource);
$this->setResource($new_image);
}
return (bool) $this->getResource();
}
return FALSE;
}
......@@ -266,16 +279,16 @@ public function save(ImageInterface $image, $destination) {
$destination = drupal_realpath($destination);
}
$function = 'image' . image_type_to_extension($image->getType(), FALSE);
$function = 'image' . image_type_to_extension($this->getType(), FALSE);
if (!function_exists($function)) {
return FALSE;
}
if ($image->getType() == IMAGETYPE_JPEG) {
if ($this->getType() == IMAGETYPE_JPEG) {
$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) {
if ($this->getType() == IMAGETYPE_PNG) {
imagealphablending($this->getResource(), FALSE);
imagesavealpha($this->getResource(), TRUE);
}
......@@ -291,15 +304,14 @@ public function save(ImageInterface $image, $destination) {
/**
* {@inheritdoc}
*/
public function getInfo(ImageInterface $image) {
$details = array();
$data = getimagesize($image->getSource());
if (isset($data) && is_array($data) && in_array($data[2], static::supportedTypes())) {
$details['type'] = $data[2];
$this->load($image->getSource(), $details);
public function parseFile(ImageInterface $image) {
$data = @getimagesize($image->getSource());
if ($data && in_array($data[2], static::supportedTypes())) {
$this->setType($data[2]);
$this->load($image);
return (bool) $this->getResource();
}
return $details;
return FALSE;
}
/**
......@@ -370,6 +382,40 @@ public function getHeight(ImageInterface $image) {
return $this->getResource() ? imagesy($this->getResource()) : NULL;
}
/**
* Gets the PHP type of the image.
*
* @return int
* The image type represented by a PHP IMAGETYPE_* constant (e.g.
* IMAGETYPE_JPEG).
*/
public function getType() {
return $this->type;
}
/**
* Sets the PHP type of the image.
*
* @param int $type
* The image type represented by a PHP IMAGETYPE_* constant (e.g.
* IMAGETYPE_JPEG).
*
* @return this
*/
public function setType($type) {
if (in_array($type, static::supportedTypes())) {
$this->type = $type;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getMimeType(ImageInterface $image) {
return $this->getType() ? image_type_to_mime_type($this->getType()) : '';
}
/**
* {@inheritdoc}
*/
......@@ -402,7 +448,22 @@ public static function isAvailable() {
/**
* {@inheritdoc}
*/
public static function supportedTypes() {
public static function getSupportedExtensions() {
$extensions = array();
foreach (static::supportedTypes() as $image_type) {
$extensions[] = Unicode::strtolower(image_type_to_extension($image_type, FALSE));
}
return $extensions;
}
/**
* Returns a list of image types supported by the toolkit.
*
* @return array
* An array of available image types. An image type is represented by a PHP
* IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG, IMAGETYPE_PNG, etc.).
*/
protected static function supportedTypes() {
return array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF);
}
}
......@@ -247,7 +247,7 @@ function testManipulations() {
$image_truecolor = imageistruecolor($toolkit->getResource());
$this->assertTrue($image_truecolor, String::format('Image %file after load is a truecolor image.', array('%file' => $file)));
if ($image->getType() == IMAGETYPE_GIF) {
if ($image->getToolkit()->getType() == IMAGETYPE_GIF) {
if ($op == 'desaturate') {
// Transparent GIFs and the imagefilter function don't work together.
$values['corners'][3][3] = 0;
......@@ -274,14 +274,14 @@ function testManipulations() {
$directory = $this->public_files_directory .'/imagetest';
file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
$file_path = $directory . '/' . $op . image_type_to_extension($image->getType());
$file_path = $directory . '/' . $op . image_type_to_extension($image->getToolkit()->getType());
$image->save($file_path);
$this->assertTrue($correct_dimensions_real, String::format('Image %file after %action action has proper dimensions.', array('%file' => $file, '%action' => $op)));
$this->assertTrue($correct_dimensions_object, String::format('Image %file object after %action action is reporting the proper height and width values.', array('%file' => $file, '%action' => $op)));
// JPEG colors will always be messed up due to compression.
if ($image->getType() != IMAGETYPE_JPEG) {
if ($image->getToolkit()->getType() != IMAGETYPE_JPEG) {
// Now check each of the corners to ensure color correctness.
foreach ($values['corners'] as $key => $corner) {
// Get the location of the corner.
......
......@@ -38,7 +38,7 @@ function testLoad() {
$image = $this->getImage();
$this->assertTrue(is_object($image), 'Returned an object.');
$this->assertEqual($image->getToolkitId(), 'test', 'Image had toolkit set.');
$this->assertToolkitOperationsCalled(array('load', 'get_info'));
$this->assertToolkitOperationsCalled(array('parseFile'));
}
/**
......
......@@ -68,7 +68,7 @@ function setUp() {
*/
protected function getImage() {
$image = $this->imageFactory->get($this->file, 'test');
$this->assertTrue($image->isExisting(), 'Image was loaded.');
$this->assertTrue($image->isValid(), 'Image was loaded.');
return $image;
}