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 if (function_exists('imagerotate')) {
220 'function' => 'rotate',
221 'arguments' => ['degrees' => 5, 'background' => '#FF00FF'], // Fuchsia background.
224 'corners' => array_fill(0, 4, $this->fuchsia),
227 'function' => 'rotate',
228 'arguments' => ['degrees' => 90, 'background' => '#FF00FF'], // Fuchsia background.
231 'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
233 'rotate_transparent_5' => [
234 'function' => 'rotate',
235 'arguments' => ['degrees' => 5],
238 'corners' => array_fill(0, 4, $this->rotateTransparent),
240 'rotate_transparent_90' => [
241 'function' => 'rotate',
242 'arguments' => ['degrees' => 90],
245 'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
250 // Systems using non-bundled GD2 don't have imagefilter. Test if available.
251 if (function_exists('imagefilter')) {
254 'function' => 'desaturate',
258 // Grayscale corners are a bit funky. Each of the corners are a shade of
259 // gray. The values of these were determined simply by looking at the
260 // final image to see what desaturated colors end up being.
262 array_fill(0, 3, 76) + [3 => 0],
263 array_fill(0, 3, 149) + [3 => 0],
264 array_fill(0, 3, 29) + [3 => 0],
265 array_fill(0, 3, 225) + [3 => 127]
271 // Prepare a directory for test file results.
272 $directory = Settings::get('file_public_path') . '/imagetest';
273 file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
275 foreach ($files as $file) {
276 foreach ($operations as $op => $values) {
277 // Load up a fresh image.
278 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
279 $toolkit = $image->getToolkit();
280 if (!$image->isValid()) {
281 $this->fail(SafeMarkup::format('Could not load image %file.', ['%file' => $file]));
284 $image_original_type = $image->getToolkit()->getType();
286 // All images should be converted to truecolor when loaded.
287 $image_truecolor = imageistruecolor($toolkit->getResource());
288 $this->assertTrue($image_truecolor, SafeMarkup::format('Image %file after load is a truecolor image.', ['%file' => $file]));
290 // Store the original GD resource.
291 $old_res = $toolkit->getResource();
293 // Perform our operation.
294 $image->apply($values['function'], $values['arguments']);
296 // If the operation replaced the resource, check that the old one has
298 $new_res = $toolkit->getResource();
299 if ($new_res !== $old_res) {
300 $this->assertFalse(is_resource($old_res), SafeMarkup::format("'%operation' destroyed the original resource.", ['%operation' => $values['function']]));
303 // To keep from flooding the test with assert values, make a general
304 // value for whether each group of values fail.
305 $correct_dimensions_real = TRUE;
306 $correct_dimensions_object = TRUE;
308 if (imagesy($toolkit->getResource()) != $values['height'] || imagesx($toolkit->getResource()) != $values['width']) {
309 $correct_dimensions_real = FALSE;
312 // Check that the image object has an accurate record of the dimensions.
313 if ($image->getWidth() != $values['width'] || $image->getHeight() != $values['height']) {
314 $correct_dimensions_object = FALSE;
317 $file_path = $directory . '/' . $op . image_type_to_extension($image->getToolkit()->getType());
318 $image->save($file_path);
320 $this->assertTrue($correct_dimensions_real, SafeMarkup::format('Image %file after %action action has proper dimensions.', ['%file' => $file, '%action' => $op]));
321 $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]));
323 // JPEG colors will always be messed up due to compression. So we skip
324 // these tests if the original or the result is in jpeg format.
325 if ($image->getToolkit()->getType() != IMAGETYPE_JPEG && $image_original_type != IMAGETYPE_JPEG) {
326 // Now check each of the corners to ensure color correctness.
327 foreach ($values['corners'] as $key => $corner) {
328 // The test gif that does not have transparency color set is a
330 if ($file === 'image-test-no-transparency.gif') {
331 if ($op == 'desaturate') {
332 // For desaturating, keep the expected color from the test
333 // data, but set alpha channel to fully opaque.
336 elseif ($corner === $this->transparent) {
337 // Set expected pixel to yellow where the others have
339 $corner = $this->yellow;
343 // Get the location of the corner.
350 $x = $image->getWidth() - 1;
354 $x = $image->getWidth() - 1;
355 $y = $image->getHeight() - 1;
359 $y = $image->getHeight() - 1;
362 $color = $this->getPixelColor($image, $x, $y);
363 // We also skip the color test for transparency for gif <-> png
364 // conversion. The convert operation cannot handle that correctly.
365 if ($image->getToolkit()->getType() == $image_original_type || $corner != $this->transparent) {
366 $correct_colors = $this->colorsAreEqual($color, $corner);
367 $this->assertTrue($correct_colors, SafeMarkup::format('Image %file object after %action action has the correct color placement at corner %corner.',
368 ['%file' => $file, '%action' => $op, '%corner' => $key]));
373 // Check that saved image reloads without raising PHP errors.
374 $image_reloaded = $this->imageFactory->get($file_path);
375 $resource = $image_reloaded->getToolkit()->getResource();
379 // Test creation of image from scratch, and saving to storage.
380 foreach ([IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG] as $type) {
381 $image = $this->imageFactory->get();
382 $image->createNew(50, 20, image_type_to_extension($type, FALSE), '#ffff00');
383 $file = 'from_null' . image_type_to_extension($type);
384 $file_path = $directory . '/' . $file ;
385 $this->assertEqual(50, $image->getWidth(), SafeMarkup::format('Image file %file has the correct width.', ['%file' => $file]));
386 $this->assertEqual(20, $image->getHeight(), SafeMarkup::format('Image file %file has the correct height.', ['%file' => $file]));
387 $this->assertEqual(image_type_to_mime_type($type), $image->getMimeType(), SafeMarkup::format('Image file %file has the correct MIME type.', ['%file' => $file]));
388 $this->assertTrue($image->save($file_path), SafeMarkup::format('Image %file created anew from a null image was saved.', ['%file' => $file]));
390 // Reload saved image.
391 $image_reloaded = $this->imageFactory->get($file_path);
392 if (!$image_reloaded->isValid()) {
393 $this->fail(SafeMarkup::format('Could not load image %file.', ['%file' => $file]));
396 $this->assertEqual(50, $image_reloaded->getWidth(), SafeMarkup::format('Image file %file has the correct width.', ['%file' => $file]));
397 $this->assertEqual(20, $image_reloaded->getHeight(), SafeMarkup::format('Image file %file has the correct height.', ['%file' => $file]));
398 $this->assertEqual(image_type_to_mime_type($type), $image_reloaded->getMimeType(), SafeMarkup::format('Image file %file has the correct MIME type.', ['%file' => $file]));
399 if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF) {
400 $this->assertEqual('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), SafeMarkup::format('Image file %file has the correct transparent color channel set.', ['%file' => $file]));
403 $this->assertEqual(NULL, $image_reloaded->getToolkit()->getTransparentColor(), SafeMarkup::format('Image file %file has no color channel set.', ['%file' => $file]));
407 // Test failures of the 'create_new' operation.
408 $image = $this->imageFactory->get();
409 $image->createNew(-50, 20);
410 $this->assertFalse($image->isValid(), 'CreateNew with negative width fails.');
411 $image->createNew(50, 20, 'foo');
412 $this->assertFalse($image->isValid(), 'CreateNew with invalid extension fails.');
413 $image->createNew(50, 20, 'gif', '#foo');
414 $this->assertFalse($image->isValid(), 'CreateNew with invalid color hex string fails.');
415 $image->createNew(50, 20, 'gif', '#ff0000');
416 $this->assertTrue($image->isValid(), 'CreateNew with valid arguments validates the Image.');
420 * Tests that GD resources are freed from memory.
422 public function testResourceDestruction() {
423 // Test that an Image object going out of scope releases its GD resource.
424 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/image-test.png');
425 $res = $image->getToolkit()->getResource();
426 $this->assertTrue(is_resource($res), 'Successfully loaded image resource.');
428 $this->assertFalse(is_resource($res), 'Image resource was destroyed after losing scope.');
430 // Test that 'create_new' operation does not leave orphaned GD resources.
431 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/image-test.png');
432 $old_res = $image->getToolkit()->getResource();
433 // Check if resource has been created successfully.
434 $this->assertTrue(is_resource($old_res));
435 $image->createNew(20, 20);
436 $new_res = $image->getToolkit()->getResource();
437 // Check if the original resource has been destroyed.
438 $this->assertFalse(is_resource($old_res));
439 // Check if a new resource has been created successfully.
440 $this->assertTrue(is_resource($new_res));
444 * Tests for GIF images with transparency.
446 public function testGifTransparentImages() {
447 // Prepare a directory for test file results.
448 $directory = Settings::get('file_public_path') . '/imagetest';
449 file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
451 // Test loading an indexed GIF image with transparent color set.
452 // Color at top-right pixel should be fully transparent.
453 $file = 'image-test-transparent-indexed.gif';
454 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
455 $resource = $image->getToolkit()->getResource();
456 $color_index = imagecolorat($resource, $image->getWidth() - 1, 0);
457 $color = array_values(imagecolorsforindex($resource, $color_index));
458 $this->assertEqual($this->rotateTransparent, $color, "Image {$file} after load has full transparent color at corner 1.");
460 // Test deliberately creating a GIF image with no transparent color set.
461 // Color at top-right pixel should be fully transparent while in memory,
462 // fully opaque after flushing image to file.
463 $file = 'image-test-no-transparent-color-set.gif';
464 $file_path = $directory . '/' . $file ;
466 $image = $this->imageFactory->get();
467 $image->createNew(50, 20, 'gif', NULL);
468 $resource = $image->getToolkit()->getResource();
469 $color_index = imagecolorat($resource, $image->getWidth() - 1, 0);
470 $color = array_values(imagecolorsforindex($resource, $color_index));
471 $this->assertEqual($this->rotateTransparent, $color, "New GIF image with no transparent color set after creation has full transparent color at corner 1.");
473 $this->assertTrue($image->save($file_path), "New GIF image {$file} was saved.");
475 $image_reloaded = $this->imageFactory->get($file_path);
476 $resource = $image_reloaded->getToolkit()->getResource();
477 $color_index = imagecolorat($resource, $image_reloaded->getWidth() - 1, 0);
478 $color = array_values(imagecolorsforindex($resource, $color_index));
479 // Check explicitly for alpha == 0 as the rest of the color has been
480 // compressed and may have slight difference from full white.
481 $this->assertEqual(0, $color[3], "New GIF image {$file} after reload has no transparent color at corner 1.");
483 // Test loading an image whose transparent color index is out of range.
484 // This image was generated by taking an initial image with a palette size
485 // of 6 colors, and setting the transparent color index to 6 (one higher
486 // than the largest allowed index), as follows:
488 // $image = imagecreatefromgif('core/modules/simpletest/files/image-test.gif');
489 // imagecolortransparent($image, 6);
490 // imagegif($image, 'core/modules/simpletest/files/image-test-transparent-out-of-range.gif');
492 // This allows us to test that an image with an out-of-range color index
493 // can be loaded correctly.
494 $file = 'image-test-transparent-out-of-range.gif';
495 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
496 $toolkit = $image->getToolkit();
498 if (!$image->isValid()) {
499 $this->fail(SafeMarkup::format('Could not load image %file.', ['%file' => $file]));
502 // All images should be converted to truecolor when loaded.
503 $image_truecolor = imageistruecolor($toolkit->getResource());
504 $this->assertTrue($image_truecolor, SafeMarkup::format('Image %file after load is a truecolor image.', ['%file' => $file]));
509 * Tests calling a missing image operation plugin.
511 public function testMissingOperation() {
513 // Test that the image factory is set to use the GD toolkit.
514 $this->assertEqual($this->imageFactory->getToolkitId(), 'gd', 'The image factory is set to use the \'gd\' image toolkit.');
516 // An image file that will be tested.
517 $file = 'image-test.png';
519 // Load up a fresh image.
520 $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/' . $file);
521 if (!$image->isValid()) {
522 $this->fail(SafeMarkup::format('Could not load image %file.', ['%file' => $file]));
525 // Try perform a missing toolkit operation.
526 $this->assertFalse($image->apply('missing_op', []), 'Calling a missing image toolkit operation plugin fails.');