df6af8dab9e8c5fc299a49702ff801aa9ece2a31
[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\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;
11
12 /**
13  * Tests that core image manipulations work properly through Imagemagick.
14  *
15  * @group Imagemagick
16  */
17 class ToolkitImagemagickTest extends BrowserTestBase {
18
19   use TestFileCreationTrait;
20
21   /**
22    * The image factory service.
23    *
24    * @var \Drupal\Core\Image\ImageFactory
25    */
26   protected $imageFactory;
27
28   /**
29    * A directory for image test file results.
30    *
31    * @var string
32    */
33   protected $testDirectory;
34
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];
48
49   protected $width = 40;
50   protected $height = 20;
51   // @codingStandardsIgnoreEnd
52
53   /**
54    * Modules to enable.
55    *
56    * @var array
57    */
58   protected static $modules = [
59     'system',
60     'simpletest',
61     'file_test',
62     'imagemagick',
63     'file_mdm',
64     'file_mdm_exif',
65   ];
66
67   /**
68    * {@inheritdoc}
69    */
70   public function setUp() {
71     parent::setUp();
72
73     // Create an admin user.
74     $admin_user = $this->drupalCreateUser([
75       'administer site configuration',
76     ]);
77     $this->drupalLogin($admin_user);
78
79     // Set the image factory.
80     $this->imageFactory = $this->container->get('image.factory');
81
82     // Prepare a directory for test file results.
83     $this->testDirectory = 'public://imagetest';
84   }
85
86   /**
87    * Helper to setup the image toolkit.
88    *
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.
93    */
94   protected function setUpToolkit($binaries, $check_path = TRUE) {
95     // Change the toolkit.
96     \Drupal::configFactory()->getEditable('system.image')
97       ->set('toolkit', 'imagemagick')
98       ->save();
99
100     // Execute tests with selected binaries.
101     \Drupal::configFactory()->getEditable('imagemagick.settings')
102       ->set('debug', TRUE)
103       ->set('binaries', $binaries)
104       ->set('quality', 100)
105       ->save();
106
107     if ($check_path) {
108       // The test can only be executed if binaries are available on the shell
109       // path.
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
114         // are installed.
115         $this->markTestSkipped("Tests for '{$binaries}' cannot run because the binaries are not available on the shell path.");
116       }
117     }
118
119     // Set the toolkit on the image factory.
120     $this->imageFactory->setToolkitId('imagemagick');
121
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.');
124
125     // Prepare directory.
126     file_unmanaged_delete_recursive($this->testDirectory);
127     file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
128   }
129
130   /**
131    * Provides data for testManipulations.
132    *
133    * @return array[]
134    *   A simple array of simple arrays, each having the following elements:
135    *   - binaries to use for testing.
136    */
137   public function providerManipulationTest() {
138     return [
139       ['imagemagick'],
140       ['graphicsmagick'],
141     ];
142   }
143
144   /**
145    * Test image toolkit operations.
146    *
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.
150    *
151    * @param string $binaries
152    *   The graphics package binaries to use for testing.
153    *
154    * @dataProvider providerManipulationTest
155    */
156   public function testManipulations($binaries) {
157     $this->setUpToolkit($binaries);
158
159     // Typically the corner colors will be unchanged. These colors are in the
160     // order of top-left, top-right, bottom-right, bottom-left.
161     $default_corners = [
162       $this->red,
163       $this->green,
164       $this->blue,
165       $this->transparent,
166     ];
167
168     // A list of files that will be tested.
169     $files = [
170       'image-test.png',
171       'image-test.gif',
172       'image-test-no-transparency.gif',
173       'image-test.jpg',
174     ];
175
176     // Setup a list of tests to perform on each type.
177     $operations = [
178       'resize' => [
179         'function' => 'resize',
180         'arguments' => ['width' => 20, 'height' => 10],
181         'width' => 20,
182         'height' => 10,
183         'corners' => $default_corners,
184         'tolerance' => 0,
185       ],
186       'scale_x' => [
187         'function' => 'scale',
188         'arguments' => ['width' => 20],
189         'width' => 20,
190         'height' => 10,
191         'corners' => $default_corners,
192         'tolerance' => 0,
193       ],
194       'scale_y' => [
195         'function' => 'scale',
196         'arguments' => ['height' => 10],
197         'width' => 20,
198         'height' => 10,
199         'corners' => $default_corners,
200         'tolerance' => 0,
201       ],
202       'upscale_x' => [
203         'function' => 'scale',
204         'arguments' => ['width' => 80, 'upscale' => TRUE],
205         'width' => 80,
206         'height' => 40,
207         'corners' => $default_corners,
208         'tolerance' => 0,
209       ],
210       'upscale_y' => [
211         'function' => 'scale',
212         'arguments' => ['height' => 40, 'upscale' => TRUE],
213         'width' => 80,
214         'height' => 40,
215         'corners' => $default_corners,
216         'tolerance' => 0,
217       ],
218       'crop' => [
219         'function' => 'crop',
220         'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12],
221         'width' => 16,
222         'height' => 12,
223         'corners' => array_fill(0, 4, $this->white),
224         'tolerance' => 0,
225       ],
226       'scale_and_crop' => [
227         'function' => 'scale_and_crop',
228         'arguments' => ['width' => 10, 'height' => 8],
229         'width' => 10,
230         'height' => 8,
231         'corners' => array_fill(0, 4, $this->black),
232         'tolerance' => 100,
233       ],
234       'convert_jpg' => [
235         'function' => 'convert',
236         'width' => 40,
237         'height' => 20,
238         'arguments' => ['extension' => 'jpeg'],
239         'mimetype' => 'image/jpeg',
240         'corners' => $default_corners,
241         'tolerance' => 0,
242       ],
243       'convert_gif' => [
244         'function' => 'convert',
245         'width' => 40,
246         'height' => 20,
247         'arguments' => ['extension' => 'gif'],
248         'mimetype' => 'image/gif',
249         'corners' => $default_corners,
250         'tolerance' => 15,
251       ],
252       'convert_png' => [
253         'function' => 'convert',
254         'width' => 40,
255         'height' => 20,
256         'arguments' => ['extension' => 'png'],
257         'mimetype' => 'image/png',
258         'corners' => $default_corners,
259         'tolerance' => 5,
260       ],
261       'rotate_5' => [
262         'function' => 'rotate',
263         'arguments' => [
264           'degrees' => 5,
265           'background' => '#FF00FF',
266           'resize_filter' => 'Box',
267         ],
268         'width' => 41,
269         'height' => 23,
270         'corners' => array_fill(0, 4, $this->fuchsia),
271         'tolerance' => 5,
272       ],
273       'rotate_minus_10' => [
274         'function' => 'rotate',
275         'arguments' => [
276           'degrees' => -10,
277           'background' => '#FF00FF',
278           'resize_filter' => 'Box',
279         ],
280         'width' => 41,
281         'height' => 26,
282         'corners' => array_fill(0, 4, $this->fuchsia),
283         'tolerance' => 15,
284       ],
285       'rotate_90' => [
286         'function' => 'rotate',
287         'arguments' => ['degrees' => 90, 'background' => '#FF00FF'],
288         'width' => 20,
289         'height' => 40,
290         'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
291         'tolerance' => 0,
292       ],
293       'rotate_transparent_5' => [
294         'function' => 'rotate',
295         'arguments' => ['degrees' => 5, 'resize_filter' => 'Box'],
296         'width' => 41,
297         'height' => 23,
298         'corners' => array_fill(0, 4, $this->transparent),
299         'tolerance' => 0,
300       ],
301       'rotate_transparent_90' => [
302         'function' => 'rotate',
303         'arguments' => ['degrees' => 90],
304         'width' => 20,
305         'height' => 40,
306         'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
307         'tolerance' => 0,
308       ],
309       'desaturate' => [
310         'function' => 'desaturate',
311         'arguments' => [],
312         'height' => 20,
313         'width' => 40,
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.
317         'corners' => [
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],
322         ],
323         // @todo tolerance here is too high. Check reasons.
324         'tolerance' => 17000,
325       ],
326     ];
327
328     // Prepare a copy of test files.
329     $this->getTestFiles('image');
330
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.");
338           continue 2;
339         }
340
341         // Check that no multi-frame information is set.
342         $this->assertIdentical(1, $image->getToolkit()->getFrames());
343
344         // Perform our operation.
345         $image->apply($values['function'], $values['arguments']);
346
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());
352
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',
361           ])) {
362             continue;
363           }
364         }
365
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());
371
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']}).");
375         }
376
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;
381
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;
387         }
388
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;
394         }
395
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}.");
398
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;
407             }
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;
412             }
413             // Get the location of the corner.
414             switch ($key) {
415               case 0:
416                 $x = 0;
417                 $y = 0;
418                 break;
419
420               case 1:
421                 $x = $image->getWidth() - 1;
422                 $y = 0;
423                 break;
424
425               case 2:
426                 $x = $image->getWidth() - 1;
427                 $y = $image->getHeight() - 1;
428                 break;
429
430               case 3:
431                 $x = 0;
432                 $y = $image->getHeight() - 1;
433                 break;
434
435             }
436             $color = $this->getPixelColor($image, $x, $y);
437             $this->colorsAreClose($color, $corner, $values['tolerance'], $file, $op);
438           }
439         }
440       }
441     }
442
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.");
453
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'.");
458         continue;
459       }
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.");
465       }
466       else {
467         $this->assertEqual(NULL, $image_reloaded->getToolkit()->getTransparentColor(), "Image file '$file' has no color channel set.");
468       }
469     }
470
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.');
481
482     // Test saving image files with filenames having non-ascii characters.
483     $file_names = [
484       'greek εικόνα δοκιμής.png',
485       'russian Тестовое изображение.png',
486       'simplified chinese 测试图片.png',
487       'japanese 試験画像.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',
496     ];
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)) {
500         continue;
501       }
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.");
508     }
509
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
517     // have been reset.
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());
521
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.
525     $image_files = [
526       [
527         'path' => drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg',
528         'orientation' => 8,
529       ],
530       [
531         'path' => 'public://test-exif.jpeg',
532         'orientation' => 8,
533       ],
534       [
535         'path' => 'dummy-remote://test-exif.jpeg',
536         'orientation' => 8,
537       ],
538       [
539         'path' => 'public://image-test.jpg',
540         'orientation' => NULL,
541       ],
542       [
543         'path' => 'public://image-test.png',
544         'orientation' => NULL,
545       ],
546       [
547         'path' => 'public://image-test.gif',
548         'orientation' => NULL,
549       ],
550       [
551         'path' => NULL,
552         'orientation' => NULL,
553       ],
554     ];
555     foreach ($image_files as $image_file) {
556       // Get image using 'identify'.
557       \Drupal::configFactory()->getEditable('imagemagick.settings')
558         ->set('use_identify', TRUE)
559         ->save();
560       $image = $this->imageFactory->get($image_file['path']);
561       $this->assertIdentical($image_file['orientation'], $image->getToolkit()->getExifOrientation());
562     }
563
564     // Test multi-frame GIF image.
565     $image_files = [
566       [
567         'source' => drupal_get_path('module', 'imagemagick') . '/misc/test-multi-frame.gif',
568         'destination' => $this->testDirectory . '/test-multi-frame.gif',
569         'width' => 60,
570         'height' => 29,
571         'frames' => 13,
572         'scaled_width' => 30,
573         'scaled_height' => 15,
574         'rotated_width' => 33,
575         'rotated_height' => 26,
576       ],
577     ];
578     // Get images using 'identify'.
579     \Drupal::configFactory()->getEditable('imagemagick.settings')
580       ->set('use_identify', TRUE)
581       ->save();
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());
587
588       // Scaling should preserve frames.
589       $image->scale(30);
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());
595
596       // Rotating should preserve frames.
597       $image->rotate(24);
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());
603
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());
612     }
613   }
614
615   /**
616    * Legacy methods tests.
617    *
618    * @param string $binaries
619    *   The graphics package binaries to use for testing.
620    *
621    * @dataProvider providerManipulationTest
622    *
623    * @todo remove in 8.x-3.0.
624    *
625    * @group legacy
626    */
627   public function testManipulationsLegacy($binaries) {
628     $this->setUpToolkit($binaries);
629
630     // Check package.
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']);
635
636     // Typically the corner colors will be unchanged. These colors are in the
637     // order of top-left, top-right, bottom-right, bottom-left.
638     $default_corners = [
639       $this->red,
640       $this->green,
641       $this->blue,
642       $this->transparent,
643     ];
644
645     // A list of files that will be tested.
646     $files = [
647       'image-test.png',
648       'image-test.gif',
649       'image-test-no-transparency.gif',
650       'image-test.jpg',
651     ];
652
653     // Setup a list of tests to perform on each type.
654     $operations = [
655       'resize' => [
656         'function' => 'resize',
657         'arguments' => ['width' => 20, 'height' => 10],
658         'width' => 20,
659         'height' => 10,
660         'corners' => $default_corners,
661         'tolerance' => 0,
662       ],
663       'scale_x' => [
664         'function' => 'scale',
665         'arguments' => ['width' => 20],
666         'width' => 20,
667         'height' => 10,
668         'corners' => $default_corners,
669         'tolerance' => 0,
670       ],
671       'scale_y' => [
672         'function' => 'scale',
673         'arguments' => ['height' => 10],
674         'width' => 20,
675         'height' => 10,
676         'corners' => $default_corners,
677         'tolerance' => 0,
678       ],
679       'upscale_x' => [
680         'function' => 'scale',
681         'arguments' => ['width' => 80, 'upscale' => TRUE],
682         'width' => 80,
683         'height' => 40,
684         'corners' => $default_corners,
685         'tolerance' => 0,
686       ],
687       'upscale_y' => [
688         'function' => 'scale',
689         'arguments' => ['height' => 40, 'upscale' => TRUE],
690         'width' => 80,
691         'height' => 40,
692         'corners' => $default_corners,
693         'tolerance' => 0,
694       ],
695       'crop' => [
696         'function' => 'crop',
697         'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12],
698         'width' => 16,
699         'height' => 12,
700         'corners' => array_fill(0, 4, $this->white),
701         'tolerance' => 0,
702       ],
703       'scale_and_crop' => [
704         'function' => 'scale_and_crop',
705         'arguments' => ['width' => 10, 'height' => 8],
706         'width' => 10,
707         'height' => 8,
708         'corners' => array_fill(0, 4, $this->black),
709         'tolerance' => 100,
710       ],
711       'convert_jpg' => [
712         'function' => 'convert',
713         'width' => 40,
714         'height' => 20,
715         'arguments' => ['extension' => 'jpeg'],
716         'mimetype' => 'image/jpeg',
717         'corners' => $default_corners,
718         'tolerance' => 0,
719       ],
720       'convert_gif' => [
721         'function' => 'convert',
722         'width' => 40,
723         'height' => 20,
724         'arguments' => ['extension' => 'gif'],
725         'mimetype' => 'image/gif',
726         'corners' => $default_corners,
727         'tolerance' => 15,
728       ],
729       'convert_png' => [
730         'function' => 'convert',
731         'width' => 40,
732         'height' => 20,
733         'arguments' => ['extension' => 'png'],
734         'mimetype' => 'image/png',
735         'corners' => $default_corners,
736         'tolerance' => 5,
737       ],
738       'rotate_5' => [
739         'function' => 'rotate',
740         'arguments' => [
741           'degrees' => 5,
742           'background' => '#FF00FF',
743           'resize_filter' => 'Box',
744         ],
745         'width' => 41,
746         'height' => 23,
747         'corners' => array_fill(0, 4, $this->fuchsia),
748         'tolerance' => 5,
749       ],
750       'rotate_minus_10' => [
751         'function' => 'rotate',
752         'arguments' => [
753           'degrees' => -10,
754           'background' => '#FF00FF',
755           'resize_filter' => 'Box',
756         ],
757         'width' => 41,
758         'height' => 26,
759         'corners' => array_fill(0, 4, $this->fuchsia),
760         'tolerance' => 15,
761       ],
762       'rotate_90' => [
763         'function' => 'rotate',
764         'arguments' => ['degrees' => 90, 'background' => '#FF00FF'],
765         'width' => 20,
766         'height' => 40,
767         'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
768         'tolerance' => 0,
769       ],
770       'rotate_transparent_5' => [
771         'function' => 'rotate',
772         'arguments' => ['degrees' => 5, 'resize_filter' => 'Box'],
773         'width' => 41,
774         'height' => 23,
775         'corners' => array_fill(0, 4, $this->transparent),
776         'tolerance' => 0,
777       ],
778       'rotate_transparent_90' => [
779         'function' => 'rotate',
780         'arguments' => ['degrees' => 90],
781         'width' => 20,
782         'height' => 40,
783         'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
784         'tolerance' => 0,
785       ],
786       'desaturate' => [
787         'function' => 'desaturate',
788         'arguments' => [],
789         'height' => 20,
790         'width' => 40,
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.
794         'corners' => [
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],
799         ],
800         // @todo tolerance here is too high. Check reasons.
801         'tolerance' => 17000,
802       ],
803     ];
804
805     // Prepare a copy of test files.
806     $this->getTestFiles('image');
807
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.");
815           continue 2;
816         }
817
818         // Check that no multi-frame information is set.
819         $this->assertIdentical(1, $image->getToolkit()->getFrames());
820
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());
825
826         // Perform our operation.
827         $image->apply($values['function'], $values['arguments']);
828
829         // Save image.
830         $file_path = $this->testDirectory . '/' . $op . substr($file, -4);
831         $this->assertTrue($image->save($file_path));
832
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());
837
838         // Reload image.
839         $image = $this->imageFactory->get($file_path);
840         $this->assertTrue($image->isValid());
841
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());
857       }
858     }
859
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.
863     $image_files = [
864       [
865         'path' => drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg',
866         'orientation' => 8,
867       ],
868       [
869         'path' => 'public://test-exif.jpeg',
870         'orientation' => 8,
871       ],
872       [
873         'path' => 'dummy-remote://test-exif.jpeg',
874         'orientation' => 8,
875       ],
876       [
877         'path' => 'public://image-test.jpg',
878         'orientation' => NULL,
879       ],
880       [
881         'path' => 'public://image-test.png',
882         'orientation' => NULL,
883       ],
884       [
885         'path' => 'public://image-test.gif',
886         'orientation' => NULL,
887       ],
888       [
889         'path' => NULL,
890         'orientation' => NULL,
891       ],
892     ];
893
894     foreach ($image_files as $image_file) {
895       // Get image using 'getimagesize'.
896       \Drupal::configFactory()->getEditable('imagemagick.settings')
897         ->set('use_identify', FALSE)
898         ->save();
899       $image = $this->imageFactory->get($image_file['path']);
900       $this->assertIdentical($image_file['orientation'], $image->getToolkit()->getExifOrientation());
901     }
902   }
903
904   /**
905    * Test ImageMagick subform and settings.
906    */
907   public function testFormAndSettings() {
908     // Change the toolkit.
909     \Drupal::configFactory()->getEditable('system.image')
910       ->set('toolkit', 'imagemagick')
911       ->save();
912
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');
916     $edit = [
917       'image_toolkit' => 'gd',
918       'imagemagick[suite][path_to_binaries]' => '/foo/bar/',
919     ];
920     $this->drupalPostForm(NULL, $edit, 'Save configuration');
921     $this->assertFieldByName('image_toolkit', 'gd');
922
923     // Change the toolkit.
924     \Drupal::configFactory()->getEditable('system.image')
925       ->set('toolkit', 'imagemagick')
926       ->save();
927     $this->imageFactory->setToolkitId('imagemagick');
928     $this->assertEqual('imagemagick', $this->imageFactory->getToolkitId());
929
930     // Test default supported image extensions.
931     $this->assertEqual('gif jpe jpeg jpg png', implode(' ', $this->imageFactory->getSupportedExtensions()));
932
933     $config = \Drupal::configFactory()->getEditable('imagemagick.settings');
934
935     // Enable TIFF.
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()));
940
941     // Disable PNG.
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()));
945
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()));
953   }
954
955   /**
956    * Function for finding a pixel's RGBa values.
957    */
958   protected function getPixelColor(ImageInterface $image, $x, $y) {
959     $toolkit = $image->getToolkit();
960     $color_index = imagecolorat($toolkit->getResource(), $x, $y);
961
962     $transparent_index = imagecolortransparent($toolkit->getResource());
963     if ($color_index == $transparent_index) {
964       return [0, 0, 0, 127];
965     }
966
967     return array_values(imagecolorsforindex($toolkit->getResource(), $color_index));
968   }
969
970   /**
971    * Function to compare two colors by RGBa, within a tolerance.
972    *
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.
975    *
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.
984    * @param string $op
985    *   The image operation being tested.
986    *
987    * @return bool
988    *   TRUE if the colors differences are within tolerance, FALSE otherwise.
989    */
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) {
993       return TRUE;
994     }
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);
997     return TRUE;
998   }
999
1000   /**
1001    * Test legacy arguments handling.
1002    *
1003    * @todo remove in 8.x-3.0.
1004    *
1005    * @group legacy
1006    */
1007   public function testArgumentsLegacy() {
1008     $this->setUpToolkit('imagemagick');
1009
1010     // Prepare a copy of test files.
1011     $this->getTestFiles('image');
1012
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.");
1017     }
1018
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");
1025
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.
1035       ->add(
1036         "-addz 150",
1037         ImagemagickExecArguments::POST_SOURCE,
1038         ImagemagickExecArguments::APPEND,
1039         [
1040           'foo' => 'bar',
1041           'qux' => 'der',
1042         ]
1043       )
1044       ->add(
1045         "-addz 200",
1046         ImagemagickExecArguments::POST_SOURCE,
1047         ImagemagickExecArguments::APPEND,
1048         [
1049           'wey' => 'lod',
1050           'foo' => 'bar',
1051         ]
1052       );
1053
1054     // Test find arguments skipping identifiers.
1055     $this->assertSame([
1056       0 => '-hoxi 76',
1057       1 => '-resize 100x75!',
1058       2 => '>!>INTERNAL',
1059       3 => '-quality 75',
1060       5 => '>!>GATEAU',
1061       7 => '-addz 150',
1062       8 => '-addz 200',
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'));
1073
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());
1078   }
1079
1080   /**
1081    * Test arguments handling.
1082    */
1083   public function testArguments() {
1084     $this->setUpToolkit('imagemagick');
1085
1086     // Prepare a copy of test files.
1087     $this->getTestFiles('image');
1088
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.");
1093     }
1094
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.
1110       ->add(
1111         "-addz 150",
1112         ImagemagickExecArguments::POST_SOURCE,
1113         ImagemagickExecArguments::APPEND,
1114         [
1115           'foo' => 'bar',
1116           'qux' => 'der',
1117         ]
1118       )
1119       ->add(
1120         "-addz 200",
1121         ImagemagickExecArguments::POST_SOURCE,
1122         ImagemagickExecArguments::APPEND,
1123         [
1124           'wey' => 'lod',
1125           'foo' => 'bar',
1126         ]
1127       );
1128
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']));
1136
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));
1140
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));
1147
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));
1153   }
1154
1155   /**
1156    * Test module arguments alter hook.
1157    */
1158   public function testArgumentsAlterHook() {
1159     $this->setUpToolkit('imagemagick');
1160
1161     $fmdm = $this->container->get('file_metadata_manager');
1162
1163     // Change the Advanced Colorspace setting, must be included in the command
1164     // line.
1165     \Drupal::configFactory()->getEditable('imagemagick.settings')
1166       ->set('advanced.colorspace', 'GRAY')
1167       ->save();
1168
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.");
1175     }
1176
1177     // Check the source colorspace.
1178     $this->assertSame('SRGB', $image->getToolkit()->getColorspace());
1179
1180     // Setup a list of arguments.
1181     $image->getToolkit()->arguments()
1182       ->add("-resize 100x75!")
1183       ->add("-quality 75");
1184
1185     // Save the derived image.
1186     $image->save($image_uri . '.derived');
1187
1188     // Check expected command line.
1189     if (substr(PHP_OS, 0, 3) === 'WIN') {
1190       $expected = "-resize 100x75! -quality 75 -colorspace \"GRAY\"";
1191     }
1192     else {
1193       $expected = "-resize 100x75! -quality 75 -colorspace 'GRAY'";
1194     }
1195     $this->assertSame($expected, $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1196
1197     // Check that the colorspace has been actually changed in the file.
1198     Cache::InvalidateTags([
1199       'config:imagemagick.file_metadata_plugin.imagemagick_identify',
1200     ]);
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());
1206
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)
1212       ->save();
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\"";
1220     }
1221     else {
1222       $expected = "-resize 100x75! -quality 75 -colorspace 'GRAY'";
1223     }
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)
1229       ->save();
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\"";
1237     }
1238     else {
1239       $expected = "-debug All -resize 100x75! -quality 75 -colorspace 'GRAY'";
1240     }
1241     $this->assertSame('', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
1242     $this->assertSame($expected, $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
1243   }
1244
1245   /**
1246    * Test missing command on ExecManager.
1247    */
1248   public function testExecManagerCommandNotFound() {
1249     $exec_manager = \Drupal::service('imagemagick.exec_manager');
1250     $output = '';
1251     $error = '';
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);
1255   }
1256
1257   /**
1258    * Test timeout on ExecManager.
1259    */
1260   public function testExecManagerTimeout() {
1261     $exec_manager = \Drupal::service('imagemagick.exec_manager');
1262     $output = '';
1263     $error = '';
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);
1270   }
1271
1272 }