Version 1
[yaffs-website] / web / modules / contrib / imagemagick / tests / src / Functional / ToolkitImagemagickTest.php
diff --git a/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickTest.php b/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickTest.php
new file mode 100644 (file)
index 0000000..2322581
--- /dev/null
@@ -0,0 +1,677 @@
+<?php
+
+namespace Drupal\Tests\imagemagick\Functional;
+
+use Drupal\Core\Image\ImageInterface;
+use Drupal\Tests\TestFileCreationTrait;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests that core image manipulations work properly through Imagemagick.
+ *
+ * @group Imagemagick
+ */
+class ToolkitImagemagickTest extends BrowserTestBase {
+
+  use TestFileCreationTrait;
+
+  /**
+   * The image factory service.
+   *
+   * @var \Drupal\Core\Image\ImageFactory
+   */
+  protected $imageFactory;
+
+  /**
+   * A directory for image test file results.
+   *
+   * @var string
+   */
+  protected $testDirectory;
+
+  // Colors that are used in testing.
+  // @codingStandardsIgnoreStart
+  protected $black             = [  0,   0,   0,   0];
+  protected $red               = [255,   0,   0,   0];
+  protected $green             = [  0, 255,   0,   0];
+  protected $blue              = [  0,   0, 255,   0];
+  protected $yellow            = [255, 255,   0,   0];
+  protected $fuchsia           = [255,   0, 255,   0];
+  protected $cyan              = [  0, 255, 255,   0];
+  protected $white             = [255, 255, 255,   0];
+  protected $grey              = [128, 128, 128,   0];
+  protected $transparent       = [  0,   0,   0, 127];
+  protected $rotateTransparent = [255, 255, 255, 127];
+
+  protected $width = 40;
+  protected $height = 20;
+  // @codingStandardsIgnoreEnd
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  protected static $modules = [
+    'system',
+    'simpletest',
+    'file_test',
+    'imagemagick',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create an admin user.
+    $admin_user = $this->drupalCreateUser([
+      'administer site configuration',
+    ]);
+    $this->drupalLogin($admin_user);
+
+    // Set the image factory.
+    $this->imageFactory = $this->container->get('image.factory');
+
+    // Prepare a directory for test file results.
+    $this->testDirectory = 'public://imagetest';
+  }
+
+  /**
+   * Provides data for testManipulations.
+   *
+   * @return array[]
+   *   A simple array of simple arrays, each having the following elements:
+   *   - binaries to use for testing.
+   */
+  public function providerManipulationTest() {
+    return [
+      ['imagemagick'],
+      ['graphicsmagick'],
+    ];
+  }
+
+  /**
+   * Test image toolkit operations.
+   *
+   * Since PHP can't visually check that our images have been manipulated
+   * properly, build a list of expected color values for each of the corners and
+   * the expected height and widths for the final images.
+   *
+   * @param string $binaries
+   *   The graphics package binaries to use for testing.
+   *
+   * @dataProvider providerManipulationTest
+   */
+  public function testManipulations($binaries) {
+    // Change the toolkit.
+    \Drupal::configFactory()->getEditable('system.image')
+      ->set('toolkit', 'imagemagick')
+      ->save();
+
+    // Execute tests with selected binaries.
+    // The test can only be executed if binaries are available on the shell
+    // path.
+    \Drupal::configFactory()->getEditable('imagemagick.settings')
+      ->set('debug', TRUE)
+      ->set('binaries', $binaries)
+      ->set('quality', 100)
+      ->save();
+    $status = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick')->checkPath('');
+    if (!empty($status['errors'])) {
+      // Bots running automated test on d.o. do not have binaries installed,
+      // so the test will be skipped; it can be run locally where binaries are
+      // installed.
+      $this->markTestSkipped("Tests for '{$binaries}' cannot run because the binaries are not available on the shell path.");
+    }
+
+    // Set the toolkit on the image factory.
+    $this->imageFactory->setToolkitId('imagemagick');
+
+    // Test that the image factory is set to use the Imagemagick toolkit.
+    $this->assertEqual($this->imageFactory->getToolkitId(), 'imagemagick', 'The image factory is set to use the \'imagemagick\' image toolkit.');
+
+    // Prepare directory.
+    file_unmanaged_delete_recursive($this->testDirectory);
+    file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+
+    // Typically the corner colors will be unchanged. These colors are in the
+    // order of top-left, top-right, bottom-right, bottom-left.
+    $default_corners = [
+      $this->red,
+      $this->green,
+      $this->blue,
+      $this->transparent,
+    ];
+
+    // A list of files that will be tested.
+    $files = [
+      'image-test.png',
+      'image-test.gif',
+      'image-test-no-transparency.gif',
+      'image-test.jpg',
+    ];
+
+    // Setup a list of tests to perform on each type.
+    $operations = [
+      'resize' => [
+        'function' => 'resize',
+        'arguments' => ['width' => 20, 'height' => 10],
+        'width' => 20,
+        'height' => 10,
+        'corners' => $default_corners,
+        'tolerance' => 0,
+      ],
+      'scale_x' => [
+        'function' => 'scale',
+        'arguments' => ['width' => 20],
+        'width' => 20,
+        'height' => 10,
+        'corners' => $default_corners,
+        'tolerance' => 0,
+      ],
+      'scale_y' => [
+        'function' => 'scale',
+        'arguments' => ['height' => 10],
+        'width' => 20,
+        'height' => 10,
+        'corners' => $default_corners,
+        'tolerance' => 0,
+      ],
+      'upscale_x' => [
+        'function' => 'scale',
+        'arguments' => ['width' => 80, 'upscale' => TRUE],
+        'width' => 80,
+        'height' => 40,
+        'corners' => $default_corners,
+        'tolerance' => 0,
+      ],
+      'upscale_y' => [
+        'function' => 'scale',
+        'arguments' => ['height' => 40, 'upscale' => TRUE],
+        'width' => 80,
+        'height' => 40,
+        'corners' => $default_corners,
+        'tolerance' => 0,
+      ],
+      'crop' => [
+        'function' => 'crop',
+        'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12],
+        'width' => 16,
+        'height' => 12,
+        'corners' => array_fill(0, 4, $this->white),
+        'tolerance' => 0,
+      ],
+      'scale_and_crop' => [
+        'function' => 'scale_and_crop',
+        'arguments' => ['width' => 10, 'height' => 8],
+        'width' => 10,
+        'height' => 8,
+        'corners' => array_fill(0, 4, $this->black),
+        'tolerance' => 100,
+      ],
+      'convert_jpg' => [
+        'function' => 'convert',
+        'width' => 40,
+        'height' => 20,
+        'arguments' => ['extension' => 'jpeg'],
+        'mimetype' => 'image/jpeg',
+        'corners' => $default_corners,
+        'tolerance' => 0,
+      ],
+      'convert_gif' => [
+        'function' => 'convert',
+        'width' => 40,
+        'height' => 20,
+        'arguments' => ['extension' => 'gif'],
+        'mimetype' => 'image/gif',
+        'corners' => $default_corners,
+        'tolerance' => 15,
+      ],
+      'convert_png' => [
+        'function' => 'convert',
+        'width' => 40,
+        'height' => 20,
+        'arguments' => ['extension' => 'png'],
+        'mimetype' => 'image/png',
+        'corners' => $default_corners,
+        'tolerance' => 5,
+      ],
+      'rotate_5' => [
+        'function' => 'rotate',
+        'arguments' => [
+          'degrees' => 5,
+          'background' => '#FF00FF',
+          'resize_filter' => 'Box',
+        ],
+        'width' => 41,
+        'height' => 23,
+        'corners' => array_fill(0, 4, $this->fuchsia),
+        'tolerance' => 5,
+      ],
+      'rotate_minus_10' => [
+        'function' => 'rotate',
+        'arguments' => [
+          'degrees' => -10,
+          'background' => '#FF00FF',
+          'resize_filter' => 'Box',
+        ],
+        'width' => 41,
+        'height' => 26,
+        'corners' => array_fill(0, 4, $this->fuchsia),
+        'tolerance' => 15,
+      ],
+      'rotate_90' => [
+        'function' => 'rotate',
+        'arguments' => ['degrees' => 90, 'background' => '#FF00FF'],
+        'width' => 20,
+        'height' => 40,
+        'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
+        'tolerance' => 0,
+      ],
+      'rotate_transparent_5' => [
+        'function' => 'rotate',
+        'arguments' => ['degrees' => 5, 'resize_filter' => 'Box'],
+        'width' => 41,
+        'height' => 23,
+        'corners' => array_fill(0, 4, $this->transparent),
+        'tolerance' => 0,
+      ],
+      'rotate_transparent_90' => [
+        'function' => 'rotate',
+        'arguments' => ['degrees' => 90],
+        'width' => 20,
+        'height' => 40,
+        'corners' => [$this->transparent, $this->red, $this->green, $this->blue],
+        'tolerance' => 0,
+      ],
+      'desaturate' => [
+        'function' => 'desaturate',
+        'arguments' => [],
+        'height' => 20,
+        'width' => 40,
+        // Grayscale corners are a bit funky. Each of the corners are a shade of
+        // gray. The values of these were determined simply by looking at the
+        // final image to see what desaturated colors end up being.
+        'corners' => [
+          array_fill(0, 3, 76) + [3 => 0],
+          array_fill(0, 3, 149) + [3 => 0],
+          array_fill(0, 3, 29) + [3 => 0],
+          array_fill(0, 3, 225) + [3 => 127],
+        ],
+        // @todo tolerance here is too high. Check reasons.
+        'tolerance' => 17000,
+      ],
+    ];
+
+    // Prepare a copy of test files.
+    $this->getTestFiles('image');
+
+    foreach ($files as $file) {
+      foreach ($operations as $op => $values) {
+        // Load up a fresh image.
+        $image = $this->imageFactory->get('public://' . $file);
+        if (!$image->isValid()) {
+          $this->fail("Could not load image $file.");
+          continue 2;
+        }
+
+        // Check that no multi-frame information is set.
+        $this->assertNull($image->getToolkit()->getFrames());
+
+        // Perform our operation.
+        $image->apply($values['function'], $values['arguments']);
+
+        // Save and reload image.
+        $file_path = $this->testDirectory . '/' . $op . substr($file, -4);
+        $image->save($file_path);
+        $image = $this->imageFactory->get($file_path);
+        $this->assertTrue($image->isValid());
+
+        // @todo GraphicsMagick specifics, temporarily adjust tests.
+        $package = $image->getToolkit()->getPackage();
+        if ($package === 'graphicsmagick') {
+          // @todo Issues with crop on GIF files, investigate.
+          if (in_array($file, ['image-test.gif', 'image-test-no-transparency.gif']) && in_array($op, ['crop', 'scale_and_crop'])) {
+            continue;
+          }
+        }
+
+        // Reload with GD to be able to check results at pixel level.
+        $image = $this->imageFactory->get($file_path, 'gd');
+        $toolkit = $image->getToolkit();
+        $toolkit->getResource();
+        $this->assertTrue($image->isValid());
+
+        // Check MIME type if needed.
+        if (isset($values['mimetype'])) {
+          $this->assertEqual($values['mimetype'], $toolkit->getMimeType(), "Image '$file' after '$op' action has proper MIME type ({$values['mimetype']}).");
+        }
+
+        // To keep from flooding the test with assert values, make a general
+        // value for whether each group of values fail.
+        $correct_dimensions_real = TRUE;
+        $correct_dimensions_object = TRUE;
+
+        // Check the real dimensions of the image first.
+        $actual_toolkit_width = imagesx($toolkit->getResource());
+        $actual_toolkit_height = imagesy($toolkit->getResource());
+        if ($actual_toolkit_height != $values['height'] || $actual_toolkit_width != $values['width']) {
+          $correct_dimensions_real = FALSE;
+        }
+
+        // Check that the image object has an accurate record of the dimensions.
+        $actual_image_width = $image->getWidth();
+        $actual_image_height = $image->getHeight();
+        if ($actual_image_width != $values['width'] || $actual_image_height != $values['height']) {
+          $correct_dimensions_object = FALSE;
+        }
+
+        $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}.");
+        $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}.");
+
+        // JPEG colors will always be messed up due to compression.
+        if ($image->getToolkit()->getType() != IMAGETYPE_JPEG) {
+          // Now check each of the corners to ensure color correctness.
+          foreach ($values['corners'] as $key => $corner) {
+            // The test gif that does not have transparency has yellow where the
+            // others have transparent.
+            if ($file === 'image-test-no-transparency.gif' && $corner === $this->transparent && $op != 'rotate_transparent_5') {
+              $corner = $this->yellow;
+            }
+            // The test jpg when converted to other formats has yellow where the
+            // others have transparent.
+            if ($file === 'image-test.jpg' && $corner === $this->transparent && in_array($op, ['convert_gif', 'convert_png'])) {
+              $corner = $this->yellow;
+            }
+            // Get the location of the corner.
+            switch ($key) {
+              case 0:
+                $x = 0;
+                $y = 0;
+                break;
+
+              case 1:
+                $x = $image->getWidth() - 1;
+                $y = 0;
+                break;
+
+              case 2:
+                $x = $image->getWidth() - 1;
+                $y = $image->getHeight() - 1;
+                break;
+
+              case 3:
+                $x = 0;
+                $y = $image->getHeight() - 1;
+                break;
+
+            }
+            $color = $this->getPixelColor($image, $x, $y);
+            $correct_colors = $this->colorsAreClose($color, $corner, $values['tolerance']);
+          }
+        }
+      }
+    }
+
+    // Test creation of image from scratch, and saving to storage.
+    foreach ([IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG] as $type) {
+      $image = $this->imageFactory->get();
+      $image->createNew(50, 20, image_type_to_extension($type, FALSE), '#ffff00');
+      $file = 'from_null' . image_type_to_extension($type);
+      $file_path = $this->testDirectory . '/' . $file;
+      $this->assertEqual(50, $image->getWidth(), "Image file '$file' has the correct width.");
+      $this->assertEqual(20, $image->getHeight(), "Image file '$file' has the correct height.");
+      $this->assertEqual(image_type_to_mime_type($type), $image->getMimeType(), "Image file '$file' has the correct MIME type.");
+      $this->assertTrue($image->save($file_path), "Image '$file' created anew from a null image was saved.");
+
+      // Reload saved image.
+      $image_reloaded = $this->imageFactory->get($file_path, 'gd');
+      if (!$image_reloaded->isValid()) {
+        $this->fail("Could not load image '$file'.");
+        continue;
+      }
+      $this->assertEqual(50, $image_reloaded->getWidth(), "Image file '$file' has the correct width.");
+      $this->assertEqual(20, $image_reloaded->getHeight(), "Image file '$file' has the correct height.");
+      $this->assertEqual(image_type_to_mime_type($type), $image_reloaded->getMimeType(), "Image file '$file' has the correct MIME type.");
+      if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF) {
+        $this->assertEqual('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), "Image file '$file' has the correct transparent color channel set.");
+      }
+      else {
+        $this->assertEqual(NULL, $image_reloaded->getToolkit()->getTransparentColor(), "Image file '$file' has no color channel set.");
+      }
+    }
+
+    // Test failures of CreateNew.
+    $image = $this->imageFactory->get();
+    $image->createNew(-50, 20);
+    $this->assertFalse($image->isValid(), 'CreateNew with negative width fails.');
+    $image->createNew(50, 20, 'foo');
+    $this->assertFalse($image->isValid(), 'CreateNew with invalid extension fails.');
+    $image->createNew(50, 20, 'gif', '#foo');
+    $this->assertFalse($image->isValid(), 'CreateNew with invalid color hex string fails.');
+    $image->createNew(50, 20, 'gif', '#ff0000');
+    $this->assertTrue($image->isValid(), 'CreateNew with valid arguments validates the Image.');
+
+    // Test saving image files with filenames having non-ascii characters.
+    $file_names = [
+      'greek εικόνα δοκιμής.png',
+      'russian Тестовое изображение.png',
+      'simplified chinese 测试图片.png',
+      'japanese 試験画像.png',
+      'arabic صورة الاختبار.png',
+      'armenian փորձարկման պատկերը.png',
+      'bengali পরীক্ষা ইমেজ.png',
+      'hebraic תמונת בדיקה.png',
+      'hindi परीक्षण छवि.png',
+      'viet hình ảnh thử nghiệm.png',
+      'viet \'with quotes\' hình ảnh thử nghiệm.png',
+      'viet "with double quotes" hình ảnh thử nghiệm.png',
+    ];
+    foreach ($file_names as $file) {
+      $image = $this->imageFactory->get();
+      $image->createNew(50, 20, 'png');
+      $file_path = $this->testDirectory . '/' . $file;
+      $image->save($file_path);
+      $image_reloaded = $this->imageFactory->get($file_path, 'gd');
+      $this->assertTrue($image_reloaded->isValid(), "Image file '$file' loaded successfully.");
+    }
+
+    // Test handling a file stored through a remote stream wrapper.
+    $image = $this->imageFactory->get('dummy-remote://image-test.png');
+    // Source file should be equal to the copied local temp source file.
+    $this->assertEqual(filesize('dummy-remote://image-test.png'), filesize($image->getToolkit()->getSourceLocalPath()));
+    $image->desaturate();
+    $image->save('dummy-remote://remote-image-test.png');
+    // Destination file should exists, and destination local temp file should
+    // have been reset.
+    $this->assertTrue(file_exists($image->getToolkit()->getDestination()));
+    $this->assertEqual('dummy-remote://remote-image-test.png', $image->getToolkit()->getDestination());
+    $this->assertIdentical('', $image->getToolkit()->getDestinationLocalPath());
+
+    // Test retrieval of EXIF information.
+    $image_files = [
+      [
+        'path' => drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg',
+        'orientation' => 8,
+      ],
+      [
+        'path' => 'public://image-test.jpg',
+        'orientation' => NULL,
+      ],
+      [
+        'path' => 'public://image-test.png',
+        'orientation' => NULL,
+      ],
+      [
+        'path' => 'public://image-test.gif',
+        'orientation' => NULL,
+      ],
+      [
+        'path' => NULL,
+        'orientation' => NULL,
+      ],
+    ];
+
+    foreach ($image_files as $image_file) {
+      // Get image using 'identify'.
+      \Drupal::configFactory()->getEditable('imagemagick.settings')
+        ->set('use_identify', TRUE)
+        ->save();
+      $image = $this->imageFactory->get($image_file['path']);
+      $this->assertIdentical($image_file['orientation'], $image->getToolkit()->getExifOrientation());
+
+      // Get image using 'getimagesize'.
+      \Drupal::configFactory()->getEditable('imagemagick.settings')
+        ->set('use_identify', FALSE)
+        ->save();
+      $image = $this->imageFactory->get($image_file['path']);
+      $this->assertIdentical($image_file['orientation'], $image->getToolkit()->getExifOrientation());
+    }
+
+    // Test multi-frame GIF image.
+    $image_files = [
+      [
+        'source' => drupal_get_path('module', 'imagemagick') . '/misc/test-multi-frame.gif',
+        'destination' => $this->testDirectory . '/test-multi-frame.gif',
+        'width' => 60,
+        'height' => 29,
+        'frames' => 13,
+        'scaled_width' => 30,
+        'scaled_height' => 15,
+        'rotated_width' => 33,
+        'rotated_height' => 26,
+      ],
+    ];
+
+    // Get images using 'identify'.
+    \Drupal::configFactory()->getEditable('imagemagick.settings')
+      ->set('use_identify', TRUE)
+      ->save();
+    foreach ($image_files as $image_file) {
+      $image = $this->imageFactory->get($image_file['source']);
+      $this->assertIdentical($image_file['width'], $image->getWidth());
+      $this->assertIdentical($image_file['height'], $image->getHeight());
+      $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
+
+      // Scaling should preserve frames.
+      $image->scale(30);
+      $image->save($image_file['destination']);
+      $image = $this->imageFactory->get($image_file['destination']);
+      $this->assertIdentical($image_file['scaled_width'], $image->getWidth());
+      $this->assertIdentical($image_file['scaled_height'], $image->getHeight());
+      $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
+
+      // Rotating should preserve frames.
+      $image->rotate(24);
+      $image->save($image_file['destination']);
+      $image = $this->imageFactory->get($image_file['destination']);
+      $this->assertIdentical($image_file['rotated_width'], $image->getWidth());
+      $this->assertIdentical($image_file['rotated_height'], $image->getHeight());
+      $this->assertIdentical($image_file['frames'], $image->getToolkit()->getFrames());
+
+      // Converting to PNG should drop frames.
+      $image->convert('png');
+      $this->assertNull($image->getToolkit()->getFrames());
+      $image->save($image_file['destination']);
+      $image = $this->imageFactory->get($image_file['destination']);
+      $this->assertIdentical($image_file['rotated_width'], $image->getWidth());
+      $this->assertIdentical($image_file['rotated_height'], $image->getHeight());
+      $this->assertNull($image->getToolkit()->getFrames());
+    }
+  }
+
+  /**
+   * Test ImageMagick subform and settings.
+   */
+  public function testFormAndSettings() {
+    // Change the toolkit.
+    \Drupal::configFactory()->getEditable('system.image')
+      ->set('toolkit', 'imagemagick')
+      ->save();
+
+    // Test form is accepting wrong binaries path while setting toolkit to GD.
+    $this->drupalGet('admin/config/media/image-toolkit');
+    $this->assertFieldByName('image_toolkit', 'imagemagick');
+    $edit = [
+      'image_toolkit' => 'gd',
+      'imagemagick[suite][path_to_binaries]' => '/foo/bar',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save configuration');
+    $this->assertFieldByName('image_toolkit', 'gd');
+
+    // Change the toolkit.
+    \Drupal::configFactory()->getEditable('system.image')
+      ->set('toolkit', 'imagemagick')
+      ->save();
+    $this->imageFactory->setToolkitId('imagemagick');
+    $this->assertEqual('imagemagick', $this->imageFactory->getToolkitId());
+
+    // Test default supported image extensions.
+    $this->assertEqual('gif jpe jpeg jpg png', implode(' ', $this->imageFactory->getSupportedExtensions()));
+
+    $config = \Drupal::configFactory()->getEditable('imagemagick.settings');
+
+    // Enable TIFF.
+    $image_formats = $config->get('image_formats');
+    $image_formats['TIFF']['enabled'] = TRUE;
+    $config->set('image_formats', $image_formats)->save();
+    $this->assertEqual('gif jpe jpeg jpg png tif tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
+
+    // Disable PNG.
+    $image_formats['PNG']['enabled'] = FALSE;
+    $config->set('image_formats', $image_formats)->save();
+    $this->assertEqual('gif jpe jpeg jpg tif tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
+
+    // Disable some extensions.
+    $image_formats['TIFF']['exclude_extensions'] = 'tif, gif';
+    $config->set('image_formats', $image_formats)->save();
+    $this->assertEqual('gif jpe jpeg jpg tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
+    $image_formats['JPEG']['exclude_extensions'] = 'jpe, jpg';
+    $config->set('image_formats', $image_formats)->save();
+    $this->assertEqual('gif jpeg tiff', implode(' ', $this->imageFactory->getSupportedExtensions()));
+  }
+
+  /**
+   * Function for finding a pixel's RGBa values.
+   */
+  protected function getPixelColor(ImageInterface $image, $x, $y) {
+    $toolkit = $image->getToolkit();
+    $color_index = imagecolorat($toolkit->getResource(), $x, $y);
+
+    $transparent_index = imagecolortransparent($toolkit->getResource());
+    if ($color_index == $transparent_index) {
+      return array(0, 0, 0, 127);
+    }
+
+    return array_values(imagecolorsforindex($toolkit->getResource(), $color_index));
+  }
+
+  /**
+   * Function to compare two colors by RGBa, within a tolerance.
+   *
+   * Very basic, just compares the sum of the squared differences for each of
+   * the R, G, B, A components of two colors against a 'tolerance' value.
+   *
+   * @param int[] $actual
+   *   The actual RGBA array.
+   * @param int[] $expected
+   *   The expected RGBA array.
+   * @param int $tolerance
+   *   The acceptable difference between the colors.
+   *
+   * @return bool
+   *   TRUE if the colors differences are within tolerance, FALSE otherwise.
+   */
+  protected function colorsAreClose(array $actual, array $expected, $tolerance) {
+    // Fully transparent colors are equal, regardless of RGB.
+    if ($actual[3] == 127 && $expected[3] == 127) {
+      return TRUE;
+    }
+    $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);
+    $this->assertLessThanOrEqual($tolerance, $distance, "Actual: {" . implode(',', $actual) . "}, Expected: {" . implode(',', $expected) . "}, Distance: " . $distance . ", Tolerance: " . $tolerance);
+    return TRUE;
+  }
+
+}