3 namespace Drupal\KernelTests\Core\Image;
5 use Drupal\Core\Image\ImageInterface;
6 use Drupal\Component\Utility\SafeMarkup;
7 use Drupal\Core\Site\Settings;
8 use Drupal\KernelTests\KernelTestBase;
11 * Tests that core image manipulations work properly: scale, resize, rotate,
12 * crop, scale and crop, and desaturate.
16 class ToolkitGdTest extends KernelTestBase {
19 * The image factory service.
21 * @var \Drupal\Core\Image\ImageFactory
23 protected $imageFactory;
25 // Colors that are used in testing.
26 protected $black = [0, 0, 0, 0];
27 protected $red = [255, 0, 0, 0];
28 protected $green = [0, 255, 0, 0];
29 protected $blue = [0, 0, 255, 0];
30 protected $yellow = [255, 255, 0, 0];
31 protected $white = [255, 255, 255, 0];
32 protected $transparent = [0, 0, 0, 127];
33 // Used as rotate background colors.
34 protected $fuchsia = [255, 0, 255, 0];
35 protected $rotateTransparent = [255, 255, 255, 127];
37 protected $width = 40;
38 protected $height = 20;
45 public static $modules = ['system', 'simpletest'];
50 protected function setUp() {
53 // Set the image factory service.
54 $this->imageFactory = $this->container->get('image.factory');
57 protected function checkRequirements() {
58 // GD2 support is available.
59 if (!function_exists('imagegd2')) {
61 'Image manipulations for the GD toolkit cannot run because the GD toolkit is not available.',
64 return parent::checkRequirements();
68 * Function to compare two colors by RGBa.
70 public function colorsAreEqual($color_a, $color_b) {
71 // Fully transparent pixels are equal, regardless of RGB.
72 if ($color_a[3] == 127 && $color_b[3] == 127) {
76 foreach ($color_a as $key => $value) {
77 if ($color_b[$key] != $value) {
86 * Function for finding a pixel's RGBa values.
88 public function getPixelColor(ImageInterface $image, $x, $y) {
89 $toolkit = $image->getToolkit();
90 $color_index = imagecolorat($toolkit->getResource(), $x, $y);
92 $transparent_index = imagecolortransparent($toolkit->getResource());
93 if ($color_index == $transparent_index) {
94 return [0, 0, 0, 127];
97 return array_values(imagecolorsforindex($toolkit->getResource(), $color_index));
101 * Since PHP can't visually check that our images have been manipulated
102 * properly, build a list of expected color values for each of the corners and
103 * the expected height and widths for the final images.
105 public function testManipulations() {
107 // Test that the image factory is set to use the GD toolkit.
108 $this->assertEqual($this->imageFactory->getToolkitId(), 'gd', 'The image factory is set to use the \'gd\' image toolkit.');
110 // Test the list of supported extensions.
111 $expected_extensions = ['png', 'gif', 'jpeg', 'jpg', 'jpe'];
112 $supported_extensions = $this->imageFactory->getSupportedExtensions();
113 $this->assertEqual($expected_extensions, array_intersect($expected_extensions, $supported_extensions));
115 // Test that the supported extensions map to correct internal GD image
117 $expected_image_types = [
118 'png' => IMAGETYPE_PNG,
119 'gif' => IMAGETYPE_GIF,
120 'jpeg' => IMAGETYPE_JPEG,
121 'jpg' => IMAGETYPE_JPEG,
122 'jpe' => IMAGETYPE_JPEG
124 $image = $this->imageFactory->get();
125 foreach ($expected_image_types as $extension => $expected_image_type) {
126 $image_type = $image->getToolkit()->extensionToImageType($extension);
127 $this->assertSame($expected_image_type, $image_type);
130 // Typically the corner colors will be unchanged. These colors are in the
131 // order of top-left, top-right, bottom-right, bottom-left.
132 $default_corners = [$this->red, $this->green, $this->blue, $this->transparent];
134 // A list of files that will be tested.
138 'image-test-no-transparency.gif',
142 // Setup a list of tests to perform on each type.
145 'function' => 'resize',
146 'arguments' => ['width' => 20, 'height' => 10],
149 'corners' => $default_corners,
152 'function' => 'scale',
153 'arguments' => ['width' => 20],
156 'corners' => $default_corners,
159 'function' => 'scale',
160 'arguments' => ['height' => 10],
163 'corners' => $default_corners,
166 'function' => 'scale',
167 'arguments' => ['width' => 80, 'upscale' => TRUE],
170 'corners' => $default_corners,
173 'function' => 'scale',
174 'arguments' => ['height' => 40, 'upscale' => TRUE],
177 'corners' => $default_corners,
180 'function' => 'crop',
181 'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12],
184 'corners' => array_fill(0, 4, $this->white),
186 'scale_and_crop' => [
187 'function' => 'scale_and_crop',
188 'arguments' => ['width' => 10, 'height' => 8],
191 'corners' => array_fill(0, 4, $this->black),
194 'function' => 'convert',
197 'arguments' => ['extension' => 'jpeg'],
198 'corners' => $default_corners,
201 'function' => 'convert',
204 'arguments' => ['extension' => 'gif'],
205 'corners' => $default_corners,
208 'function' => 'convert',
211 'arguments' => ['extension' => 'png'],
212 'corners' => $default_corners,
216 // Systems using non-bundled GD2 don't have imagerotate. Test if available.
217 // @todo Remove the version check once
218 // https://www.drupal.org/project/drupal/issues/2670966 is resolved.
219 if (function_exists('imagerotate') && (version_compare(phpversion(), '7.0.26') < 0)) {
222 'function' => 'rotate',
223 // Fuchsia background.
224 'arguments' => ['degrees' => 5, 'background' => '#FF00FF'],
227 'corners' => array_fill(0, 4, $this->fuchsia),
230 'function' => 'rotate',
231 // Fuchsia background.
232 'arguments' => ['degrees' => 90, 'background' => '#FF00FF'],
235 'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
237 'rotate_transparent_5' => [
238 'function' => 'rotate',
239 'arguments' => ['degrees' => 5],
242 'corners' => array_fill(0, 4, $this->rotateTransparent),
244 'rotate_transparent_90' => [
245 'function' => 'rotate',
246 'arguments' => ['degrees' => 90],
249 'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
254 // Systems using non-bundled GD2 don't have imagefilter. Test if available.
255 if (function_exists('imagefilter')) {
258 'function' => 'desaturate',
262 // Grayscale corners are a bit funky. Each of the corners are a shade of
263 // gray. The values of these were determined simply by looking at the
264 // final image to see what desaturated colors end up being.
266 array_fill(0, 3, 76) + [3 => 0],
267 array_fill(0, 3, 149) + [3 => 0],
268 array_fill(0, 3, 29) + [3 => 0],
269 array_fill(0, 3, 225) + [3 => 127]
275 // Prepare a directory for test file results.
276 $directory = Settings::get('file_public_path') . '/imagetest';
277 file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
279 foreach ($files as $file) {
280 foreach ($operations as $op => $values) {
281 // Load up a fresh image.
282 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
283 $toolkit = $image->getToolkit();
284 if (!$image->isValid()) {
285 $this->fail(SafeMarkup::format('Could not load image %file.', ['%file' => $file]));
288 $image_original_type = $image->getToolkit()->getType();
290 // All images should be converted to truecolor when loaded.
291 $image_truecolor = imageistruecolor($toolkit->getResource());
292 $this->assertTrue($image_truecolor, SafeMarkup::format('Image %file after load is a truecolor image.', ['%file' => $file]));
294 // Store the original GD resource.
295 $old_res = $toolkit->getResource();
297 // Perform our operation.
298 $image->apply($values['function'], $values['arguments']);
300 // If the operation replaced the resource, check that the old one has
302 $new_res = $toolkit->getResource();
303 if ($new_res !== $old_res) {
304 $this->assertFalse(is_resource($old_res), SafeMarkup::format("'%operation' destroyed the original resource.", ['%operation' => $values['function']]));
307 // To keep from flooding the test with assert values, make a general
308 // value for whether each group of values fail.
309 $correct_dimensions_real = TRUE;
310 $correct_dimensions_object = TRUE;
312 if (imagesy($toolkit->getResource()) != $values['height'] || imagesx($toolkit->getResource()) != $values['width']) {
313 $correct_dimensions_real = FALSE;
316 // Check that the image object has an accurate record of the dimensions.
317 if ($image->getWidth() != $values['width'] || $image->getHeight() != $values['height']) {
318 $correct_dimensions_object = FALSE;
321 $file_path = $directory . '/' . $op . image_type_to_extension($image->getToolkit()->getType());
322 $image->save($file_path);
324 $this->assertTrue($correct_dimensions_real, SafeMarkup::format('Image %file after %action action has proper dimensions.', ['%file' => $file, '%action' => $op]));
325 $this->assertTrue($correct_dimensions_object, SafeMarkup::format('Image %file object after %action action is reporting the proper height and width values.', ['%file' => $file, '%action' => $op]));
327 // JPEG colors will always be messed up due to compression. So we skip
328 // these tests if the original or the result is in jpeg format.
329 if ($image->getToolkit()->getType() != IMAGETYPE_JPEG && $image_original_type != IMAGETYPE_JPEG) {
330 // Now check each of the corners to ensure color correctness.
331 foreach ($values['corners'] as $key => $corner) {
332 // The test gif that does not have transparency color set is a
334 if ($file === 'image-test-no-transparency.gif') {
335 if ($op == 'desaturate') {
336 // For desaturating, keep the expected color from the test
337 // data, but set alpha channel to fully opaque.
340 elseif ($corner === $this->transparent) {
341 // Set expected pixel to yellow where the others have
343 $corner = $this->yellow;
347 // Get the location of the corner.
354 $x = $image->getWidth() - 1;
358 $x = $image->getWidth() - 1;
359 $y = $image->getHeight() - 1;
363 $y = $image->getHeight() - 1;
366 $color = $this->getPixelColor($image, $x, $y);
367 // We also skip the color test for transparency for gif <-> png
368 // conversion. The convert operation cannot handle that correctly.
369 if ($image->getToolkit()->getType() == $image_original_type || $corner != $this->transparent) {
370 $correct_colors = $this->colorsAreEqual($color, $corner);
371 $this->assertTrue($correct_colors, SafeMarkup::format('Image %file object after %action action has the correct color placement at corner %corner.',
372 ['%file' => $file, '%action' => $op, '%corner' => $key]));
377 // Check that saved image reloads without raising PHP errors.
378 $image_reloaded = $this->imageFactory->get($file_path);
379 $resource = $image_reloaded->getToolkit()->getResource();
383 // Test creation of image from scratch, and saving to storage.
384 foreach ([IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG] as $type) {
385 $image = $this->imageFactory->get();
386 $image->createNew(50, 20, image_type_to_extension($type, FALSE), '#ffff00');
387 $file = 'from_null' . image_type_to_extension($type);
388 $file_path = $directory . '/' . $file;
389 $this->assertEqual(50, $image->getWidth(), SafeMarkup::format('Image file %file has the correct width.', ['%file' => $file]));
390 $this->assertEqual(20, $image->getHeight(), SafeMarkup::format('Image file %file has the correct height.', ['%file' => $file]));
391 $this->assertEqual(image_type_to_mime_type($type), $image->getMimeType(), SafeMarkup::format('Image file %file has the correct MIME type.', ['%file' => $file]));
392 $this->assertTrue($image->save($file_path), SafeMarkup::format('Image %file created anew from a null image was saved.', ['%file' => $file]));
394 // Reload saved image.
395 $image_reloaded = $this->imageFactory->get($file_path);
396 if (!$image_reloaded->isValid()) {
397 $this->fail(SafeMarkup::format('Could not load image %file.', ['%file' => $file]));
400 $this->assertEqual(50, $image_reloaded->getWidth(), SafeMarkup::format('Image file %file has the correct width.', ['%file' => $file]));
401 $this->assertEqual(20, $image_reloaded->getHeight(), SafeMarkup::format('Image file %file has the correct height.', ['%file' => $file]));
402 $this->assertEqual(image_type_to_mime_type($type), $image_reloaded->getMimeType(), SafeMarkup::format('Image file %file has the correct MIME type.', ['%file' => $file]));
403 if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF) {
404 $this->assertEqual('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), SafeMarkup::format('Image file %file has the correct transparent color channel set.', ['%file' => $file]));
407 $this->assertEqual(NULL, $image_reloaded->getToolkit()->getTransparentColor(), SafeMarkup::format('Image file %file has no color channel set.', ['%file' => $file]));
411 // Test failures of the 'create_new' operation.
412 $image = $this->imageFactory->get();
413 $image->createNew(-50, 20);
414 $this->assertFalse($image->isValid(), 'CreateNew with negative width fails.');
415 $image->createNew(50, 20, 'foo');
416 $this->assertFalse($image->isValid(), 'CreateNew with invalid extension fails.');
417 $image->createNew(50, 20, 'gif', '#foo');
418 $this->assertFalse($image->isValid(), 'CreateNew with invalid color hex string fails.');
419 $image->createNew(50, 20, 'gif', '#ff0000');
420 $this->assertTrue($image->isValid(), 'CreateNew with valid arguments validates the Image.');
424 * Tests that GD resources are freed from memory.
426 public function testResourceDestruction() {
427 // Test that an Image object going out of scope releases its GD resource.
428 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/image-test.png');
429 $res = $image->getToolkit()->getResource();
430 $this->assertTrue(is_resource($res), 'Successfully loaded image resource.');
432 $this->assertFalse(is_resource($res), 'Image resource was destroyed after losing scope.');
434 // Test that 'create_new' operation does not leave orphaned GD resources.
435 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/image-test.png');
436 $old_res = $image->getToolkit()->getResource();
437 // Check if resource has been created successfully.
438 $this->assertTrue(is_resource($old_res));
439 $image->createNew(20, 20);
440 $new_res = $image->getToolkit()->getResource();
441 // Check if the original resource has been destroyed.
442 $this->assertFalse(is_resource($old_res));
443 // Check if a new resource has been created successfully.
444 $this->assertTrue(is_resource($new_res));
448 * Tests for GIF images with transparency.
450 public function testGifTransparentImages() {
451 // Prepare a directory for test file results.
452 $directory = Settings::get('file_public_path') . '/imagetest';
453 file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
455 // Test loading an indexed GIF image with transparent color set.
456 // Color at top-right pixel should be fully transparent.
457 $file = 'image-test-transparent-indexed.gif';
458 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
459 $resource = $image->getToolkit()->getResource();
460 $color_index = imagecolorat($resource, $image->getWidth() - 1, 0);
461 $color = array_values(imagecolorsforindex($resource, $color_index));
462 $this->assertEqual($this->rotateTransparent, $color, "Image {$file} after load has full transparent color at corner 1.");
464 // Test deliberately creating a GIF image with no transparent color set.
465 // Color at top-right pixel should be fully transparent while in memory,
466 // fully opaque after flushing image to file.
467 $file = 'image-test-no-transparent-color-set.gif';
468 $file_path = $directory . '/' . $file;
470 $image = $this->imageFactory->get();
471 $image->createNew(50, 20, 'gif', NULL);
472 $resource = $image->getToolkit()->getResource();
473 $color_index = imagecolorat($resource, $image->getWidth() - 1, 0);
474 $color = array_values(imagecolorsforindex($resource, $color_index));
475 $this->assertEqual($this->rotateTransparent, $color, "New GIF image with no transparent color set after creation has full transparent color at corner 1.");
477 $this->assertTrue($image->save($file_path), "New GIF image {$file} was saved.");
479 $image_reloaded = $this->imageFactory->get($file_path);
480 $resource = $image_reloaded->getToolkit()->getResource();
481 $color_index = imagecolorat($resource, $image_reloaded->getWidth() - 1, 0);
482 $color = array_values(imagecolorsforindex($resource, $color_index));
483 // Check explicitly for alpha == 0 as the rest of the color has been
484 // compressed and may have slight difference from full white.
485 $this->assertEqual(0, $color[3], "New GIF image {$file} after reload has no transparent color at corner 1.");
487 // Test loading an image whose transparent color index is out of range.
488 // This image was generated by taking an initial image with a palette size
489 // of 6 colors, and setting the transparent color index to 6 (one higher
490 // than the largest allowed index), as follows:
492 // $image = imagecreatefromgif('core/modules/simpletest/files/image-test.gif');
493 // imagecolortransparent($image, 6);
494 // imagegif($image, 'core/modules/simpletest/files/image-test-transparent-out-of-range.gif');
496 // This allows us to test that an image with an out-of-range color index
497 // can be loaded correctly.
498 $file = 'image-test-transparent-out-of-range.gif';
499 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
500 $toolkit = $image->getToolkit();
502 if (!$image->isValid()) {
503 $this->fail(SafeMarkup::format('Could not load image %file.', ['%file' => $file]));
506 // All images should be converted to truecolor when loaded.
507 $image_truecolor = imageistruecolor($toolkit->getResource());
508 $this->assertTrue($image_truecolor, SafeMarkup::format('Image %file after load is a truecolor image.', ['%file' => $file]));
513 * Tests calling a missing image operation plugin.
515 public function testMissingOperation() {
517 // Test that the image factory is set to use the GD toolkit.
518 $this->assertEqual($this->imageFactory->getToolkitId(), 'gd', 'The image factory is set to use the \'gd\' image toolkit.');
520 // An image file that will be tested.
521 $file = 'image-test.png';
523 // Load up a fresh image.
524 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
525 if (!$image->isValid()) {
526 $this->fail(SafeMarkup::format('Could not load image %file.', ['%file' => $file]));
529 // Try perform a missing toolkit operation.
530 $this->assertFalse($image->apply('missing_op', []), 'Calling a missing image toolkit operation plugin fails.');