Newer
Older
Dries Buytaert
committed
<?php
namespace Drupal\system\Tests\Image;
use Drupal\Core\Image\ImageInterface;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\simpletest\KernelTestBase;
Dries Buytaert
committed
/**
* Tests that core image manipulations work properly: scale, resize, rotate,
* crop, scale and crop, and desaturate.
*
* @group Image
Dries Buytaert
committed
*/
Angie Byron
committed
class ToolkitGdTest extends KernelTestBase {
catch
committed
/**
* The image factory service.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
Dries Buytaert
committed
// Colors that are used in testing.
protected $black = array(0, 0, 0, 0);
protected $red = array(255, 0, 0, 0);
protected $green = array(0, 255, 0, 0);
protected $blue = array(0, 0, 255, 0);
protected $yellow = array(255, 255, 0, 0);
protected $white = array(255, 255, 255, 0);
Angie Byron
committed
protected $transparent = array(0, 0, 0, 127);
// Used as rotate background colors.
protected $fuchsia = array(255, 0, 255, 0);
protected $rotateTransparent = array(255, 255, 255, 127);
Dries Buytaert
committed
protected $width = 40;
protected $height = 20;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'simpletest');
catch
committed
/**
* {@inheritdoc}
*/
Alex Pott
committed
protected function setUp() {
catch
committed
parent::setUp();
// Set the image factory service.
$this->imageFactory = $this->container->get('image.factory');
}
protected function checkRequirements() {
// GD2 support is available.
if (!function_exists('imagegd2')) {
return array(
'Image manipulations for the GD toolkit cannot run because the GD toolkit is not available.',
);
}
return parent::checkRequirements();
}
Dries Buytaert
committed
/**
* Function to compare two colors by RGBa.
*/
function colorsAreEqual($color_a, $color_b) {
// Fully transparent pixels are equal, regardless of RGB.
if ($color_a[3] == 127 && $color_b[3] == 127) {
return TRUE;
}
foreach ($color_a as $key => $value) {
if ($color_b[$key] != $value) {
return FALSE;
}
}
return TRUE;
}
/**
* Function for finding a pixel's RGBa values.
*/
function getPixelColor(ImageInterface $image, $x, $y) {
Alex Pott
committed
$toolkit = $image->getToolkit();
$color_index = imagecolorat($toolkit->getResource(), $x, $y);
Dries Buytaert
committed
Alex Pott
committed
$transparent_index = imagecolortransparent($toolkit->getResource());
Dries Buytaert
committed
if ($color_index == $transparent_index) {
return array(0, 0, 0, 127);
}
Alex Pott
committed
return array_values(imagecolorsforindex($toolkit->getResource(), $color_index));
Dries Buytaert
committed
}
/**
* Since PHP can't visually check that our images have been manipulated
* properly, build a list of expected color values for each of the corners and
* the expected height and widths for the final images.
*/
function testManipulations() {
catch
committed
// 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.');
Dries Buytaert
committed
// Typically the corner colors will be unchanged. These colors are in the
// order of top-left, top-right, bottom-right, bottom-left.
$default_corners = array($this->red, $this->green, $this->blue, $this->transparent);
// A list of files that will be tested.
$files = array(
'image-test.png',
'image-test.gif',
Angie Byron
committed
'image-test-no-transparency.gif',
Dries Buytaert
committed
'image-test.jpg',
);
// Setup a list of tests to perform on each type.
$operations = array(
'resize' => array(
'function' => 'resize',
Alex Pott
committed
'arguments' => array('width' => 20, 'height' => 10),
Dries Buytaert
committed
'width' => 20,
'height' => 10,
'corners' => $default_corners,
),
'scale_x' => array(
'function' => 'scale',
Alex Pott
committed
'arguments' => array('width' => 20),
Dries Buytaert
committed
'width' => 20,
'height' => 10,
'corners' => $default_corners,
),
'scale_y' => array(
'function' => 'scale',
Alex Pott
committed
'arguments' => array('height' => 10),
Dries Buytaert
committed
'width' => 20,
'height' => 10,
'corners' => $default_corners,
),
'upscale_x' => array(
'function' => 'scale',
Alex Pott
committed
'arguments' => array('width' => 80, 'upscale' => TRUE),
Dries Buytaert
committed
'width' => 80,
'height' => 40,
'corners' => $default_corners,
),
'upscale_y' => array(
'function' => 'scale',
Alex Pott
committed
'arguments' => array('height' => 40, 'upscale' => TRUE),
Dries Buytaert
committed
'width' => 80,
'height' => 40,
'corners' => $default_corners,
),
'crop' => array(
'function' => 'crop',
Alex Pott
committed
'arguments' => array('x' => 12, 'y' => 4, 'width' => 16, 'height' => 12),
Dries Buytaert
committed
'width' => 16,
'height' => 12,
'corners' => array_fill(0, 4, $this->white),
),
'scale_and_crop' => array(
Alex Pott
committed
'function' => 'scale_and_crop',
'arguments' => array('width' => 10, 'height' => 8),
Dries Buytaert
committed
'width' => 10,
'height' => 8,
'corners' => array_fill(0, 4, $this->black),
),
Angie Byron
committed
'convert_jpg' => array(
'function' => 'convert',
'width' => 40,
'height' => 20,
'arguments' => array('extension' => 'jpeg'),
'corners' => $default_corners,
),
'convert_gif' => array(
'function' => 'convert',
'width' => 40,
'height' => 20,
'arguments' => array('extension' => 'gif'),
'corners' => $default_corners,
),
'convert_png' => array(
'function' => 'convert',
'width' => 40,
'height' => 20,
'arguments' => array('extension' => 'png'),
'corners' => $default_corners,
),
Dries Buytaert
committed
);
Dries Buytaert
committed
// Systems using non-bundled GD2 don't have imagerotate. Test if available.
if (function_exists('imagerotate')) {
Dries Buytaert
committed
$operations += array(
'rotate_5' => array(
'function' => 'rotate',
Angie Byron
committed
'arguments' => array('degrees' => 5, 'background' => '#FF00FF'), // Fuchsia background.
'width' => 41,
'height' => 23,
Dries Buytaert
committed
'corners' => array_fill(0, 4, $this->fuchsia),
),
'rotate_90' => array(
'function' => 'rotate',
Angie Byron
committed
'arguments' => array('degrees' => 90, 'background' => '#FF00FF'), // Fuchsia background.
Dries Buytaert
committed
'width' => 20,
'height' => 40,
'corners' => array($this->transparent, $this->red, $this->green, $this->blue),
Dries Buytaert
committed
),
'rotate_transparent_5' => array(
'function' => 'rotate',
Alex Pott
committed
'arguments' => array('degrees' => 5),
'width' => 41,
'height' => 23,
'corners' => array_fill(0, 4, $this->rotateTransparent),
Dries Buytaert
committed
),
'rotate_transparent_90' => array(
'function' => 'rotate',
Alex Pott
committed
'arguments' => array('degrees' => 90),
Dries Buytaert
committed
'width' => 20,
'height' => 40,
'corners' => array($this->transparent, $this->red, $this->green, $this->blue),
),
);
}
// Systems using non-bundled GD2 don't have imagefilter. Test if available.
if (function_exists('imagefilter')) {
Dries Buytaert
committed
$operations += array(
'desaturate' => array(
'function' => 'desaturate',
'arguments' => array(),
'height' => 20,
'width' => 40,
// Grayscale corners are a bit funky. Each of the corners are a shade of
// gray. The values of these were determined simply by looking at the
// final image to see what desaturated colors end up being.
'corners' => array(
array_fill(0, 3, 76) + array(3 => 0),
array_fill(0, 3, 149) + array(3 => 0),
array_fill(0, 3, 29) + array(3 => 0),
array_fill(0, 3, 225) + array(3 => 127)
Dries Buytaert
committed
),
),
);
}
// Prepare a directory for test file results.
$directory = $this->publicFilesDirectory . '/imagetest';
file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
Dries Buytaert
committed
foreach ($files as $file) {
foreach ($operations as $op => $values) {
// Load up a fresh image.
catch
committed
$image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
Alex Pott
committed
$toolkit = $image->getToolkit();
Alex Pott
committed
if (!$image->isValid()) {
$this->fail(SafeMarkup::format('Could not load image %file.', array('%file' => $file)));
Dries Buytaert
committed
continue 2;
}
Angie Byron
committed
$image_original_type = $image->getToolkit()->getType();
Dries Buytaert
committed
// All images should be converted to truecolor when loaded.
Alex Pott
committed
$image_truecolor = imageistruecolor($toolkit->getResource());
$this->assertTrue($image_truecolor, SafeMarkup::format('Image %file after load is a truecolor image.', array('%file' => $file)));
// Store the original GD resource.
$old_res = $toolkit->getResource();
Dries Buytaert
committed
// Perform our operation.
Alex Pott
committed
$image->apply($values['function'], $values['arguments']);
Dries Buytaert
committed
// If the operation replaced the resource, check that the old one has
// been destroyed.
$new_res = $toolkit->getResource();
if ($new_res !== $old_res) {
$this->assertFalse(is_resource($old_res), SafeMarkup::format("'%operation' destroyed the original resource.", ['%operation' => $values['function']]));
}
Dries Buytaert
committed
// To keep from flooding the test with assert values, make a general
// value for whether each group of values fail.
$correct_dimensions_real = TRUE;
$correct_dimensions_object = TRUE;
Alex Pott
committed
if (imagesy($toolkit->getResource()) != $values['height'] || imagesx($toolkit->getResource()) != $values['width']) {
Dries Buytaert
committed
$correct_dimensions_real = FALSE;
}
// Check that the image object has an accurate record of the dimensions.
if ($image->getWidth() != $values['width'] || $image->getHeight() != $values['height']) {
Dries Buytaert
committed
$correct_dimensions_object = FALSE;
}
Alex Pott
committed
$file_path = $directory . '/' . $op . image_type_to_extension($image->getToolkit()->getType());
$image->save($file_path);
Dries Buytaert
committed
$this->assertTrue($correct_dimensions_real, SafeMarkup::format('Image %file after %action action has proper dimensions.', array('%file' => $file, '%action' => $op)));
$this->assertTrue($correct_dimensions_object, SafeMarkup::format('Image %file object after %action action is reporting the proper height and width values.', array('%file' => $file, '%action' => $op)));
Angie Byron
committed
// JPEG colors will always be messed up due to compression. So we skip
// these tests if the original or the result is in jpeg format.
if ($image->getToolkit()->getType() != IMAGETYPE_JPEG && $image_original_type != IMAGETYPE_JPEG) {
// Now check each of the corners to ensure color correctness.
foreach ($values['corners'] as $key => $corner) {
Alex Pott
committed
// The test gif that does not have transparency color set is a
// special case.
if ($file === 'image-test-no-transparency.gif') {
if ($op == 'desaturate') {
// For desaturating, keep the expected color from the test
// data, but set alpha channel to fully opaque.
$corner[3] = 0;
}
elseif ($corner === $this->transparent) {
// Set expected pixel to yellow where the others have
// transparent.
$corner = $this->yellow;
}
Angie Byron
committed
}
Alex Pott
committed
// Get the location of the corner.
switch ($key) {
case 0:
$x = 0;
$y = 0;
break;
case 1:
Angie Byron
committed
$x = $image->getWidth() - 1;
$y = 0;
break;
case 2:
Angie Byron
committed
$x = $image->getWidth() - 1;
$y = $image->getHeight() - 1;
break;
case 3:
$x = 0;
Angie Byron
committed
$y = $image->getHeight() - 1;
break;
}
$color = $this->getPixelColor($image, $x, $y);
Angie Byron
committed
// We also skip the color test for transparency for gif <-> png
// conversion. The convert operation cannot handle that correctly.
if ($image->getToolkit()->getType() == $image_original_type || $corner != $this->transparent) {
$correct_colors = $this->colorsAreEqual($color, $corner);
$this->assertTrue($correct_colors, SafeMarkup::format('Image %file object after %action action has the correct color placement at corner %corner.',
Angie Byron
committed
array('%file' => $file, '%action' => $op, '%corner' => $key)));
}
Dries Buytaert
committed
}
// Check that saved image reloads without raising PHP errors.
$image_reloaded = $this->imageFactory->get($file_path);
$resource = $image_reloaded->getToolkit()->getResource();
Dries Buytaert
committed
}
}
// 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_extension($type, FALSE), '#ffff00');
$file = 'from_null' . image_type_to_extension($type);
$file_path = $directory . '/' . $file ;
$this->assertEqual(50, $image->getWidth(), SafeMarkup::format('Image file %file has the correct width.', array('%file' => $file)));
$this->assertEqual(20, $image->getHeight(), SafeMarkup::format('Image file %file has the correct height.', array('%file' => $file)));
$this->assertEqual(image_type_to_mime_type($type), $image->getMimeType(), SafeMarkup::format('Image file %file has the correct MIME type.', array('%file' => $file)));
$this->assertTrue($image->save($file_path), SafeMarkup::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(SafeMarkup::format('Could not load image %file.', array('%file' => $file)));
continue;
}
$this->assertEqual(50, $image_reloaded->getWidth(), SafeMarkup::format('Image file %file has the correct width.', array('%file' => $file)));
$this->assertEqual(20, $image_reloaded->getHeight(), SafeMarkup::format('Image file %file has the correct height.', array('%file' => $file)));
$this->assertEqual(image_type_to_mime_type($type), $image_reloaded->getMimeType(), SafeMarkup::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(), SafeMarkup::format('Image file %file has the correct transparent color channel set.', array('%file' => $file)));
}
$this->assertEqual(NULL, $image_reloaded->getToolkit()->getTransparentColor(), SafeMarkup::format('Image file %file has no color channel set.', array('%file' => $file)));
}
}
// Test failures of the 'create_new' operation.
$image = $this->imageFactory->get();
$image->createNew(-50, 20);
$this->assertFalse($image->isValid(), 'CreateNew with negative width fails.');
$image->createNew(50, 20, 'foo');
$this->assertFalse($image->isValid(), 'CreateNew with invalid extension fails.');
$image->createNew(50, 20, 'gif', '#foo');
$this->assertFalse($image->isValid(), 'CreateNew with invalid color hex string fails.');
$image->createNew(50, 20, 'gif', '#ff0000');
$this->assertTrue($image->isValid(), 'CreateNew with valid arguments validates the Image.');
Dries Buytaert
committed
}
Alex Pott
committed
/**
* Tests that GD resources are freed from memory.
*/
public function testResourceDestruction() {
// Test that an Image object going out of scope releases its GD resource.
$image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/image-test.png');
$res = $image->getToolkit()->getResource();
$this->assertTrue(is_resource($res), 'Successfully loaded image resource.');
$image = NULL;
$this->assertFalse(is_resource($res), 'Image resource was destroyed after losing scope.');
// Test that 'create_new' operation does not leave orphaned GD resources.
$image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/image-test.png');
$old_res = $image->getToolkit()->getResource();
// Check if resource has been created successfully.
$this->assertTrue(is_resource($old_res));
$image->createNew(20, 20);
$new_res = $image->getToolkit()->getResource();
// Check if the original resource has been destroyed.
$this->assertFalse(is_resource($old_res));
// Check if a new resource has been created successfully.
$this->assertTrue(is_resource($new_res));
}
Alex Pott
committed
* Tests for GIF images with transparency.
Alex Pott
committed
function testGifTransparentImages() {
// Prepare a directory for test file results.
$directory = $this->publicFilesDirectory . '/imagetest';
Alex Pott
committed
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
// Test loading an indexed GIF image with transparent color set.
// Color at top-right pixel should be fully transparent.
$file = 'image-test-transparent-indexed.gif';
$image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
$resource = $image->getToolkit()->getResource();
$color_index = imagecolorat($resource, $image->getWidth() - 1, 0);
$color = array_values(imagecolorsforindex($resource, $color_index));
$this->assertEqual($this->rotateTransparent, $color, "Image {$file} after load has full transparent color at corner 1.");
// Test deliberately creating a GIF image with no transparent color set.
// Color at top-right pixel should be fully transparent while in memory,
// fully opaque after flushing image to file.
$file = 'image-test-no-transparent-color-set.gif';
$file_path = $directory . '/' . $file ;
// Create image.
$image = $this->imageFactory->get();
$image->createNew(50, 20, 'gif', NULL);
$resource = $image->getToolkit()->getResource();
$color_index = imagecolorat($resource, $image->getWidth() - 1, 0);
$color = array_values(imagecolorsforindex($resource, $color_index));
$this->assertEqual($this->rotateTransparent, $color, "New GIF image with no transparent color set after creation has full transparent color at corner 1.");
// Save image.
$this->assertTrue($image->save($file_path), "New GIF image {$file} was saved.");
// Reload image.
$image_reloaded = $this->imageFactory->get($file_path);
$resource = $image_reloaded->getToolkit()->getResource();
$color_index = imagecolorat($resource, $image_reloaded->getWidth() - 1, 0);
$color = array_values(imagecolorsforindex($resource, $color_index));
// Check explicitly for alpha == 0 as the rest of the color has been
// compressed and may have slight difference from full white.
$this->assertEqual(0, $color[3], "New GIF image {$file} after reload has no transparent color at corner 1.");
// Test loading an image whose transparent color index is out of range.
// This image was generated by taking an initial image with a palette size
// of 6 colors, and setting the transparent color index to 6 (one higher
// than the largest allowed index), as follows:
// @code
// $image = imagecreatefromgif('core/modules/simpletest/files/image-test.gif');
// imagecolortransparent($image, 6);
// imagegif($image, 'core/modules/simpletest/files/image-test-transparent-out-of-range.gif');
// @endcode
// This allows us to test that an image with an out-of-range color index
// can be loaded correctly.
$file = 'image-test-transparent-out-of-range.gif';
$image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
$toolkit = $image->getToolkit();
if (!$image->isValid()) {
$this->fail(SafeMarkup::format('Could not load image %file.', array('%file' => $file)));
}
else {
// All images should be converted to truecolor when loaded.
$image_truecolor = imageistruecolor($toolkit->getResource());
$this->assertTrue($image_truecolor, SafeMarkup::format('Image %file after load is a truecolor image.', array('%file' => $file)));
Alex Pott
committed
/**
* 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(SafeMarkup::format('Could not load image %file.', array('%file' => $file)));
Alex Pott
committed
}
// Try perform a missing toolkit operation.
$this->assertFalse($image->apply('missing_op', array()), 'Calling a missing image toolkit operation plugin fails.');
}