diff --git a/core/lib/Drupal/Core/Image/Image.php b/core/lib/Drupal/Core/Image/Image.php index e170991..9f9f8f2 100644 --- a/core/lib/Drupal/Core/Image/Image.php +++ b/core/lib/Drupal/Core/Image/Image.php @@ -151,6 +151,13 @@ public function apply($operation, array $arguments = array()) { /** * {@inheritdoc} */ + public function createNew($width, $height, $mime_type = 'image/png', $transparent_color = '#FFFFFF') { + return $this->apply('create_new', array('width' => $width, 'height' => $height, 'mime_type' => $mime_type, 'transparent_color' => $transparent_color)); + } + + /** + * {@inheritdoc} + */ public function crop($x, $y, $width, $height = NULL) { return $this->apply('crop', array('x' => $x, 'y' => $y, 'width' => $width, 'height' => $height)); } diff --git a/core/lib/Drupal/Core/Image/ImageInterface.php b/core/lib/Drupal/Core/Image/ImageInterface.php index 02db478..f60aaa8 100644 --- a/core/lib/Drupal/Core/Image/ImageInterface.php +++ b/core/lib/Drupal/Core/Image/ImageInterface.php @@ -110,6 +110,25 @@ public function apply($operation, array $arguments = array()); public function save($destination = NULL); /** + * Prepares a new image, without loading it from a file. + * + * @param int $width + * The width of the new image, in pixels. + * @param int $height + * The height of the new image, in pixels. + * @param string $mime_type + * (Optional) The MIME type of the image (e.g. 'image/png', image/gif', + * etc.). Defaults to 'image/png'. + * @param string $transparent_color + * (Optional) The hexadecimal string representing the color to be used + * for transparency, needed for GIF images. Defaults to '#FFFFFF' (white). + * + * @return bool + * TRUE on success, FALSE on failure. + */ + public function createNew($width, $height, $mime_type = 'image/png', $transparent_color = '#FFFFFF'); + + /** * Scales an image while maintaining aspect ratio. * * The resulting image can be smaller for one or both target dimensions. diff --git a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationBase.php b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationBase.php index 9bff2a4..e4e9b9f 100644 --- a/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationBase.php +++ b/core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationBase.php @@ -59,10 +59,10 @@ public function __construct(array $configuration, $plugin_id, array $plugin_defi /** * 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. + * Image toolkit implementers should provide a toolkit operation base class + * 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 */ diff --git a/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php index 3ccb44f..56324f5 100644 --- a/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php +++ b/core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php @@ -7,6 +7,7 @@ namespace Drupal\system\Plugin\ImageToolkit; +use Drupal\Component\Utility\Color; use Drupal\Component\Utility\Unicode; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\ImageToolkit\ImageToolkitBase; @@ -121,16 +122,21 @@ protected function load() { return TRUE; } else { - // Convert indexed images to true color, so that filters work - // correctly and don't result in unnecessary dither. - $new_image = $this->createTmp($this->getType(), imagesx($resource), imagesy($resource)); - if ($ret = (bool) $new_image) { - imagecopy($new_image, $resource, 0, 0, 0, 0, imagesx($resource), imagesy($resource)); + // Convert indexed images to truecolor, copying the image to a new + // truecolor resource, so that filters work correctly and don't result + // in unnecessary dither. + $data = array( + 'width' => imagesx($resource), + 'height' => imagesy($resource), + 'mime_type' => $this->getMimeType(), + 'transparent_color' => $this->getTransparentColor(), + ); + if ($this->apply('create_new', $data)) { + imagecopy($this->getResource(), $resource, 0, 0, 0, 0, imagesx($resource), imagesy($resource)); imagedestroy($resource); - $this->setResource($new_image); } - return $ret; } + return (bool) $this->getResource(); } return FALSE; } @@ -195,57 +201,35 @@ public function parseFile() { } /** - * Creates a truecolor image preserving transparency from a provided image. + * Gets the color set for transparency in GIF images. * - * @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 - * The new height of the new image, in pixels. - * - * @return resource - * A GD image handle. + * @return string|null + * A color string like '#rrggbb', or NULL if not set or not relevant. */ - public function createTmp($type, $width, $height) { - $res = imagecreatetruecolor($width, $height); - - if ($type == IMAGETYPE_GIF) { - // Find out if a transparent color is set, will return -1 if no - // transparent color has been defined in the image. - $transparent = imagecolortransparent($this->getResource()); - if ($transparent >= 0) { - // Find out the number of colors in the image palette. It will be 0 for - // truecolor images. - $palette_size = imagecolorstotal($this->getResource()); - if ($palette_size == 0 || $transparent < $palette_size) { - // Set the transparent color in the new resource, either if it is a - // truecolor image or if the transparent color is part of the palette. - // Since the index of the transparency color is a property of the - // image rather than of the palette, it is possible that an image - // could be created with this index set outside the palette size. - $transparent_color = imagecolorsforindex($this->getResource(), $transparent); - $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); - - // Flood with our new transparent color. - imagefill($res, 0, 0, $transparent); - imagecolortransparent($res, $transparent); - } - } - } - elseif ($type == IMAGETYPE_PNG) { - imagealphablending($res, FALSE); - $transparency = imagecolorallocatealpha($res, 0, 0, 0, 127); - imagefill($res, 0, 0, $transparency); - imagealphablending($res, TRUE); - imagesavealpha($res, TRUE); + public function getTransparentColor() { + if (!$this->getResource() || $this->getType() != IMAGETYPE_GIF) { + return NULL; } - else { - imagefill($res, 0, 0, imagecolorallocate($res, 255, 255, 255)); + // Find out if a transparent color is set, will return -1 if no + // transparent color has been defined in the image. + $transparent = imagecolortransparent($this->getResource()); + if ($transparent >= 0) { + // Find out the number of colors in the image palette. It will be 0 for + // truecolor images. + $palette_size = imagecolorstotal($this->getResource()); + if ($palette_size == 0 || $transparent < $palette_size) { + // Return the transparent color, either if it is a truecolor image + // or if the transparent color is part of the palette. + // Since the index of the transparent color is a property of the + // image rather than of the palette, it is possible that an image + // could be created with this index set outside the palette size. + $rgb = imagecolorsforindex($this->getResource(), $transparent); + unset($rgb['alpha']); + return Color::rgbToHex($rgb); + } + return NULL; } - - return $res; + return NULL; } /** diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/CreateNew.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/CreateNew.php new file mode 100644 index 0000000..1692cbb --- /dev/null +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/CreateNew.php @@ -0,0 +1,94 @@ + array( + 'description' => 'The width of the image, in pixels', + ), + 'height' => array( + 'description' => 'The height of the image, in pixels', + ), + 'mime_type' => array( + 'description' => 'The MIME type of the image', + 'required' => FALSE, + 'default' => 'image/png', + ), + 'transparent_color' => array( + 'description' => 'The RGB hex color for GIF transparency', + 'required' => FALSE, + 'default' => '#FFFFFF', + ), + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(array $arguments) { + if (!$res = imagecreatetruecolor($arguments['width'], $arguments['height'])) { + return FALSE; + } + switch ($arguments['mime_type']) { + case 'image/png': + imagealphablending($res, FALSE); + $transparency = imagecolorallocatealpha($res, 0, 0, 0, 127); + imagefill($res, 0, 0, $transparency); + imagealphablending($res, TRUE); + imagesavealpha($res, TRUE); + $this->getToolkit()->setType(IMAGETYPE_PNG); + break; + + case 'image/gif': + if (empty($arguments['transparent_color'])) { + // No transparency color specified, fill white. + $fill_color = imagecolorallocate($res, 255, 255, 255); + } + else { + $fill_rgb = Color::hexToRgb($arguments['transparent_color']); + $fill_color = imagecolorallocate($res, $fill_rgb['red'], $fill_rgb['green'], $fill_rgb['blue']); + imagecolortransparent($res, $fill_color); + } + imagefill($res, 0, 0, $fill_color); + $this->getToolkit()->setType(IMAGETYPE_GIF); + break; + + case 'image/jpeg': + imagefill($res, 0, 0, imagecolorallocate($res, 255, 255, 255)); + $this->getToolkit()->setType(IMAGETYPE_JPEG); + break; + + default: + return FALSE; + + } + + $this->getToolkit()->setResource($res); + return TRUE; + } + +} diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Crop.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Crop.php index 95d8fd7..5529129 100644 --- a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Crop.php +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Crop.php @@ -80,17 +80,24 @@ protected function validateArguments(array $arguments) { * {@inheritdoc} */ protected function execute(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; + // Create a new resource of the required dimensions, and copy and resize + // the original resource on it with resampling. Destroy the original + // resource upon success. + $original_resource = $this->getToolkit()->getResource(); + $data = array( + 'width' => $arguments['width'], + 'height' => $arguments['height'], + 'mime_type' => $this->getToolkit()->getMimeType(), + 'transparent_color' => $this->getToolkit()->getTransparentColor() + ); + if ($this->getToolkit()->apply('create_new', $data)) { + if (!imagecopyresampled($this->getToolkit()->getResource(), $original_resource, 0, 0, $arguments['x'], $arguments['y'], $arguments['width'], $arguments['height'], $arguments['width'], $arguments['height'])) { + return FALSE; + } + imagedestroy($original_resource); + return TRUE; } - - // Destroy the original image and return the modified image. - imagedestroy($this->getToolkit()->getResource()); - $this->getToolkit()->setResource($res); - - return TRUE; + return FALSE; } } diff --git a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Resize.php b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Resize.php index bc192c7..4bf7e65 100644 --- a/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Resize.php +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Resize.php @@ -59,17 +59,24 @@ protected function validateArguments(array $arguments) { * {@inheritdoc} */ protected function execute(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(), $this->getToolkit()->getHeight())) { - return FALSE; + // Create a new resource of the required dimensions, and copy and resize + // the original resource on it with resampling. Destroy the original + // resource upon success. + $original_resource = $this->getToolkit()->getResource(); + $data = array( + 'width' => $arguments['width'], + 'height' => $arguments['height'], + 'mime_type' => $this->getToolkit()->getMimeType(), + 'transparent_color' => $this->getToolkit()->getTransparentColor() + ); + if ($this->getToolkit()->apply('create_new', $data)) { + if (!imagecopyresampled($this->getToolkit()->getResource(), $original_resource, 0, 0, 0, 0, $arguments['width'], $arguments['height'], imagesx($original_resource), imagesy($original_resource))) { + return FALSE; + } + imagedestroy($original_resource); + return TRUE; } - - imagedestroy($this->getToolkit()->getResource()); - // Update image object. - $this->getToolkit()->setResource($res); - - return TRUE; + return FALSE; } } diff --git a/core/modules/system/src/Tests/Image/ToolkitGdTest.php b/core/modules/system/src/Tests/Image/ToolkitGdTest.php index f3869be..70e6273 100644 --- a/core/modules/system/src/Tests/Image/ToolkitGdTest.php +++ b/core/modules/system/src/Tests/Image/ToolkitGdTest.php @@ -122,6 +122,12 @@ function testManipulations() { // Setup a list of tests to perform on each type. $operations = array( + 'create_new' => array( + 'function' => 'create_new', + 'arguments' => array('width' => 20, 'height' => 10, 'transparent_color' => '#ffff00'), // Yellow color for transparency of GIF file. + 'width' => 20, + 'height' => 10, + ), 'resize' => array( 'function' => 'resize', 'arguments' => array('width' => 20, 'height' => 10), @@ -228,6 +234,10 @@ function testManipulations() { ); } + // Prepare a directory for test file results. + $directory = $this->public_files_directory .'/imagetest'; + file_prepare_directory($directory, FILE_CREATE_DIRECTORY); + foreach ($files as $file) { foreach ($operations as $op => $values) { // Load up a fresh image. @@ -249,6 +259,18 @@ function testManipulations() { } } + // CreateNew operation requires specifying MIME type, and color at corners + // is dependent on that. + if ($op == 'create_new') { + $values['arguments']['mime_type'] = $image->getMimeType(); + if ($image->getToolkit()->getType() != IMAGETYPE_JPEG) { + $values['corners'] = array_fill(0, 4, $this->transparent); + } + else { + $values['corners'] = array_fill(0, 4, $this->white); + } + } + // Perform our operation. $image->apply($values['function'], $values['arguments']); @@ -267,8 +289,6 @@ function testManipulations() { $correct_dimensions_object = FALSE; } - $directory = $this->public_files_directory .'/imagetest'; - file_prepare_directory($directory, FILE_CREATE_DIRECTORY); $file_path = $directory . '/' . $op . image_type_to_extension($image->getToolkit()->getType()); $image->save($file_path); @@ -276,7 +296,7 @@ function testManipulations() { $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->getToolkit()->getType() != IMAGETYPE_JPEG) { + if ($image->getToolkit()->getType() != IMAGETYPE_JPEG || $op == 'create_new') { // Now check each of the corners to ensure color correctness. foreach ($values['corners'] as $key => $corner) { // Get the location of the corner. @@ -306,8 +326,43 @@ function testManipulations() { // Check that saved image reloads without raising PHP errors. $image_reloaded = $this->imageFactory->get($file_path); + + // Check that expected color (yellow) was saved as transparent + // in GIF file. + if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF && $op == 'create_new') { + $this->assertEqual('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), String::format('Image file %file has the correct transparent color channel set.', array('%file' => $op . image_type_to_extension($image->getToolkit()->getType())))); + } + } + } + + // Test creation of image from scratch, and saving to storage. + foreach (array(IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG) as $type) { + $image = $this->imageFactory->get(); + $image->createNew(50, 20, image_type_to_mime_type($type), '#ffff00'); + $file = 'from_null' . image_type_to_extension($type); + $file_path = $directory . '/' . $file ; + $this->assertEqual(50, $image->getWidth(), String::format('Image file %file has the correct width.', array('%file' => $file))); + $this->assertEqual(20, $image->getHeight(), String::format('Image file %file has the correct height.', array('%file' => $file))); + $this->assertEqual(image_type_to_mime_type($type), $image->getMimeType(), String::format('Image file %file has the correct MIME type.', array('%file' => $file))); + $this->assertTrue($image->save($file_path), String::format('Image %file created anew from a null image was saved.', array('%file' => $file))); + + // Reload saved image. + $image_reloaded = $this->imageFactory->get($file_path); + if (!$image_reloaded->isValid()) { + $this->fail(String::format('Could not load image %file.', array('%file' => $file))); + continue; + } + $this->assertEqual(50, $image_reloaded->getWidth(), String::format('Image file %file has the correct width.', array('%file' => $file))); + $this->assertEqual(20, $image_reloaded->getHeight(), String::format('Image file %file has the correct height.', array('%file' => $file))); + $this->assertEqual(image_type_to_mime_type($type), $image_reloaded->getMimeType(), String::format('Image file %file has the correct MIME type.', array('%file' => $file))); + if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF) { + $this->assertEqual('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), String::format('Image file %file has the correct transparent color channel set.', array('%file' => $file))); + } + else { + $this->assertEqual(NULL, $image_reloaded->getToolkit()->getTransparentColor(), String::format('Image file %file has no color channel set.', array('%file' => $file))); } } + } /** diff --git a/core/modules/system/src/Tests/Image/ToolkitTestBase.php b/core/modules/system/src/Tests/Image/ToolkitTestBase.php index a04dbb3..9f9b0fc 100644 --- a/core/modules/system/src/Tests/Image/ToolkitTestBase.php +++ b/core/modules/system/src/Tests/Image/ToolkitTestBase.php @@ -88,6 +88,7 @@ function assertToolkitOperationsCalled(array $expected) { 'rotate', 'crop', 'desaturate', + 'create_new', 'scale', 'scale_and_crop', 'my_operation', @@ -134,6 +135,7 @@ function imageTestReset() { 'rotate' => array(), 'crop' => array(), 'desaturate' => array(), + 'create_new' => array(), 'scale' => array(), 'scale_and_crop' => array(), );