3 namespace Drupal\Tests\imagemagick\Functional;
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Image\ImageInterface;
7 use Drupal\Tests\TestFileCreationTrait;
8 use Drupal\Tests\BrowserTestBase;
9 use Drupal\file_mdm\FileMetadataInterface;
10 use Drupal\imagemagick\ImagemagickExecArguments;
13 * Tests that core image manipulations work properly through Imagemagick.
17 class ToolkitImagemagickTest extends BrowserTestBase {
19 use TestFileCreationTrait;
22 * The image factory service.
24 * @var \Drupal\Core\Image\ImageFactory
26 protected $imageFactory;
29 * A directory for image test file results.
33 protected $testDirectory;
35 // Colors that are used in testing.
36 // @codingStandardsIgnoreStart
37 protected $black = [ 0, 0, 0, 0];
38 protected $red = [255, 0, 0, 0];
39 protected $green = [ 0, 255, 0, 0];
40 protected $blue = [ 0, 0, 255, 0];
41 protected $yellow = [255, 255, 0, 0];
42 protected $fuchsia = [255, 0, 255, 0];
43 protected $cyan = [ 0, 255, 255, 0];
44 protected $white = [255, 255, 255, 0];
45 protected $grey = [128, 128, 128, 0];
46 protected $transparent = [ 0, 0, 0, 127];
47 protected $rotateTransparent = [255, 255, 255, 127];
49 protected $width = 40;
50 protected $height = 20;
51 // @codingStandardsIgnoreEnd
58 protected static $modules = [
70 public function setUp() {
73 // Create an admin user.
74 $admin_user = $this->drupalCreateUser([
75 'administer site configuration',
77 $this->drupalLogin($admin_user);
79 // Set the image factory.
80 $this->imageFactory = $this->container->get('image.factory');
82 // Prepare a directory for test file results.
83 $this->testDirectory = 'public://imagetest';
87 * Helper to setup the image toolkit.
89 * @param string $binaries
90 * The graphics package binaries to use for testing.
91 * @param bool $check_path
92 * Whether the path to binaries should be tested.
94 protected function setUpToolkit($binaries, $check_path = TRUE) {
95 // Change the toolkit.
96 \Drupal::configFactory()->getEditable('system.image')
97 ->set('toolkit', 'imagemagick')
100 // Execute tests with selected binaries.
101 \Drupal::configFactory()->getEditable('imagemagick.settings')
103 ->set('binaries', $binaries)
104 ->set('quality', 100)
108 // The test can only be executed if binaries are available on the shell
110 $status = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick')->getExecManager()->checkPath('');
111 if (!empty($status['errors'])) {
112 // Bots running automated test on d.o. do not have binaries installed,
113 // so the test will be skipped; it can be run locally where binaries
115 $this->markTestSkipped("Tests for '{$binaries}' cannot run because the binaries are not available on the shell path.");
119 // Set the toolkit on the image factory.
120 $this->imageFactory->setToolkitId('imagemagick');
122 // Test that the image factory is set to use the Imagemagick toolkit.
123 $this->assertEqual($this->imageFactory->getToolkitId(), 'imagemagick', 'The image factory is set to use the \'imagemagick\' image toolkit.');
125 // Prepare directory.
126 file_unmanaged_delete_recursive($this->testDirectory);
127 file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
131 * Provides data for testManipulations.
134 * A simple array of simple arrays, each having the following elements:
135 * - binaries to use for testing.
137 public function providerManipulationTest() {
145 * Test image toolkit operations.
147 * Since PHP can't visually check that our images have been manipulated
148 * properly, build a list of expected color values for each of the corners and
149 * the expected height and widths for the final images.
151 * @param string $binaries
152 * The graphics package binaries to use for testing.
154 * @dataProvider providerManipulationTest
156 public function testManipulations($binaries) {
157 $this->setUpToolkit($binaries);
159 // Typically the corner colors will be unchanged. These colors are in the
160 // order of top-left, top-right, bottom-right, bottom-left.
168 // A list of files that will be tested.
172 'image-test-no-transparency.gif',
176 // Setup a list of tests to perform on each type.
179 'function' => 'resize',
180 'arguments' => ['width' => 20, 'height' => 10],
183 'corners' => $default_corners,
187 'function' => 'scale',
188 'arguments' => ['width' => 20],
191 'corners' => $default_corners,
195 'function' => 'scale',
196 'arguments' => ['height' => 10],
199 'corners' => $default_corners,
203 'function' => 'scale',
204 'arguments' => ['width' => 80, 'upscale' => TRUE],
207 'corners' => $default_corners,
211 'function' => 'scale',
212 'arguments' => ['height' => 40, 'upscale' => TRUE],
215 'corners' => $default_corners,
219 'function' => 'crop',
220 'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12],
223 'corners' => array_fill(0, 4, $this->white),
226 'scale_and_crop' => [
227 'function' => 'scale_and_crop',
228 'arguments' => ['width' => 10, 'height' => 8],
231 'corners' => array_fill(0, 4, $this->black),
235 'function' => 'convert',
238 'arguments' => ['extension' => 'jpeg'],
239 'mimetype' => 'image/jpeg',
240 'corners' => $default_corners,
244 'function' => 'convert',
247 'arguments' => ['extension' => 'gif'],
248 'mimetype' => 'image/gif',
249 'corners' => $default_corners,
253 'function' => 'convert',
256 'arguments' => ['extension' => 'png'],
257 'mimetype' => 'image/png',
258 'corners' => $default_corners,
262 'function' => 'rotate',
265 'background' => '#FF00FF',
266 'resize_filter' => 'Box',
270 'corners' => array_fill(0, 4, $this->fuchsia),
273 'rotate_minus_10' => [
274 'function' => 'rotate',
277 'background' => '#FF00FF',
278 'resize_filter' => 'Box',
282 'corners' => array_fill(0, 4, $this->fuchsia),
286 'function' => 'rotate',
287 'arguments' => ['degrees' => 90, 'background' => '#FF00FF'],
290 'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
293 'rotate_transparent_5' => [
294 'function' => 'rotate',
295 'arguments' => ['degrees' => 5, 'resize_filter' => 'Box'],
298 'corners' => array_fill(0, 4, $this->transparent),
301 'rotate_transparent_90' => [
302 'function' => 'rotate',
303 'arguments' => ['degrees' => 90],
306 'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
310 'function' => 'desaturate',
314 // Grayscale corners are a bit funky. Each of the corners are a shade of
315 // gray. The values of these were determined simply by looking at the
316 // final image to see what desaturated colors end up being.
318 array_fill(0, 3, 76) + [3 => 0],
319 array_fill(0, 3, 149) + [3 => 0],
320 array_fill(0, 3, 29) + [3 => 0],
321 array_fill(0, 3, 225) + [3 => 127],
323 // @todo tolerance here is too high. Check reasons.
324 'tolerance' => 17000,
328 // Prepare a copy of test files.
329 $this->getTestFiles('image');
331 foreach ($files as $file) {
332 $image_uri = 'public://' . $file;
333 foreach ($operations as $op => $values) {
334 // Load up a fresh image.
335 $image = $this->imageFactory->get($image_uri);
336 if (!$image->isValid()) {
337 $this->fail("Could not load image $file.");
341 // Check that no multi-frame information is set.
342 $this->assertIdentical(1, $image->getToolkit()->getFrames());
344 // Perform our operation.
345 $image->apply($values['function'], $values['arguments']);
347 // Save and reload image.
348 $file_path = $this->testDirectory . '/' . $op . substr($file, -4);
349 $this->assertTrue($image->save($file_path));
350 $image = $this->imageFactory->get($file_path);
351 $this->assertTrue($image->isValid());
353 // @todo Suite specifics, temporarily adjust tests.
354 $package = $image->getToolkit()->getExecManager()->getPackage();
355 if ($package === 'graphicsmagick') {
356 // @todo Issues with crop and convert on GIF files, investigate.
357 if (in_array($file, [
358 'image-test.gif', 'image-test-no-transparency.gif',
359 ]) && in_array($op, [
360 'crop', 'scale_and_crop', 'convert_png',
366 // Reload with GD to be able to check results at pixel level.
367 $image = $this->imageFactory->get($file_path, 'gd');
368 $toolkit = $image->getToolkit();
369 $toolkit->getResource();
370 $this->assertTrue($image->isValid());
372 // Check MIME type if needed.
373 if (isset($values['mimetype'])) {
374 $this->assertEqual($values['mimetype'], $toolkit->getMimeType(), "Image '$file' after '$op' action has proper MIME type ({$values['mimetype']}).");
377 // To keep from flooding the test with assert values, make a general
378 // value for whether each group of values fail.
379 $correct_dimensions_real = TRUE;
380 $correct_dimensions_object = TRUE;
382 // Check the real dimensions of the image first.
383 $actual_toolkit_width = imagesx($toolkit->getResource());
384 $actual_toolkit_height = imagesy($toolkit->getResource());
385 if ($actual_toolkit_height != $values['height'] || $actual_toolkit_width != $values['width']) {
386 $correct_dimensions_real = FALSE;
389 // Check that the image object has an accurate record of the dimensions.
390 $actual_image_width = $image->getWidth();
391 $actual_image_height = $image->getHeight();
392 if ($actual_image_width != $values['width'] || $actual_image_height != $values['height']) {
393 $correct_dimensions_object = FALSE;
396 $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}.");
397 $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}.");
399 // JPEG colors will always be messed up due to compression.
400 if ($image->getToolkit()->getType() != IMAGETYPE_JPEG) {
401 // Now check each of the corners to ensure color correctness.
402 foreach ($values['corners'] as $key => $corner) {
403 // The test gif that does not have transparency has yellow where the
404 // others have transparent.
405 if ($file === 'image-test-no-transparency.gif' && $corner === $this->transparent && $op != 'rotate_transparent_5') {
406 $corner = $this->yellow;
408 // The test jpg when converted to other formats has yellow where the
409 // others have transparent.
410 if ($file === 'image-test.jpg' && $corner === $this->transparent && in_array($op, ['convert_gif', 'convert_png'])) {
411 $corner = $this->yellow;
413 // Get the location of the corner.
421 $x = $image->getWidth() - 1;
426 $x = $image->getWidth() - 1;
427 $y = $image->getHeight() - 1;
432 $y = $image->getHeight() - 1;
436 $color = $this->getPixelColor($image, $x, $y);
437 $this->colorsAreClose($color, $corner, $values['tolerance'], $file, $op);
443 // Test creation of image from scratch, and saving to storage.
444 foreach ([IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG] as $type) {
445 $image = $this->imageFactory->get();
446 $image->createNew(50, 20, image_type_to_extension($type, FALSE), '#ffff00');
447 $file = 'from_null' . image_type_to_extension($type);
448 $file_path = $this->testDirectory . '/' . $file;
449 $this->assertEqual(50, $image->getWidth(), "Image file '$file' has the correct width.");
450 $this->assertEqual(20, $image->getHeight(), "Image file '$file' has the correct height.");
451 $this->assertEqual(image_type_to_mime_type($type), $image->getMimeType(), "Image file '$file' has the correct MIME type.");
452 $this->assertTrue($image->save($file_path), "Image '$file' created anew from a null image was saved.");
454 // Reload saved image.
455 $image_reloaded = $this->imageFactory->get($file_path, 'gd');
456 if (!$image_reloaded->isValid()) {
457 $this->fail("Could not load image '$file'.");
460 $this->assertEqual(50, $image_reloaded->getWidth(), "Image file '$file' has the correct width.");
461 $this->assertEqual(20, $image_reloaded->getHeight(), "Image file '$file' has the correct height.");
462 $this->assertEqual(image_type_to_mime_type($type), $image_reloaded->getMimeType(), "Image file '$file' has the correct MIME type.");
463 if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF) {
464 $this->assertEqual('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), "Image file '$file' has the correct transparent color channel set.");
467 $this->assertEqual(NULL, $image_reloaded->getToolkit()->getTransparentColor(), "Image file '$file' has no color channel set.");
471 // Test failures of CreateNew.
472 $image = $this->imageFactory->get();
473 $image->createNew(-50, 20);
474 $this->assertFalse($image->isValid(), 'CreateNew with negative width fails.');
475 $image->createNew(50, 20, 'foo');
476 $this->assertFalse($image->isValid(), 'CreateNew with invalid extension fails.');
477 $image->createNew(50, 20, 'gif', '#foo');
478 $this->assertFalse($image->isValid(), 'CreateNew with invalid color hex string fails.');
479 $image->createNew(50, 20, 'gif', '#ff0000');
480 $this->assertTrue($image->isValid(), 'CreateNew with valid arguments validates the Image.');
482 // Test saving image files with filenames having non-ascii characters.
484 'greek εικόνα δοκιμής.png',
485 'russian Тестовое изображение.png',
486 'simplified chinese 测试图片.png',
488 'arabic صورة الاختبار.png',
489 'armenian փորձարկման պատկերը.png',
490 'bengali পরীক্ষা ইমেজ.png',
491 'hebraic תמונת בדיקה.png',
492 'hindi परीक्षण छवि.png',
493 'viet hình ảnh thử nghiệm.png',
494 'viet \'with quotes\' hình ảnh thử nghiệm.png',
495 'viet "with double quotes" hình ảnh thử nghiệm.png',
497 foreach ($file_names as $file) {
498 // On Windows, skip filenames with non-allowed characters.
499 if (substr(PHP_OS, 0, 3) === 'WIN' && preg_match('/[:*?"<>|]/', $file)) {
502 $image = $this->imageFactory->get();
503 $this->assertTrue($image->createNew(50, 20, 'png'));
504 $file_path = $this->testDirectory . '/' . $file;
505 $this->assertTrue($image->save($file_path), $file);
506 $image_reloaded = $this->imageFactory->get($file_path, 'gd');
507 $this->assertTrue($image_reloaded->isValid(), "Image file '$file' loaded successfully.");
510 // Test handling a file stored through a remote stream wrapper.
511 $image = $this->imageFactory->get('dummy-remote://image-test.png');
512 // Source file should be equal to the copied local temp source file.
513 $this->assertEqual(filesize('dummy-remote://image-test.png'), filesize($image->getToolkit()->arguments()->getSourceLocalPath()));
514 $image->desaturate();
515 $this->assertTrue($image->save('dummy-remote://remote-image-test.png'));
516 // Destination file should exists, and destination local temp file should
518 $this->assertTrue(file_exists($image->getToolkit()->arguments()->getDestination()));
519 $this->assertEqual('dummy-remote://remote-image-test.png', $image->getToolkit()->arguments()->getDestination());
520 $this->assertIdentical('', $image->getToolkit()->arguments()->getDestinationLocalPath());
522 // Test retrieval of EXIF information.
523 file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg', 'public://', FILE_EXISTS_REPLACE);
524 // The image files that will be tested.
527 'path' => drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg',
531 'path' => 'public://test-exif.jpeg',
535 'path' => 'dummy-remote://test-exif.jpeg',
539 'path' => 'public://image-test.jpg',
540 'orientation' => NULL,
543 'path' => 'public://image-test.png',
544 'orientation' => NULL,
547 'path' => 'public://image-test.gif',
548 'orientation' => NULL,
552 'orientation' => NULL,
555 foreach ($image_files as $image_file) {
556 // Get image using 'identify'.
557 \Drupal::configFactory()->getEditable('imagemagick.settings')
558 ->set('use_identify', TRUE)
560 $image = $this->imageFactory->get($image_file['path']);
561 $this->assertIdentical($image_file['orientation'], $image->getToolkit()->getExifOrientation());
564 // Test multi-frame GIF image.
567 'source' => drupal_get_path('module', 'imagemagick') . '/misc/test-multi-frame.gif',
568 'destination' => $this->testDirectory . '/test-multi-frame.gif',
572 'scaled_width' => 30,
573 'scaled_height' => 15,
574 'rotated_width' => 33,
575 'rotated_height' => 26,
578 // Get images using 'identify'.
579 \Drupal::configFactory()->getEditable('imagemagick.settings')
580 ->set('use_identify', TRUE)
582 foreach ($image_files as $image_file) {
583 $image = $this->imageFactory->get($image_file['source']);
584 $this->assertIdentical($image_file['width'], $image->getWidth());
585 $this->assertIdentical($image_file['height'], $image->getHeight());
586 $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
588 // Scaling should preserve frames.
590 $this->assertTrue($image->save($image_file['destination']));
591 $image = $this->imageFactory->get($image_file['destination']);
592 $this->assertIdentical($image_file['scaled_width'], $image->getWidth());
593 $this->assertIdentical($image_file['scaled_height'], $image->getHeight());
594 $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
596 // Rotating should preserve frames.
598 $this->assertTrue($image->save($image_file['destination']));
599 $image = $this->imageFactory->get($image_file['destination']);
600 $this->assertIdentical($image_file['rotated_width'], $image->getWidth());
601 $this->assertIdentical($image_file['rotated_height'], $image->getHeight());
602 $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
604 // Converting to PNG should drop frames.
605 $image->convert('png');
606 $this->assertTrue($image->save($image_file['destination']));
607 $image = $this->imageFactory->get($image_file['destination']);
608 $this->assertIdentical(1, $image->getToolkit()->getFrames());
609 $this->assertIdentical($image_file['rotated_width'], $image->getWidth());
610 $this->assertIdentical($image_file['rotated_height'], $image->getHeight());
611 $this->assertIdentical(1, $image->getToolkit()->getFrames());
616 * Legacy methods tests.
618 * @param string $binaries
619 * The graphics package binaries to use for testing.
621 * @dataProvider providerManipulationTest
623 * @todo remove in 8.x-3.0.
627 public function testManipulationsLegacy($binaries) {
628 $this->setUpToolkit($binaries);
631 $toolkit = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick');
632 $this->assertSame($binaries, $toolkit->getPackage());
633 $this->assertNotNull($toolkit->getPackageLabel());
634 $this->assertSame([], $toolkit->checkPath('')['errors']);
636 // Typically the corner colors will be unchanged. These colors are in the
637 // order of top-left, top-right, bottom-right, bottom-left.
645 // A list of files that will be tested.
649 'image-test-no-transparency.gif',
653 // Setup a list of tests to perform on each type.
656 'function' => 'resize',
657 'arguments' => ['width' => 20, 'height' => 10],
660 'corners' => $default_corners,
664 'function' => 'scale',
665 'arguments' => ['width' => 20],
668 'corners' => $default_corners,
672 'function' => 'scale',
673 'arguments' => ['height' => 10],
676 'corners' => $default_corners,
680 'function' => 'scale',
681 'arguments' => ['width' => 80, 'upscale' => TRUE],
684 'corners' => $default_corners,
688 'function' => 'scale',
689 'arguments' => ['height' => 40, 'upscale' => TRUE],
692 'corners' => $default_corners,
696 'function' => 'crop',
697 'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12],
700 'corners' => array_fill(0, 4, $this->white),
703 'scale_and_crop' => [
704 'function' => 'scale_and_crop',
705 'arguments' => ['width' => 10, 'height' => 8],
708 'corners' => array_fill(0, 4, $this->black),
712 'function' => 'convert',
715 'arguments' => ['extension' => 'jpeg'],
716 'mimetype' => 'image/jpeg',
717 'corners' => $default_corners,
721 'function' => 'convert',
724 'arguments' => ['extension' => 'gif'],
725 'mimetype' => 'image/gif',
726 'corners' => $default_corners,
730 'function' => 'convert',
733 'arguments' => ['extension' => 'png'],
734 'mimetype' => 'image/png',
735 'corners' => $default_corners,
739 'function' => 'rotate',
742 'background' => '#FF00FF',
743 'resize_filter' => 'Box',
747 'corners' => array_fill(0, 4, $this->fuchsia),
750 'rotate_minus_10' => [
751 'function' => 'rotate',
754 'background' => '#FF00FF',
755 'resize_filter' => 'Box',
759 'corners' => array_fill(0, 4, $this->fuchsia),
763 'function' => 'rotate',
764 'arguments' => ['degrees' => 90, 'background' => '#FF00FF'],
767 'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
770 'rotate_transparent_5' => [
771 'function' => 'rotate',
772 'arguments' => ['degrees' => 5, 'resize_filter' => 'Box'],
775 'corners' => array_fill(0, 4, $this->transparent),
778 'rotate_transparent_90' => [
779 'function' => 'rotate',
780 'arguments' => ['degrees' => 90],
783 'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
787 'function' => 'desaturate',
791 // Grayscale corners are a bit funky. Each of the corners are a shade of
792 // gray. The values of these were determined simply by looking at the
793 // final image to see what desaturated colors end up being.
795 array_fill(0, 3, 76) + [3 => 0],
796 array_fill(0, 3, 149) + [3 => 0],
797 array_fill(0, 3, 29) + [3 => 0],
798 array_fill(0, 3, 225) + [3 => 127],
800 // @todo tolerance here is too high. Check reasons.
801 'tolerance' => 17000,
805 // Prepare a copy of test files.
806 $this->getTestFiles('image');
808 foreach ($files as $file) {
809 $image_uri = 'public://' . $file;
810 foreach ($operations as $op => $values) {
811 // Load up a fresh image.
812 $image = $this->imageFactory->get($image_uri);
813 if (!$image->isValid()) {
814 $this->fail("Could not load image $file.");
818 // Check that no multi-frame information is set.
819 $this->assertIdentical(1, $image->getToolkit()->getFrames());
821 // Legacy source tests.
822 $this->assertSame($image_uri, $image->getToolkit()->getSource());
823 $this->assertSame($image->getToolkit()->arguments()->getSourceLocalPath(), $image->getToolkit()->getSourceLocalPath());
824 $this->assertSame($image->getToolkit()->arguments()->getSourceFormat(), $image->getToolkit()->getSourceFormat());
826 // Perform our operation.
827 $image->apply($values['function'], $values['arguments']);
830 $file_path = $this->testDirectory . '/' . $op . substr($file, -4);
831 $this->assertTrue($image->save($file_path));
833 // Legacy destination tests.
834 $this->assertSame($file_path, $image->getToolkit()->getDestination());
835 $this->assertSame('', $image->getToolkit()->getDestinationLocalPath());
836 $this->assertNotNull($image->getToolkit()->arguments()->getSourceFormat(), $image->getToolkit()->getDestinationFormat());
839 $image = $this->imageFactory->get($file_path);
840 $this->assertTrue($image->isValid());
842 // Legacy set methods.
843 $image->getToolkit()->setSourceLocalPath('bar');
844 $image->getToolkit()->setSourceFormat('PNG');
845 $image->getToolkit()->setDestination('foo');
846 $image->getToolkit()->setDestinationLocalPath('baz');
847 $image->getToolkit()->setDestinationFormat('GIF');
848 $this->assertSame('bar', $image->getToolkit()->arguments()->getSourceLocalPath());
849 $this->assertSame('PNG', $image->getToolkit()->arguments()->getSourceFormat());
850 $this->assertSame('foo', $image->getToolkit()->arguments()->getDestination());
851 $this->assertSame('baz', $image->getToolkit()->arguments()->getDestinationLocalPath());
852 $this->assertSame('GIF', $image->getToolkit()->arguments()->getDestinationFormat());
853 $image->getToolkit()->setSourceFormatFromExtension('jpg');
854 $image->getToolkit()->setDestinationFormatFromExtension('jpg');
855 $this->assertSame('JPEG', $image->getToolkit()->arguments()->getSourceFormat());
856 $this->assertSame('JPEG', $image->getToolkit()->arguments()->getDestinationFormat());
860 // Test retrieval of EXIF information.
861 file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg', 'public://', FILE_EXISTS_REPLACE);
862 // The image files that will be tested.
865 'path' => drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg',
869 'path' => 'public://test-exif.jpeg',
873 'path' => 'dummy-remote://test-exif.jpeg',
877 'path' => 'public://image-test.jpg',
878 'orientation' => NULL,
881 'path' => 'public://image-test.png',
882 'orientation' => NULL,
885 'path' => 'public://image-test.gif',
886 'orientation' => NULL,
890 'orientation' => NULL,
894 foreach ($image_files as $image_file) {
895 // Get image using 'getimagesize'.
896 \Drupal::configFactory()->getEditable('imagemagick.settings')
897 ->set('use_identify', FALSE)
899 $image = $this->imageFactory->get($image_file['path']);
900 $this->assertIdentical($image_file['orientation'], $image->getToolkit()->getExifOrientation());
905 * Test ImageMagick subform and settings.
907 public function testFormAndSettings() {
908 // Change the toolkit.
909 \Drupal::configFactory()->getEditable('system.image')
910 ->set('toolkit', 'imagemagick')
913 // Test form is accepting wrong binaries path while setting toolkit to GD.
914 $this->drupalGet('admin/config/media/image-toolkit');
915 $this->assertFieldByName('image_toolkit', 'imagemagick');
917 'image_toolkit' => 'gd',
918 'imagemagick[suite][path_to_binaries]' => '/foo/bar/',
920 $this->drupalPostForm(NULL, $edit, 'Save configuration');
921 $this->assertFieldByName('image_toolkit', 'gd');
923 // Change the toolkit.
924 \Drupal::configFactory()->getEditable('system.image')
925 ->set('toolkit', 'imagemagick')
927 $this->imageFactory->setToolkitId('imagemagick');
928 $this->assertEqual('imagemagick', $this->imageFactory->getToolkitId());
930 // Test default supported image extensions.
931 $this->assertEqual('gif jpe jpeg jpg png', implode(' ', $this->imageFactory->getSupportedExtensions()));
933 $config = \Drupal::configFactory()->getEditable('imagemagick.settings');
936 $image_formats = $config->get('image_formats');
937 $image_formats['TIFF']['enabled'] = TRUE;
938 $config->set('image_formats', $image_formats)->save();
939 $this->assertEqual('gif jpe jpeg jpg png tif tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
942 $image_formats['PNG']['enabled'] = FALSE;
943 $config->set('image_formats', $image_formats)->save();
944 $this->assertEqual('gif jpe jpeg jpg tif tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
946 // Disable some extensions.
947 $image_formats['TIFF']['exclude_extensions'] = 'tif, gif';
948 $config->set('image_formats', $image_formats)->save();
949 $this->assertEqual('gif jpe jpeg jpg tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
950 $image_formats['JPEG']['exclude_extensions'] = 'jpe, jpg';
951 $config->set('image_formats', $image_formats)->save();
952 $this->assertEqual('gif jpeg tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
956 * Function for finding a pixel's RGBa values.
958 protected function getPixelColor(ImageInterface $image, $x, $y) {
959 $toolkit = $image->getToolkit();
960 $color_index = imagecolorat($toolkit->getResource(), $x, $y);
962 $transparent_index = imagecolortransparent($toolkit->getResource());
963 if ($color_index == $transparent_index) {
964 return [0, 0, 0, 127];
967 return array_values(imagecolorsforindex($toolkit->getResource(), $color_index));
971 * Function to compare two colors by RGBa, within a tolerance.
973 * Very basic, just compares the sum of the squared differences for each of
974 * the R, G, B, A components of two colors against a 'tolerance' value.
976 * @param int[] $actual
977 * The actual RGBA array.
978 * @param int[] $expected
979 * The expected RGBA array.
980 * @param int $tolerance
981 * The acceptable difference between the colors.
982 * @param string $file
983 * The image file being tested.
985 * The image operation being tested.
988 * TRUE if the colors differences are within tolerance, FALSE otherwise.
990 protected function colorsAreClose(array $actual, array $expected, $tolerance, $file, $op) {
991 // Fully transparent colors are equal, regardless of RGB.
992 if ($actual[3] == 127 && $expected[3] == 127) {
995 $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);
996 $this->assertLessThanOrEqual($tolerance, $distance, "Actual: {" . implode(',', $actual) . "}, Expected: {" . implode(',', $expected) . "}, Distance: " . $distance . ", Tolerance: " . $tolerance . ", File: " . $file . ", Operation: " . $op);
1001 * Test legacy arguments handling.
1003 * @todo remove in 8.x-3.0.
1007 public function testArgumentsLegacy() {
1008 $this->setUpToolkit('imagemagick');
1010 // Prepare a copy of test files.
1011 $this->getTestFiles('image');
1013 $image_uri = "public://image-test.png";
1014 $image = $this->imageFactory->get($image_uri);
1015 if (!$image->isValid()) {
1016 $this->fail("Could not load image $image_uri.");
1019 // Setup a list of arguments.
1020 $image->getToolkit()->addArgument("-resize 100x75!");
1021 // Internal argument.
1022 $image->getToolkit()->addArgument(">!>INTERNAL");
1023 $image->getToolkit()->addArgument("-quality 75");
1024 $image->getToolkit()->prependArgument("-hoxi 76");
1026 // Use methods introduced in 8.x-2.3.
1027 $image->getToolkit()->arguments()
1028 // Pre source argument.
1029 ->add("-density 25", ImagemagickExecArguments::PRE_SOURCE)
1030 // Another internal argument.
1031 ->add("GATEAU", ImagemagickExecArguments::INTERNAL)
1032 // Another pre source argument.
1033 ->add("-auchocolat 90", ImagemagickExecArguments::PRE_SOURCE)
1034 // Add two arguments with additional info.
1037 ImagemagickExecArguments::POST_SOURCE,
1038 ImagemagickExecArguments::APPEND,
1046 ImagemagickExecArguments::POST_SOURCE,
1047 ImagemagickExecArguments::APPEND,
1054 // Test find arguments skipping identifiers.
1057 1 => '-resize 100x75!',
1063 ], $image->getToolkit()->getArguments());
1064 $this->assertSame([2], array_keys($image->getToolkit()->arguments()->find('/^INTERNAL/')));
1065 $this->assertSame([5], array_keys($image->getToolkit()->arguments()->find('/^GATEAU/')));
1066 $this->assertSame([6], array_keys($image->getToolkit()->arguments()->find('/^\-auchocolat/')));
1067 $this->assertSame([7, 8], array_keys($image->getToolkit()->arguments()->find('/^\-addz/')));
1068 $this->assertSame([7, 8], array_keys($image->getToolkit()->arguments()->find('/.*/', NULL, ['foo' => 'bar'])));
1069 $this->assertSame([], $image->getToolkit()->arguments()->find('/.*/', NULL, ['arw' => 'moo']));
1070 $this->assertSame(2, $image->getToolkit()->findArgument('>!>INTERNAL'));
1071 $this->assertSame(5, $image->getToolkit()->findArgument('>!>GATEAU'));
1072 $this->assertFalse($image->getToolkit()->findArgument('-auchocolat'));
1074 // Check resulting command line strings.
1075 $this->assertSame('-density 25 -auchocolat 90', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
1076 $this->assertSame("-hoxi 76 -resize 100x75! -quality 75 -addz 150 -addz 200", $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1077 $this->assertSame("-hoxi 76 -resize 100x75! -quality 75 -addz 150 -addz 200", $image->getToolkit()->getStringForBinary());
1081 * Test arguments handling.
1083 public function testArguments() {
1084 $this->setUpToolkit('imagemagick');
1086 // Prepare a copy of test files.
1087 $this->getTestFiles('image');
1089 $image_uri = "public://image-test.png";
1090 $image = $this->imageFactory->get($image_uri);
1091 if (!$image->isValid()) {
1092 $this->fail("Could not load image $image_uri.");
1095 // Setup a list of arguments.
1096 $image->getToolkit()->arguments()
1097 ->add("-resize 100x75!")
1098 // Internal argument.
1099 ->add("INTERNAL", ImagemagickExecArguments::INTERNAL)
1100 ->add("-quality 75")
1101 // Prepend argument.
1102 ->add("-hoxi 76", ImagemagickExecArguments::POST_SOURCE, 0)
1103 // Pre source argument.
1104 ->add("-density 25", ImagemagickExecArguments::PRE_SOURCE)
1105 // Another internal argument.
1106 ->add("GATEAU", ImagemagickExecArguments::INTERNAL)
1107 // Another pre source argument.
1108 ->add("-auchocolat 90", ImagemagickExecArguments::PRE_SOURCE)
1109 // Add two arguments with additional info.
1112 ImagemagickExecArguments::POST_SOURCE,
1113 ImagemagickExecArguments::APPEND,
1121 ImagemagickExecArguments::POST_SOURCE,
1122 ImagemagickExecArguments::APPEND,
1129 // Test find arguments skipping identifiers.
1130 $this->assertSame([2], array_keys($image->getToolkit()->arguments()->find('/^INTERNAL/')));
1131 $this->assertSame([5], array_keys($image->getToolkit()->arguments()->find('/^GATEAU/')));
1132 $this->assertSame([6], array_keys($image->getToolkit()->arguments()->find('/^\-auchocolat/')));
1133 $this->assertSame([7, 8], array_keys($image->getToolkit()->arguments()->find('/^\-addz/')));
1134 $this->assertSame([7, 8], array_keys($image->getToolkit()->arguments()->find('/.*/', NULL, ['foo' => 'bar'])));
1135 $this->assertSame([], $image->getToolkit()->arguments()->find('/.*/', NULL, ['arw' => 'moo']));
1137 // Check resulting command line strings.
1138 $this->assertSame('-density 25 -auchocolat 90', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
1139 $this->assertSame("-hoxi 76 -resize 100x75! -quality 75 -addz 150 -addz 200", $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1141 // Add arguments with a specific index.
1142 $image->getToolkit()->arguments()
1143 ->add("-ix aa", ImagemagickExecArguments::POST_SOURCE, 4)
1144 ->add("-ix bb", ImagemagickExecArguments::POST_SOURCE, 4);
1145 $this->assertSame([4, 5], array_keys($image->getToolkit()->arguments()->find('/^\-ix/')));
1146 $this->assertSame("-hoxi 76 -resize 100x75! -quality 75 -ix bb -ix aa -addz 150 -addz 200", $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1148 // Create a new image and inspect the arguments.
1149 $image->createNew(100, 200);
1150 $this->assertSame([0], array_keys($image->getToolkit()->arguments()->find('/^./', NULL, ['image_toolkit_operation' => 'create_new'])));
1151 $this->assertSame([0], array_keys($image->getToolkit()->arguments()->find('/^./', NULL, ['image_toolkit_operation_plugin_id' => 'imagemagick_create_new'])));
1152 $this->assertSame("-size 100x200 xc:transparent", $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1156 * Test module arguments alter hook.
1158 public function testArgumentsAlterHook() {
1159 $this->setUpToolkit('imagemagick');
1161 $fmdm = $this->container->get('file_metadata_manager');
1163 // Change the Advanced Colorspace setting, must be included in the command
1165 \Drupal::configFactory()->getEditable('imagemagick.settings')
1166 ->set('advanced.colorspace', 'GRAY')
1169 // Prepare a copy of test files.
1170 $this->getTestFiles('image');
1171 $image_uri = "public://image-test.png";
1172 $image = $this->imageFactory->get($image_uri);
1173 if (!$image->isValid()) {
1174 $this->fail("Could not load image $image_uri.");
1177 // Check the source colorspace.
1178 $this->assertSame('SRGB', $image->getToolkit()->getColorspace());
1180 // Setup a list of arguments.
1181 $image->getToolkit()->arguments()
1182 ->add("-resize 100x75!")
1183 ->add("-quality 75");
1185 // Save the derived image.
1186 $image->save($image_uri . '.derived');
1188 // Check expected command line.
1189 if (substr(PHP_OS, 0, 3) === 'WIN') {
1190 $expected = "-resize 100x75! -quality 75 -colorspace \"GRAY\"";
1193 $expected = "-resize 100x75! -quality 75 -colorspace 'GRAY'";
1195 $this->assertSame($expected, $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1197 // Check that the colorspace has been actually changed in the file.
1198 Cache::InvalidateTags([
1199 'config:imagemagick.file_metadata_plugin.imagemagick_identify',
1201 $fmdm->release($image_uri . '.derived');
1202 $image_md = $fmdm->uri($image_uri . '.derived');
1203 $image = $this->imageFactory->get($image_uri . '.derived');
1204 $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $image_md->isMetadataLoaded('imagemagick_identify'));
1205 $this->assertSame('GRAY', $image->getToolkit()->getColorspace());
1207 // Change the Prepend settings, must be included in the command line.
1208 // Once before the source image.
1209 \Drupal::configFactory()->getEditable('imagemagick.settings')
1210 ->set('prepend', '-debug All')
1211 ->set('prepend_pre_source', TRUE)
1213 $image = $this->imageFactory->get($image_uri);
1214 $image->getToolkit()->arguments()
1215 ->add("-resize 100x75!")
1216 ->add("-quality 75");
1217 $image->save($image_uri . '.derived');
1218 if (substr(PHP_OS, 0, 3) === 'WIN') {
1219 $expected = "-resize 100x75! -quality 75 -colorspace \"GRAY\"";
1222 $expected = "-resize 100x75! -quality 75 -colorspace 'GRAY'";
1224 $this->assertSame('-debug All', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
1225 $this->assertSame($expected, $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1226 // Then after the source image.
1227 \Drupal::configFactory()->getEditable('imagemagick.settings')
1228 ->set('prepend_pre_source', FALSE)
1230 $image = $this->imageFactory->get($image_uri);
1231 $image->getToolkit()->arguments()
1232 ->add("-resize 100x75!")
1233 ->add("-quality 75");
1234 $image->save($image_uri . '.derived');
1235 if (substr(PHP_OS, 0, 3) === 'WIN') {
1236 $expected = "-debug All -resize 100x75! -quality 75 -colorspace \"GRAY\"";
1239 $expected = "-debug All -resize 100x75! -quality 75 -colorspace 'GRAY'";
1241 $this->assertSame('', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
1242 $this->assertSame($expected, $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1246 * Test missing command on ExecManager.
1248 public function testExecManagerCommandNotFound() {
1249 $exec_manager = \Drupal::service('imagemagick.exec_manager');
1252 $expected = substr(PHP_OS, 0, 3) !== 'WIN' ? 127 : 1;
1253 $ret = $exec_manager->runOsShell('pinkpanther', '-inspector Clouseau', 'blake', $output, $error);
1254 $this->assertEquals($expected, $ret, $error);
1258 * Test timeout on ExecManager.
1260 public function testExecManagerTimeout() {
1261 $exec_manager = \Drupal::service('imagemagick.exec_manager');
1264 $expected = substr(PHP_OS, 0, 3) !== 'WIN' ? 143 : 1;
1265 // Set a short timeout (1 sec.) and run a process that is expected to last
1266 // longer (10 secs.). Should return a 'terminate' exit code.
1267 $exec_manager->setTimeout(1);
1268 $ret = $exec_manager->runOsShell('sleep', '10', 'sleep', $output, $error);
1269 $this->assertEquals($expected, $ret, $error);