2322581ffa91ff34b90bc1b2104278c94c70d54f
[yaffs-website] / web / modules / contrib / imagemagick / tests / src / Functional / ToolkitImagemagickTest.php
1 <?php
2
3 namespace Drupal\Tests\imagemagick\Functional;
4
5 use Drupal\Core\Image\ImageInterface;
6 use Drupal\Tests\TestFileCreationTrait;
7 use Drupal\Tests\BrowserTestBase;
8
9 /**
10  * Tests that core image manipulations work properly through Imagemagick.
11  *
12  * @group Imagemagick
13  */
14 class ToolkitImagemagickTest extends BrowserTestBase {
15
16   use TestFileCreationTrait;
17
18   /**
19    * The image factory service.
20    *
21    * @var \Drupal\Core\Image\ImageFactory
22    */
23   protected $imageFactory;
24
25   /**
26    * A directory for image test file results.
27    *
28    * @var string
29    */
30   protected $testDirectory;
31
32   // Colors that are used in testing.
33   // @codingStandardsIgnoreStart
34   protected $black             = [  0,   0,   0,   0];
35   protected $red               = [255,   0,   0,   0];
36   protected $green             = [  0, 255,   0,   0];
37   protected $blue              = [  0,   0, 255,   0];
38   protected $yellow            = [255, 255,   0,   0];
39   protected $fuchsia           = [255,   0, 255,   0];
40   protected $cyan              = [  0, 255, 255,   0];
41   protected $white             = [255, 255, 255,   0];
42   protected $grey              = [128, 128, 128,   0];
43   protected $transparent       = [  0,   0,   0, 127];
44   protected $rotateTransparent = [255, 255, 255, 127];
45
46   protected $width = 40;
47   protected $height = 20;
48   // @codingStandardsIgnoreEnd
49
50   /**
51    * Modules to enable.
52    *
53    * @var array
54    */
55   protected static $modules = [
56     'system',
57     'simpletest',
58     'file_test',
59     'imagemagick',
60   ];
61
62   /**
63    * {@inheritdoc}
64    */
65   public function setUp() {
66     parent::setUp();
67
68     // Create an admin user.
69     $admin_user = $this->drupalCreateUser([
70       'administer site configuration',
71     ]);
72     $this->drupalLogin($admin_user);
73
74     // Set the image factory.
75     $this->imageFactory = $this->container->get('image.factory');
76
77     // Prepare a directory for test file results.
78     $this->testDirectory = 'public://imagetest';
79   }
80
81   /**
82    * Provides data for testManipulations.
83    *
84    * @return array[]
85    *   A simple array of simple arrays, each having the following elements:
86    *   - binaries to use for testing.
87    */
88   public function providerManipulationTest() {
89     return [
90       ['imagemagick'],
91       ['graphicsmagick'],
92     ];
93   }
94
95   /**
96    * Test image toolkit operations.
97    *
98    * Since PHP can't visually check that our images have been manipulated
99    * properly, build a list of expected color values for each of the corners and
100    * the expected height and widths for the final images.
101    *
102    * @param string $binaries
103    *   The graphics package binaries to use for testing.
104    *
105    * @dataProvider providerManipulationTest
106    */
107   public function testManipulations($binaries) {
108     // Change the toolkit.
109     \Drupal::configFactory()->getEditable('system.image')
110       ->set('toolkit', 'imagemagick')
111       ->save();
112
113     // Execute tests with selected binaries.
114     // The test can only be executed if binaries are available on the shell
115     // path.
116     \Drupal::configFactory()->getEditable('imagemagick.settings')
117       ->set('debug', TRUE)
118       ->set('binaries', $binaries)
119       ->set('quality', 100)
120       ->save();
121     $status = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick')->checkPath('');
122     if (!empty($status['errors'])) {
123       // Bots running automated test on d.o. do not have binaries installed,
124       // so the test will be skipped; it can be run locally where binaries are
125       // installed.
126       $this->markTestSkipped("Tests for '{$binaries}' cannot run because the binaries are not available on the shell path.");
127     }
128
129     // Set the toolkit on the image factory.
130     $this->imageFactory->setToolkitId('imagemagick');
131
132     // Test that the image factory is set to use the Imagemagick toolkit.
133     $this->assertEqual($this->imageFactory->getToolkitId(), 'imagemagick', 'The image factory is set to use the \'imagemagick\' image toolkit.');
134
135     // Prepare directory.
136     file_unmanaged_delete_recursive($this->testDirectory);
137     file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
138
139     // Typically the corner colors will be unchanged. These colors are in the
140     // order of top-left, top-right, bottom-right, bottom-left.
141     $default_corners = [
142       $this->red,
143       $this->green,
144       $this->blue,
145       $this->transparent,
146     ];
147
148     // A list of files that will be tested.
149     $files = [
150       'image-test.png',
151       'image-test.gif',
152       'image-test-no-transparency.gif',
153       'image-test.jpg',
154     ];
155
156     // Setup a list of tests to perform on each type.
157     $operations = [
158       'resize' => [
159         'function' => 'resize',
160         'arguments' => ['width' => 20, 'height' => 10],
161         'width' => 20,
162         'height' => 10,
163         'corners' => $default_corners,
164         'tolerance' => 0,
165       ],
166       'scale_x' => [
167         'function' => 'scale',
168         'arguments' => ['width' => 20],
169         'width' => 20,
170         'height' => 10,
171         'corners' => $default_corners,
172         'tolerance' => 0,
173       ],
174       'scale_y' => [
175         'function' => 'scale',
176         'arguments' => ['height' => 10],
177         'width' => 20,
178         'height' => 10,
179         'corners' => $default_corners,
180         'tolerance' => 0,
181       ],
182       'upscale_x' => [
183         'function' => 'scale',
184         'arguments' => ['width' => 80, 'upscale' => TRUE],
185         'width' => 80,
186         'height' => 40,
187         'corners' => $default_corners,
188         'tolerance' => 0,
189       ],
190       'upscale_y' => [
191         'function' => 'scale',
192         'arguments' => ['height' => 40, 'upscale' => TRUE],
193         'width' => 80,
194         'height' => 40,
195         'corners' => $default_corners,
196         'tolerance' => 0,
197       ],
198       'crop' => [
199         'function' => 'crop',
200         'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12],
201         'width' => 16,
202         'height' => 12,
203         'corners' => array_fill(0, 4, $this->white),
204         'tolerance' => 0,
205       ],
206       'scale_and_crop' => [
207         'function' => 'scale_and_crop',
208         'arguments' => ['width' => 10, 'height' => 8],
209         'width' => 10,
210         'height' => 8,
211         'corners' => array_fill(0, 4, $this->black),
212         'tolerance' => 100,
213       ],
214       'convert_jpg' => [
215         'function' => 'convert',
216         'width' => 40,
217         'height' => 20,
218         'arguments' => ['extension' => 'jpeg'],
219         'mimetype' => 'image/jpeg',
220         'corners' => $default_corners,
221         'tolerance' => 0,
222       ],
223       'convert_gif' => [
224         'function' => 'convert',
225         'width' => 40,
226         'height' => 20,
227         'arguments' => ['extension' => 'gif'],
228         'mimetype' => 'image/gif',
229         'corners' => $default_corners,
230         'tolerance' => 15,
231       ],
232       'convert_png' => [
233         'function' => 'convert',
234         'width' => 40,
235         'height' => 20,
236         'arguments' => ['extension' => 'png'],
237         'mimetype' => 'image/png',
238         'corners' => $default_corners,
239         'tolerance' => 5,
240       ],
241       'rotate_5' => [
242         'function' => 'rotate',
243         'arguments' => [
244           'degrees' => 5,
245           'background' => '#FF00FF',
246           'resize_filter' => 'Box',
247         ],
248         'width' => 41,
249         'height' => 23,
250         'corners' => array_fill(0, 4, $this->fuchsia),
251         'tolerance' => 5,
252       ],
253       'rotate_minus_10' => [
254         'function' => 'rotate',
255         'arguments' => [
256           'degrees' => -10,
257           'background' => '#FF00FF',
258           'resize_filter' => 'Box',
259         ],
260         'width' => 41,
261         'height' => 26,
262         'corners' => array_fill(0, 4, $this->fuchsia),
263         'tolerance' => 15,
264       ],
265       'rotate_90' => [
266         'function' => 'rotate',
267         'arguments' => ['degrees' => 90, 'background' => '#FF00FF'],
268         'width' => 20,
269         'height' => 40,
270         'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
271         'tolerance' => 0,
272       ],
273       'rotate_transparent_5' => [
274         'function' => 'rotate',
275         'arguments' => ['degrees' => 5, 'resize_filter' => 'Box'],
276         'width' => 41,
277         'height' => 23,
278         'corners' => array_fill(0, 4, $this->transparent),
279         'tolerance' => 0,
280       ],
281       'rotate_transparent_90' => [
282         'function' => 'rotate',
283         'arguments' => ['degrees' => 90],
284         'width' => 20,
285         'height' => 40,
286         'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
287         'tolerance' => 0,
288       ],
289       'desaturate' => [
290         'function' => 'desaturate',
291         'arguments' => [],
292         'height' => 20,
293         'width' => 40,
294         // Grayscale corners are a bit funky. Each of the corners are a shade of
295         // gray. The values of these were determined simply by looking at the
296         // final image to see what desaturated colors end up being.
297         'corners' => [
298           array_fill(0, 3, 76) + [3 => 0],
299           array_fill(0, 3, 149) + [3 => 0],
300           array_fill(0, 3, 29) + [3 => 0],
301           array_fill(0, 3, 225) + [3 => 127],
302         ],
303         // @todo tolerance here is too high. Check reasons.
304         'tolerance' => 17000,
305       ],
306     ];
307
308     // Prepare a copy of test files.
309     $this->getTestFiles('image');
310
311     foreach ($files as $file) {
312       foreach ($operations as $op => $values) {
313         // Load up a fresh image.
314         $image = $this->imageFactory->get('public://' . $file);
315         if (!$image->isValid()) {
316           $this->fail("Could not load image $file.");
317           continue 2;
318         }
319
320         // Check that no multi-frame information is set.
321         $this->assertNull($image->getToolkit()->getFrames());
322
323         // Perform our operation.
324         $image->apply($values['function'], $values['arguments']);
325
326         // Save and reload image.
327         $file_path = $this->testDirectory . '/' . $op . substr($file, -4);
328         $image->save($file_path);
329         $image = $this->imageFactory->get($file_path);
330         $this->assertTrue($image->isValid());
331
332         // @todo GraphicsMagick specifics, temporarily adjust tests.
333         $package = $image->getToolkit()->getPackage();
334         if ($package === 'graphicsmagick') {
335           // @todo Issues with crop on GIF files, investigate.
336           if (in_array($file, ['image-test.gif', 'image-test-no-transparency.gif']) && in_array($op, ['crop', 'scale_and_crop'])) {
337             continue;
338           }
339         }
340
341         // Reload with GD to be able to check results at pixel level.
342         $image = $this->imageFactory->get($file_path, 'gd');
343         $toolkit = $image->getToolkit();
344         $toolkit->getResource();
345         $this->assertTrue($image->isValid());
346
347         // Check MIME type if needed.
348         if (isset($values['mimetype'])) {
349           $this->assertEqual($values['mimetype'], $toolkit->getMimeType(), "Image '$file' after '$op' action has proper MIME type ({$values['mimetype']}).");
350         }
351
352         // To keep from flooding the test with assert values, make a general
353         // value for whether each group of values fail.
354         $correct_dimensions_real = TRUE;
355         $correct_dimensions_object = TRUE;
356
357         // Check the real dimensions of the image first.
358         $actual_toolkit_width = imagesx($toolkit->getResource());
359         $actual_toolkit_height = imagesy($toolkit->getResource());
360         if ($actual_toolkit_height != $values['height'] || $actual_toolkit_width != $values['width']) {
361           $correct_dimensions_real = FALSE;
362         }
363
364         // Check that the image object has an accurate record of the dimensions.
365         $actual_image_width = $image->getWidth();
366         $actual_image_height = $image->getHeight();
367         if ($actual_image_width != $values['width'] || $actual_image_height != $values['height']) {
368           $correct_dimensions_object = FALSE;
369         }
370
371         $this->assertTrue($correct_dimensions_real, "Image '$file' after '$op' action has proper dimensions. Expected {$values['width']}x{$values['height']}, actual {$actual_toolkit_width}x{$actual_toolkit_height}.");
372         $this->assertTrue($correct_dimensions_object, "Image '$file' object after '$op' action is reporting the proper height and width values.  Expected {$values['width']}x{$values['height']}, actual {$actual_image_width}x{$actual_image_height}.");
373
374         // JPEG colors will always be messed up due to compression.
375         if ($image->getToolkit()->getType() != IMAGETYPE_JPEG) {
376           // Now check each of the corners to ensure color correctness.
377           foreach ($values['corners'] as $key => $corner) {
378             // The test gif that does not have transparency has yellow where the
379             // others have transparent.
380             if ($file === 'image-test-no-transparency.gif' && $corner === $this->transparent && $op != 'rotate_transparent_5') {
381               $corner = $this->yellow;
382             }
383             // The test jpg when converted to other formats has yellow where the
384             // others have transparent.
385             if ($file === 'image-test.jpg' && $corner === $this->transparent && in_array($op, ['convert_gif', 'convert_png'])) {
386               $corner = $this->yellow;
387             }
388             // Get the location of the corner.
389             switch ($key) {
390               case 0:
391                 $x = 0;
392                 $y = 0;
393                 break;
394
395               case 1:
396                 $x = $image->getWidth() - 1;
397                 $y = 0;
398                 break;
399
400               case 2:
401                 $x = $image->getWidth() - 1;
402                 $y = $image->getHeight() - 1;
403                 break;
404
405               case 3:
406                 $x = 0;
407                 $y = $image->getHeight() - 1;
408                 break;
409
410             }
411             $color = $this->getPixelColor($image, $x, $y);
412             $correct_colors = $this->colorsAreClose($color, $corner, $values['tolerance']);
413           }
414         }
415       }
416     }
417
418     // Test creation of image from scratch, and saving to storage.
419     foreach ([IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG] as $type) {
420       $image = $this->imageFactory->get();
421       $image->createNew(50, 20, image_type_to_extension($type, FALSE), '#ffff00');
422       $file = 'from_null' . image_type_to_extension($type);
423       $file_path = $this->testDirectory . '/' . $file;
424       $this->assertEqual(50, $image->getWidth(), "Image file '$file' has the correct width.");
425       $this->assertEqual(20, $image->getHeight(), "Image file '$file' has the correct height.");
426       $this->assertEqual(image_type_to_mime_type($type), $image->getMimeType(), "Image file '$file' has the correct MIME type.");
427       $this->assertTrue($image->save($file_path), "Image '$file' created anew from a null image was saved.");
428
429       // Reload saved image.
430       $image_reloaded = $this->imageFactory->get($file_path, 'gd');
431       if (!$image_reloaded->isValid()) {
432         $this->fail("Could not load image '$file'.");
433         continue;
434       }
435       $this->assertEqual(50, $image_reloaded->getWidth(), "Image file '$file' has the correct width.");
436       $this->assertEqual(20, $image_reloaded->getHeight(), "Image file '$file' has the correct height.");
437       $this->assertEqual(image_type_to_mime_type($type), $image_reloaded->getMimeType(), "Image file '$file' has the correct MIME type.");
438       if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF) {
439         $this->assertEqual('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), "Image file '$file' has the correct transparent color channel set.");
440       }
441       else {
442         $this->assertEqual(NULL, $image_reloaded->getToolkit()->getTransparentColor(), "Image file '$file' has no color channel set.");
443       }
444     }
445
446     // Test failures of CreateNew.
447     $image = $this->imageFactory->get();
448     $image->createNew(-50, 20);
449     $this->assertFalse($image->isValid(), 'CreateNew with negative width fails.');
450     $image->createNew(50, 20, 'foo');
451     $this->assertFalse($image->isValid(), 'CreateNew with invalid extension fails.');
452     $image->createNew(50, 20, 'gif', '#foo');
453     $this->assertFalse($image->isValid(), 'CreateNew with invalid color hex string fails.');
454     $image->createNew(50, 20, 'gif', '#ff0000');
455     $this->assertTrue($image->isValid(), 'CreateNew with valid arguments validates the Image.');
456
457     // Test saving image files with filenames having non-ascii characters.
458     $file_names = [
459       'greek εικόνα δοκιμής.png',
460       'russian Тестовое изображение.png',
461       'simplified chinese 测试图片.png',
462       'japanese 試験画像.png',
463       'arabic صورة الاختبار.png',
464       'armenian փորձարկման պատկերը.png',
465       'bengali পরীক্ষা ইমেজ.png',
466       'hebraic תמונת בדיקה.png',
467       'hindi परीक्षण छवि.png',
468       'viet hình ảnh thử nghiệm.png',
469       'viet \'with quotes\' hình ảnh thử nghiệm.png',
470       'viet "with double quotes" hình ảnh thử nghiệm.png',
471     ];
472     foreach ($file_names as $file) {
473       $image = $this->imageFactory->get();
474       $image->createNew(50, 20, 'png');
475       $file_path = $this->testDirectory . '/' . $file;
476       $image->save($file_path);
477       $image_reloaded = $this->imageFactory->get($file_path, 'gd');
478       $this->assertTrue($image_reloaded->isValid(), "Image file '$file' loaded successfully.");
479     }
480
481     // Test handling a file stored through a remote stream wrapper.
482     $image = $this->imageFactory->get('dummy-remote://image-test.png');
483     // Source file should be equal to the copied local temp source file.
484     $this->assertEqual(filesize('dummy-remote://image-test.png'), filesize($image->getToolkit()->getSourceLocalPath()));
485     $image->desaturate();
486     $image->save('dummy-remote://remote-image-test.png');
487     // Destination file should exists, and destination local temp file should
488     // have been reset.
489     $this->assertTrue(file_exists($image->getToolkit()->getDestination()));
490     $this->assertEqual('dummy-remote://remote-image-test.png', $image->getToolkit()->getDestination());
491     $this->assertIdentical('', $image->getToolkit()->getDestinationLocalPath());
492
493     // Test retrieval of EXIF information.
494     $image_files = [
495       [
496         'path' => drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg',
497         'orientation' => 8,
498       ],
499       [
500         'path' => 'public://image-test.jpg',
501         'orientation' => NULL,
502       ],
503       [
504         'path' => 'public://image-test.png',
505         'orientation' => NULL,
506       ],
507       [
508         'path' => 'public://image-test.gif',
509         'orientation' => NULL,
510       ],
511       [
512         'path' => NULL,
513         'orientation' => NULL,
514       ],
515     ];
516
517     foreach ($image_files as $image_file) {
518       // Get image using 'identify'.
519       \Drupal::configFactory()->getEditable('imagemagick.settings')
520         ->set('use_identify', TRUE)
521         ->save();
522       $image = $this->imageFactory->get($image_file['path']);
523       $this->assertIdentical($image_file['orientation'], $image->getToolkit()->getExifOrientation());
524
525       // Get image using 'getimagesize'.
526       \Drupal::configFactory()->getEditable('imagemagick.settings')
527         ->set('use_identify', FALSE)
528         ->save();
529       $image = $this->imageFactory->get($image_file['path']);
530       $this->assertIdentical($image_file['orientation'], $image->getToolkit()->getExifOrientation());
531     }
532
533     // Test multi-frame GIF image.
534     $image_files = [
535       [
536         'source' => drupal_get_path('module', 'imagemagick') . '/misc/test-multi-frame.gif',
537         'destination' => $this->testDirectory . '/test-multi-frame.gif',
538         'width' => 60,
539         'height' => 29,
540         'frames' => 13,
541         'scaled_width' => 30,
542         'scaled_height' => 15,
543         'rotated_width' => 33,
544         'rotated_height' => 26,
545       ],
546     ];
547
548     // Get images using 'identify'.
549     \Drupal::configFactory()->getEditable('imagemagick.settings')
550       ->set('use_identify', TRUE)
551       ->save();
552     foreach ($image_files as $image_file) {
553       $image = $this->imageFactory->get($image_file['source']);
554       $this->assertIdentical($image_file['width'], $image->getWidth());
555       $this->assertIdentical($image_file['height'], $image->getHeight());
556       $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
557
558       // Scaling should preserve frames.
559       $image->scale(30);
560       $image->save($image_file['destination']);
561       $image = $this->imageFactory->get($image_file['destination']);
562       $this->assertIdentical($image_file['scaled_width'], $image->getWidth());
563       $this->assertIdentical($image_file['scaled_height'], $image->getHeight());
564       $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
565
566       // Rotating should preserve frames.
567       $image->rotate(24);
568       $image->save($image_file['destination']);
569       $image = $this->imageFactory->get($image_file['destination']);
570       $this->assertIdentical($image_file['rotated_width'], $image->getWidth());
571       $this->assertIdentical($image_file['rotated_height'], $image->getHeight());
572       $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
573
574       // Converting to PNG should drop frames.
575       $image->convert('png');
576       $this->assertNull($image->getToolkit()->getFrames());
577       $image->save($image_file['destination']);
578       $image = $this->imageFactory->get($image_file['destination']);
579       $this->assertIdentical($image_file['rotated_width'], $image->getWidth());
580       $this->assertIdentical($image_file['rotated_height'], $image->getHeight());
581       $this->assertNull($image->getToolkit()->getFrames());
582     }
583   }
584
585   /**
586    * Test ImageMagick subform and settings.
587    */
588   public function testFormAndSettings() {
589     // Change the toolkit.
590     \Drupal::configFactory()->getEditable('system.image')
591       ->set('toolkit', 'imagemagick')
592       ->save();
593
594     // Test form is accepting wrong binaries path while setting toolkit to GD.
595     $this->drupalGet('admin/config/media/image-toolkit');
596     $this->assertFieldByName('image_toolkit', 'imagemagick');
597     $edit = [
598       'image_toolkit' => 'gd',
599       'imagemagick[suite][path_to_binaries]' => '/foo/bar',
600     ];
601     $this->drupalPostForm(NULL, $edit, 'Save configuration');
602     $this->assertFieldByName('image_toolkit', 'gd');
603
604     // Change the toolkit.
605     \Drupal::configFactory()->getEditable('system.image')
606       ->set('toolkit', 'imagemagick')
607       ->save();
608     $this->imageFactory->setToolkitId('imagemagick');
609     $this->assertEqual('imagemagick', $this->imageFactory->getToolkitId());
610
611     // Test default supported image extensions.
612     $this->assertEqual('gif jpe jpeg jpg png', implode(' ', $this->imageFactory->getSupportedExtensions()));
613
614     $config = \Drupal::configFactory()->getEditable('imagemagick.settings');
615
616     // Enable TIFF.
617     $image_formats = $config->get('image_formats');
618     $image_formats['TIFF']['enabled'] = TRUE;
619     $config->set('image_formats', $image_formats)->save();
620     $this->assertEqual('gif jpe jpeg jpg png tif tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
621
622     // Disable PNG.
623     $image_formats['PNG']['enabled'] = FALSE;
624     $config->set('image_formats', $image_formats)->save();
625     $this->assertEqual('gif jpe jpeg jpg tif tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
626
627     // Disable some extensions.
628     $image_formats['TIFF']['exclude_extensions'] = 'tif, gif';
629     $config->set('image_formats', $image_formats)->save();
630     $this->assertEqual('gif jpe jpeg jpg tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
631     $image_formats['JPEG']['exclude_extensions'] = 'jpe, jpg';
632     $config->set('image_formats', $image_formats)->save();
633     $this->assertEqual('gif jpeg tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
634   }
635
636   /**
637    * Function for finding a pixel's RGBa values.
638    */
639   protected function getPixelColor(ImageInterface $image, $x, $y) {
640     $toolkit = $image->getToolkit();
641     $color_index = imagecolorat($toolkit->getResource(), $x, $y);
642
643     $transparent_index = imagecolortransparent($toolkit->getResource());
644     if ($color_index == $transparent_index) {
645       return array(0, 0, 0, 127);
646     }
647
648     return array_values(imagecolorsforindex($toolkit->getResource(), $color_index));
649   }
650
651   /**
652    * Function to compare two colors by RGBa, within a tolerance.
653    *
654    * Very basic, just compares the sum of the squared differences for each of
655    * the R, G, B, A components of two colors against a 'tolerance' value.
656    *
657    * @param int[] $actual
658    *   The actual RGBA array.
659    * @param int[] $expected
660    *   The expected RGBA array.
661    * @param int $tolerance
662    *   The acceptable difference between the colors.
663    *
664    * @return bool
665    *   TRUE if the colors differences are within tolerance, FALSE otherwise.
666    */
667   protected function colorsAreClose(array $actual, array $expected, $tolerance) {
668     // Fully transparent colors are equal, regardless of RGB.
669     if ($actual[3] == 127 && $expected[3] == 127) {
670       return TRUE;
671     }
672     $distance = pow(($actual[0] - $expected[0]), 2) + pow(($actual[1] - $expected[1]), 2) + pow(($actual[2] - $expected[2]), 2) + pow(($actual[3] - $expected[3]), 2);
673     $this->assertLessThanOrEqual($tolerance, $distance, "Actual: {" . implode(',', $actual) . "}, Expected: {" . implode(',', $expected) . "}, Distance: " . $distance . ", Tolerance: " . $tolerance);
674     return TRUE;
675   }
676
677 }