Upgraded imagemagick and manually altered pdf to image module to handle changes....
authorJeff Veit <jeff.veit@gmail.com>
Tue, 13 Nov 2018 18:50:15 +0000 (18:50 +0000)
committerJeff Veit <jeff.veit@gmail.com>
Tue, 13 Nov 2018 18:50:15 +0000 (18:50 +0000)
30 files changed:
web/modules/contrib/imagemagick/README.txt
web/modules/contrib/imagemagick/composer.json [new file with mode: 0644]
web/modules/contrib/imagemagick/config/install/imagemagick.file_metadata_plugin.imagemagick_identify.yml [new file with mode: 0644]
web/modules/contrib/imagemagick/config/install/imagemagick.settings.yml
web/modules/contrib/imagemagick/config/schema/imagemagick.schema.yml
web/modules/contrib/imagemagick/imagemagick.api.php
web/modules/contrib/imagemagick/imagemagick.info.yml
web/modules/contrib/imagemagick/imagemagick.install
web/modules/contrib/imagemagick/imagemagick.module
web/modules/contrib/imagemagick/imagemagick.services.yml
web/modules/contrib/imagemagick/misc/test-exif-icc.jpeg [new file with mode: 0644]
web/modules/contrib/imagemagick/src/ImagemagickExecArguments.php [new file with mode: 0644]
web/modules/contrib/imagemagick/src/ImagemagickExecManager.php [new file with mode: 0644]
web/modules/contrib/imagemagick/src/ImagemagickExecManagerInterface.php [new file with mode: 0644]
web/modules/contrib/imagemagick/src/ImagemagickFormatMapper.php
web/modules/contrib/imagemagick/src/ImagemagickFormatMapperInterface.php
web/modules/contrib/imagemagick/src/ImagemagickMimeTypeMapper.php [new file with mode: 0644]
web/modules/contrib/imagemagick/src/Plugin/FileMetadata/ImagemagickIdentify.php [new file with mode: 0644]
web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/ImagemagickToolkit.php
web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Convert.php
web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/CreateNew.php
web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Crop.php
web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Desaturate.php
web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/ImagemagickImageToolkitOperationBase.php
web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Resize.php
web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Rotate.php
web/modules/contrib/imagemagick/src/Todo2311679.php [deleted file]
web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickFileMetadataTest.php [new file with mode: 0644]
web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickTest.php
web/modules/contrib/pdf_to_imagefield/pdf_to_imagefield.module

index 7fc011ea5fa60819a7c596aeb6b1b7fbc5d4c450..1cb13da0e7810fb70b07deb8d045345910d6343e 100644 (file)
@@ -11,20 +11,40 @@ To submit bug reports and feature suggestions, or to track changes:
 
 -- REQUIREMENTS --
 
+* PHP 5.6 or higher
+
+* Drupal 8.3.0 or higher
+
 * Either ImageMagick (http://www.imagemagick.org) or GraphicsMagick
-  (http://www.graphicsmagick.org) need to be installed on your server
-  and the convert binary needs to be accessible and executable from PHP.
+  (http://www.graphicsmagick.org) need to be installed on your server and the
+  convert binary needs to be accessible and executable from PHP.
 
 * The PHP configuration must allow invocation of proc_open() (which is
   security-wise identical to exec()).
 
+* File Metadata Manager module 8.x-1.1 or higher
+
+* Composer based installation process is needed to install the module
+  dependencies, see https://www.drupal.org/node/2718229
+
 Consult your server administrator or hosting provider if you are unsure about
 these requirements.
 
 
 -- INSTALLATION --
 
-* Install as usual, see https://drupal.org/node/70151 for further information.
+* Install the required module packages with Composer. From the Drupal
+  installation root directory, type
+
+    $ composer require drupal/imagemagick:^2.1
+
+  This will download both the ImageMagick module and any dependent module
+  (namely, the File Metadata Manager module).
+
+* Enable the module. Navigate to Manage > Extend. Check the box next to the
+  ImageMagick module and then click the 'Install' button at the bottom. If
+  File Metadata Manager is not already installed, the system will prompt you
+  to confirm installing it too. Just confirm and proceed.
 
 
 -- CONFIGURATION --
diff --git a/web/modules/contrib/imagemagick/composer.json b/web/modules/contrib/imagemagick/composer.json
new file mode 100644 (file)
index 0000000..4e0e46e
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "name": "drupal/imagemagick",
+    "type": "drupal-module",
+    "description": "Provides an image toolkit to integrate ImageMagick with the Image API.",
+    "require": {
+        "drupal/core": "^8.3",
+        "drupal/file_mdm": "^1.1",
+        "drupal/file_mdm_exif": "^1.1"
+    }
+}
diff --git a/web/modules/contrib/imagemagick/config/install/imagemagick.file_metadata_plugin.imagemagick_identify.yml b/web/modules/contrib/imagemagick/config/install/imagemagick.file_metadata_plugin.imagemagick_identify.yml
new file mode 100644 (file)
index 0000000..3947809
--- /dev/null
@@ -0,0 +1,7 @@
+configuration:
+  cache:
+    override: FALSE
+    settings:
+      enabled: TRUE
+      expiration: 172800
+      disallowed_paths: {  }
index d91e5abebf88655606fe86f686670a7170808119..237593e12b611535ddbbb4383c51b4674cfa4ce1 100644 (file)
@@ -2,6 +2,7 @@ quality: 75
 binaries: 'imagemagick'
 path_to_binaries: ''
 prepend: ''
+prepend_pre_source: FALSE
 log_warnings: TRUE
 debug: FALSE
 use_identify: TRUE
index a89306424ef9ba651eaf740362228f4c21d4b9b2..95df7aee2eb0996d770b256d7ca1871da67f18c5 100644 (file)
@@ -17,6 +17,9 @@ imagemagick.settings:
     prepend:
       type: string
       label: 'Prepend arguments'
+    prepend_pre_source:
+      type: boolean
+      label: 'Execute the arguments before loading the source image'
     log_warnings:
       type: boolean
       label: 'Log command executions returning with non-zero code'
@@ -46,3 +49,12 @@ imagemagick.settings:
         profile:
           type: string
           label: 'Color profile path'
+
+
+imagemagick.file_metadata_plugin.imagemagick_identify:
+  type: config_object
+  label: 'imagemagick_identify file metadata plugin settings'
+  mapping:
+    configuration:
+      type: file_mdm.plugin.configuration
+      label: 'imagemagick_identify plugin settings'
index e11cd26e0bf2bf71ce87b051e68a03b74512b8cf..51478cba08a83be0a3f02b9e0527005fbb99fd92 100644 (file)
  * Modules can also decide to move files from remote systems to the local
  * file system to allow processing.
  *
- * @param \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit $toolkit
- *   The Imagemagick toolkit instance to alter.
+ * @param \Drupal\imagemagick\ImagemagickExecArguments $arguments
+ *   The ImageMagick/GraphicsMagick execution arguments object.
  *
  * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::parseFile()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getSource()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::setSourceLocalPath()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getSourceLocalPath()
+ * @see \Drupal\imagemagick\ImagemagickExecArguments::getSource()
+ * @see \Drupal\imagemagick\ImagemagickExecArguments::setSourceLocalPath()
+ * @see \Drupal\imagemagick\ImagemagickExecArguments::getSourceLocalPath()
  */
-function hook_imagemagick_pre_parse_file_alter(\Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit $toolkit) {
+function hook_imagemagick_pre_parse_file_alter(\Drupal\imagemagick\ImagemagickExecArguments $arguments) {
 }
 
 /**
@@ -36,14 +36,14 @@ function hook_imagemagick_pre_parse_file_alter(\Drupal\imagemagick\Plugin\ImageT
  * to move temporary files from the local file system to remote destination
  * systems.
  *
- * @param \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit $toolkit
- *   The Imagemagick toolkit instance to alter.
+ * @param \Drupal\imagemagick\ImagemagickExecArguments $arguments
+ *   The ImageMagick/GraphicsMagick execution arguments object.
  *
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getDestination()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getDestinationLocalPath()
  * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::save()
+ * @see \Drupal\imagemagick\ImagemagickExecArguments::getDestination()
+ * @see \Drupal\imagemagick\ImagemagickExecArguments::getDestinationLocalPath()
  */
-function hook_imagemagick_post_save_alter(\Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit $toolkit) {
+function hook_imagemagick_post_save_alter(\Drupal\imagemagick\ImagemagickExecArguments $arguments) {
 }
 
 /**
@@ -55,35 +55,33 @@ function hook_imagemagick_post_save_alter(\Drupal\imagemagick\Plugin\ImageToolki
  * The toolkit provides methods to prepend, add, find, get and reset
  * arguments that have already been set by image effects.
  *
+ * In addition to arguments that are passed to the binaries command line for
+ * execution, it is possible to push arguments to be used only by the toolkit
+ * or the hooks. You can add/get/find such arguments by specifying
+ * ImagemagickExecArguments::INTERNAL as the argument $mode in the methods.
+ *
  * ImageMagick automatically converts the target image to the format denoted by
  * the file extension. However, since changing the file extension is not always
  * an option, you can specify an alternative image format via
- * $toolkit->setDestinationFormat('format'), where 'format' is a string
- * denoting an Imagemagick supported format.
+ * $arguments->setDestinationFormat('format'), where 'format' is a string
+ * denoting an Imagemagick supported format, or via
+ * $arguments->setDestinationFormatFromExtension('extension'), where
+ * 'extension' is a string denoting an image file extension.
+ *
  * When the destination format is set, it is passed to ImageMagick's convert
  * binary with the syntax "[format]:[destination]".
  *
- * @param \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit $toolkit
- *   The Imagemagick toolkit instance to alter.
+ * @param \Drupal\imagemagick\ImagemagickExecArguments $arguments
+ *   The ImageMagick/GraphicsMagick execution arguments object.
  * @param string $command
- *   The Imagemagick binary being called.
+ *   The ImageMagick/GraphicsMagick command being called.
  *
  * @see http://www.imagemagick.org/script/command-line-processing.php#output
  * @see http://www.imagemagick.org/Usage/files/#save
  *
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getArguments()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::prependArgument()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::addArgument()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::findArgument()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::resetArguments()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getSource()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::setSourceLocalPath()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getSourceLocalPath()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getDestination()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::setDestinationLocalPath()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::getDestinationLocalPath()
+ * @see \Drupal\imagemagick\ImagemagickExecArguments
  * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::convert()
- * @see \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit::identify()
+ * @see \Drupal\imagemagick\Plugin\FileMetadata\ImagemagickIdentify::identify()
  */
-function hook_imagemagick_arguments_alter(\Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit $toolkit, $command) {
+function hook_imagemagick_arguments_alter(\Drupal\imagemagick\ImagemagickExecArguments $arguments, $command) {
 }
index 0c5d35e9cba2f4590c7acc86ad1bc0c508542a02..1481440a63153e46c47d7f84406ec3044d1dbb9f 100644 (file)
@@ -5,10 +5,12 @@ description: 'Provides ImageMagick integration.'
 package: Media
 configure: system.image_toolkit_settings
 dependencies:
-  - system (>=8.1.0)
+  - file_mdm:file_mdm
+  - file_mdm:file_mdm_exif
+  - drupal:system (>=8.3.0)
 
-# Information added by Drupal.org packaging script on 2017-03-06
-version: '8.x-1.0-alpha6'
+# Information added by Drupal.org packaging script on 2018-02-05
+version: '8.x-2.3'
 core: '8.x'
 project: 'imagemagick'
-datestamp: 1488787686
+datestamp: 1517855585
index 12aa0c05c5d0eecbeddc192faa7fcb2bef72daa8..1b7800553e3f860169d08066a39114acfa699131 100644 (file)
@@ -36,30 +36,27 @@ function imagemagick_requirements($phase) {
 }
 
 /**
- * @addtogroup updates-8.x-1.0-alpha
- * @{
+ * Enable file_mdm module.
  */
-
-/**
- * Adds the 'locale' config setting.
- */
-function imagemagick_update_8001() {
-  $config_factory = \Drupal::configFactory();
-  $setting = $config_factory->getEditable('imagemagick.settings')
-    ->set('locale', 'en_US.UTF-8')
-    ->save(TRUE);
+function imagemagick_update_8201() {
+  \Drupal::service('module_installer')->install([
+    'file_mdm',
+  ]);
 }
 
 /**
- * Adds the 'log_warnings' config setting.
+ * Adds the 'prepend_pre_source' config setting.
  */
-function imagemagick_update_8002() {
+function imagemagick_update_8202() {
   $config_factory = \Drupal::configFactory();
   $setting = $config_factory->getEditable('imagemagick.settings')
-    ->set('log_warnings', TRUE)
+    ->set('prepend_pre_source', FALSE)
     ->save(TRUE);
 }
 
 /**
- * @} End of "addtogroup updates-8.x-1.0-alpha".
+ * Clear caches to discover service changes.
  */
+function imagemagick_update_8203() {
+  // Empty function.
+}
index e6f0e8b063f0ec6d44723578992d41940f3dd031..13bb01bf06c125de3ae78d103c5faee7c1fa1f38 100644 (file)
@@ -5,19 +5,19 @@
  * Provides ImageMagick integration.
  */
 
-use Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit;
+use Drupal\imagemagick\ImagemagickExecArguments;
 
 /**
  * Implements hook_imagemagick_pre_parse_file_alter().
  */
-function imagemagick_imagemagick_pre_parse_file_alter(ImagemagickToolkit $toolkit) {
+function imagemagick_imagemagick_pre_parse_file_alter(ImagemagickExecArguments $arguments) {
   // Convert source image URI to filepath.
-  $local_path = $toolkit->getSourceLocalPath();
+  $local_path = $arguments->getSourceLocalPath();
   if (empty($local_path)) {
-    $source = $toolkit->getSource();
+    $source = $arguments->getSource();
     if (!file_valid_uri($source)) {
       // The value of $source is likely a file path already.
-      $toolkit->setSourceLocalPath($source);
+      $arguments->setSourceLocalPath($source);
     }
     else {
       // If we can resolve the realpath of the file, then the file is local and
@@ -25,7 +25,7 @@ function imagemagick_imagemagick_pre_parse_file_alter(ImagemagickToolkit $toolki
       $file_system = \Drupal::service('file_system');
       $path = $file_system->realpath($source);
       if ($path) {
-        $toolkit->setSourceLocalPath($path);
+        $arguments->setSourceLocalPath($path);
       }
       else {
         // We are working with a remote file, copy the remote source file to a
@@ -33,8 +33,8 @@ function imagemagick_imagemagick_pre_parse_file_alter(ImagemagickToolkit $toolki
         $temp_path = $file_system->tempnam('temporary://', 'imagemagick_');
         $file_system->unlink($temp_path);
         $temp_path .= '.' . pathinfo($source, PATHINFO_EXTENSION);
-        $path = file_unmanaged_copy($toolkit->getSource(), $temp_path, FILE_EXISTS_ERROR);
-        $toolkit->setSourceLocalPath($file_system->realpath($path));
+        $path = file_unmanaged_copy($arguments->getSource(), $temp_path, FILE_EXISTS_ERROR);
+        $arguments->setSourceLocalPath($file_system->realpath($path));
       }
     }
   }
@@ -43,22 +43,22 @@ function imagemagick_imagemagick_pre_parse_file_alter(ImagemagickToolkit $toolki
 /**
  * Implements hook_imagemagick_arguments_alter().
  */
-function imagemagick_imagemagick_arguments_alter(ImagemagickToolkit $toolkit, $command) {
+function imagemagick_imagemagick_arguments_alter(ImagemagickExecArguments $arguments, $command) {
   $config = \Drupal::config('imagemagick.settings');
 
   // Add prepended arguments if needed.
   if ($prepend = $config->get('prepend')) {
-    $toolkit->prependArgument($prepend);
+    $arguments->add($prepend, $config->get('prepend_pre_source') ? ImagemagickExecArguments::PRE_SOURCE : ImagemagickExecArguments::POST_SOURCE, 0);
   }
 
   if ($command == 'convert') {
     // Convert destination image URI to filepath.
-    $local_path = $toolkit->getDestinationLocalPath();
+    $local_path = $arguments->getDestinationLocalPath();
     if (empty($local_path)) {
-      $destination = $toolkit->getDestination();
+      $destination = $arguments->getDestination();
       if (!file_valid_uri($destination)) {
         // The value of $destination is likely a file path already.
-        $toolkit->setDestinationLocalPath($destination);
+        $arguments->setDestinationLocalPath($destination);
       }
       else {
         // If we can resolve the realpath of the file, then the file is local
@@ -66,7 +66,7 @@ function imagemagick_imagemagick_arguments_alter(ImagemagickToolkit $toolkit, $c
         $file_system = \Drupal::service('file_system');
         $path = $file_system->realpath($destination);
         if ($path) {
-          $toolkit->setDestinationLocalPath($path);
+          $arguments->setDestinationLocalPath($path);
         }
         else {
           // We are working with a remote file, set the local destination to
@@ -74,33 +74,33 @@ function imagemagick_imagemagick_arguments_alter(ImagemagickToolkit $toolkit, $c
           $temp_path = $file_system->tempnam('temporary://', 'imagemagick_');
           $file_system->unlink($temp_path);
           $temp_path .= '.' . pathinfo($destination, PATHINFO_EXTENSION);
-          $toolkit->setDestinationLocalPath($file_system->realpath($temp_path));
+          $arguments->setDestinationLocalPath($file_system->realpath($temp_path));
         }
       }
     }
 
-    // Change image density.
-    if ($toolkit->findArgument('-density') === FALSE && $density = (int) $config->get('advanced.density')) {
-      $toolkit->addArgument("-density {$density} -units PixelsPerInch");
+    // Change output image resolution to 72 ppi, if specified in settings.
+    if (empty($arguments->find('/^\-density/')) && $density = (int) $config->get('advanced.density')) {
+      $arguments->add("-density {$density} -units PixelsPerInch");
     }
 
     // Apply color profile.
     if ($profile = $config->get('advanced.profile')) {
       if (file_exists($profile)) {
-        $toolkit->addArgument('-profile ' . $toolkit->escapeShellArg($profile));
+        $arguments->add('-profile ' . $arguments->escape($profile));
       }
     }
     // Or alternatively apply colorspace.
     elseif ($colorspace = $config->get('advanced.colorspace')) {
       // Do not hi-jack settings made by effects.
-      if ($toolkit->findArgument('-colorspace') === FALSE) {
-        $toolkit->addArgument('-colorspace ' . $toolkit->escapeShellArg($colorspace));
+      if (empty($arguments->find('/^\-colorspace/'))) {
+        $arguments->add('-colorspace ' . $arguments->escape($colorspace));
       }
     }
 
     // Change image quality.
-    if ($toolkit->findArgument('-quality') === FALSE) {
-      $toolkit->addArgument('-quality ' . \Drupal::config('imagemagick.settings')->get('quality'));
+    if (empty($arguments->find('/^\-quality/'))) {
+      $arguments->add('-quality ' . \Drupal::config('imagemagick.settings')->get('quality'));
     }
   }
 }
@@ -108,13 +108,13 @@ function imagemagick_imagemagick_arguments_alter(ImagemagickToolkit $toolkit, $c
 /**
  * Implements hook_imagemagick_post_save_alter().
  */
-function imagemagick_imagemagick_post_save_alter(ImagemagickToolkit $toolkit) {
+function imagemagick_imagemagick_post_save_alter(ImagemagickExecArguments $arguments) {
   $file_system = \Drupal::service('file_system');
-  $destination = $toolkit->getDestination();
+  $destination = $arguments->getDestination();
   $path = $file_system->realpath($destination);
   if (!$path) {
     // We are working with a remote file, so move the temp file to the final
     // destination, replacing any existinf file with the same name.
-    file_unmanaged_move($toolkit->getDestinationLocalPath(), $toolkit->getDestination(), FILE_EXISTS_REPLACE);
+    file_unmanaged_move($arguments->getDestinationLocalPath(), $arguments->getDestination(), FILE_EXISTS_REPLACE);
   }
 }
index f5148cdd5f2a2acf60f92ac2dc2d076a0236ef07..87e2e3f30c66f3760c76745dbd0992470ddaf0e4 100644 (file)
@@ -1,7 +1,10 @@
 services:
   imagemagick.format_mapper:
     class: Drupal\imagemagick\ImagemagickFormatMapper
-    arguments: ['@cache.default', '@imagemagick.todo2311679', '@config.factory', '@config.typed']
-  imagemagick.todo2311679:
-    class: Drupal\imagemagick\Todo2311679
-    arguments: ['@module_handler']
+    arguments: ['@cache.default', '@imagemagick.mime_type_mapper', '@config.factory', '@config.typed']
+  imagemagick.exec_manager:
+    class: Drupal\imagemagick\ImagemagickExecManager
+    arguments: ['@logger.channel.image', '@config.factory', '@app.root', '@current_user', '@imagemagick.format_mapper', '@module_handler']
+  imagemagick.mime_type_mapper:
+    class: Drupal\imagemagick\ImagemagickMimeTypeMapper
+    arguments: ['@file.mime_type.guesser.extension']
diff --git a/web/modules/contrib/imagemagick/misc/test-exif-icc.jpeg b/web/modules/contrib/imagemagick/misc/test-exif-icc.jpeg
new file mode 100644 (file)
index 0000000..d15439f
Binary files /dev/null and b/web/modules/contrib/imagemagick/misc/test-exif-icc.jpeg differ
diff --git a/web/modules/contrib/imagemagick/src/ImagemagickExecArguments.php b/web/modules/contrib/imagemagick/src/ImagemagickExecArguments.php
new file mode 100644 (file)
index 0000000..0666c75
--- /dev/null
@@ -0,0 +1,634 @@
+<?php
+
+namespace Drupal\imagemagick;
+
+/**
+ * Stores arguments for execution of ImageMagick/GraphicsMagick commands.
+ */
+class ImagemagickExecArguments {
+
+  /**
+   * An identifier to be used for arguments internal to the toolkit.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Do not prefix arguments
+   *   to mark them internal, add them with ImageMagickExecArguments::INTERNAL
+   *   instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
+   */
+  const INTERNAL_ARGUMENT_IDENTIFIER = '>!>';
+
+  /**
+   * Default index for adding arguments.
+   */
+  const APPEND = -1;
+
+  /**
+   * Mode for arguments to be placed before the source path.
+   */
+  const PRE_SOURCE = 0;
+
+  /**
+   * Mode for arguments to be placed after the source path.
+   */
+  const POST_SOURCE = 1;
+
+  /**
+   * Mode for arguments not to be placed on the command line.
+   */
+  const INTERNAL = 2;
+
+  /**
+   * The ImageMagick execution manager service.
+   *
+   * @var \Drupal\imagemagick\ImagemagickExecManagerInterface
+   */
+  protected $execManager;
+
+  /**
+   * The array of command line arguments to be used by 'convert'.
+   *
+   * @var string[]
+   */
+  protected $arguments = [];
+
+  /**
+   * Path of the image file.
+   *
+   * @var string
+   */
+  protected $source = '';
+
+  /**
+   * The local filesystem path to the source image file.
+   *
+   * @var string
+   */
+  protected $sourceLocalPath = '';
+
+  /**
+   * The source image format.
+   *
+   * @var string
+   */
+  protected $sourceFormat = '';
+
+  /**
+   * The source image frames to access.
+   *
+   * @var string
+   */
+  protected $sourceFrames;
+
+  /**
+   * The image destination URI/path on saving.
+   *
+   * @var string
+   */
+  protected $destination = NULL;
+
+  /**
+   * The local filesystem path to the image destination.
+   *
+   * @var string
+   */
+  protected $destinationLocalPath = '';
+
+  /**
+   * The image destination format on saving.
+   *
+   * @var string
+   */
+  protected $destinationFormat = '';
+
+  /**
+   * Constructs an ImagemagickExecArguments object.
+   *
+   * @param \Drupal\imagemagick\ImagemagickExecManagerInterface $exec_manager
+   *   The ImageMagick execution manager service.
+   */
+  public function __construct(ImagemagickExecManagerInterface $exec_manager) {
+    $this->execManager = $exec_manager;
+  }
+
+  /**
+   * Gets the command line arguments for the binary.
+   *
+   * @return string[]
+   *   The array of command line arguments.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments methods to manipulate arguments directly.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
+   */
+  public function getArguments() {
+    @trigger_error('getArguments() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments methods to manipulate arguments directly. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    $ret = [];
+    foreach ($this->arguments as $i => $a) {
+      if (in_array($a['mode'], [self::POST_SOURCE, self::INTERNAL])) {
+        $ret[$i] = ($a['mode'] === self::INTERNAL ? self::INTERNAL_ARGUMENT_IDENTIFIER : '') . $a['argument'];
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * Gets the command line arguments string for the binary.
+   *
+   * Removes any argument used internally within the toolkit.
+   *
+   * @return string
+   *   The sring of command line arguments.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::toString()
+   *   instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
+   */
+  public function getStringForBinary() {
+    @trigger_error('getStringForBinary() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::toString() instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    return $this->toString(self::POST_SOURCE);
+  }
+
+  /**
+   * Gets a portion of the command line arguments string.
+   *
+   * @param int $mode
+   *   The mode of the string on the command line. Can be self::PRE_SOURCE or
+   *   self::POST_SOURCE.
+   *
+   * @return string
+   *   The sring of command line arguments.
+   */
+  public function toString($mode) {
+    if (!$this->arguments) {
+      return '';
+    }
+    $ret = [];
+    foreach ($this->arguments as $a) {
+      if ($a['mode'] === $mode) {
+        $ret[] = $a['argument'];
+      }
+    }
+    return implode(' ', $ret);
+  }
+
+  /**
+   * Adds a command line argument.
+   *
+   * @param string $arg
+   *   The command line argument to be added.
+   *
+   * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::add() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
+   */
+  public function addArgument($arg) {
+    @trigger_error('addArgument() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::add() instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    if (strpos($arg, self::INTERNAL_ARGUMENT_IDENTIFIER) === 0) {
+      @trigger_error('Adding internal arguments prefixing them with ImagemagickExecArguments::INTERNAL_ARGUMENT_IDENTIFIER is deprecated in 8.x-2.3, will be removed in 8.x-3.0. ::add() them with ImageMagickExecArguments::INTERNAL instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+      return $this->add(substr($arg, strlen(self::INTERNAL_ARGUMENT_IDENTIFIER)), self::INTERNAL);
+    }
+    return $this->add($arg);
+  }
+
+  /**
+   * Prepends a command line argument.
+   *
+   * @param string $arg
+   *   The command line argument to be prepended.
+   *
+   * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::add() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
+   */
+  public function prependArgument($arg) {
+    @trigger_error('prependArgument() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::add() instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    if (strpos($arg, self::INTERNAL_ARGUMENT_IDENTIFIER) === 0) {
+      @trigger_error('Adding internal arguments prefixing them with ImagemagickExecArguments::INTERNAL_ARGUMENT_IDENTIFIER is deprecated in 8.x-2.3, will be removed in 8.x-3.0. ::add() them with ImageMagickExecArguments::INTERNAL instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+      return $this->add(substr($arg, strlen(self::INTERNAL_ARGUMENT_IDENTIFIER)), self::INTERNAL, 0);
+    }
+    return $this->add($arg, self::POST_SOURCE, 0);
+  }
+
+  /**
+   * Adds a command line argument.
+   *
+   * @param string $argument
+   *   The command line argument to be added.
+   * @param int $mode
+   *   (optional) The mode of the argument in the command line. Determines if
+   *   the argument should be placed before or after the source image file path.
+   *   Defaults to self::POST_SOURCE.
+   * @param int $index
+   *   (optional) The position of the argument in the arguments array.
+   *   Reflects the sequence of arguments in the command line. Defaults to
+   *   self::APPEND.
+   * @param array $info
+   *   (optional) An optional array with information about the argument.
+   *   Defaults to an empty array.
+   *
+   * @return $this
+   */
+  public function add($argument, $mode = self::POST_SOURCE, $index = self::APPEND, array $info = []) {
+    $argument = [
+      'argument' => $argument,
+      'mode' => $mode,
+      'info' => $info,
+    ];
+    if ($index === self::APPEND) {
+      $this->arguments[] = $argument;
+    }
+    elseif ($index === 0) {
+      array_unshift($this->arguments, $argument);
+    }
+    else {
+      array_splice($this->arguments, $index, 0, [$argument]);
+    }
+    return $this;
+  }
+
+  /**
+   * Finds if a command line argument exists.
+   *
+   * @param string $arg
+   *   The command line argument to be found.
+   *
+   * @return bool
+   *   Returns the array key for the argument if it is found in the array,
+   *   FALSE otherwise.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::find() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
+   */
+  public function findArgument($arg) {
+    @trigger_error('findArgument() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::find() instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    if (strpos($arg, self::INTERNAL_ARGUMENT_IDENTIFIER) === 0) {
+      @trigger_error('Adding internal arguments prefixing them with ImagemagickExecArguments::INTERNAL_ARGUMENT_IDENTIFIER is deprecated in 8.x-2.3, will be removed in 8.x-3.0. ::add() them with ImageMagickExecArguments::INTERNAL instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+      foreach ($this->getArguments() as $i => $a) {
+        if (strpos($a, $arg) === 0) {
+          return $i;
+        }
+      }
+      return FALSE;
+    }
+    $matches = $this->find('/^' . preg_quote($arg, '/') . '/', self::POST_SOURCE);
+    if (!empty($matches)) {
+      $keys = array_keys($matches);
+      return $keys[0];
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns an array of the indexes of arguments matching specific criteria.
+   *
+   * @param string $regex
+   *   The regular expression pattern to be matched in the argument.
+   * @param int $mode
+   *   (optional) If set, limits the search to the mode of the argument.
+   *   Defaults to NULL.
+   * @param array $info
+   *   (optional) If set, limits the search to the arguments whose $info array
+   *   key/values match the key/values specified. Defaults to an empty array.
+   *
+   * @return array
+   *   Returns an array with the matching arguments.
+   */
+  public function find($regex, $mode = NULL, array $info = []) {
+    $ret = [];
+    foreach ($this->arguments as $i => $a) {
+      if ($mode !== NULL && $a['mode'] !== $mode) {
+        continue;
+
+      }
+      if (!empty($info)) {
+        $intersect = array_intersect($info, $a['info']);
+        if ($intersect != $info) {
+          continue;
+
+        }
+      }
+      if (preg_match($regex, $a['argument']) === 1) {
+        $ret[$i] = $a;
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * Removes a command line argument.
+   *
+   * @param int $index
+   *   The index of the command line argument to be removed.
+   *
+   * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::remove() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2936615
+   */
+  public function removeArgument($index) {
+    @trigger_error('removeArgument() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::remove() instead. See https://www.drupal.org/project/imagemagick/issues/2936615.', E_USER_DEPRECATED);
+    return $this->remove($index);
+  }
+
+  /**
+   * Removes a command line argument.
+   *
+   * @param int $index
+   *   The index of the command line argument to be removed.
+   *
+   * @return $this
+   */
+  public function remove($index) {
+    if (isset($this->arguments[$index])) {
+      unset($this->arguments[$index]);
+    }
+    return $this;
+  }
+
+  /**
+   * Resets the command line arguments.
+   *
+   * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::reset() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2936615
+   */
+  public function resetArguments() {
+    @trigger_error('resetArguments() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::reset() instead. See https://www.drupal.org/project/imagemagick/issues/2936615.', E_USER_DEPRECATED);
+    return $this->reset();
+  }
+
+  /**
+   * Resets the command line arguments.
+   *
+   * @return $this
+   */
+  public function reset() {
+    $this->arguments = [];
+    return $this;
+  }
+
+  /**
+   * Returns the count of command line arguments.
+   *
+   * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::find() instead,
+   *   then count the result.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2936615
+   */
+  public function countArguments() {
+    @trigger_error('countArguments() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::find() instead, then count the result. See https://www.drupal.org/project/imagemagick/issues/2936615.', E_USER_DEPRECATED);
+    return count($this->arguments);
+  }
+
+  /**
+   * Sets the path of the source image file.
+   *
+   * @param string $source
+   *   The source path of the image file.
+   *
+   * @return $this
+   */
+  public function setSource($source) {
+    $this->source = $source;
+    return $this;
+  }
+
+  /**
+   * Gets the path of the source image file.
+   *
+   * @return string
+   *   The source path of the image file, or an empty string if the source is
+   *   not set.
+   */
+  public function getSource() {
+    return $this->source;
+  }
+
+  /**
+   * Sets the local filesystem path to the image file.
+   *
+   * @param string $path
+   *   A filesystem path.
+   *
+   * @return $this
+   */
+  public function setSourceLocalPath($path) {
+    $this->sourceLocalPath = $path;
+    return $this;
+  }
+
+  /**
+   * Gets the local filesystem path to the image file.
+   *
+   * @return string
+   *   A filesystem path.
+   */
+  public function getSourceLocalPath() {
+    return $this->sourceLocalPath;
+  }
+
+  /**
+   * Sets the source image format.
+   *
+   * @param string $format
+   *   The image format.
+   *
+   * @return $this
+   */
+  public function setSourceFormat($format) {
+    $this->sourceFormat = $this->execManager->getFormatMapper()->isFormatEnabled($format) ? $format : '';
+    return $this;
+  }
+
+  /**
+   * Sets the source image format from an image file extension.
+   *
+   * @param string $extension
+   *   The image file extension.
+   *
+   * @return $this
+   */
+  public function setSourceFormatFromExtension($extension) {
+    $this->sourceFormat = $this->execManager->getFormatMapper()->getFormatFromExtension($extension) ?: '';
+    return $this;
+  }
+
+  /**
+   * Gets the source image format.
+   *
+   * @return string
+   *   The source image format.
+   */
+  public function getSourceFormat() {
+    return $this->sourceFormat;
+  }
+
+  /**
+   * Sets the source image frames to access.
+   *
+   * @param string $frames
+   *   The frames in '[n]' string format.
+   *
+   * @return $this
+   *
+   * @see http://www.imagemagick.org/script/command-line-processing.php
+   */
+  public function setSourceFrames($frames) {
+    $this->sourceFrames = $frames;
+    return $this;
+  }
+
+  /**
+   * Gets the source image frames to access.
+   *
+   * @return string
+   *   The frames in '[n]' string format.
+   *
+   * @see http://www.imagemagick.org/script/command-line-processing.php
+   */
+  public function getSourceFrames() {
+    return $this->sourceFrames;
+  }
+
+  /**
+   * Sets the image destination URI/path on saving.
+   *
+   * @param string $destination
+   *   The image destination URI/path.
+   *
+   * @return $this
+   */
+  public function setDestination($destination) {
+    $this->destination = $destination;
+    return $this;
+  }
+
+  /**
+   * Gets the image destination URI/path on saving.
+   *
+   * @return string
+   *   The image destination URI/path.
+   */
+  public function getDestination() {
+    return $this->destination;
+  }
+
+  /**
+   * Sets the local filesystem path to the destination image file.
+   *
+   * @param string $path
+   *   A filesystem path.
+   *
+   * @return $this
+   */
+  public function setDestinationLocalPath($path) {
+    $this->destinationLocalPath = $path;
+    return $this;
+  }
+
+  /**
+   * Gets the local filesystem path to the destination image file.
+   *
+   * @return string
+   *   A filesystem path.
+   */
+  public function getDestinationLocalPath() {
+    return $this->destinationLocalPath;
+  }
+
+  /**
+   * Sets the image destination format.
+   *
+   * When set, it is passed to the convert binary in the syntax
+   * "[format]:[destination]", where [format] is a string denoting an
+   * ImageMagick's image format.
+   *
+   * @param string $format
+   *   The image destination format.
+   *
+   * @return $this
+   */
+  public function setDestinationFormat($format) {
+    $this->destinationFormat = $format;
+    return $this;
+  }
+
+  /**
+   * Sets the image destination format from an image file extension.
+   *
+   * When set, it is passed to the convert binary in the syntax
+   * "[format]:[destination]", where [format] is a string denoting an
+   * ImageMagick's image format.
+   *
+   * @param string $extension
+   *   The destination image file extension.
+   *
+   * @return $this
+   */
+  public function setDestinationFormatFromExtension($extension) {
+    $this->destinationFormat = $this->execManager->getFormatMapper()->getFormatFromExtension($extension) ?: '';
+    return $this;
+  }
+
+  /**
+   * Gets the image destination format.
+   *
+   * When set, it is passed to the convert binary in the syntax
+   * "[format]:[destination]", where [format] is a string denoting an
+   * ImageMagick's image format.
+   *
+   * @return string
+   *   The image destination format.
+   */
+  public function getDestinationFormat() {
+    return $this->destinationFormat;
+  }
+
+  /**
+   * Escapes a string.
+   *
+   * @param string $arg
+   *   The string to escape.
+   *
+   * @return string
+   *   An escaped string for use in the
+   *   ImagemagickExecManagerInterface::execute method.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::escape()
+   *   instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2936680
+   */
+  public function escapeShellArg($arg) {
+    @trigger_error('escapeShellArg() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::escape() instead. See https://www.drupal.org/project/imagemagick/issues/2936680.', E_USER_DEPRECATED);
+    return $this->escape($arg);
+  }
+
+  /**
+   * Escapes a string.
+   *
+   * @param string $argument
+   *   The string to escape.
+   *
+   * @return string
+   *   An escaped string for use in the
+   *   ImagemagickExecManagerInterface::execute method.
+   */
+  public function escape($argument) {
+    return $this->execManager->escapeShellArg($argument);
+  }
+
+}
diff --git a/web/modules/contrib/imagemagick/src/ImagemagickExecManager.php b/web/modules/contrib/imagemagick/src/ImagemagickExecManager.php
new file mode 100644 (file)
index 0000000..7439506
--- /dev/null
@@ -0,0 +1,541 @@
+<?php
+
+namespace Drupal\imagemagick;
+
+use Drupal\Component\Utility\Timer;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Process\Process;
+
+/**
+ * Manage execution of ImageMagick/GraphicsMagick commands.
+ */
+class ImagemagickExecManager implements ImagemagickExecManagerInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * Whether we are running on Windows OS.
+   *
+   * @var bool
+   */
+  protected $isWindows;
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $appRoot;
+
+  /**
+   * The execution timeout.
+   *
+   * @var int
+   */
+  protected $timeout = 60;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The logger service.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The format mapper service.
+   *
+   * @var \Drupal\imagemagick\ImagemagickFormatMapperInterface
+   */
+  protected $formatMapper;
+
+  /**
+   * Constructs an ImagemagickExecManager object.
+   *
+   * @param \Psr\Log\LoggerInterface $logger
+   *   A logger instance.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param string $app_root
+   *   The app root.
+   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
+   *   The current user.
+   * @param \Drupal\imagemagick\ImagemagickFormatMapperInterface $format_mapper
+   *   The format mapper service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
+   */
+  public function __construct(LoggerInterface $logger, ConfigFactoryInterface $config_factory, $app_root, AccountProxyInterface $current_user, ImagemagickFormatMapperInterface $format_mapper, ModuleHandlerInterface $module_handler) {
+    $this->logger = $logger;
+    $this->configFactory = $config_factory;
+    $this->appRoot = $app_root;
+    $this->currentUser = $current_user;
+    $this->formatMapper = $format_mapper;
+    $this->moduleHandler = $module_handler;
+    $this->isWindows = substr(PHP_OS, 0, 3) === 'WIN';
+  }
+
+  /**
+   * Returns the format mapper.
+   *
+   * @return \Drupal\imagemagick\ImagemagickFormatMapperInterface
+   *   The format mapper service.
+   *
+   * @todo in 8.x-3.0, add this method to the interface.
+   */
+  public function getFormatMapper() {
+    return $this->formatMapper;
+  }
+
+  /**
+   * Returns the module handler.
+   *
+   * @return \Drupal\Core\Extension\ModuleHandlerInterface
+   *   The module handler service.
+   *
+   * @todo in 8.x-3.0, add this method to the interface.
+   */
+  public function getModuleHandler() {
+    return $this->moduleHandler;
+  }
+
+  /**
+   * Sets the execution timeout (max. runtime).
+   *
+   * To disable the timeout, set this value to null.
+   *
+   * @param int|null $timeout
+   *   The timeout in seconds.
+   *
+   * @return $this
+   *
+   * @todo in 8.x-3.0, add this method to the interface.
+   */
+  public function setTimeout($timeout) {
+    $this->timeout = $timeout;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPackage($package = NULL) {
+    if ($package === NULL) {
+      $package = $this->configFactory->get('imagemagick.settings')->get('binaries');
+    }
+    return $package;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPackageLabel($package = NULL) {
+    switch ($this->getPackage($package)) {
+      case 'imagemagick':
+        return $this->t('ImageMagick');
+
+      case 'graphicsmagick':
+        return $this->t('GraphicsMagick');
+
+      default:
+        return $package;
+
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkPath($path, $package = NULL) {
+    $status = [
+      'output' => '',
+      'errors' => [],
+    ];
+
+    // Execute gm or convert based on settings.
+    $package = $package ?: $this->getPackage();
+    $binary = $package === 'imagemagick' ? 'convert' : 'gm';
+    $executable = $this->getExecutable($binary, $path);
+
+    // If a path is given, we check whether the binary exists and can be
+    // invoked.
+    if (!empty($path)) {
+      // Check whether the given file exists.
+      if (!is_file($executable)) {
+        $status['errors'][] = $this->t('The @suite executable %file does not exist.', ['@suite' => $this->getPackageLabel($package), '%file' => $executable]);
+      }
+      // If it exists, check whether we can execute it.
+      elseif (!is_executable($executable)) {
+        $status['errors'][] = $this->t('The @suite file %file is not executable.', ['@suite' => $this->getPackageLabel($package), '%file' => $executable]);
+      }
+    }
+
+    // In case of errors, check for open_basedir restrictions.
+    if ($status['errors'] && ($open_basedir = ini_get('open_basedir'))) {
+      $status['errors'][] = $this->t('The PHP <a href=":php-url">open_basedir</a> security restriction is set to %open-basedir, which may prevent to locate the @suite executable.', [
+        '@suite' => $this->getPackageLabel($package),
+        '%open-basedir' => $open_basedir,
+        ':php-url' => 'http://php.net/manual/en/ini.core.php#ini.open-basedir',
+      ]);
+    }
+
+    // Unless we had errors so far, try to invoke convert.
+    if (!$status['errors']) {
+      $error = NULL;
+      $this->runOsShell($executable, '-version', $package, $status['output'], $error);
+      if ($error !== '') {
+        $status['errors'][] = $error;
+      }
+    }
+
+    return $status;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($command, ImagemagickExecArguments $arguments, &$output = NULL, &$error = NULL, $path = NULL) {
+    switch ($command) {
+      case 'convert':
+        $binary = $this->getPackage() === 'imagemagick' ? 'convert' : 'gm';
+        break;
+
+      case 'identify':
+        $binary = $this->getPackage() === 'imagemagick' ? 'identify' : 'gm';
+        break;
+
+    }
+    $cmd = $this->getExecutable($binary, $path);
+
+    if ($source_path = $arguments->getSourceLocalPath()) {
+      if (($source_frames = $arguments->getSourceFrames()) !== NULL) {
+        $source_path .= $source_frames;
+      }
+      $source_path = $this->escapeShellArg($source_path);
+    }
+
+    if ($destination_path = $arguments->getDestinationLocalPath()) {
+      $destination_path = $this->escapeShellArg($destination_path);
+      // If the format of the derivative image has to be changed, concatenate
+      // the new image format and the destination path, delimited by a colon.
+      // @see http://www.imagemagick.org/script/command-line-processing.php#output
+      if (($format = $arguments->getDestinationFormat()) !== '') {
+        $destination_path = $format . ':' . $destination_path;
+      }
+    }
+
+    switch ($command) {
+      case 'identify':
+        switch ($this->getPackage()) {
+          case 'imagemagick':
+            // @codingStandardsIgnoreStart
+            // ImageMagick syntax:
+            // identify [arguments] source
+            // @codingStandardsIgnoreEnd
+            $cmdline = $arguments->toString(ImagemagickExecArguments::PRE_SOURCE);
+            // @todo BC layer. In 8.x-3.0, remove adding post source path
+            // arguments.
+            if (($post = $arguments->toString(ImagemagickExecArguments::POST_SOURCE)) !== '') {
+              $cmdline .= ' ' . $post;
+            }
+            $cmdline .= ' ' . $source_path;
+            break;
+
+          case 'graphicsmagick':
+            // @codingStandardsIgnoreStart
+            // GraphicsMagick syntax:
+            // gm identify [arguments] source
+            // @codingStandardsIgnoreEnd
+            $cmdline = 'identify ' . $arguments->toString(ImagemagickExecArguments::PRE_SOURCE);
+            // @todo BC layer. In 8.x-3.0, remove adding post source path
+            // arguments.
+            if (($post = $arguments->toString(ImagemagickExecArguments::POST_SOURCE)) !== '') {
+              $cmdline .= ' ' . $post;
+            }
+            $cmdline .= ' ' . $source_path;
+            break;
+
+        }
+        break;
+
+      case 'convert':
+        switch ($this->getPackage()) {
+          case 'imagemagick':
+            // @codingStandardsIgnoreStart
+            // ImageMagick syntax:
+            // convert input [arguments] output
+            // @see http://www.imagemagick.org/Usage/basics/#cmdline
+            // @codingStandardsIgnoreEnd
+            $cmdline = '';
+            if (($pre = $arguments->toString(ImagemagickExecArguments::PRE_SOURCE)) !== '') {
+              $cmdline .= $pre . ' ';
+            }
+            $cmdline .= $source_path . ' ' . $arguments->toString(ImagemagickExecArguments::POST_SOURCE) . ' ' . $destination_path;
+            break;
+
+          case 'graphicsmagick':
+            // @codingStandardsIgnoreStart
+            // GraphicsMagick syntax:
+            // gm convert [arguments] input output
+            // @see http://www.graphicsmagick.org/GraphicsMagick.html
+            // @codingStandardsIgnoreEnd
+            $cmdline = 'convert ';
+            if (($pre = $arguments->toString(ImagemagickExecArguments::PRE_SOURCE)) !== '') {
+              $cmdline .= $pre . ' ';
+            }
+            $cmdline .= $arguments->toString(ImagemagickExecArguments::POST_SOURCE) . ' ' . $source_path . ' ' . $destination_path;
+            break;
+
+        }
+        break;
+
+    }
+
+    $return_code = $this->runOsShell($cmd, $cmdline, $this->getPackage(), $output, $error);
+
+    if ($return_code !== FALSE) {
+      // If the executable returned a non-zero code, log to the watchdog.
+      if ($return_code != 0) {
+        if ($error === '') {
+          // If there is no error message, and allowed in config, log a
+          // warning.
+          if ($this->configFactory->get('imagemagick.settings')->get('log_warnings') === TRUE) {
+            $this->logger->warning("@suite returned with code @code [command: @command @cmdline]", [
+              '@suite' => $this->getPackageLabel(),
+              '@code' => $return_code,
+              '@command' => $cmd,
+              '@cmdline' => $cmdline,
+            ]);
+          }
+        }
+        else {
+          // Log $error with context information.
+          $this->logger->error("@suite error @code: @error [command: @command @cmdline]", [
+            '@suite' => $this->getPackageLabel(),
+            '@code' => $return_code,
+            '@error' => $error,
+            '@command' => $cmd,
+            '@cmdline' => $cmdline,
+          ]);
+        }
+        // Executable exited with an error code, return FALSE.
+        return FALSE;
+      }
+
+      // The shell command was executed successfully.
+      return TRUE;
+    }
+    // The shell command could not be executed.
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function runOsShell($command, $arguments, $id, &$output = NULL, &$error = NULL) {
+    $command_line = $command . ' ' . $arguments;
+    $output = '';
+    $error = '';
+
+    Timer::start('imagemagick:runOsShell');
+    $process = new Process($command_line, $this->appRoot);
+    $process->setTimeout($this->timeout);
+    try {
+      $process->run();
+      $output = utf8_encode($process->getOutput());
+      $error = utf8_encode($process->getErrorOutput());
+      $return_code = $process->getExitCode();
+    }
+    catch (\Exception $e) {
+      $error = $e->getMessage();
+      $return_code = $process->getExitCode() ? $process->getExitCode() : 1;
+    }
+    $execution_time = Timer::stop('imagemagick:runOsShell')['time'];
+
+    // Process debugging information if required.
+    if ($this->configFactory->get('imagemagick.settings')->get('debug')) {
+      $this->debugMessage('@suite command: <pre>@raw</pre> executed in @execution_timems', [
+        '@suite' => $this->getPackageLabel($id),
+        '@raw' => print_r($command_line, TRUE),
+        '@execution_time' => $execution_time,
+      ]);
+      if ($output !== '') {
+        $this->debugMessage('@suite output: <pre>@raw</pre>', [
+          '@suite' => $this->getPackageLabel($id),
+          '@raw' => print_r($output, TRUE),
+        ]);
+      }
+      if ($error !== '') {
+        $this->debugMessage('@suite error @return_code: <pre>@raw</pre>', [
+          '@suite' => $this->getPackageLabel($id),
+          '@return_code' => $return_code,
+          '@raw' => print_r($error, TRUE),
+        ]);
+      }
+    }
+
+    return $return_code;
+  }
+
+  /**
+   * Logs a debug message, and shows it on the screen for authorized users.
+   *
+   * @param string $message
+   *   The debug message.
+   * @param string[] $context
+   *   Context information.
+   */
+  public function debugMessage($message, array $context) {
+    $this->logger->debug($message, $context);
+    if ($this->currentUser->hasPermission('administer site configuration')) {
+      // Strips raw text longer than 10 lines to optimize displaying.
+      if (isset($context['@raw'])) {
+        $raw = explode("\n", $context['@raw']);
+        if (count($raw) > 10) {
+          $tmp = [];
+          for ($i = 0; $i < 9; $i++) {
+            $tmp[] = $raw[$i];
+          }
+          $tmp[] = (string) $this->t('[Further text stripped. The watchdog log has the full text.]');
+          $context['@raw'] = implode("\n", $tmp);
+        }
+      }
+      // @codingStandardsIgnoreLine
+      drupal_set_message($this->t($message, $context), 'status', TRUE);
+    }
+  }
+
+  /**
+   * Gets the list of locales installed on the server.
+   *
+   * @return string
+   *   The string resulting from the execution of 'locale -a' in *nix systems.
+   */
+  public function getInstalledLocales() {
+    $output = '';
+    if ($this->isWindows === FALSE) {
+      $this->runOsShell('locale', '-a', 'locale', $output);
+    }
+    else {
+      $output = (string) $this->t("List not available on Windows servers.");
+    }
+    return $output;
+  }
+
+  /**
+   * Returns the full path to the executable.
+   *
+   * @param string $binary
+   *   The program to execute, typically 'convert', 'identify' or 'gm'.
+   * @param string $path
+   *   (optional) A custom path to the folder of the executable. When left
+   *   empty, the setting imagemagick.settings.path_to_binaries is taken.
+   *
+   * @return string
+   *   The full path to the executable.
+   */
+  protected function getExecutable($binary, $path = NULL) {
+    // $path is only passed from the validation of the image toolkit form, on
+    // which the path to convert is configured. @see ::checkPath()
+    if (!isset($path)) {
+      $path = $this->configFactory->get('imagemagick.settings')->get('path_to_binaries');
+    }
+
+    $executable = $binary;
+    if ($this->isWindows) {
+      $executable .= '.exe';
+    }
+
+    return $path . $executable;
+  }
+
+  /**
+   * Escapes a string.
+   *
+   * PHP escapeshellarg() drops non-ascii characters, this is a replacement.
+   *
+   * Stop-gap replacement until core issue #1561214 has been solved. Solution
+   * proposed in #1502924-8.
+   *
+   * PHP escapeshellarg() on Windows also drops % (percentage sign) characters.
+   * We prevent this by replacing it with a pattern that should be highly
+   * unlikely to appear in the string itself and does not contain any
+   * "dangerous" character at all (very wide definition of dangerous). After
+   * escaping we replace that pattern back with a % character.
+   *
+   * @param string $arg
+   *   The string to escape.
+   *
+   * @return string
+   *   An escaped string for use in the ::execute method.
+   */
+  public function escapeShellArg($arg) {
+    static $percentage_sign_replace_pattern = '1357902468IMAGEMAGICKPERCENTSIGNPATTERN8642097531';
+
+    // Put the configured locale in a static to avoid multiple config get calls
+    // in the same request.
+    static $config_locale;
+
+    if (!isset($config_locale)) {
+      $config_locale = $this->configFactory->get('imagemagick.settings')->get('locale');
+      if (empty($config_locale)) {
+        $config_locale = FALSE;
+      }
+    }
+
+    if ($this->isWindows) {
+      // Temporarily replace % characters.
+      $arg = str_replace('%', $percentage_sign_replace_pattern, $arg);
+    }
+
+    // If no locale specified in config, return with standard.
+    if ($config_locale === FALSE) {
+      $arg_escaped = escapeshellarg($arg);
+    }
+    else {
+      // Get the current locale.
+      $current_locale = setlocale(LC_CTYPE, 0);
+      if ($current_locale != $config_locale) {
+        // Temporarily swap the current locale with the configured one.
+        setlocale(LC_CTYPE, $config_locale);
+        $arg_escaped = escapeshellarg($arg);
+        setlocale(LC_CTYPE, $current_locale);
+      }
+      else {
+        $arg_escaped = escapeshellarg($arg);
+      }
+    }
+
+    // Get our % characters back.
+    if ($this->isWindows) {
+      $arg_escaped = str_replace($percentage_sign_replace_pattern, '%', $arg_escaped);
+    }
+
+    return $arg_escaped;
+  }
+
+}
diff --git a/web/modules/contrib/imagemagick/src/ImagemagickExecManagerInterface.php b/web/modules/contrib/imagemagick/src/ImagemagickExecManagerInterface.php
new file mode 100644 (file)
index 0000000..252ef2d
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\imagemagick;
+
+/**
+ * Provides an interface for ImageMagick execution managers.
+ */
+interface ImagemagickExecManagerInterface {
+
+  /**
+   * Gets the binaries package in use.
+   *
+   * @param string $package
+   *   (optional) Force the graphics package.
+   *
+   * @return string
+   *   The default package ('imagemagick'|'graphicsmagick'), or the $package
+   *   argument.
+   */
+  public function getPackage($package = NULL);
+
+  /**
+   * Gets a translated label of the binaries package in use.
+   *
+   * @param string $package
+   *   (optional) Force the package.
+   *
+   * @return string
+   *   A translated label of the binaries package in use, or the $package
+   *   argument.
+   */
+  public function getPackageLabel($package = NULL);
+
+  /**
+   * Verifies file path of the executable binary by checking its version.
+   *
+   * @param string $path
+   *   The user-submitted file path to the convert binary.
+   * @param string $package
+   *   (optional) The graphics package to use.
+   *
+   * @return array
+   *   An associative array containing:
+   *   - output: The shell output of 'convert -version', if any.
+   *   - errors: A list of error messages indicating if the executable could
+   *     not be found or executed.
+   */
+  public function checkPath($path, $package = NULL);
+
+  /**
+   * Executes the convert executable as shell command.
+   *
+   * @param string $command
+   *   The executable to run.
+   * @param \Drupal\imagemagick\ImagemagickExecArguments $arguments
+   *   An ImageMagick execution arguments object.
+   * @param string &$output
+   *   (optional) A variable to assign the shell stdout to, passed by
+   *   reference.
+   * @param string &$error
+   *   (optional) A variable to assign the shell stderr to, passed by
+   *   reference.
+   * @param string $path
+   *   (optional) A custom file path to the executable binary.
+   *
+   * @return bool
+   *   TRUE if the command succeeded, FALSE otherwise. The error exit status
+   *   code integer returned by the executable is logged.
+   */
+  public function execute($command, ImagemagickExecArguments $arguments, &$output = NULL, &$error = NULL, $path = NULL);
+
+  /**
+   * Executes a command on the operating system.
+   *
+   * This differs from ::runOsCommand in the sense that here the command to be
+   * executed and its arguments are passed separately.
+   *
+   * @param string $command
+   *   The command to run.
+   * @param string $arguments
+   *   The arguments of the command to run.
+   * @param string $id
+   *   An identifier for the process to be spawned on the operating system.
+   * @param string &$output
+   *   (optional) A variable to assign the shell stdout to, passed by
+   *   reference.
+   * @param string &$error
+   *   (optional) A variable to assign the shell stderr to, passed by
+   *   reference.
+   *
+   * @return int|bool
+   *   The operating system returned code, or FALSE if it was not possible to
+   *   execute the command.
+   */
+  public function runOsShell($command, $arguments, $id, &$output = NULL, &$error = NULL);
+
+}
index e21a854848d9d766b0b5ee17f5b96cb221969448..92888a2e16c58bb71a0bb03857adab49748fdd29 100644 (file)
@@ -9,8 +9,6 @@ use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Config\Schema\SchemaCheckTrait;
 use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
-// @todo change if extension mapping service gets in, see #2311679
-use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
 
 /**
  * Provides the ImageMagick format mapper.
@@ -29,9 +27,8 @@ class ImagemagickFormatMapper implements ImagemagickFormatMapperInterface {
 
   /**
    * The MIME type guessing service.
-   * @todo change if extension mapping service gets in, see #2311679
    *
-   * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
+   * @var \Drupal\imagemagick\MimeTypeMapper
    */
   protected $mimeTypeMapper;
 
@@ -50,21 +47,19 @@ class ImagemagickFormatMapper implements ImagemagickFormatMapperInterface {
   protected $typedConfig;
 
   /**
-   * Constructs an ImagemagickFormatmapper object.
+   * Constructs an ImagemagickFormatMapper object.
    *
    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_service
    *   The cache service.
-   * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_mapper
+   * @param \Drupal\imagemagick\ImagemagickMimeTypeMapper $mime_type_mapper
    *   The MIME type mapping service.
-   *   @todo change if extension mapping service gets in, see #2311679
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory.
    * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
    *   The typed config service.
    */
-  public function __construct(CacheBackendInterface $cache_service, MimeTypeGuesserInterface $mime_type_mapper, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config) {
+  public function __construct(CacheBackendInterface $cache_service, ImagemagickMimeTypeMapper $mime_type_mapper, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config) {
     $this->cache = $cache_service;
-    // @todo change if extension mapping service gets in, see #2311679
     $this->mimeTypeMapper = $mime_type_mapper;
     $this->configFactory = $config_factory;
     $this->typedConfig = $typed_config;
@@ -84,7 +79,7 @@ class ImagemagickFormatMapper implements ImagemagickFormatMapperInterface {
     $schema_errors = $this->checkConfigSchema($this->typedConfig, 'imagemagick.settings', $data);
     if ($schema_errors !== TRUE) {
       foreach ($schema_errors as $key => $value) {
-        list($object, $path) = explode(':', $key);
+        list(, $path) = explode(':', $key);
         $components = explode('.', $path);
         if ($components[0] === 'image_formats') {
           if (isset($components[2])) {
@@ -189,7 +184,6 @@ class ImagemagickFormatMapper implements ImagemagickFormatMapperInterface {
     return $enabled_image_formats;
   }
 
-
   /**
    * Returns the enabled image file extensions, processing the config map.
    *
index e311aed450f44b8c9e748e8fd8bec9459631c0e8..97614c2e357d2b0cff6c4a5bcb02497acead2b5d 100644 (file)
@@ -4,7 +4,6 @@ namespace Drupal\imagemagick;
 
 /**
  * Provides an interface for ImageMagick format mappers.
- * )
  */
 interface ImagemagickFormatMapperInterface {
 
diff --git a/web/modules/contrib/imagemagick/src/ImagemagickMimeTypeMapper.php b/web/modules/contrib/imagemagick/src/ImagemagickMimeTypeMapper.php
new file mode 100644 (file)
index 0000000..bfe457a
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\imagemagick;
+
+use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
+
+/**
+ * Maps MIME types to file extensions.
+ */
+class ImagemagickMimeTypeMapper {
+
+  /**
+   * The extension MIME type guesser.
+   *
+   * @var \Drupal\Core\File\MimeType\ExtensionMimeTypeGuesser
+   */
+  protected $mimeTypeGuesser;
+
+  /**
+   * The MIME types mapping array after going through the module handler.
+   *
+   * Copied via Reflection from
+   * \Drupal\Core\File\MimeType\ExtensionMimeTypeGuesser.
+   *
+   * @var array
+   */
+  protected $mapping;
+
+  /**
+   * Constructs an ImagemagickMimeTypeMapper object.
+   *
+   * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $extension_mimetype_guesser
+   *   The extension MIME type guesser.
+   */
+  public function __construct(MimeTypeGuesserInterface $extension_mimetype_guesser) {
+    $this->mimeTypeGuesser = $extension_mimetype_guesser;
+  }
+
+  /**
+   * Returns the MIME types mapping array from ExtensionMimeTypeGuesser.
+   *
+   * Copied via Reflection from
+   * \Drupal\Core\File\MimeType\ExtensionMimeTypeGuesser.
+   *
+   * @return array
+   *   The MIME types mapping array.
+   */
+  protected function getMapping() {
+    if (!$this->mapping) {
+      // Guess a fake file name just to ensure the guesser loads any mapping
+      // alteration through the hooks.
+      $this->mimeTypeGuesser->guess('fake.png');
+      // Use Reflection to get a copy of the protected $mapping property in the
+      // guesser class. Get the proxied service first, then the actual mapping.
+      $reflection = new \ReflectionObject($this->mimeTypeGuesser);
+      $proxied_service = $reflection->getProperty('service');
+      $proxied_service->setAccessible(TRUE);
+      $service = $proxied_service->getValue(clone $this->mimeTypeGuesser);
+      $reflection = new \ReflectionObject($service);
+      $reflection_mapping = $reflection->getProperty('mapping');
+      $reflection_mapping->setAccessible(TRUE);
+      $this->mapping = $reflection_mapping->getValue(clone $service);
+    }
+    return $this->mapping;
+  }
+
+  /**
+   * Returns the appropriate extensions for a given MIME type.
+   *
+   * @param string $mimetype
+   *   A MIME type.
+   *
+   * @return string[]
+   *   An array of file extensions matching the MIME type, without leading dot.
+   */
+  public function getExtensionsForMimeType($mimetype) {
+    $mapping = $this->getMapping();
+    if (!in_array($mimetype, $mapping['mimetypes'])) {
+      return [];
+    }
+    $key = array_search($mimetype, $mapping['mimetypes']);
+    $extensions = array_keys($mapping['extensions'], $key, TRUE);
+    sort($extensions);
+    return $extensions;
+  }
+
+  /**
+   * Returns known MIME types.
+   *
+   * @return string[]
+   *   An array of MIME types.
+   */
+  public function getMimeTypes() {
+    return array_values($this->getMapping()['mimetypes']);
+  }
+
+}
diff --git a/web/modules/contrib/imagemagick/src/Plugin/FileMetadata/ImagemagickIdentify.php b/web/modules/contrib/imagemagick/src/Plugin/FileMetadata/ImagemagickIdentify.php
new file mode 100644 (file)
index 0000000..d8eb50d
--- /dev/null
@@ -0,0 +1,278 @@
+<?php
+
+namespace Drupal\imagemagick\Plugin\FileMetadata;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\file_mdm\FileMetadataException;
+use Drupal\file_mdm\Plugin\FileMetadata\FileMetadataPluginBase;
+use Drupal\imagemagick\ImagemagickExecArguments;
+use Drupal\imagemagick\ImagemagickExecManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * FileMetadata plugin for ImageMagick's identify results.
+ *
+ * @FileMetadata(
+ *   id = "imagemagick_identify",
+ *   title = @Translation("ImageMagick identify"),
+ *   help = @Translation("File metadata plugin for ImageMagick identify results."),
+ * )
+ */
+class ImagemagickIdentify extends FileMetadataPluginBase {
+
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The ImageMagick execution manager service.
+   *
+   * @var \Drupal\imagemagick\ImagemagickExecManagerInterface
+   */
+  protected $execManager;
+
+  /**
+   * Constructs an ImagemagickIdentify plugin.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param array $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_service
+   *   The cache service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
+   * @param \Drupal\imagemagick\ImagemagickExecManagerInterface $exec_manager
+   *   The ImageMagick execution manager service.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, CacheBackendInterface $cache_service, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ImagemagickExecManagerInterface $exec_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $cache_service, $config_factory);
+    $this->moduleHandler = $module_handler;
+    $this->execManager = $exec_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('cache.file_mdm'),
+      $container->get('config.factory'),
+      $container->get('module_handler'),
+      $container->get('imagemagick.exec_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Supported keys are:
+   *   'format' - ImageMagick's image format identifier.
+   *   'width' - Image width.
+   *   'height' - Image height.
+   *   'colorspace' - Image colorspace.
+   *   'profiles' - Image profiles.
+   *   'exif_orientation' - Image EXIF orientation (only supported formats).
+   *   'source_local_path' - The local file path from where the file was
+   *     parsed.
+   *   'frames_count' - Number of frames in the image.
+   */
+  public function getSupportedKeys($options = NULL) {
+    return [
+      'format',
+      'width',
+      'height',
+      'colorspace',
+      'profiles',
+      'exif_orientation',
+      'source_local_path',
+      'frames_count',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doGetMetadataFromFile() {
+    return $this->identify();
+  }
+
+  /**
+   * Validates a file metadata key.
+   *
+   * @return bool
+   *   TRUE if the key is valid.
+   *
+   * @throws \Drupal\file_mdm\FileMetadataException
+   *   In case the key is invalid.
+   */
+  protected function validateKey($key, $method) {
+    if (!is_string($key)) {
+      throw new FileMetadataException("Invalid metadata key specified", $this->getPluginId(), $method);
+    }
+    if (!in_array($key, $this->getSupportedKeys(), TRUE)) {
+      throw new FileMetadataException("Invalid metadata key '{$key}' specified", $this->getPluginId(), $method);
+    }
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doGetMetadata($key = NULL) {
+    if ($key === NULL) {
+      return $this->metadata;
+    }
+    else {
+      $this->validateKey($key, __FUNCTION__);
+      switch ($key) {
+        case 'source_local_path':
+          return isset($this->metadata['source_local_path']) ? $this->metadata['source_local_path'] : NULL;
+
+        case 'frames_count':
+          return isset($this->metadata['frames']) ? count($this->metadata['frames']) : 0;
+
+        default:
+          return isset($this->metadata['frames'][0][$key]) ? $this->metadata['frames'][0][$key] : NULL;
+
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSetMetadata($key, $value) {
+    $this->validateKey($key, __FUNCTION__);
+    switch ($key) {
+      case 'source_local_path':
+        $this->metadata['source_local_path'] = $value;
+        return TRUE;
+
+      case 'frames_count':
+        return FALSE;
+
+      default:
+        $this->metadata['frames'][0][$key] = $value;
+        return TRUE;
+
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doRemoveMetadata($key) {
+    $this->validateKey($key, __FUNCTION__);
+    switch ($key) {
+      case 'source_local_path':
+        if (isset($this->metadata['source_local_path'])) {
+          unset($this->metadata['source_local_path']);
+          return TRUE;
+        }
+        return FALSE;
+
+      default:
+        return FALSE;
+
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getMetadataToCache() {
+    $metadata = $this->metadata;
+    // Avoid caching the source_local_path.
+    unset($metadata['source_local_path']);
+    return $metadata;
+  }
+
+  /**
+   * Calls the identify executable on the specified file.
+   *
+   * @return array
+   *   The array with identify metadata, if the file was parsed correctly.
+   *   NULL otherwise.
+   */
+  protected function identify() {
+    $arguments = new ImagemagickExecArguments($this->execManager);
+
+    // Add source file.
+    $arguments->setSource($this->getLocalTempPath());
+
+    // Prepare the -format argument according to the graphics package in use.
+    switch ($this->execManager->getPackage()) {
+      case 'imagemagick':
+        $arguments->add(
+          '-format ' . $arguments->escape("format:%[magick]|width:%[width]|height:%[height]|colorspace:%[colorspace]|profiles:%[profiles]|exif_orientation:%[EXIF:Orientation]\\n"),
+          ImagemagickExecArguments::PRE_SOURCE
+        );
+        break;
+
+      case 'graphicsmagick':
+        $arguments->add(
+          '-format ' . $arguments->escape("format:%m|width:%w|height:%h|exif_orientation:%[EXIF:Orientation]\\n"),
+          ImagemagickExecArguments::PRE_SOURCE
+        );
+        break;
+
+    }
+
+    // Allow modules to alter source file and the command line parameters.
+    $command = 'identify';
+    $this->moduleHandler->alter('imagemagick_pre_parse_file', $arguments);
+    $this->moduleHandler->alter('imagemagick_arguments', $arguments, $command);
+
+    // Execute the 'identify' command.
+    $output = NULL;
+    $ret = $this->execManager->execute($command, $arguments, $output);
+
+    // Process results.
+    $data = [];
+    if ($ret) {
+      // Remove any CR character (GraphicsMagick on Windows produces such).
+      $output = str_replace("\r", '', $output);
+
+      // Builds the frames info.
+      $frames = [];
+      $frames_tmp = explode("\n", $output);
+      // Remove empty items at the end of the array.
+      while (empty($frames_tmp[count($frames_tmp) - 1])) {
+        array_pop($frames_tmp);
+      }
+      foreach ($frames_tmp as $i => $frame) {
+        $info = explode('|', $frame);
+        foreach ($info as $item) {
+          list($key, $value) = explode(':', $item);
+          if (trim($key) === 'profiles') {
+            $profiles_tmp = empty($value) ? [] : explode(',', $value);
+            $frames[$i][trim($key)] = $profiles_tmp;
+          }
+          else {
+            $frames[$i][trim($key)] = trim($value);
+          }
+        }
+      }
+      $data['frames'] = $frames;
+      // Adds the local file path that was resolved via
+      // hook_imagemagick_pre_parse_file implementations.
+      $data['source_local_path'] = $arguments->getSourceLocalPath();
+    }
+
+    return ($ret === TRUE) ? $data : NULL;
+  }
+
+}
index 7b5406ff43f14cb90dd3388b52854012e28c77c2..f25c74edd1afb6448290707bd22cdb56e497fdb6 100644 (file)
@@ -12,7 +12,11 @@ use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\ImageToolkit\ImageToolkitBase;
 use Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface;
+use Drupal\Core\Link;
 use Drupal\Core\Url;
+use Drupal\file_mdm\FileMetadataManagerInterface;
+use Drupal\imagemagick\ImagemagickExecArguments;
+use Drupal\imagemagick\ImagemagickExecManagerInterface;
 use Drupal\imagemagick\ImagemagickFormatMapperInterface;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -28,11 +32,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 class ImagemagickToolkit extends ImageToolkitBase {
 
   /**
-   * Whether we are running on Windows OS.
+   * EXIF orientation not fetched.
    *
-   * @var bool
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   parseFileViaIdentify() to parse image files.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2941093
    */
-  protected $isWindows;
+  const EXIF_ORIENTATION_NOT_FETCHED = -99;
 
   /**
    * The module handler service.
@@ -49,18 +56,25 @@ class ImagemagickToolkit extends ImageToolkitBase {
   protected $formatMapper;
 
   /**
-   * The app root.
+   * The file metadata manager service.
    *
-   * @var string
+   * @var \Drupal\file_mdm\FileMetadataManagerInterface
    */
-  protected $appRoot;
+  protected $fileMetadataManager;
 
   /**
-   * The array of command line arguments to be used by 'convert'.
+   * The ImageMagick execution manager service.
    *
-   * @var string[]
+   * @var \Drupal\imagemagick\ImagemagickExecManagerInterface
    */
-  protected $arguments = [];
+  protected $execManager;
+
+  /**
+   * The execution arguments object.
+   *
+   * @var \Drupal\imagemagick\ImagemagickExecArguments
+   */
+  protected $arguments;
 
   /**
    * The width of the image.
@@ -77,53 +91,32 @@ class ImagemagickToolkit extends ImageToolkitBase {
   protected $height;
 
   /**
-   * The number of frames of the image, for multi-frame images (e.g. GIF).
+   * The number of frames of the source image, for multi-frame images.
    *
    * @var int
    */
   protected $frames;
 
   /**
-   * The local filesystem path to the source image file.
-   *
-   * @var string
-   */
-  protected $sourceLocalPath = '';
-
-  /**
-   * The source image format.
-   *
-   * @var string
-   */
-  protected $sourceFormat = '';
-
-  /**
-   * Keeps a copy of source image EXIF information.
+   * Image orientation retrieved from EXIF information.
    *
-   * @var array
-   */
-  protected $exifInfo = [];
-
-  /**
-   * The image destination URI/path on saving.
-   *
-   * @var string
+   * @var int
    */
-  protected $destination = NULL;
+  protected $exifOrientation;
 
   /**
-   * The local filesystem path to the image destination.
+   * The source image colorspace.
    *
    * @var string
    */
-  protected $destinationLocalPath = '';
+  protected $colorspace;
 
   /**
-   * The image destination format on saving.
+   * The source image profiles.
    *
-   * @var string
+   * @var string[]
    */
-  protected $destinationFormat = '';
+  protected $profiles = [];
 
   /**
    * Constructs an ImagemagickToolkit object.
@@ -144,15 +137,18 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The module handler service.
    * @param \Drupal\imagemagick\ImagemagickFormatMapperInterface $format_mapper
    *   The format mapper service.
-   * @param string $app_root
-   *   The app root.
+   * @param \Drupal\file_mdm\FileMetadataManagerInterface $file_metadata_manager
+   *   The file metadata manager service.
+   * @param \Drupal\imagemagick\ImagemagickExecManagerInterface $exec_manager
+   *   The ImageMagick execution manager service.
    */
-  public function __construct(array $configuration, $plugin_id, array $plugin_definition, ImageToolkitOperationManagerInterface $operation_manager, LoggerInterface $logger, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ImagemagickFormatMapperInterface $format_mapper, $app_root) {
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, ImageToolkitOperationManagerInterface $operation_manager, LoggerInterface $logger, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ImagemagickFormatMapperInterface $format_mapper, FileMetadataManagerInterface $file_metadata_manager, ImagemagickExecManagerInterface $exec_manager) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $operation_manager, $logger, $config_factory);
     $this->moduleHandler = $module_handler;
     $this->formatMapper = $format_mapper;
-    $this->appRoot = $app_root;
-    $this->isWindows = substr(PHP_OS, 0, 3) === 'WIN';
+    $this->fileMetadataManager = $file_metadata_manager;
+    $this->execManager = $exec_manager;
+    $this->arguments = new ImagemagickExecArguments($this->execManager);
   }
 
   /**
@@ -168,7 +164,8 @@ class ImagemagickToolkit extends ImageToolkitBase {
       $container->get('config.factory'),
       $container->get('module_handler'),
       $container->get('imagemagick.format_mapper'),
-      $container->get('app.root')
+      $container->get('file_metadata_manager'),
+      $container->get('imagemagick.exec_manager')
     );
   }
 
@@ -196,21 +193,26 @@ class ImagemagickToolkit extends ImageToolkitBase {
       '#description' => $this->t('Define the image quality of processed images. Ranges from 0 to 100. Higher values mean better image quality but bigger files.'),
     ];
 
+    // Settings tabs.
+    $form['imagemagick_settings'] = [
+      '#type' => 'vertical_tabs',
+      '#tree' => FALSE,
+    ];
+
     // Graphics suite to use.
     $form['suite'] = [
       '#type' => 'details',
-      '#open' => TRUE,
-      '#collapsible' => FALSE,
       '#title' => $this->t('Graphics package'),
+      '#group' => 'imagemagick_settings',
     ];
     $options = [
-      'imagemagick' => $this->getPackageLabel('imagemagick'),
-      'graphicsmagick' => $this->getPackageLabel('graphicsmagick'),
+      'imagemagick' => $this->getExecManager()->getPackageLabel('imagemagick'),
+      'graphicsmagick' => $this->getExecManager()->getPackageLabel('graphicsmagick'),
     ];
     $form['suite']['binaries'] = [
       '#type' => 'radios',
       '#title' => $this->t('Suite'),
-      '#default_value' => $this->getPackage(),
+      '#default_value' => $this->getExecManager()->getPackage(),
       '#options' => $options,
       '#required' => TRUE,
       '#description' => $this->t("Select the graphics package to use."),
@@ -224,7 +226,7 @@ class ImagemagickToolkit extends ImageToolkitBase {
       '#description' => $this->t('If needed, the path to the package executables (<kbd>convert</kbd>, <kbd>identify</kbd>, <kbd>gm</kbd>, etc.), <b>including</b> the trailing slash/backslash. For example: <kbd>/usr/bin/</kbd> or <kbd>C:\Program Files\ImageMagick-6.3.4-Q16\</kbd>.'),
     ];
     // Version information.
-    $status = $this->checkPath($config->get('path_to_binaries'));
+    $status = $this->getExecManager()->checkPath($config->get('path_to_binaries'));
     if (empty($status['errors'])) {
       $version_info = explode("\n", preg_replace('/\r/', '', Html::escape($status['output'])));
     }
@@ -234,7 +236,7 @@ class ImagemagickToolkit extends ImageToolkitBase {
     $form['suite']['version'] = [
       '#type' => 'details',
       '#collapsible' => TRUE,
-      '#collapsed' => TRUE,
+      '#open' => TRUE,
       '#title' => $this->t('Version information'),
       '#description' => '<pre>' . implode('<br />', $version_info) . '</pre>',
     ];
@@ -242,32 +244,24 @@ class ImagemagickToolkit extends ImageToolkitBase {
     // Image formats.
     $form['formats'] = [
       '#type' => 'details',
-      '#open' => TRUE,
-      '#collapsible' => FALSE,
       '#title' => $this->t('Image formats'),
-    ];
-    // Use 'identify' command.
-    $form['formats']['use_identify'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Use "identify"'),
-      '#default_value' => $config->get('use_identify'),
-      '#description' => $this->t('Use the <kbd>identify</kbd> command to parse image files to determine image format and dimensions. If not selected, the PHP <kbd>getimagesize</kbd> function will be used, BUT this will limit the image formats supported by the toolkit.'),
+      '#group' => 'imagemagick_settings',
     ];
     // Image formats enabled in the toolkit.
     $form['formats']['enabled'] = [
       '#type' => 'item',
-      '#title' => $this->t('Enabled images'),
+      '#title' => $this->t('Currently enabled images'),
       '#description' => $this->t("@suite formats: %formats<br />Image file extensions: %extensions", [
         '%formats' => implode(', ', $this->formatMapper->getEnabledFormats()),
         '%extensions' => Unicode::strtolower(implode(', ', static::getSupportedExtensions())),
-        '@suite' => $this->getPackageLabel(),
+        '@suite' => $this->getExecManager()->getPackageLabel(),
       ]),
     ];
     // Image formats map.
     $form['formats']['mapping'] = [
       '#type' => 'details',
       '#collapsible' => TRUE,
-      '#collapsed' => TRUE,
+      '#open' => TRUE,
       '#title' => $this->t('Enable/disable image formats'),
       '#description' => $this->t("Edit the map below to enable/disable image formats. Enabled image file extensions will be determined by the enabled formats, through their MIME types. More information in the module's README.txt"),
     ];
@@ -278,16 +272,17 @@ class ImagemagickToolkit extends ImageToolkitBase {
     ];
     // Image formats supported by the package.
     if (empty($status['errors'])) {
-      $this->addArgument('-list format');
-      $this->imagemagickExec('convert', $output);
-      $this->resetArguments();
+      $this->arguments()->add('-list format', ImagemagickExecArguments::PRE_SOURCE);
+      $output = NULL;
+      $this->getExecManager()->execute('convert', $this->arguments(), $output);
+      $this->arguments()->reset();
       $formats_info = implode('<br />', explode("\n", preg_replace('/\r/', '', Html::escape($output))));
       $form['formats']['list'] = [
         '#type' => 'details',
         '#collapsible' => TRUE,
-        '#collapsed' => TRUE,
+        '#open' => FALSE,
         '#title' => $this->t('Format list'),
-        '#description' => $this->t("Supported image formats returned by executing <kbd>'convert -list format'</kbd>. <b>Note:</b> these are the formats supported by the installed @suite executable, <b>not</b> by the toolkit.<br /><br />", ['@suite' => $this->getPackageLabel()]),
+        '#description' => $this->t("Supported image formats returned by executing <kbd>'convert -list format'</kbd>. <b>Note:</b> these are the formats supported by the installed @suite executable, <b>not</b> by the toolkit.<br /><br />", ['@suite' => $this->getExecManager()->getPackageLabel()]),
       ];
       $form['formats']['list']['list'] = [
         '#markup' => "<pre>" . $formats_info . "</pre>",
@@ -297,25 +292,78 @@ class ImagemagickToolkit extends ImageToolkitBase {
     // Execution options.
     $form['exec'] = [
       '#type' => 'details',
-      '#open' => TRUE,
-      '#collapsible' => FALSE,
       '#title' => $this->t('Execution options'),
+      '#group' => 'imagemagick_settings',
+    ];
+
+    // Use 'identify' command.
+    $form['exec']['use_identify'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Use "identify"'),
+      '#default_value' => $config->get('use_identify'),
+      '#description' => $this->t('<strong>This setting is deprecated and will be removed in the next major release of the Imagemagick module. Leave it enabled to ensure smooth transition.</strong>') . ' ' . $this->t('Use the <kbd>identify</kbd> command to parse image files to determine image format and dimensions. If not selected, the PHP <kbd>getimagesize</kbd> function will be used, BUT this will limit the image formats supported by the toolkit.'),
+    ];
+    // Cache metadata.
+    $configure_link = Link::fromTextAndUrl(
+      $this->t('Configure File Metadata Manager'),
+      Url::fromRoute('file_mdm.settings')
+    );
+    $form['exec']['metadata_caching'] = [
+      '#type' => 'item',
+      '#title' => $this->t("Cache image metadata"),
+      '#description' => $this->t("The File Metadata Manager module allows to cache image metadata. This reduces file I/O and <kbd>shell</kbd> calls. @configure.", [
+        '@configure' => $configure_link->toString(),
+      ]),
     ];
     // Prepend arguments.
     $form['exec']['prepend'] = [
-      '#type' => 'textfield',
+      '#type' => 'details',
+      '#collapsible' => FALSE,
+      '#open' => TRUE,
       '#title' => $this->t('Prepend arguments'),
+      '#description' => $this->t("Use this to add e.g. <kbd><a href=':limit-url'>-limit</a></kbd> or <kbd><a href=':debug-url'>-debug</a></kbd> arguments in front of the others when executing the <kbd>identify</kbd> and <kbd>convert</kbd> commands. Select 'Before source' to execute the arguments before loading the source image.", [
+        ':limit-url' => 'https://www.imagemagick.org/script/command-line-options.php#limit',
+        ':debug-url' => 'https://www.imagemagick.org/script/command-line-options.php#debug',
+      ]),
+    ];
+    $form['exec']['prepend']['container'] = [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => ['container-inline'],
+      ],
+    ];
+    $form['exec']['prepend']['container']['prepend'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Arguments'),
       '#default_value' => $config->get('prepend'),
       '#required' => FALSE,
-      '#description' => $this->t('Use this to add e.g. <kbd>-limit</kbd> or <kbd>-debug</kbd> arguments in front of the others when executing the <kbd>identify</kbd> and <kbd>convert</kbd> commands.'),
     ];
+    $form['exec']['prepend']['container']['prepend_pre_source'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Before source'),
+      '#default_value' => $config->get('prepend_pre_source'),
+    ];
+
     // Locale.
     $form['exec']['locale'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Locale'),
       '#default_value' => $config->get('locale'),
       '#required' => FALSE,
-      '#description' => $this->t("The locale to be used to prepare the command passed to executables. The default, <kbd>'en_US.UTF-8'</kbd>, should work in most cases. If that is not available on the server, enter another locale. On *nix servers, type <kbd>'locale -a'</kbd> in a shell window to see a list of all locales available."),
+      '#description' => $this->t("The locale to be used to prepare the command passed to executables. The default, <kbd>'en_US.UTF-8'</kbd>, should work in most cases. If that is not available on the server, enter another locale. 'Installed Locales' below provides a list of locales installed on the server."),
+    ];
+    // Installed locales.
+    $locales = $this->getExecManager()->getInstalledLocales();
+    $locales_info = implode('<br />', explode("\n", preg_replace('/\r/', '', Html::escape($locales))));
+    $form['exec']['installed_locales'] = [
+      '#type' => 'details',
+      '#collapsible' => TRUE,
+      '#open' => FALSE,
+      '#title' => $this->t('Installed locales'),
+      '#description' => $this->t("This is the list of all locales available on this server. It is the output of executing <kbd>'locale -a'</kbd> on the operating system."),
+    ];
+    $form['exec']['installed_locales']['list'] = [
+      '#markup' => "<pre>" . $locales_info . "</pre>",
     ];
     // Log warnings.
     $form['exec']['log_warnings'] = [
@@ -337,9 +385,8 @@ class ImagemagickToolkit extends ImageToolkitBase {
     // Advanced image settings.
     $form['advanced'] = [
       '#type' => 'details',
-      '#collapsible' => TRUE,
-      '#collapsed' => TRUE,
       '#title' => $this->t('Advanced image settings'),
+      '#group' => 'imagemagick_settings',
     ];
     $form['advanced']['density'] = [
       '#type' => 'checkbox',
@@ -383,6 +430,16 @@ class ImagemagickToolkit extends ImageToolkitBase {
     return $form;
   }
 
+  /**
+   * Returns the ImageMagick execution manager service.
+   *
+   * @return \Drupal\imagemagick\ImagemagickExecManagerInterface
+   *   The ImageMagick execution manager service.
+   */
+  public function getExecManager() {
+    return $this->execManager;
+  }
+
   /**
    * Gets the binaries package in use.
    *
@@ -392,12 +449,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    * @return string
    *   The default package ('imagemagick'|'graphicsmagick'), or the $package
    *   argument.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecManagerInterface::getPackage() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function getPackage($package = NULL) {
-    if ($package === NULL) {
-      $package = $this->configFactory->get('imagemagick.settings')->get('binaries');
-    }
-    return $package;
+    @trigger_error('getPackage() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecManagerInterface::getPackage() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    return $this->getExecManager()->getPackage($package);
   }
 
   /**
@@ -409,19 +469,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    * @return string
    *   A translated label of the binaries package in use, or the $package
    *   argument.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecManagerInterface::getPackageLabel() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function getPackageLabel($package = NULL) {
-    switch ($this->getPackage($package)) {
-      case 'imagemagick':
-        return $this->t('ImageMagick');
-
-      case 'graphicsmagick':
-        return $this->t('GraphicsMagick');
-
-      default:
-        return $package;
-
-    }
+    @trigger_error('getPackageLabel() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecManagerInterface::getPackageLabel() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    return $this->getExecManager()->getPackageLabel($package);
   }
 
   /**
@@ -437,53 +493,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   - output: The shell output of 'convert -version', if any.
    *   - errors: A list of error messages indicating if the executable could
    *     not be found or executed.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecManagerInterface::checkPath() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function checkPath($path, $package = NULL) {
-    $status = [
-      'output' => '',
-      'errors' => [],
-    ];
-
-    // Execute gm or convert based on settings.
-    $package = $package ?: $this->getPackage();
-    $binary = $package === 'imagemagick' ? 'convert' : 'gm';
-    $executable = $this->getExecutable($binary, $path);
-
-    // If a path is given, we check whether the binary exists and can be
-    // invoked.
-    if (!empty($path)) {
-      // Check whether the given file exists.
-      if (!is_file($executable)) {
-        $status['errors'][] = $this->t('The @suite executable %file does not exist.', ['@suite' => $this->getPackageLabel($package), '%file' => $executable]);
-      }
-      // If it exists, check whether we can execute it.
-      elseif (!is_executable($executable)) {
-        $status['errors'][] = $this->t('The @suite file %file is not executable.', ['@suite' => $this->getPackageLabel($package), '%file' => $executable]);
-      }
-    }
-
-    // In case of errors, check for open_basedir restrictions.
-    if ($status['errors'] && ($open_basedir = ini_get('open_basedir'))) {
-      $status['errors'][] = $this->t('The PHP <a href=":php-url">open_basedir</a> security restriction is set to %open-basedir, which may prevent to locate the @suite executable.', [
-        '@suite' => $this->getPackageLabel($package),
-        '%open-basedir' => $open_basedir,
-        ':php-url' => 'http://php.net/manual/en/ini.core.php#ini.open-basedir',
-      ]);
-    }
-
-    // Unless we had errors so far, try to invoke convert.
-    if (!$status['errors']) {
-      $error = NULL;
-      $this->runOsShell($executable, '-version', $package, $status['output'], $error);
-      if ($error !== '') {
-        // $error normally needs check_plain(), but file system errors on
-        // Windows use a unknown encoding. check_plain() would eliminate the
-        // entire string.
-        $status['errors'][] = $error;
-      }
-    }
-
-    return $status;
+    @trigger_error('checkPath() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecManagerInterface::checkPath() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    return $this->getExecManager()->checkPath($path, $package);
   }
 
   /**
@@ -492,7 +510,9 @@ class ImagemagickToolkit extends ImageToolkitBase {
   public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
     try {
       // Check that the format map contains valid YAML.
-      $image_formats = Yaml::decode($form_state->getValue(['imagemagick', 'formats', 'mapping', 'image_formats']));
+      $image_formats = Yaml::decode($form_state->getValue([
+        'imagemagick', 'formats', 'mapping', 'image_formats',
+      ]));
       // Validate the enabled image formats.
       $errors = $this->formatMapper->validateMap($image_formats);
       if ($errors) {
@@ -507,7 +527,9 @@ class ImagemagickToolkit extends ImageToolkitBase {
     // it will prevent the entire image toolkit selection form from being
     // submitted.
     if ($form_state->getValue(['image_toolkit']) === 'imagemagick') {
-      $status = $this->checkPath($form_state->getValue(['imagemagick', 'suite', 'path_to_binaries']), $form_state->getValue(['imagemagick', 'suite', 'binaries']));
+      $status = $this->getExecManager()->checkPath($form_state->getValue([
+        'imagemagick', 'suite', 'path_to_binaries',
+      ]), $form_state->getValue(['imagemagick', 'suite', 'binaries']));
       if ($status['errors']) {
         $form_state->setErrorByName('imagemagick][suite][path_to_binaries', new FormattableMarkup(implode('<br />', $status['errors']), []));
       }
@@ -518,20 +540,48 @@ class ImagemagickToolkit extends ImageToolkitBase {
    * {@inheritdoc}
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
-    $this->configFactory->getEditable('imagemagick.settings')
-      ->set('quality', $form_state->getValue(['imagemagick', 'quality']))
-      ->set('binaries', $form_state->getValue(['imagemagick', 'suite', 'binaries']))
-      ->set('path_to_binaries', $form_state->getValue(['imagemagick', 'suite', 'path_to_binaries']))
-      ->set('use_identify', $form_state->getValue(['imagemagick', 'formats', 'use_identify']))
-      ->set('image_formats', Yaml::decode($form_state->getValue(['imagemagick', 'formats', 'mapping', 'image_formats'])))
-      ->set('prepend', $form_state->getValue(['imagemagick', 'exec', 'prepend']))
-      ->set('locale', $form_state->getValue(['imagemagick', 'exec', 'locale']))
-      ->set('log_warnings', (bool) $form_state->getValue(['imagemagick', 'exec', 'log_warnings']))
-      ->set('debug', $form_state->getValue(['imagemagick', 'exec', 'debug']))
-      ->set('advanced.density', $form_state->getValue(['imagemagick', 'advanced', 'density']))
-      ->set('advanced.colorspace', $form_state->getValue(['imagemagick', 'advanced', 'colorspace']))
-      ->set('advanced.profile', $form_state->getValue(['imagemagick', 'advanced', 'profile']))
-      ->save();
+    $config = $this->configFactory->getEditable('imagemagick.settings');
+    $config
+      ->set('quality', (int) $form_state->getValue([
+        'imagemagick', 'quality',
+      ]))
+      ->set('binaries', (string) $form_state->getValue([
+        'imagemagick', 'suite', 'binaries',
+      ]))
+      ->set('path_to_binaries', (string) $form_state->getValue([
+        'imagemagick', 'suite', 'path_to_binaries',
+      ]))
+      ->set('use_identify', (bool) $form_state->getValue([
+        'imagemagick', 'exec', 'use_identify',
+      ]))
+      ->set('image_formats', Yaml::decode($form_state->getValue([
+        'imagemagick', 'formats', 'mapping', 'image_formats',
+      ])))
+      ->set('prepend', (string) $form_state->getValue([
+        'imagemagick', 'exec', 'prepend', 'container', 'prepend',
+      ]))
+      ->set('prepend_pre_source', (bool) $form_state->getValue([
+        'imagemagick', 'exec', 'prepend', 'container', 'prepend_pre_source',
+      ]))
+      ->set('locale', (string) $form_state->getValue([
+        'imagemagick', 'exec', 'locale',
+      ]))
+      ->set('log_warnings', (bool) $form_state->getValue([
+        'imagemagick', 'exec', 'log_warnings',
+      ]))
+      ->set('debug', (bool) $form_state->getValue([
+        'imagemagick', 'exec', 'debug',
+      ]))
+      ->set('advanced.density', (int) $form_state->getValue([
+        'imagemagick', 'advanced', 'density',
+      ]))
+      ->set('advanced.colorspace', (string) $form_state->getValue([
+        'imagemagick', 'advanced', 'colorspace',
+      ]))
+      ->set('advanced.profile', (string) $form_state->getValue([
+        'imagemagick', 'advanced', 'profile',
+      ]));
+    $config->save();
   }
 
   /**
@@ -541,14 +591,53 @@ class ImagemagickToolkit extends ImageToolkitBase {
     return ((bool) $this->getMimeType());
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setSource($source) {
+    parent::setSource($source);
+    $this->arguments()->setSource($source);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSource() {
+    return $this->arguments()->getSource();
+  }
+
   /**
    * Gets the local filesystem path to the image file.
    *
    * @return string
    *   A filesystem path.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ::ensureSourceLocalPath() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function getSourceLocalPath() {
-    return $this->sourceLocalPath;
+    @trigger_error('getSourceLocalPath() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::ensureSourceLocalPath() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    return $this->ensureSourceLocalPath();
+  }
+
+  /**
+   * Ensures that the local filesystem path to the image file exists.
+   *
+   * @return string
+   *   A filesystem path.
+   */
+  public function ensureSourceLocalPath() {
+    // If sourceLocalPath is NULL, then ensure it is prepared. This can
+    // happen if image was identified via cached metadata: the cached data are
+    // available, but the temp file path is not resolved, or even the temp file
+    // could be missing if it was copied locally from a remote file system.
+    if (!$this->arguments()->getSourceLocalPath() && $this->getSource()) {
+      $this->moduleHandler->alter('imagemagick_pre_parse_file', $this->arguments);
+    }
+    return $this->arguments()->getSourceLocalPath();
   }
 
   /**
@@ -558,9 +647,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   A filesystem path.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::setSourceLocalPath() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function setSourceLocalPath($path) {
-    $this->sourceLocalPath = $path;
+    @trigger_error('setSourceLocalPath() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::setSourceLocalPath() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    $this->arguments()->setSourceLocalPath($path);
     return $this;
   }
 
@@ -569,9 +664,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *
    * @return string
    *   The source image format.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::getSourceFormat() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function getSourceFormat() {
-    return $this->sourceFormat;
+    @trigger_error('getSourceFormat() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::getSourceFormat() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    return $this->arguments()->getSourceFormat();
   }
 
   /**
@@ -581,9 +682,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The image format.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::setSourceFormat() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function setSourceFormat($format) {
-    $this->sourceFormat = $this->formatMapper->isFormatEnabled($format) ? $format : '';
+    @trigger_error('setSourceFormat() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::setSourceFormat() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    $this->arguments()->setSourceFormat($format);
     return $this;
   }
 
@@ -594,43 +701,104 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The image file extension.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::setSourceFormatFromExtension() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function setSourceFormatFromExtension($extension) {
-    $format = $this->formatMapper->getFormatFromExtension($extension);
-    $this->sourceFormat = $format ?: '';
+    @trigger_error('setSourceFormatFromExtension() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::setSourceFormatFromExtension() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    $this->arguments()->setSourceFormatFromExtension($extension);
     return $this;
   }
 
   /**
    * Gets the source EXIF orientation.
    *
-   * @return integer
+   * @return int
    *   The source EXIF orientation.
    */
   public function getExifOrientation() {
-    if (empty($this->exifInfo)) {
-      $this->parseExifData();
+    if ($this->exifOrientation === static::EXIF_ORIENTATION_NOT_FETCHED) {
+      if ($this->getSource() !== NULL) {
+        $file_md = $this->fileMetadataManager->uri($this->getSource());
+        if ($file_md->getLocalTempPath() === NULL) {
+          $file_md->setLocalTempPath($this->ensureSourceLocalPath());
+        }
+        $orientation = $file_md->getMetadata('exif', 'Orientation');
+        $this->setExifOrientation(isset($orientation['value']) ? $orientation['value'] : NULL);
+      }
+      else {
+        $this->setExifOrientation(NULL);
+      }
     }
-    return isset($this->exifInfo['Orientation']) ? $this->exifInfo['Orientation'] : NULL;
+    return $this->exifOrientation;
   }
 
   /**
    * Sets the source EXIF orientation.
    *
-   * @param integer|null $exif_orientation
+   * @param int|null $exif_orientation
    *   The EXIF orientation.
    *
    * @return $this
    */
   public function setExifOrientation($exif_orientation) {
-    $this->exifInfo['Orientation'] = !empty($exif_orientation) ? ((int) $exif_orientation !== 0 ? (int) $exif_orientation : NULL) : NULL;
+    $this->exifOrientation = $exif_orientation ? (int) $exif_orientation : NULL;
+    return $this;
+  }
+
+  /**
+   * Gets the source colorspace.
+   *
+   * @return string
+   *   The source colorspace.
+   */
+  public function getColorspace() {
+    return $this->colorspace;
+  }
+
+  /**
+   * Sets the source colorspace.
+   *
+   * @param string $colorspace
+   *   The image colorspace.
+   *
+   * @return $this
+   */
+  public function setColorspace($colorspace) {
+    $this->colorspace = Unicode::strtoupper($colorspace);
+    return $this;
+  }
+
+  /**
+   * Gets the source profiles.
+   *
+   * @return string[]
+   *   The source profiles.
+   */
+  public function getProfiles() {
+    return $this->profiles;
+  }
+
+  /**
+   * Sets the source profiles.
+   *
+   * @param array $profiles
+   *   The image profiles.
+   *
+   * @return $this
+   */
+  public function setProfiles(array $profiles) {
+    $this->profiles = $profiles;
     return $this;
   }
 
   /**
    * Gets the source image number of frames.
    *
-   * @return integer
+   * @return int
    *   The number of frames of the image.
    */
   public function getFrames() {
@@ -640,7 +808,7 @@ class ImagemagickToolkit extends ImageToolkitBase {
   /**
    * Sets the source image number of frames.
    *
-   * @param integer|null $frames
+   * @param int|null $frames
    *   The number of frames of the image.
    *
    * @return $this
@@ -655,9 +823,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *
    * @return string
    *   The image destination URI/path.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::getDestination() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function getDestination() {
-    return $this->destination;
+    @trigger_error('getDestination() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::getDestination() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    return $this->arguments()->getDestination();
   }
 
   /**
@@ -667,9 +841,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The image destination URI/path.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::setDestination() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function setDestination($destination) {
-    $this->destination = $destination;
+    @trigger_error('setDestination() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::setDestination() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    $this->arguments()->setDestination($destination);
     return $this;
   }
 
@@ -678,9 +858,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *
    * @return string
    *   A filesystem path.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::getDestinationLocalPath() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function getDestinationLocalPath() {
-    return $this->destinationLocalPath;
+    @trigger_error('getDestinationLocalPath() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::getDestinationLocalPath() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    return $this->arguments()->getDestinationLocalPath();
   }
 
   /**
@@ -690,9 +876,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   A filesystem path.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::setDestinationLocalPath() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function setDestinationLocalPath($path) {
-    $this->destinationLocalPath = $path;
+    @trigger_error('setDestinationLocalPath() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::setDestinationLocalPath() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    $this->arguments()->setDestinationLocalPath($path);
     return $this;
   }
 
@@ -705,9 +897,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *
    * @return string
    *   The image destination format.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::getDestinationFormat() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function getDestinationFormat() {
-    return $this->destinationFormat;
+    @trigger_error('getDestinationFormat() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::getDestinationFormat() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    return $this->arguments()->getDestinationFormat();
   }
 
   /**
@@ -721,9 +919,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The image destination format.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::setDestinationFormat() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function setDestinationFormat($format) {
-    $this->destinationFormat = $this->formatMapper->isFormatEnabled($format) ? $format : '';
+    @trigger_error('setDestinationFormat() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::setDestinationFormat() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    $this->arguments()->setDestinationFormat($this->formatMapper->isFormatEnabled($format) ? $format : '');
     return $this;
   }
 
@@ -738,10 +942,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The destination image file extension.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImagemagickExecArguments::setDestinationFormatFromExtension() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2938375
    */
   public function setDestinationFormatFromExtension($extension) {
-    $format = $this->formatMapper->getFormatFromExtension($extension);
-    $this->destinationFormat = $format ?: '';
+    @trigger_error('setDestinationFormatFromExtension() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImagemagickExecArguments::setDestinationFormatFromExtension() instead. See https://www.drupal.org/project/imagemagick/issues/2938375.', E_USER_DEPRECATED);
+    $this->arguments()->setDestinationFormatFromExtension($extension);
     return $this;
   }
 
@@ -789,7 +998,17 @@ class ImagemagickToolkit extends ImageToolkitBase {
    * {@inheritdoc}
    */
   public function getMimeType() {
-    return $this->formatMapper->getMimeTypeFromFormat($this->getSourceFormat());
+    return $this->formatMapper->getMimeTypeFromFormat($this->arguments()->getSourceFormat());
+  }
+
+  /**
+   * Returns the current ImagemagickExecArguments object.
+   *
+   * @return \Drupal\imagemagick\ImagemagickExecArguments
+   *   The current ImagemagickExecArguments object.
+   */
+  public function arguments() {
+    return $this->arguments;
   }
 
   /**
@@ -797,9 +1016,33 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *
    * @return string[]
    *   The array of command line arguments.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::arguments()
+   *   instead, using ImagemagickExecArguments methods to manipulate arguments.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
    */
   public function getArguments() {
-    return $this->arguments ?: [];
+    @trigger_error('getArguments() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ::arguments() instead, using ImagemagickExecArguments methods to manipulate arguments. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    return $this->arguments()->getArguments();
+  }
+
+  /**
+   * Gets the command line arguments string for the binary.
+   *
+   * Removes any argument used internally within the toolkit.
+   *
+   * @return string
+   *   The string of command line arguments.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImageMagickExecArguments::toString() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
+   */
+  public function getStringForBinary() {
+    @trigger_error('getStringForBinary() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImageMagickExecArguments::toString() instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    return $this->arguments()->getStringForBinary();
   }
 
   /**
@@ -809,9 +1052,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The command line argument to be added.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImageMagickExecArguments::add() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
    */
   public function addArgument($arg) {
-    $this->arguments[] = $arg;
+    @trigger_error('addArgument() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImageMagickExecArguments::add() instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    $this->arguments()->addArgument($arg);
     return $this;
   }
 
@@ -822,9 +1071,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The command line argument to be prepended.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImageMagickExecArguments::add() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
    */
   public function prependArgument($arg) {
-    array_unshift($this->arguments, $arg);
+    @trigger_error('prependArgument() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImageMagickExecArguments::add() instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    $this->arguments()->prependArgument($arg);
     return $this;
   }
 
@@ -837,14 +1092,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    * @return bool
    *   Returns the array key for the argument if it is found in the array,
    *   FALSE otherwise.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImageMagickExecArguments::find() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2925780
    */
   public function findArgument($arg) {
-    foreach ($this->getArguments() as $i => $a) {
-      if (strpos($a, $arg) === 0) {
-        return $i;
-      }
-    }
-    return FALSE;
+    @trigger_error('findArgument() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImageMagickExecArguments::find() instead. See https://www.drupal.org/project/imagemagick/issues/2925780.', E_USER_DEPRECATED);
+    return $this->arguments()->findArgument($arg);
   }
 
   /**
@@ -854,11 +1110,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   The index of the command line argument to be removed.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImageMagickExecArguments::remove() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2936615
    */
   public function removeArgument($index) {
-    if (isset($this->arguments[$index])) {
-      unset($this->arguments[$index]);
-    }
+    @trigger_error('removeArgument() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImageMagickExecArguments::remove() instead. See https://www.drupal.org/project/imagemagick/issues/2936615.', E_USER_DEPRECATED);
+    $this->arguments()->removeArgument($index);
     return $this;
   }
 
@@ -866,9 +1126,15 @@ class ImagemagickToolkit extends ImageToolkitBase {
    * Resets the command line arguments.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImageMagickExecArguments::reset() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2936615
    */
   public function resetArguments() {
-    $this->arguments = [];
+    @trigger_error('resetArguments() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImageMagickExecArguments::reset() instead. See https://www.drupal.org/project/imagemagick/issues/2936615.', E_USER_DEPRECATED);
+    $this->arguments()->resetArguments();
     return $this;
   }
 
@@ -876,86 +1142,47 @@ class ImagemagickToolkit extends ImageToolkitBase {
    * Returns the count of command line arguments.
    *
    * @return $this
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImageMagickExecArguments::find() instead, then count the result.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2936615
    */
   public function countArguments() {
-    return count($this->arguments);
+    @trigger_error('countArguments() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImageMagickExecArguments::find() instead, then count the result. See https://www.drupal.org/project/imagemagick/issues/2936615.', E_USER_DEPRECATED);
+    return $this->arguments()->countArguments();
   }
 
   /**
    * Escapes a string.
    *
-   * PHP escapeshellarg() drops non-ascii characters, this is a replacement.
-   *
-   * Stop-gap replacement until core issue #1561214 has been solved. Solution
-   * proposed in #1502924-8.
-   *
-   * PHP escapeshellarg() on Windows also drops % (percentage sign) characters.
-   * We prevent this by replacing it with a pattern that should be highly
-   * unlikely to appear in the string itself and does not contain any
-   * "dangerous" character at all (very wide definition of dangerous). After
-   * escaping we replace that pattern back with a % character.
-   *
    * @param string $arg
    *   The string to escape.
    *
    * @return string
-   *   An escaped string for use in the ::imagemagickExec method.
+   *   An escaped string for use in the
+   *   ImagemagickExecManagerInterface::execute method.
+   *
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   ImageMagickExecArguments::escape() instead.
+   *
+   * @see https://www.drupal.org/project/imagemagick/issues/2936680
    */
   public function escapeShellArg($arg) {
-    static $percentage_sign_replace_pattern = '1357902468IMAGEMAGICKPERCENTSIGNPATTERN8642097531';
-
-    // Put the configured locale in a static to avoid multiple config get calls
-    // in the same request.
-    static $config_locale;
-
-    if (!isset($config_locale)) {
-      $config_locale = $this->configFactory->get('imagemagick.settings')->get('locale');
-      if (empty($config_locale)) {
-        $config_locale = FALSE;
-      }
-    }
-
-    if ($this->isWindows) {
-      // Temporarily replace % characters.
-      $arg = str_replace('%', $percentage_sign_replace_pattern, $arg);
-    }
-
-    // If no locale specified in config, return with standard.
-    if ($config_locale === FALSE) {
-      $arg_escaped = escapeshellarg($arg);
-    }
-    else {
-      // Get the current locale.
-      $current_locale = setlocale(LC_CTYPE, 0);
-      if ($current_locale != $config_locale) {
-        // Temporarily swap the current locale with the configured one.
-        setlocale(LC_CTYPE, $config_locale);
-        $arg_escaped = escapeshellarg($arg);
-        setlocale(LC_CTYPE, $current_locale);
-      }
-      else {
-        $arg_escaped = escapeshellarg($arg);
-      }
-    }
-
-    // Get our % characters back.
-    if ($this->isWindows) {
-      $arg_escaped = str_replace($percentage_sign_replace_pattern, '%', $arg_escaped);
-    }
-
-    return $arg_escaped;
+    @trigger_error('escapeShellArg() is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use ImageMagickExecArguments::escape() instead. See https://www.drupal.org/project/imagemagick/issues/2936680.', E_USER_DEPRECATED);
+    return $this->getExecManager()->escapeShellArg($arg);
   }
 
   /**
    * {@inheritdoc}
    */
   public function save($destination) {
-    $this->setDestination($destination);
+    $this->arguments()->setDestination($destination);
     if ($ret = $this->convert()) {
       // Allow modules to alter the destination file.
-      $this->moduleHandler->alter('imagemagick_post_save', $this);
+      $this->moduleHandler->alter('imagemagick_post_save', $this->arguments);
       // Reset local path to allow saving to other file.
-      $this->setDestinationLocalPath('');
+      $this->arguments()->setDestinationLocalPath('');
     }
     return $ret;
   }
@@ -964,8 +1191,6 @@ class ImagemagickToolkit extends ImageToolkitBase {
    * {@inheritdoc}
    */
   public function parseFile() {
-    // Allow modules to alter the source file.
-    $this->moduleHandler->alter('imagemagick_pre_parse_file', $this);
     if ($this->configFactory->get('imagemagick.settings')->get('use_identify')) {
       return $this->parseFileViaIdentify();
     }
@@ -981,397 +1206,118 @@ class ImagemagickToolkit extends ImageToolkitBase {
    *   TRUE if the file could be found and is an image, FALSE otherwise.
    */
   protected function parseFileViaIdentify() {
-    // Prepare the -format argument according to the graphics package in use.
-    switch ($this->getPackage()) {
-      case 'imagemagick':
-        $this->addArgument('-format ' . $this->escapeShellArg("format:%[magick]|width:%[width]|height:%[height]|exif_orientation:%[EXIF:Orientation]\\n"));
-        break;
-
-      case 'graphicsmagick':
-        $this->addArgument('-format ' . $this->escapeShellArg("format:%m|width:%w|height:%h|exif_orientation:%[EXIF:Orientation]\\n"));
-        break;
-
+    // Get 'imagemagick_identify' metadata for this image. The file metadata
+    // plugin will fetch it from the file via the ::identify() method if data
+    // is not already available.
+    $file_md = $this->fileMetadataManager->uri($this->getSource());
+    $data = $file_md->getMetadata('imagemagick_identify');
+
+    // No data, return.
+    if (!$data) {
+      return FALSE;
     }
 
-    if ($identify_output = $this->identify()) {
-      $frames = explode("\n", $identify_output);
-
-      // Remove empty items at the end of the array.
-      while (empty($frames[count($frames) - 1])) {
-        array_pop($frames);
-      }
-
-      // If remaining items are more than one, we have a multi-frame image.
-      if (count($frames) > 1) {
-        $this->setFrames(count($frames));
-      }
-
-      // Take information from the first frame.
-      $info = explode('|', $frames[0]);
-      $data = [];
-      foreach ($info as $item) {
-        list($key, $value) = explode(':', $item);
-        $data[trim($key)] = trim($value);
-      }
-      $format = isset($data['format']) ? $data['format'] : NULL;
-      if ($this->formatMapper->isFormatEnabled($format)) {
-        $this
-          ->setSourceFormat($format)
-          ->setWidth((int) $data['width'])
-          ->setHeight((int) $data['height'])
-          ->setExifOrientation($data['exif_orientation']);
-        return TRUE;
-      }
+    // Sets the local file path to the one retrieved by identify if available.
+    if ($source_local_path = $file_md->getMetadata('imagemagick_identify', 'source_local_path')) {
+      $this->arguments()->setSourceLocalPath($source_local_path);
     }
-    return FALSE;
-  }
 
-  /**
-   * Parses the image file using the PHP getimagesize() function.
-   *
-   * @return bool
-   *   TRUE if the file could be found and is an image, FALSE otherwise.
-   */
-  protected function parseFileViaGetImageSize() {
-    if ($data = @getimagesize($this->getSourceLocalPath())) {
-      $format = $this->formatMapper->getFormatFromExtension(image_type_to_extension($data[2], FALSE));
-      if ($format) {
-        $this
-          ->setSourceFormat($format)
-          ->setWidth($data[0])
-          ->setHeight($data[1]);
-        return TRUE;
+    // Process parsed data from the first frame.
+    $format = $file_md->getMetadata('imagemagick_identify', 'format');
+    if ($this->formatMapper->isFormatEnabled($format)) {
+      $this
+        ->setWidth((int) $file_md->getMetadata('imagemagick_identify', 'width'))
+        ->setHeight((int) $file_md->getMetadata('imagemagick_identify', 'height'))
+        ->setExifOrientation($file_md->getMetadata('imagemagick_identify', 'exif_orientation'))
+        ->setFrames($file_md->getMetadata('imagemagick_identify', 'frames_count'));
+      $this->arguments()
+        ->setSourceFormat($format);
+      // Only Imagemagick allows to get colorspace and profiles information
+      // via 'identify'.
+      if ($this->getExecManager()->getPackage() === 'imagemagick') {
+        $this->setColorspace($file_md->getMetadata('imagemagick_identify', 'colorspace'));
+        $this->setProfiles($file_md->getMetadata('imagemagick_identify', 'profiles'));
       }
-    };
-    return FALSE;
-  }
-
-  /**
-   * Parses the image file EXIF data using the PHP read_exif_data() function.
-   *
-   * @return $this
-   */
-  protected function parseExifData() {
-    $continue = TRUE;
-    // Test to see if EXIF is supported by the image format.
-    $mime_type = $this->getMimeType();
-    if (!in_array($mime_type, ['image/jpeg', 'image/tiff'])) {
-      // Not an EXIF enabled image.
-      $continue = FALSE;
-    }
-    $local_path = $this->getSourceLocalPath();
-    if ($continue && empty($local_path)) {
-      // No file path available. Most likely a new image from scratch.
-      $continue = FALSE;
-    }
-    if ($continue && !function_exists('exif_read_data')) {
-      // No PHP EXIF extension enabled, return.
-      $this->logger->error('The PHP EXIF extension is not installed. The \'imagemagick\' toolkit is unable to automatically determine image orientation.');
-      $continue = FALSE;
-    }
-    if ($continue && ($exif_data = @exif_read_data($this->getSourceLocalPath()))) {
-      $this->exifInfo = $exif_data;
-      return $this;
+      return TRUE;
     }
-    $this->setExifOrientation(NULL);
-    return $this;
-  }
 
-  /**
-   * Calls the identify executable on the specified file.
-   *
-   * @return bool
-   *   TRUE if the file could be identified, FALSE otherwise.
-   */
-  protected function identify() {
-    // Allow modules to alter the command line parameters.
-    $command = 'identify';
-    $this->moduleHandler->alter('imagemagick_arguments', $this, $command);
-
-    // Executes the command.
-    $output = NULL;
-    $ret = $this->imagemagickExec($command, $output);
-    $this->resetArguments();
-    return ($ret === TRUE) ? $output : FALSE;
+    return FALSE;
   }
 
   /**
-   * Calls the convert executable with the specified arguments.
+   * Parses the image file using the file metadata 'getimagesize' plugin.
    *
    * @return bool
-   *   TRUE if the file could be converted, FALSE otherwise.
-   */
-  protected function convert() {
-    // Allow modules to alter the command line parameters.
-    $command = 'convert';
-    $this->moduleHandler->alter('imagemagick_arguments', $this, $command);
-
-    // Executes the command.
-    return $this->imagemagickExec($command) === TRUE ? file_exists($this->getDestinationLocalPath()) : FALSE;
-  }
-
-  /**
-   * Executes the convert executable as shell command.
+   *   TRUE if the file could be found and is an image, FALSE otherwise.
    *
-   * @param string $command
-   *   The executable to run.
-   * @param string $command_args
-   *   A string containing arguments to pass to the command, which must have
-   *   been passed through $this->escapeShellArg() already.
-   * @param string &$output
-   *   (optional) A variable to assign the shell stdout to, passed by reference.
-   * @param string &$error
-   *   (optional) A variable to assign the shell stderr to, passed by reference.
-   * @param string $path
-   *   (optional) A custom file path to the executable binary.
+   * @deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use
+   *   parseFileViaIdentify() instead.
    *
-   * @return mixed
-   *   The return value depends on the shell command result:
-   *   - Boolean TRUE if the command succeeded.
-   *   - Boolean FALSE if the shell process could not be executed.
-   *   - Error exit status code integer returned by the executable.
+   * @see https://www.drupal.org/project/imagemagick/issues/2938377
    */
-  protected function imagemagickExec($command, &$output = NULL, &$error = NULL, $path = NULL) {
-    switch ($command) {
-      case 'convert':
-        $binary = $this->getPackage() === 'imagemagick' ? 'convert' : 'gm';
-        break;
-
-      case 'identify':
-        $binary = $this->getPackage() === 'imagemagick' ? 'identify' : 'gm';
-        break;
-
-    }
-    $cmd = $this->getExecutable($binary, $path);
-
-    if ($source_path = $this->getSourceLocalPath()) {
-      $source_path = $this->escapeShellArg($source_path);
-    }
-
-    if ($destination_path = $this->getDestinationLocalPath()) {
-      $destination_path = $this->escapeShellArg($destination_path);
-      // If the format of the derivative image has to be changed, concatenate
-      // the new image format and the destination path, delimited by a colon.
-      // @see http://www.imagemagick.org/script/command-line-processing.php#output
-      if (($format = $this->getDestinationFormat()) !== '') {
-        $destination_path = $format . ':' . $destination_path;
-      }
-    }
-
-    switch($command) {
-      case 'identify':
-        switch($this->getPackage()) {
-          case 'imagemagick':
-            // ImageMagick syntax:
-            // identify [arguments] source
-            $cmdline = implode(' ', $this->getArguments()) . ' ' . $source_path;
-            break;
-
-          case 'graphicsmagick':
-            // GraphicsMagick syntax:
-            // gm identify [arguments] source
-            $cmdline = 'identify ' . implode(' ', $this->getArguments()) . ' ' . $source_path;
-            break;
-
-        }
-        break;
-
-      case 'convert':
-        switch($this->getPackage()) {
-          case 'imagemagick':
-            // ImageMagick syntax:
-            // convert input [arguments] output
-            // @see http://www.imagemagick.org/Usage/basics/#cmdline
-            $cmdline = $source_path . ' ' . implode(' ', $this->getArguments()) . ' ' . $destination_path;
-            break;
-
-          case 'graphicsmagick':
-            // GraphicsMagick syntax:
-            // gm convert [arguments] input output
-            // @see http://www.graphicsmagick.org/GraphicsMagick.html
-            $cmdline = 'convert ' . implode(' ', $this->getArguments()) . ' '  . $source_path . ' ' . $destination_path;
-            break;
+  protected function parseFileViaGetImageSize() {
+    @trigger_error('Image file parsing via \'getimagesize\' is deprecated in 8.x-2.3, will be removed in 8.x-3.0. Use parsing via \'identify\' instead. See https://www.drupal.org/project/imagemagick/issues/2938377.', E_USER_DEPRECATED);
+    // Allow modules to alter the source file.
+    $this->moduleHandler->alter('imagemagick_pre_parse_file', $this->arguments);
 
-        }
-        break;
+    // Get 'getimagesize' metadata for this image.
+    $file_md = $this->fileMetadataManager->uri($this->getSource());
+    $data = $file_md->getMetadata('getimagesize');
 
+    // No data, return.
+    if (!$data) {
+      return FALSE;
     }
 
-    $return_code = $this->runOsShell($cmd, $cmdline, $this->getPackage(), $output, $error);
-
-    if ($return_code !== FALSE) {
-      // If the executable returned a non-zero code, log to the watchdog.
-      if ($return_code != 0) {
-        if ($error === '') {
-          // If there is no error message, and allowed in config, log a
-          // warning.
-          if ($this->configFactory->get('imagemagick.settings')->get('log_warnings') === TRUE) {
-            $this->logger->warning("@suite returned with code @code [command: @command @cmdline]", [
-              '@suite' => $this->getPackageLabel(),
-              '@code' => $return_code,
-              '@command' => $cmd,
-              '@cmdline' => $cmdline,
-            ]);
-          }
-        }
-        else {
-          // Log $error with context information.
-          $this->logger->error("@suite error @code: @error [command: @command @cmdline]", [
-            '@suite' => $this->getPackageLabel(),
-            '@code' => $return_code,
-            '@error' => $error,
-            '@command' => $cmd,
-            '@cmdline' => $cmdline,
-          ]);
-        }
-        // Executable exited with an error code, return it.
-        return $return_code;
-      }
-
-      // The shell command was executed successfully.
+    // Process parsed data.
+    $format = $this->formatMapper->getFormatFromExtension(image_type_to_extension($data[2], FALSE));
+    if ($format) {
+      $this
+        ->setWidth($data[0])
+        ->setHeight($data[1])
+        // 'getimagesize' cannot provide information on number of frames in an
+        // image and EXIF orientation, so set to defaults.
+        ->setExifOrientation(static::EXIF_ORIENTATION_NOT_FETCHED)
+        ->setFrames(NULL);
+      $this->arguments()
+        ->setSourceFormat($format);
       return TRUE;
     }
-    // The shell command could not be executed.
-    return FALSE;
-  }
-
-  /**
-   * Executes a command on the operating system.
-   *
-   * @param string $command
-   *   The command to run.
-   * @param string $arguments
-   *   The arguments of the command to run.
-   * @param string $id
-   *   An identifier for the process to be spawned on the operating system.
-   * @param string &$output
-   *   (optional) A variable to assign the shell stdout to, passed by
-   *   reference.
-   * @param string &$error
-   *   (optional) A variable to assign the shell stderr to, passed by
-   *   reference.
-   *
-   * @return int|bool
-   *   The operating system returned code, or FALSE if it was not possible to
-   *   execute the command.
-   */
-  protected function runOsShell($command, $arguments, $id, &$output = NULL, &$error = NULL) {
-    if ($this->isWindows) {
-      // Use Window's start command with the /B flag to make the process run in
-      // the background and avoid a shell command line window from showing up.
-      // @see http://us3.php.net/manual/en/function.exec.php#56599
-      // Use /D to run the command from PHP's current working directory so the
-      // file paths don't have to be absolute.
-      $command = 'start "' . $id . '" /D ' . $this->escapeShellArg($this->appRoot) . ' /B ' . $this->escapeShellArg($command);
-    }
-    $command_line = $command . ' ' . $arguments;
-
-    // Executes the command on the OS via proc_open().
-    $descriptors = [
-      // This is stdin.
-      0 => ['pipe', 'r'],
-      // This is stdout.
-      1 => ['pipe', 'w'],
-      // This is stderr.
-      2 => ['pipe', 'w'],
-    ];
-
-    if ($h = proc_open($command_line, $descriptors, $pipes, $this->appRoot)) {
-      $output = '';
-      while (!feof($pipes[1])) {
-        $output .= fgets($pipes[1]);
-      }
-      $output = utf8_encode($output);
-      $error = '';
-      while (!feof($pipes[2])) {
-        $error .= fgets($pipes[2]);
-      }
-      $error = utf8_encode($error);
-      fclose($pipes[0]);
-      fclose($pipes[1]);
-      fclose($pipes[2]);
-      $return_code = proc_close($h);
-    }
-    else {
-      $return_code = FALSE;
-    }
 
-    // Process debugging information if required.
-    if ($this->configFactory->get('imagemagick.settings')->get('debug')) {
-      $this->debugMessage('@suite command: <pre>@raw</pre>', [
-        '@suite' => $this->getPackageLabel($id),
-        '@raw' => print_r($command_line, TRUE),
-      ]);
-      if ($output !== '') {
-        $this->debugMessage('@suite output: <pre>@raw</pre>', [
-          '@suite' => $this->getPackageLabel($id),
-          '@raw' => print_r($output, TRUE),
-        ]);
-      }
-      if ($error !== '') {
-        $this->debugMessage('@suite error @return_code: <pre>@raw</pre>', [
-          '@suite' => $this->getPackageLabel($id),
-          '@return_code' => $return_code,
-          '@raw' => print_r($error, TRUE),
-        ]);
-      }
-    }
-
-    return $return_code;
+    return FALSE;
   }
 
   /**
-   * Logs a debug message, and shows it on the screen for authorized users.
+   * Calls the convert executable with the specified arguments.
    *
-   * @param string $message
-   *   The debug message.
-   * @param string[] $context
-   *   Context information.
+   * @return bool
+   *   TRUE if the file could be converted, FALSE otherwise.
    */
-  public function debugMessage($message, array $context) {
-    $this->logger->debug($message, $context);
-    if (\Drupal::currentUser()->hasPermission('administer site configuration')) {
-      // Strips raw text longer than 10 lines to optimize displaying.
-      if (isset($context['@raw'])) {
-        $raw = explode("\n", $context['@raw']);
-        if (count($raw) > 10) {
-          $tmp = [];
-          for ($i = 0; $i < 9; $i++) {
-            $tmp[] = $raw[$i];
-          }
-          $tmp[] = (string) $this->t('[Further text stripped. The watchdog log has the full text.]');
-          $context['@raw'] = implode("\n", $tmp);
-        }
-      }
-      drupal_set_message($this->t($message, $context), 'status', TRUE);
-    }
-  }
+  protected function convert() {
+    $config = $this->configFactory->get('imagemagick.settings');
 
-  /**
-   * Returns the full path to the executable.
-   *
-   * @param string $binary
-   *   The program to execute, typically 'convert', 'identify' or 'gm'.
-   * @param string $path
-   *   (optional) A custom path to the folder of the executable. When left
-   *   empty, the setting imagemagick.settings.path_to_binaries is taken.
-   *
-   * @return string
-   *   The full path to the executable.
-   */
-  public function getExecutable($binary, $path = NULL) {
-    // $path is only passed from the validation of the image toolkit form, on
-    // which the path to convert is configured. @see ::checkPath()
-    if (!isset($path)) {
-      $path = $this->configFactory->get('imagemagick.settings')->get('path_to_binaries');
-    }
+    // Ensure sourceLocalPath is prepared.
+    $this->ensureSourceLocalPath();
 
-    $executable = $binary;
-    if ($this->isWindows) {
-      $executable .= '.exe';
+    // Allow modules to alter the command line parameters.
+    $command = 'convert';
+    $this->moduleHandler->alter('imagemagick_arguments', $this->arguments, $command);
+
+    // Delete any cached file metadata for the destination image file, before
+    // creating a new one, and release the URI from the manager so that
+    // metadata will not stick in the same request.
+    $this->fileMetadataManager->deleteCachedMetadata($this->arguments()->getDestination());
+    $this->fileMetadataManager->release($this->arguments()->getDestination());
+
+    // When destination format differs from source format, and source image
+    // is multi-frame, convert only the first frame.
+    $destination_format = $this->arguments()->getDestinationFormat() ?: $this->arguments()->getSourceFormat();
+    if ($this->arguments()->getSourceFormat() !== $destination_format && ($this->getFrames() === NULL || $this->getFrames() > 1)) {
+      $this->arguments()->setSourceFrames('[0]');
     }
 
-    return $path . $executable;
+    // Execute the command and return.
+    return $this->getExecManager()->execute($command, $this->arguments) && file_exists($this->arguments()->getDestinationLocalPath());
   }
 
   /**
@@ -1388,7 +1334,7 @@ class ImagemagickToolkit extends ImageToolkitBase {
       ]);
     }
     else {
-      $status = $this->checkPath($this->configFactory->get('imagemagick.settings')->get('path_to_binaries'));
+      $status = $this->getExecManager()->checkPath($this->configFactory->get('imagemagick.settings')->get('path_to_binaries'));
       if (!empty($status['errors'])) {
         // Can not execute 'convert'.
         $severity = REQUIREMENT_ERROR;
@@ -1401,9 +1347,10 @@ class ImagemagickToolkit extends ImageToolkitBase {
         // No errors, report the version information.
         $severity = REQUIREMENT_INFO;
         $version_info = explode("\n", preg_replace('/\r/', '', Html::escape($status['output'])));
+        $value = array_shift($version_info);
         $more_info_available = FALSE;
         foreach ($version_info as $key => $item) {
-          if (stripos($item, 'feature') !== FALSE || $key > 4) {
+          if (stripos($item, 'feature') !== FALSE || $key > 3) {
             $more_info_available = TRUE;
             break;
 
@@ -1419,15 +1366,31 @@ class ImagemagickToolkit extends ImageToolkitBase {
         ]);
       }
     }
-    return [
+    $requirements = [
       'imagemagick' => [
         'title' => $this->t('ImageMagick'),
+        'value' => isset($value) ? $value : NULL,
         'description' => [
           '#markup' => implode('<br />', $reported_info),
         ],
         'severity' => $severity,
       ],
     ];
+
+    // Warn if parsing via 'getimagesize'.
+    // @todo remove in 8.x-3.0.
+    if ($this->configFactory->getEditable('imagemagick.settings')->get('use_identify') === FALSE) {
+      $requirements['imagemagick_getimagesize'] = [
+        'title' => $this->t('ImageMagick'),
+        'value' => $this->t('Use "identify" to parse image files'),
+        'description' => $this->t('The toolkit is set to use the <kbd>getimagesize</kbd> PHP function to parse image files. This functionality will be dropped in the next major release of the Imagemagick module. Go to the <a href=":url">Image toolkit</a> settings page, and ensure that the \'Use "identify"\' flag in the \'Execution options\' tab is selected.', [
+          ':url' => Url::fromRoute('system.image_toolkit_settings')->toString(),
+        ]),
+        'severity' => REQUIREMENT_WARNING,
+      ];
+    }
+
+    return $requirements;
   }
 
   /**
index 7015d4a5ec2724c18a0afc6c84799c408934084e..b5865ec016f24fd9b4cdc6620568d0d7887f0e64 100644 (file)
@@ -40,16 +40,7 @@ class Convert extends ImagemagickImageToolkitOperationBase {
    * {@inheritdoc}
    */
   protected function execute(array $arguments) {
-    // When source image is multi-frame, convert only the first frame.
-    if ($this->getToolkit()->getFrames()) {
-      $path = $this->getToolkit()->getSourceLocalPath();
-      if (strripos($path, '[0]', -3) === FALSE) {
-        $this->getToolkit()->setSourceLocalPath($path . '[0]');
-      }
-    }
-    $this->getToolkit()
-      ->setFrames(NULL)
-      ->setDestinationFormatFromExtension($arguments['extension']);
+    $this->getToolkit()->arguments()->setDestinationFormatFromExtension($arguments['extension']);
     return TRUE;
   }
 
index 1b0c087b7735407e6b707559b680334b5cf575cd..37636cf7d36245cd930ed8607c74abd997fd9e3a 100644 (file)
@@ -75,24 +75,27 @@ class CreateNew extends ImagemagickImageToolkitOperationBase {
    */
   protected function execute(array $arguments) {
     $this->getToolkit()
-      ->resetArguments()
-      ->setSourceLocalPath('')
-      ->setSourceFormatFromExtension($arguments['extension'])
       ->setWidth($arguments['width'])
       ->setHeight($arguments['height'])
       ->setExifOrientation(NULL)
-      ->setFrames(NULL);
+      ->setColorspace($this->getToolkit()->getExecManager()->getPackage() === 'imagemagick' ? 'sRGB' : NULL)
+      ->setProfiles([])
+      ->setFrames(1);
+    $this->getToolkit()->arguments()
+      ->setSourceFormatFromExtension($arguments['extension'])
+      ->setSourceLocalPath('')
+      ->reset();
     $arg = '-size ' . $arguments['width'] . 'x' . $arguments['height'];
 
     // Transparent color syntax for GIF files differs by package.
     if ($arguments['extension'] === 'gif') {
-      switch ($this->getToolkit()->getPackage()) {
+      switch ($this->getToolkit()->getExecManager()->getPackage()) {
         case 'imagemagick':
-          $arg .= ' xc:transparent -transparent-color ' . $this->getToolkit()->escapeShellArg($arguments['transparent_color']);
+          $arg .= ' xc:transparent -transparent-color ' . $this->escapeArgument($arguments['transparent_color']);
           break;
 
         case 'graphicsmagick':
-          $arg .= ' xc:' . $this->getToolkit()->escapeShellArg($arguments['transparent_color']) . ' -transparent ' . $this->getToolkit()->escapeShellArg($arguments['transparent_color']);
+          $arg .= ' xc:' . $this->escapeArgument($arguments['transparent_color']) . ' -transparent ' . $this->escapeArgument($arguments['transparent_color']);
           break;
 
       }
@@ -101,7 +104,7 @@ class CreateNew extends ImagemagickImageToolkitOperationBase {
       $arg .= ' xc:transparent';
     }
 
-    $this->getToolkit()->addArgument($arg);
+    $this->addArgument($arg);
     return TRUE;
   }
 
index eedd1ebca85d2ad480359ab3f160552d289680dc..04f621868e67ec75ec04f99f94508f2555a5f4a0 100644 (file)
@@ -76,7 +76,7 @@ class Crop extends ImagemagickImageToolkitOperationBase {
     // Even though the crop effect in Drupal core does not allow for negative
     // offsets, ImageMagick supports them. Also note: if $x and $y are set to
     // NULL then crop will create tiled images so we convert these to ints.
-    $this->getToolkit()->addArgument(sprintf('-crop %dx%d%+d%+d!', $arguments['width'], $arguments['height'], $arguments['x'], $arguments['y']));
+    $this->addArgument(sprintf('-crop %dx%d%+d%+d!', $arguments['width'], $arguments['height'], $arguments['x'], $arguments['y']));
     $this->getToolkit()->setWidth($arguments['width'])->setHeight($arguments['height']);
     return TRUE;
   }
index 571d133ab87a696b7764d27b4bfc6b6784fe5479..cb796523b7f7c97cf5f90734a686345d67060ea9 100644 (file)
@@ -27,7 +27,7 @@ class Desaturate extends ImagemagickImageToolkitOperationBase {
    * {@inheritdoc}
    */
   protected function execute(array $arguments) {
-    $this->getToolkit()->addArgument('-colorspace GRAY');
+    $this->addArgument('-colorspace GRAY');
     return TRUE;
   }
 
index e870245b02c174a98376c597ba49fe4e83091844..3ec26c771fedef080973037d9be672f878b775f5 100644 (file)
@@ -3,16 +3,68 @@
 namespace Drupal\imagemagick\Plugin\ImageToolkit\Operation\imagemagick;
 
 use Drupal\Core\ImageToolkit\ImageToolkitOperationBase;
+use Drupal\imagemagick\ImagemagickExecArguments;
 
+/**
+ * Base image toolkit operation class for Imagemagick.
+ */
 abstract class ImagemagickImageToolkitOperationBase extends ImageToolkitOperationBase {
 
   /**
    * The correctly typed image toolkit for imagemagick operations.
    *
    * @return \Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit
+   *   The correctly typed image toolkit for imagemagick operations.
    */
+  // @codingStandardsIgnoreStart
   protected function getToolkit() {
     return parent::getToolkit();
   }
+  // @codingStandardsIgnoreEnd
+
+  /**
+   * Helper to add a command line argument.
+   *
+   * Adds the originating operation and plugin id to the $info array.
+   *
+   * @param string $argument
+   *   The command line argument to be added.
+   * @param int $mode
+   *   (optional) The mode of the argument in the command line. Determines if
+   *   the argument should be placed before or after the source image file path.
+   *   Defaults to ImagemagickExecArguments::POST_SOURCE.
+   * @param int $index
+   *   (optional) The position of the argument in the arguments array.
+   *   Reflects the sequence of arguments in the command line. Defaults to
+   *   ImagemagickExecArguments::APPEND.
+   * @param array $info
+   *   (optional) An optional array with information about the argument.
+   *   Defaults to an empty array.
+   *
+   * @return \Drupal\imagemagick\ImagemagickExecArguments
+   *   The Imagemagick arguments.
+   */
+  protected function addArgument($argument, $mode = ImagemagickExecArguments::POST_SOURCE, $index = ImagemagickExecArguments::APPEND, array $info = []) {
+    $plugin_definition = $this->getPluginDefinition();
+    $info = array_merge($info, [
+      'image_toolkit_operation' => $plugin_definition['operation'],
+      'image_toolkit_operation_plugin_id' => $plugin_definition['id'],
+    ]);
+    return $this->getToolkit()->arguments()->add($argument, $mode, $index, $info);
+  }
+
+  /**
+   * Helper to escape a command line argument.
+   *
+   * @param string $argument
+   *   The string to escape.
+   *
+   * @return string
+   *   An escaped string for use in the
+   *   ImagemagickExecManagerInterface::execute method.
+   */
+  protected function escapeArgument($argument) {
+    return $this->getToolkit()->arguments()->escape($argument);
+  }
 
 }
index e1c25831d2ea48e2351ed22ddbb65d04ce06e288..66e610a973d7f7b1ddf2fa107b29ac8211c72334 100644 (file)
@@ -58,9 +58,9 @@ class Resize extends ImagemagickImageToolkitOperationBase {
    */
   protected function execute(array $arguments = []) {
     if (!empty($arguments['filter'])) {
-      $this->getToolkit()->addArgument('-filter ' . $arguments['filter']);
+      $this->addArgument('-filter ' . $arguments['filter']);
     }
-    $this->getToolkit()->addArgument('-resize ' . $arguments['width'] . 'x' . $arguments['height'] . '!');
+    $this->addArgument('-resize ' . $arguments['width'] . 'x' . $arguments['height'] . '!');
     $this->getToolkit()->setWidth($arguments['width'])->setHeight($arguments['height']);
     return TRUE;
   }
index f54eeacabcdf08f9ebebd2b8b7b6c72a9503b602..eab120ac89cb4652248c92f86fdd18548f84573d 100644 (file)
@@ -62,15 +62,20 @@ class Rotate extends ImagemagickImageToolkitOperationBase {
    */
   protected function execute(array $arguments) {
     // Rotate.
-    $arg = '-background ' . $this->getToolkit()->escapeShellArg($arguments['background']);
+    $arg = '-background ' . $this->escapeArgument($arguments['background']);
     $arg .= ' -rotate ' . $arguments['degrees'];
     $arg .= ' +repage';
-    $this->getToolkit()->addArgument($arg);
+    $this->addArgument($arg);
 
     // Need to resize the image after rotation to make sure it complies with
     // the dimensions expected, calculated via the Rectangle class.
     $box = new Rectangle($this->getToolkit()->getWidth(), $this->getToolkit()->getHeight());
     $box = $box->rotate((float) $arguments['degrees']);
-    return $this->getToolkit()->apply('resize', ['width' => $box->getBoundingWidth(), 'height' => $box->getBoundingHeight(), 'filter' => $arguments['resize_filter']]);
+    return $this->getToolkit()->apply('resize', [
+      'width' => $box->getBoundingWidth(),
+      'height' => $box->getBoundingHeight(),
+      'filter' => $arguments['resize_filter'],
+    ]);
   }
+
 }
diff --git a/web/modules/contrib/imagemagick/src/Todo2311679.php b/web/modules/contrib/imagemagick/src/Todo2311679.php
deleted file mode 100644 (file)
index fa536cb..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-/**
- * @todo #2311679, this is a stop-gap workaround
- * remove this once core has a solution in place.
- */
-
-namespace Drupal\imagemagick;
-
-use Drupal\Core\File\MimeType\ExtensionMimeTypeGuesser;
-
-/**
- * Makes possible to guess the MIME type of a file using its extension.
- */
-class Todo2311679 extends ExtensionMimeTypeGuesser {
-
-  public function getExtensionsForMimeType($mimetype) {
-    if ($this->mapping === NULL) {
-      $mapping = $this->defaultMapping;
-      // Allow modules to alter the default mapping.
-      $this->moduleHandler->alter('file_mimetype_mapping', $mapping);
-      $this->mapping = $mapping;
-    }
-    if (!in_array($mimetype, $this->mapping['mimetypes'])) {
-      return [];
-    }
-    $key = array_search($mimetype, $this->mapping['mimetypes']);
-    $extensions = array_keys($this->mapping['extensions'], $key, TRUE);
-    sort($extensions);
-    return $extensions;
-  }
-
-  public function getMimeTypes() {
-    if ($this->mapping === NULL) {
-      $mapping = $this->defaultMapping;
-      // Allow modules to alter the default mapping.
-      $this->moduleHandler->alter('file_mimetype_mapping', $mapping);
-      $this->mapping = $mapping;
-    }
-    return array_values($this->mapping['mimetypes']);
-  }
-
-}
diff --git a/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickFileMetadataTest.php b/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickFileMetadataTest.php
new file mode 100644 (file)
index 0000000..1a5683a
--- /dev/null
@@ -0,0 +1,1131 @@
+<?php
+
+namespace Drupal\Tests\imagemagick\Functional;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Tests\TestFileCreationTrait;
+use Drupal\file_mdm\FileMetadataInterface;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests that Imagemagick integrates properly with File Metadata Manager.
+ *
+ * @group Imagemagick
+ */
+class ToolkitImagemagickFileMetadataTest 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;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  protected static $modules = [
+    'system',
+    'simpletest',
+    'file_test',
+    'imagemagick',
+    'file_mdm',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Set the image factory.
+    $this->imageFactory = $this->container->get('image.factory');
+
+    // Prepare a directory for test file results.
+    $this->testDirectory = 'public://imagetest';
+
+    // Change the toolkit.
+    \Drupal::configFactory()->getEditable('system.image')
+      ->set('toolkit', 'imagemagick')
+      ->save();
+    \Drupal::configFactory()->getEditable('imagemagick.settings')
+      ->set('debug', FALSE)
+      ->set('binaries', 'imagemagick')
+      ->set('quality', 100)
+      ->save();
+
+    // Set the toolkit on the image factory.
+    $this->imageFactory->setToolkitId('imagemagick');
+  }
+
+  /**
+   * Provides data for testFileMetadata.
+   *
+   * @return array[]
+   *   A simple array of simple arrays, each having the following elements:
+   *   - binaries to use for testing.
+   *   - parsing method to use for testing.
+   */
+  public function providerFileMetadataTest() {
+    return [
+      ['imagemagick', 'imagemagick_identify'],
+      ['graphicsmagick', 'imagemagick_identify'],
+    ];
+  }
+
+  /**
+   * Test image toolkit integration with file metadata manager.
+   *
+   * @param string $binaries
+   *   The graphics package binaries to use for testing.
+   * @param string $parsing_method
+   *   The parsing method to use for testing.
+   *
+   * @dataProvider providerFileMetadataTest
+   */
+  public function testFileMetadata($binaries, $parsing_method) {
+    $config = \Drupal::configFactory()->getEditable('imagemagick.settings');
+    $config_mdm = \Drupal::configFactory()->getEditable('file_mdm.settings');
+
+    // Reset file_mdm settings.
+    $config_mdm
+      ->set('metadata_cache.enabled', TRUE)
+      ->set('metadata_cache.disallowed_paths', [])
+      ->save();
+
+    // Execute tests with selected binaries.
+    // The test can only be executed if binaries are available on the shell
+    // path.
+    $config
+      ->set('binaries', $binaries)
+      ->set('use_identify', $parsing_method === 'imagemagick_identify')
+      ->save();
+    $status = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick')->getExecManager()->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.");
+    }
+
+    // A list of files that will be tested.
+    $files = [
+      'public://image-test.png' => [
+        'width' => 40,
+        'height' => 20,
+        'frames' => 1,
+        'mimetype' => 'image/png',
+        'colorspace' => 'SRGB',
+        'profiles' => [],
+      ],
+      'public://image-test.gif' => [
+        'width' => 40,
+        'height' => 20,
+        'frames' => 1,
+        'mimetype' => 'image/gif',
+        'colorspace' => 'SRGB',
+        'profiles' => [],
+      ],
+      'dummy-remote://image-test.jpg' => [
+        'width' => 40,
+        'height' => 20,
+        'frames' => 1,
+        'mimetype' => 'image/jpeg',
+        'colorspace' => 'SRGB',
+        'profiles' => [],
+      ],
+      'public://test-multi-frame.gif' => [
+        'skip_dimensions_check' => TRUE,
+        'frames' => 13,
+        'mimetype' => 'image/gif',
+        'colorspace' => 'SRGB',
+        'profiles' => [],
+      ],
+      'public://test-exif.jpeg' => [
+        'skip_dimensions_check' => TRUE,
+        'frames' => 1,
+        'mimetype' => 'image/jpeg',
+        'colorspace' => 'SRGB',
+        'profiles' => ['exif'],
+      ],
+      'public://test-exif-icc.jpeg' => [
+        'skip_dimensions_check' => TRUE,
+        'frames' => 1,
+        'mimetype' => 'image/jpeg',
+        'colorspace' => 'SRGB',
+        'profiles' => ['exif', 'icc'],
+      ],
+    ];
+
+    // Setup a list of tests to perform on each type.
+    $operations = [
+      'resize' => [
+        'function' => 'resize',
+        'arguments' => ['width' => 20, 'height' => 10],
+        'width' => 20,
+        'height' => 10,
+      ],
+      'scale_x' => [
+        'function' => 'scale',
+        'arguments' => ['width' => 20],
+        'width' => 20,
+        'height' => 10,
+      ],
+      // Fuchsia background.
+      'rotate_5' => [
+        'function' => 'rotate',
+        'arguments' => ['degrees' => 5, 'background' => '#FF00FF'],
+        'width' => 41,
+        'height' => 23,
+      ],
+      'convert_jpg' => [
+        'function' => 'convert',
+        'width' => 40,
+        'height' => 20,
+        'arguments' => ['extension' => 'jpeg'],
+        'mimetype' => 'image/jpeg',
+      ],
+    ];
+
+    // The file metadata manager service.
+    $fmdm = $this->container->get('file_metadata_manager');
+
+    // Prepare a copy of test files.
+    $this->getTestFiles('image');
+    file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-multi-frame.gif', 'public://', FILE_EXISTS_REPLACE);
+    file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg', 'public://', FILE_EXISTS_REPLACE);
+    file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-exif-icc.jpeg', 'public://', FILE_EXISTS_REPLACE);
+
+    // Perform tests without caching.
+    $config_mdm->set('metadata_cache.enabled', FALSE)->save();
+    foreach ($files as $source_uri => $source_image_data) {
+      $this->assertFalse($fmdm->has($source_uri));
+      $source_image_md = $fmdm->uri($source_uri);
+      $this->assertTrue($fmdm->has($source_uri));
+      $first = TRUE;
+      file_unmanaged_delete_recursive($this->testDirectory);
+      file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+      foreach ($operations as $op => $values) {
+        // Load up a fresh image.
+        if ($first) {
+          $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        $source_image = $this->imageFactory->get($source_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($source_image_data['mimetype'], $source_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+          $this->assertEquals($source_image_data['profiles'], $source_image->getToolkit()->getProfiles());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertIdentical($source_image_data['height'], $source_image->getHeight());
+          $this->assertIdentical($source_image_data['width'], $source_image->getWidth());
+        }
+
+        // Perform our operation.
+        $source_image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $saved_uri = $this->testDirectory . '/' . $op . substr($source_uri, -4);
+        $this->assertFalse($fmdm->has($saved_uri));
+        $this->assertTrue($source_image->save($saved_uri));
+        $this->assertFalse($fmdm->has($saved_uri));
+
+        // Reload saved image and check data.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        $saved_image = $this->imageFactory->get($saved_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($values['function'] === 'convert' ? $values['mimetype'] : $source_image_data['mimetype'], $saved_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+          $this->assertEquals($source_image_data['profiles'], $source_image->getToolkit()->getProfiles());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image->getHeight());
+          $this->assertEqual($values['width'], $saved_image->getWidth());
+        }
+        $fmdm->release($saved_uri);
+
+        // Get metadata via the file_mdm service.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        // Should not be available at this stage.
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $saved_image_md->isMetadataLoaded($parsing_method));
+        // Get metadata from file.
+        $saved_image_md->getMetadata($parsing_method);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 'height'));
+          $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 'width'));
+        }
+        $fmdm->release($saved_uri);
+
+        $first = FALSE;
+      }
+      $fmdm->release($source_uri);
+      $this->assertFalse($fmdm->has($source_uri));
+    }
+
+    // Perform tests with caching.
+    $config_mdm->set('metadata_cache.enabled', TRUE)->save();
+    foreach ($files as $source_uri => $source_image_data) {
+      $first = TRUE;
+      file_unmanaged_delete_recursive($this->testDirectory);
+      file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+      foreach ($operations as $op => $values) {
+        // Load up a fresh image.
+        $this->assertFalse($fmdm->has($source_uri));
+        $source_image_md = $fmdm->uri($source_uri);
+        $this->assertTrue($fmdm->has($source_uri));
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded($parsing_method));
+        $source_image = $this->imageFactory->get($source_uri);
+        if ($first) {
+          // First time load, metadata loaded from file.
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          // Further loads, metadata loaded from cache.
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        $this->assertIdentical($source_image_data['mimetype'], $source_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+          $this->assertEquals($source_image_data['profiles'], $source_image->getToolkit()->getProfiles());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertIdentical($source_image_data['height'], $source_image->getHeight());
+          $this->assertIdentical($source_image_data['width'], $source_image->getWidth());
+        }
+
+        // Perform our operation.
+        $source_image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $saved_uri = $this->testDirectory . '/' . $op . substr($source_uri, -4);
+        $this->assertFalse($fmdm->has($saved_uri));
+        $this->assertTrue($source_image->save($saved_uri));
+        $this->assertFalse($fmdm->has($saved_uri));
+
+        // Reload saved image and check data.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        $saved_image = $this->imageFactory->get($saved_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($values['function'] === 'convert' ? $values['mimetype'] : $source_image_data['mimetype'], $saved_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+          $this->assertEquals($source_image_data['profiles'], $source_image->getToolkit()->getProfiles());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image->getHeight());
+          $this->assertEqual($values['width'], $saved_image->getWidth());
+        }
+        $fmdm->release($saved_uri);
+
+        // Get metadata via the file_mdm service. Should be cached.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        // Should not be available at this stage.
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $saved_image_md->isMetadataLoaded($parsing_method));
+        // Get metadata from cache.
+        $saved_image_md->getMetadata($parsing_method);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $saved_image_md->isMetadataLoaded($parsing_method));
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 'height'));
+          $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 'width'));
+        }
+        $fmdm->release($saved_uri);
+
+        // We release the source image FileMetadata at each cycle to ensure
+        // that metadata is read from cache.
+        $fmdm->release($source_uri);
+        $this->assertFalse($fmdm->has($source_uri));
+
+        $first = FALSE;
+      }
+    }
+
+    // Open source images again after deleting the temp folder files.
+    // Source image data should now be cached, but temp files non existing.
+    // Therefore we test that the toolkit can create a new temp file copy.
+    // Note: on Windows, temp imagemagick file names have a
+    // imaNNN.tmp.[image_extension] pattern so we cannot scan for
+    // 'imagemagick'.
+    $directory_scan = file_scan_directory('temporary://', '/ima.*/');
+    $this->assertGreaterThan(0, count($directory_scan));
+    foreach ($directory_scan as $file) {
+      file_unmanaged_delete($file->uri);
+    }
+    $directory_scan = file_scan_directory('temporary://', '/ima.*/');
+    $this->assertEquals(0, count($directory_scan));
+    foreach ($files as $source_uri => $source_image_data) {
+      file_unmanaged_delete_recursive($this->testDirectory);
+      file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+      foreach ($operations as $op => $values) {
+        // Load up the source image. Parsing should be fully cached now.
+        $fmdm->release($source_uri);
+        $source_image_md = $fmdm->uri($source_uri);
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded($parsing_method));
+        $source_image = $this->imageFactory->get($source_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $source_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($source_image_data['mimetype'], $source_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+          $this->assertEquals($source_image_data['profiles'], $source_image->getToolkit()->getProfiles());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertIdentical($source_image_data['height'], $source_image->getHeight());
+          $this->assertIdentical($source_image_data['width'], $source_image->getWidth());
+        }
+
+        // Perform our operation.
+        $source_image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $saved_uri = $this->testDirectory . '/' . $op . substr($source_uri, -4);
+        $this->assertFalse($fmdm->has($saved_uri));
+        $this->assertTrue($source_image->save($saved_uri));
+        $this->assertFalse($fmdm->has($saved_uri));
+
+        // Reload saved image and check data.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        $saved_image = $this->imageFactory->get($saved_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($values['function'] === 'convert' ? $values['mimetype'] : $source_image_data['mimetype'], $saved_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+          $this->assertEquals($source_image_data['profiles'], $source_image->getToolkit()->getProfiles());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image->getHeight());
+          $this->assertEqual($values['width'], $saved_image->getWidth());
+        }
+        $fmdm->release($saved_uri);
+
+        // Get metadata via the file_mdm service. Should be cached.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        // Should not be available at this stage.
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $saved_image_md->isMetadataLoaded($parsing_method));
+        // Get metadata from cache.
+        $saved_image_md->getMetadata($parsing_method);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $saved_image_md->isMetadataLoaded($parsing_method));
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 'height'));
+          $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 'width'));
+        }
+        $fmdm->release($saved_uri);
+      }
+      $fmdm->release($source_uri);
+      $this->assertFalse($fmdm->has($source_uri));
+    }
+
+    // Files in temporary:// must not be cached.
+    if ($parsing_method === 'imagemagick_identify') {
+      file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-multi-frame.gif', 'temporary://', FILE_EXISTS_REPLACE);
+      $source_uri = 'temporary://test-multi-frame.gif';
+      $fmdm->release($source_uri);
+      $source_image_md = $fmdm->uri($source_uri);
+      $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded('imagemagick_identify'));
+      $source_image = $this->imageFactory->get($source_uri);
+      $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded('imagemagick_identify'));
+      $fmdm->release($source_uri);
+      $source_image_md = $fmdm->uri($source_uri);
+      $source_image = $this->imageFactory->get($source_uri);
+      $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded('imagemagick_identify'));
+    }
+
+    // Invalidate cache, and open source images again. Now, all files should be
+    // parsed again.
+    Cache::InvalidateTags([
+      'config:imagemagick.file_metadata_plugin.imagemagick_identify',
+      'config:file_mdm.file_metadata_plugin.getimagesize',
+    ]);
+    // Disallow caching on the test results directory.
+    $config_mdm->set('metadata_cache.disallowed_paths', ['public://imagetest/*'])->save();
+    foreach ($files as $source_uri => $source_image_data) {
+      $fmdm->release($source_uri);
+    }
+    foreach ($files as $source_uri => $source_image_data) {
+      $this->assertFalse($fmdm->has($source_uri));
+      $source_image_md = $fmdm->uri($source_uri);
+      $this->assertTrue($fmdm->has($source_uri));
+      $first = TRUE;
+      file_unmanaged_delete_recursive($this->testDirectory);
+      file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+      foreach ($operations as $op => $values) {
+        // Load up a fresh image.
+        if ($first) {
+          $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        $source_image = $this->imageFactory->get($source_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($source_image_data['mimetype'], $source_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+          $this->assertEquals($source_image_data['profiles'], $source_image->getToolkit()->getProfiles());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertIdentical($source_image_data['height'], $source_image->getHeight());
+          $this->assertIdentical($source_image_data['width'], $source_image->getWidth());
+        }
+
+        // Perform our operation.
+        $source_image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $saved_uri = $this->testDirectory . '/' . $op . substr($source_uri, -4);
+        $this->assertFalse($fmdm->has($saved_uri));
+        $this->assertTrue($source_image->save($saved_uri));
+        $this->assertFalse($fmdm->has($saved_uri));
+
+        // Reload saved image and check data.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        $saved_image = $this->imageFactory->get($saved_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($values['function'] === 'convert' ? $values['mimetype'] : $source_image_data['mimetype'], $saved_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+          $this->assertEquals($source_image_data['profiles'], $source_image->getToolkit()->getProfiles());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image->getHeight());
+          $this->assertEqual($values['width'], $saved_image->getWidth());
+        }
+        $fmdm->release($saved_uri);
+
+        // Get metadata via the file_mdm service.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        // Should not be available at this stage.
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $saved_image_md->isMetadataLoaded($parsing_method));
+        // Get metadata from file.
+        $saved_image_md->getMetadata($parsing_method);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 'height'));
+          $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 'width'));
+        }
+        $fmdm->release($saved_uri);
+
+        $first = FALSE;
+      }
+      $fmdm->release($source_uri);
+      $this->assertFalse($fmdm->has($source_uri));
+    }
+  }
+
+  /**
+   * Provides data for testFileMetadataLegacy.
+   *
+   * @return array[]
+   *   A simple array of simple arrays, each having the following elements:
+   *   - binaries to use for testing.
+   *   - parsing method to use for testing.
+   *
+   * @todo remove in 8.x-3.0.
+   */
+  public function providerFileMetadataTestLegacy() {
+    return [
+      ['imagemagick', 'getimagesize'],
+      ['graphicsmagick', 'getimagesize'],
+    ];
+  }
+
+  /**
+   * Test legacy image toolkit integration with file metadata manager.
+   *
+   * @param string $binaries
+   *   The graphics package binaries to use for testing.
+   * @param string $parsing_method
+   *   The parsing method to use for testing.
+   *
+   * @todo remove in 8.x-3.0.
+   *
+   * @dataProvider providerFileMetadataTestLegacy
+   *
+   * @group legacy
+   */
+  public function testFileMetadataLegacy($binaries, $parsing_method) {
+    $config = \Drupal::configFactory()->getEditable('imagemagick.settings');
+    $config_mdm = \Drupal::configFactory()->getEditable('file_mdm.settings');
+
+    // Reset file_mdm settings.
+    $config_mdm
+      ->set('metadata_cache.enabled', TRUE)
+      ->set('metadata_cache.disallowed_paths', [])
+      ->save();
+
+    // Execute tests with selected binaries.
+    // The test can only be executed if binaries are available on the shell
+    // path.
+    $config
+      ->set('binaries', $binaries)
+      ->set('use_identify', $parsing_method === 'imagemagick_identify')
+      ->save();
+    $status = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick')->getExecManager()->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.");
+    }
+
+    // A list of files that will be tested.
+    $files = [
+      'public://image-test.png' => [
+        'width' => 40,
+        'height' => 20,
+        'frames' => 1,
+        'mimetype' => 'image/png',
+        'colorspace' => 'SRGB',
+      ],
+      'public://image-test.gif' => [
+        'width' => 40,
+        'height' => 20,
+        'frames' => 1,
+        'mimetype' => 'image/gif',
+        'colorspace' => 'SRGB',
+      ],
+      'dummy-remote://image-test.jpg' => [
+        'width' => 40,
+        'height' => 20,
+        'frames' => 1,
+        'mimetype' => 'image/jpeg',
+        'colorspace' => 'SRGB',
+      ],
+      'public://test-multi-frame.gif' => [
+        'skip_dimensions_check' => TRUE,
+        'frames' => 13,
+        'mimetype' => 'image/gif',
+        'colorspace' => 'SRGB',
+      ],
+    ];
+
+    // Setup a list of tests to perform on each type.
+    $operations = [
+      'resize' => [
+        'function' => 'resize',
+        'arguments' => ['width' => 20, 'height' => 10],
+        'width' => 20,
+        'height' => 10,
+      ],
+      'scale_x' => [
+        'function' => 'scale',
+        'arguments' => ['width' => 20],
+        'width' => 20,
+        'height' => 10,
+      ],
+      // Fuchsia background.
+      'rotate_5' => [
+        'function' => 'rotate',
+        'arguments' => ['degrees' => 5, 'background' => '#FF00FF'],
+        'width' => 41,
+        'height' => 23,
+      ],
+      'convert_jpg' => [
+        'function' => 'convert',
+        'width' => 40,
+        'height' => 20,
+        'arguments' => ['extension' => 'jpeg'],
+        'mimetype' => 'image/jpeg',
+      ],
+    ];
+
+    // The file metadata manager service.
+    $fmdm = $this->container->get('file_metadata_manager');
+
+    // Prepare a copy of test files.
+    $this->getTestFiles('image');
+    file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-multi-frame.gif', 'public://', FILE_EXISTS_REPLACE);
+
+    // Perform tests without caching.
+    $config_mdm->set('metadata_cache.enabled', FALSE)->save();
+    foreach ($files as $source_uri => $source_image_data) {
+      $this->assertFalse($fmdm->has($source_uri));
+      $source_image_md = $fmdm->uri($source_uri);
+      $this->assertTrue($fmdm->has($source_uri));
+      $first = TRUE;
+      file_unmanaged_delete_recursive($this->testDirectory);
+      file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+      foreach ($operations as $op => $values) {
+        // Load up a fresh image.
+        if ($first) {
+          $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        $source_image = $this->imageFactory->get($source_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($source_image_data['mimetype'], $source_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertIdentical($source_image_data['height'], $source_image->getHeight());
+          $this->assertIdentical($source_image_data['width'], $source_image->getWidth());
+        }
+
+        // Perform our operation.
+        $source_image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $saved_uri = $this->testDirectory . '/' . $op . substr($source_uri, -4);
+        $this->assertFalse($fmdm->has($saved_uri));
+        $this->assertTrue($source_image->save($saved_uri));
+        // In some cases the metadata can be generated on save without need to
+        // re-read it back from the image.
+        if ($binaries === 'imagemagick' &&
+          $parsing_method === 'imagemagick_identify' &&
+          $source_image->getToolkit()->getFrames() === 1
+        ) {
+          $this->assertTrue($fmdm->has($saved_uri));
+        }
+        else {
+          $this->assertFalse($fmdm->has($saved_uri));
+        }
+
+        // Reload saved image and check data.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        $saved_image = $this->imageFactory->get($saved_uri);
+        // In some cases the metadata can be generated on save without need to
+        // re-read it back from the image.
+        if ($binaries === 'imagemagick' &&
+          $parsing_method === 'imagemagick_identify' &&
+          $saved_image->getToolkit()->getFrames() === 1 &&
+          !($values['function'] === 'convert' && $source_image_data['frames'] > 1)
+        ) {
+          $this->assertIdentical(FileMetadataInterface::LOADED_BY_CODE, $saved_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        }
+        $this->assertIdentical($values['function'] === 'convert' ? $values['mimetype'] : $source_image_data['mimetype'], $saved_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image->getHeight());
+          $this->assertEqual($values['width'], $saved_image->getWidth());
+        }
+        $fmdm->release($saved_uri);
+
+        // Get metadata via the file_mdm service.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        // Should not be available at this stage.
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $saved_image_md->isMetadataLoaded($parsing_method));
+        // Get metadata from file.
+        $saved_image_md->getMetadata($parsing_method);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        switch ($parsing_method) {
+          case 'imagemagick_identify':
+            if (!isset($source_image_data['skip_dimensions_check'])) {
+              $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 'height'));
+              $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 'width'));
+            }
+            break;
+
+          case 'getimagesize':
+            if (!isset($source_image_data['skip_dimensions_check'])) {
+              $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 1));
+              $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 0));
+            }
+            break;
+
+        }
+        $fmdm->release($saved_uri);
+
+        $first = FALSE;
+      }
+      $fmdm->release($source_uri);
+      $this->assertFalse($fmdm->has($source_uri));
+    }
+
+    // Perform tests with caching.
+    $config_mdm->set('metadata_cache.enabled', TRUE)->save();
+    foreach ($files as $source_uri => $source_image_data) {
+      $first = TRUE;
+      file_unmanaged_delete_recursive($this->testDirectory);
+      file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+      foreach ($operations as $op => $values) {
+        // Load up a fresh image.
+        $this->assertFalse($fmdm->has($source_uri));
+        $source_image_md = $fmdm->uri($source_uri);
+        $this->assertTrue($fmdm->has($source_uri));
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded($parsing_method));
+        $source_image = $this->imageFactory->get($source_uri);
+        if ($first) {
+          // First time load, metadata loaded from file.
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          // Further loads, metadata loaded from cache.
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        $this->assertIdentical($source_image_data['mimetype'], $source_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertIdentical($source_image_data['height'], $source_image->getHeight());
+          $this->assertIdentical($source_image_data['width'], $source_image->getWidth());
+        }
+
+        // Perform our operation.
+        $source_image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $saved_uri = $this->testDirectory . '/' . $op . substr($source_uri, -4);
+        $this->assertFalse($fmdm->has($saved_uri));
+        $this->assertTrue($source_image->save($saved_uri));
+        if ($binaries === 'imagemagick' &&
+          $parsing_method === 'imagemagick_identify' &&
+          $source_image->getToolkit()->getFrames() === 1
+        ) {
+          $this->assertTrue($fmdm->has($saved_uri));
+        }
+        else {
+          $this->assertFalse($fmdm->has($saved_uri));
+        }
+
+        // Reload saved image and check data.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        $saved_image = $this->imageFactory->get($saved_uri);
+        if ($binaries === 'imagemagick' &&
+          $parsing_method === 'imagemagick_identify' &&
+          $saved_image->getToolkit()->getFrames() === 1 &&
+          !($values['function'] === 'convert' && $source_image_data['frames'] > 1)
+        ) {
+          $this->assertIdentical(FileMetadataInterface::LOADED_BY_CODE, $saved_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        }
+        $this->assertIdentical($values['function'] === 'convert' ? $values['mimetype'] : $source_image_data['mimetype'], $saved_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image->getHeight());
+          $this->assertEqual($values['width'], $saved_image->getWidth());
+        }
+        $fmdm->release($saved_uri);
+
+        // Get metadata via the file_mdm service. Should be cached.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        // Should not be available at this stage.
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $saved_image_md->isMetadataLoaded($parsing_method));
+        // Get metadata from cache.
+        $saved_image_md->getMetadata($parsing_method);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $saved_image_md->isMetadataLoaded($parsing_method));
+        switch ($parsing_method) {
+          case 'imagemagick_identify':
+            if (!isset($source_image_data['skip_dimensions_check'])) {
+              $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 'height'));
+              $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 'width'));
+            }
+            break;
+
+          case 'getimagesize':
+            if (!isset($source_image_data['skip_dimensions_check'])) {
+              $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 1));
+              $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 0));
+            }
+            break;
+
+        }
+        $fmdm->release($saved_uri);
+
+        // We release the source image FileMetadata at each cycle to ensure
+        // that metadata is read from cache.
+        $fmdm->release($source_uri);
+        $this->assertFalse($fmdm->has($source_uri));
+
+        $first = FALSE;
+      }
+    }
+
+    // Open source images again after deleting the temp folder files.
+    // Source image data should now be cached, but temp files non existing.
+    // Therefore we test that the toolkit can create a new temp file copy.
+    // Note: on Windows, temp imagemagick file names have a
+    // imaNNN.tmp.[image_extension] pattern so we cannot scan for
+    // 'imagemagick'.
+    $directory_scan = file_scan_directory('temporary://', '/ima.*/');
+    $this->assertGreaterThan(0, count($directory_scan));
+    foreach ($directory_scan as $file) {
+      file_unmanaged_delete($file->uri);
+    }
+    $directory_scan = file_scan_directory('temporary://', '/ima.*/');
+    $this->assertEquals(0, count($directory_scan));
+    foreach ($files as $source_uri => $source_image_data) {
+      file_unmanaged_delete_recursive($this->testDirectory);
+      file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+      foreach ($operations as $op => $values) {
+        // Load up the source image. Parsing should be fully cached now.
+        $fmdm->release($source_uri);
+        $source_image_md = $fmdm->uri($source_uri);
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded($parsing_method));
+        $source_image = $this->imageFactory->get($source_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $source_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($source_image_data['mimetype'], $source_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertIdentical($source_image_data['height'], $source_image->getHeight());
+          $this->assertIdentical($source_image_data['width'], $source_image->getWidth());
+        }
+
+        // Perform our operation.
+        $source_image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $saved_uri = $this->testDirectory . '/' . $op . substr($source_uri, -4);
+        $this->assertFalse($fmdm->has($saved_uri));
+        $this->assertTrue($source_image->save($saved_uri));
+        if ($binaries === 'imagemagick' &&
+          $parsing_method === 'imagemagick_identify' &&
+          $source_image->getToolkit()->getFrames() === 1
+        ) {
+          $this->assertTrue($fmdm->has($saved_uri));
+        }
+        else {
+          $this->assertFalse($fmdm->has($saved_uri));
+        }
+
+        // Reload saved image and check data.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        $saved_image = $this->imageFactory->get($saved_uri);
+        if ($binaries === 'imagemagick' &&
+          $parsing_method === 'imagemagick_identify' &&
+          $saved_image->getToolkit()->getFrames() === 1 &&
+          !($values['function'] === 'convert' && $source_image_data['frames'] > 1)
+        ) {
+          $this->assertIdentical(FileMetadataInterface::LOADED_BY_CODE, $saved_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        }
+        $this->assertIdentical($values['function'] === 'convert' ? $values['mimetype'] : $source_image_data['mimetype'], $saved_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image->getHeight());
+          $this->assertEqual($values['width'], $saved_image->getWidth());
+        }
+        $fmdm->release($saved_uri);
+
+        // Get metadata via the file_mdm service. Should be cached.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        // Should not be available at this stage.
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $saved_image_md->isMetadataLoaded($parsing_method));
+        // Get metadata from cache.
+        $saved_image_md->getMetadata($parsing_method);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_CACHE, $saved_image_md->isMetadataLoaded($parsing_method));
+        switch ($parsing_method) {
+          case 'imagemagick_identify':
+            if (!isset($source_image_data['skip_dimensions_check'])) {
+              $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 'height'));
+              $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 'width'));
+            }
+            break;
+
+          case 'getimagesize':
+            if (!isset($source_image_data['skip_dimensions_check'])) {
+              $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 1));
+              $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 0));
+            }
+            break;
+
+        }
+        $fmdm->release($saved_uri);
+      }
+      $fmdm->release($source_uri);
+      $this->assertFalse($fmdm->has($source_uri));
+    }
+
+    // Files in temporary:// must not be cached.
+    if ($parsing_method === 'imagemagick_identify') {
+      file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-multi-frame.gif', 'temporary://', FILE_EXISTS_REPLACE);
+      $source_uri = 'temporary://test-multi-frame.gif';
+      $fmdm->release($source_uri);
+      $source_image_md = $fmdm->uri($source_uri);
+      $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded('imagemagick_identify'));
+      $source_image = $this->imageFactory->get($source_uri);
+      $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded('imagemagick_identify'));
+      $fmdm->release($source_uri);
+      $source_image_md = $fmdm->uri($source_uri);
+      $source_image = $this->imageFactory->get($source_uri);
+      $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded('imagemagick_identify'));
+    }
+
+    // Invalidate cache, and open source images again. Now, all files should be
+    // parsed again.
+    Cache::InvalidateTags([
+      'config:imagemagick.file_metadata_plugin.imagemagick_identify',
+      'config:file_mdm.file_metadata_plugin.getimagesize',
+    ]);
+    // Disallow caching on the test results directory.
+    $config_mdm->set('metadata_cache.disallowed_paths', ['public://imagetest/*'])->save();
+    foreach ($files as $source_uri => $source_image_data) {
+      $fmdm->release($source_uri);
+    }
+    foreach ($files as $source_uri => $source_image_data) {
+      $this->assertFalse($fmdm->has($source_uri));
+      $source_image_md = $fmdm->uri($source_uri);
+      $this->assertTrue($fmdm->has($source_uri));
+      $first = TRUE;
+      file_unmanaged_delete_recursive($this->testDirectory);
+      file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+      foreach ($operations as $op => $values) {
+        // Load up a fresh image.
+        if ($first) {
+          $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        }
+        $source_image = $this->imageFactory->get($source_uri);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $source_image_md->isMetadataLoaded($parsing_method));
+        $this->assertIdentical($source_image_data['mimetype'], $source_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertIdentical($source_image_data['height'], $source_image->getHeight());
+          $this->assertIdentical($source_image_data['width'], $source_image->getWidth());
+        }
+
+        // Perform our operation.
+        $source_image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $saved_uri = $this->testDirectory . '/' . $op . substr($source_uri, -4);
+        $this->assertFalse($fmdm->has($saved_uri));
+        $this->assertTrue($source_image->save($saved_uri));
+        if ($binaries === 'imagemagick' &&
+          $parsing_method === 'imagemagick_identify' &&
+          $source_image->getToolkit()->getFrames() === 1
+        ) {
+          $this->assertTrue($fmdm->has($saved_uri));
+        }
+        else {
+          $this->assertFalse($fmdm->has($saved_uri));
+        }
+
+        // Reload saved image and check data.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        $saved_image = $this->imageFactory->get($saved_uri);
+        if ($binaries === 'imagemagick' &&
+          $parsing_method === 'imagemagick_identify' &&
+          $saved_image->getToolkit()->getFrames() === 1 &&
+          !($values['function'] === 'convert' && $source_image_data['frames'] > 1)
+        ) {
+          $this->assertIdentical(FileMetadataInterface::LOADED_BY_CODE, $saved_image_md->isMetadataLoaded($parsing_method));
+        }
+        else {
+          $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        }
+        $this->assertIdentical($values['function'] === 'convert' ? $values['mimetype'] : $source_image_data['mimetype'], $saved_image->getMimeType());
+        if ($binaries === 'imagemagick' && $parsing_method === 'imagemagick_identify') {
+          $this->assertIdentical($source_image_data['colorspace'], $source_image->getToolkit()->getColorspace());
+        }
+        if (!isset($source_image_data['skip_dimensions_check'])) {
+          $this->assertEqual($values['height'], $saved_image->getHeight());
+          $this->assertEqual($values['width'], $saved_image->getWidth());
+        }
+        $fmdm->release($saved_uri);
+
+        // Get metadata via the file_mdm service.
+        $saved_image_md = $fmdm->uri($saved_uri);
+        // Should not be available at this stage.
+        $this->assertIdentical(FileMetadataInterface::NOT_LOADED, $saved_image_md->isMetadataLoaded($parsing_method));
+        // Get metadata from file.
+        $saved_image_md->getMetadata($parsing_method);
+        $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $saved_image_md->isMetadataLoaded($parsing_method));
+        switch ($parsing_method) {
+          case 'imagemagick_identify':
+            if (!isset($source_image_data['skip_dimensions_check'])) {
+              $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 'height'));
+              $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 'width'));
+            }
+            break;
+
+          case 'getimagesize':
+            if (!isset($source_image_data['skip_dimensions_check'])) {
+              $this->assertEqual($values['height'], $saved_image_md->getMetadata($parsing_method, 1));
+              $this->assertEqual($values['width'], $saved_image_md->getMetadata($parsing_method, 0));
+            }
+            break;
+
+        }
+        $fmdm->release($saved_uri);
+
+        $first = FALSE;
+      }
+      $fmdm->release($source_uri);
+      $this->assertFalse($fmdm->has($source_uri));
+    }
+  }
+
+  /**
+   * Tests getSourceLocalPath() for re-creating local path.
+   */
+  public function testSourceLocalPath() {
+    $status = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick')->getExecManager()->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 'imagemagick' cannot run because the binaries are not available on the shell path.");
+    }
+
+    $config = \Drupal::configFactory()->getEditable('imagemagick.settings');
+    $config_mdm = \Drupal::configFactory()->getEditable('file_mdm.settings');
+
+    // The file metadata manager service.
+    $fmdm = $this->container->get('file_metadata_manager');
+
+    // The file that will be tested.
+    $source_uri = 'public://image-test.png';
+
+    // Prepare a copy of test files.
+    $this->getTestFiles('image');
+
+    // Enable metadata caching.
+    $config_mdm->set('metadata_cache.enabled', TRUE)->save();
+
+    // Parse with identify.
+    $config->set('use_identify', TRUE)->save();
+
+    // Load up the image.
+    $image = $this->imageFactory->get($source_uri);
+    $this->assertEqual($source_uri, $image->getToolkit()->getSource());
+    $this->assertEqual(drupal_realpath($source_uri), $image->getToolkit()->arguments()->getSourceLocalPath());
+
+    // Free up the URI from the file metadata manager to force reload from
+    // cache. Simulates that next imageFactory->get is from another request.
+    $fmdm->release($source_uri);
+
+    // Re-load the image, ensureLocalSourcePath should return the local path.
+    $image1 = $this->imageFactory->get($source_uri);
+    $this->assertEqual($source_uri, $image1->getToolkit()->getSource());
+    $this->assertEqual(drupal_realpath($source_uri), $image1->getToolkit()->ensureSourceLocalPath());
+  }
+
+}
index 2322581ffa91ff34b90bc1b2104278c94c70d54f..df6af8dab9e8c5fc299a49702ff801aa9ece2a31 100644 (file)
@@ -2,9 +2,12 @@
 
 namespace Drupal\Tests\imagemagick\Functional;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Image\ImageInterface;
 use Drupal\Tests\TestFileCreationTrait;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\file_mdm\FileMetadataInterface;
+use Drupal\imagemagick\ImagemagickExecArguments;
 
 /**
  * Tests that core image manipulations work properly through Imagemagick.
@@ -57,6 +60,8 @@ class ToolkitImagemagickTest extends BrowserTestBase {
     'simpletest',
     'file_test',
     'imagemagick',
+    'file_mdm',
+    'file_mdm_exif',
   ];
 
   /**
@@ -79,51 +84,36 @@ class ToolkitImagemagickTest extends BrowserTestBase {
   }
 
   /**
-   * 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.
+   * Helper to setup the image toolkit.
    *
    * @param string $binaries
    *   The graphics package binaries to use for testing.
-   *
-   * @dataProvider providerManipulationTest
+   * @param bool $check_path
+   *   Whether the path to binaries should be tested.
    */
-  public function testManipulations($binaries) {
+  protected function setUpToolkit($binaries, $check_path = TRUE) {
     // 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.");
+
+    if ($check_path) {
+      // The test can only be executed if binaries are available on the shell
+      // path.
+      $status = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick')->getExecManager()->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.
@@ -135,6 +125,36 @@ class ToolkitImagemagickTest extends BrowserTestBase {
     // Prepare directory.
     file_unmanaged_delete_recursive($this->testDirectory);
     file_prepare_directory($this->testDirectory, FILE_CREATE_DIRECTORY);
+  }
+
+  /**
+   * 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) {
+    $this->setUpToolkit($binaries);
 
     // Typically the corner colors will be unchanged. These colors are in the
     // order of top-left, top-right, bottom-right, bottom-left.
@@ -309,31 +329,36 @@ class ToolkitImagemagickTest extends BrowserTestBase {
     $this->getTestFiles('image');
 
     foreach ($files as $file) {
+      $image_uri = 'public://' . $file;
       foreach ($operations as $op => $values) {
         // Load up a fresh image.
-        $image = $this->imageFactory->get('public://' . $file);
+        $image = $this->imageFactory->get($image_uri);
         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());
+        $this->assertIdentical(1, $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);
+        $this->assertTrue($image->save($file_path));
         $image = $this->imageFactory->get($file_path);
         $this->assertTrue($image->isValid());
 
-        // @todo GraphicsMagick specifics, temporarily adjust tests.
-        $package = $image->getToolkit()->getPackage();
+        // @todo Suite specifics, temporarily adjust tests.
+        $package = $image->getToolkit()->getExecManager()->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'])) {
+          // @todo Issues with crop and convert on GIF files, investigate.
+          if (in_array($file, [
+            'image-test.gif', 'image-test-no-transparency.gif',
+          ]) && in_array($op, [
+            'crop', 'scale_and_crop', 'convert_png',
+          ])) {
             continue;
           }
         }
@@ -409,7 +434,7 @@ class ToolkitImagemagickTest extends BrowserTestBase {
 
             }
             $color = $this->getPixelColor($image, $x, $y);
-            $correct_colors = $this->colorsAreClose($color, $corner, $values['tolerance']);
+            $this->colorsAreClose($color, $corner, $values['tolerance'], $file, $op);
           }
         }
       }
@@ -470,10 +495,14 @@ class ToolkitImagemagickTest extends BrowserTestBase {
       'viet "with double quotes" hình áº£nh thá»­ nghiệm.png',
     ];
     foreach ($file_names as $file) {
+      // On Windows, skip filenames with non-allowed characters.
+      if (substr(PHP_OS, 0, 3) === 'WIN' && preg_match('/[:*?"<>|]/', $file)) {
+        continue;
+      }
       $image = $this->imageFactory->get();
-      $image->createNew(50, 20, 'png');
+      $this->assertTrue($image->createNew(50, 20, 'png'));
       $file_path = $this->testDirectory . '/' . $file;
-      $image->save($file_path);
+      $this->assertTrue($image->save($file_path), $file);
       $image_reloaded = $this->imageFactory->get($file_path, 'gd');
       $this->assertTrue($image_reloaded->isValid(), "Image file '$file' loaded successfully.");
     }
@@ -481,21 +510,31 @@ class ToolkitImagemagickTest extends BrowserTestBase {
     // 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()));
+    $this->assertEqual(filesize('dummy-remote://image-test.png'), filesize($image->getToolkit()->arguments()->getSourceLocalPath()));
     $image->desaturate();
-    $image->save('dummy-remote://remote-image-test.png');
+    $this->assertTrue($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());
+    $this->assertTrue(file_exists($image->getToolkit()->arguments()->getDestination()));
+    $this->assertEqual('dummy-remote://remote-image-test.png', $image->getToolkit()->arguments()->getDestination());
+    $this->assertIdentical('', $image->getToolkit()->arguments()->getDestinationLocalPath());
 
     // Test retrieval of EXIF information.
+    file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg', 'public://', FILE_EXISTS_REPLACE);
+    // The image files that will be tested.
     $image_files = [
       [
         'path' => drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg',
         'orientation' => 8,
       ],
+      [
+        'path' => 'public://test-exif.jpeg',
+        'orientation' => 8,
+      ],
+      [
+        'path' => 'dummy-remote://test-exif.jpeg',
+        'orientation' => 8,
+      ],
       [
         'path' => 'public://image-test.jpg',
         'orientation' => NULL,
@@ -513,7 +552,6 @@ class ToolkitImagemagickTest extends BrowserTestBase {
         'orientation' => NULL,
       ],
     ];
-
     foreach ($image_files as $image_file) {
       // Get image using 'identify'.
       \Drupal::configFactory()->getEditable('imagemagick.settings')
@@ -521,13 +559,6 @@ class ToolkitImagemagickTest extends BrowserTestBase {
         ->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.
@@ -544,7 +575,6 @@ class ToolkitImagemagickTest extends BrowserTestBase {
         'rotated_height' => 26,
       ],
     ];
-
     // Get images using 'identify'.
     \Drupal::configFactory()->getEditable('imagemagick.settings')
       ->set('use_identify', TRUE)
@@ -557,7 +587,7 @@ class ToolkitImagemagickTest extends BrowserTestBase {
 
       // Scaling should preserve frames.
       $image->scale(30);
-      $image->save($image_file['destination']);
+      $this->assertTrue($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());
@@ -565,7 +595,7 @@ class ToolkitImagemagickTest extends BrowserTestBase {
 
       // Rotating should preserve frames.
       $image->rotate(24);
-      $image->save($image_file['destination']);
+      $this->assertTrue($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());
@@ -573,12 +603,301 @@ class ToolkitImagemagickTest extends BrowserTestBase {
 
       // Converting to PNG should drop frames.
       $image->convert('png');
-      $this->assertNull($image->getToolkit()->getFrames());
-      $image->save($image_file['destination']);
+      $this->assertTrue($image->save($image_file['destination']));
       $image = $this->imageFactory->get($image_file['destination']);
+      $this->assertIdentical(1, $image->getToolkit()->getFrames());
       $this->assertIdentical($image_file['rotated_width'], $image->getWidth());
       $this->assertIdentical($image_file['rotated_height'], $image->getHeight());
-      $this->assertNull($image->getToolkit()->getFrames());
+      $this->assertIdentical(1, $image->getToolkit()->getFrames());
+    }
+  }
+
+  /**
+   * Legacy methods tests.
+   *
+   * @param string $binaries
+   *   The graphics package binaries to use for testing.
+   *
+   * @dataProvider providerManipulationTest
+   *
+   * @todo remove in 8.x-3.0.
+   *
+   * @group legacy
+   */
+  public function testManipulationsLegacy($binaries) {
+    $this->setUpToolkit($binaries);
+
+    // Check package.
+    $toolkit = \Drupal::service('image.toolkit.manager')->createInstance('imagemagick');
+    $this->assertSame($binaries, $toolkit->getPackage());
+    $this->assertNotNull($toolkit->getPackageLabel());
+    $this->assertSame([], $toolkit->checkPath('')['errors']);
+
+    // 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) {
+      $image_uri = 'public://' . $file;
+      foreach ($operations as $op => $values) {
+        // Load up a fresh image.
+        $image = $this->imageFactory->get($image_uri);
+        if (!$image->isValid()) {
+          $this->fail("Could not load image $file.");
+          continue 2;
+        }
+
+        // Check that no multi-frame information is set.
+        $this->assertIdentical(1, $image->getToolkit()->getFrames());
+
+        // Legacy source tests.
+        $this->assertSame($image_uri, $image->getToolkit()->getSource());
+        $this->assertSame($image->getToolkit()->arguments()->getSourceLocalPath(), $image->getToolkit()->getSourceLocalPath());
+        $this->assertSame($image->getToolkit()->arguments()->getSourceFormat(), $image->getToolkit()->getSourceFormat());
+
+        // Perform our operation.
+        $image->apply($values['function'], $values['arguments']);
+
+        // Save image.
+        $file_path = $this->testDirectory . '/' . $op . substr($file, -4);
+        $this->assertTrue($image->save($file_path));
+
+        // Legacy destination tests.
+        $this->assertSame($file_path, $image->getToolkit()->getDestination());
+        $this->assertSame('', $image->getToolkit()->getDestinationLocalPath());
+        $this->assertNotNull($image->getToolkit()->arguments()->getSourceFormat(), $image->getToolkit()->getDestinationFormat());
+
+        // Reload image.
+        $image = $this->imageFactory->get($file_path);
+        $this->assertTrue($image->isValid());
+
+        // Legacy set methods.
+        $image->getToolkit()->setSourceLocalPath('bar');
+        $image->getToolkit()->setSourceFormat('PNG');
+        $image->getToolkit()->setDestination('foo');
+        $image->getToolkit()->setDestinationLocalPath('baz');
+        $image->getToolkit()->setDestinationFormat('GIF');
+        $this->assertSame('bar', $image->getToolkit()->arguments()->getSourceLocalPath());
+        $this->assertSame('PNG', $image->getToolkit()->arguments()->getSourceFormat());
+        $this->assertSame('foo', $image->getToolkit()->arguments()->getDestination());
+        $this->assertSame('baz', $image->getToolkit()->arguments()->getDestinationLocalPath());
+        $this->assertSame('GIF', $image->getToolkit()->arguments()->getDestinationFormat());
+        $image->getToolkit()->setSourceFormatFromExtension('jpg');
+        $image->getToolkit()->setDestinationFormatFromExtension('jpg');
+        $this->assertSame('JPEG', $image->getToolkit()->arguments()->getSourceFormat());
+        $this->assertSame('JPEG', $image->getToolkit()->arguments()->getDestinationFormat());
+      }
+    }
+
+    // Test retrieval of EXIF information.
+    file_unmanaged_copy(drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg', 'public://', FILE_EXISTS_REPLACE);
+    // The image files that will be tested.
+    $image_files = [
+      [
+        'path' => drupal_get_path('module', 'imagemagick') . '/misc/test-exif.jpeg',
+        'orientation' => 8,
+      ],
+      [
+        'path' => 'public://test-exif.jpeg',
+        'orientation' => 8,
+      ],
+      [
+        'path' => 'dummy-remote://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 '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());
     }
   }
 
@@ -596,7 +915,7 @@ class ToolkitImagemagickTest extends BrowserTestBase {
     $this->assertFieldByName('image_toolkit', 'imagemagick');
     $edit = [
       'image_toolkit' => 'gd',
-      'imagemagick[suite][path_to_binaries]' => '/foo/bar',
+      'imagemagick[suite][path_to_binaries]' => '/foo/bar/',
     ];
     $this->drupalPostForm(NULL, $edit, 'Save configuration');
     $this->assertFieldByName('image_toolkit', 'gd');
@@ -642,7 +961,7 @@ class ToolkitImagemagickTest extends BrowserTestBase {
 
     $transparent_index = imagecolortransparent($toolkit->getResource());
     if ($color_index == $transparent_index) {
-      return array(0, 0, 0, 127);
+      return [0, 0, 0, 127];
     }
 
     return array_values(imagecolorsforindex($toolkit->getResource(), $color_index));
@@ -660,18 +979,294 @@ class ToolkitImagemagickTest extends BrowserTestBase {
    *   The expected RGBA array.
    * @param int $tolerance
    *   The acceptable difference between the colors.
+   * @param string $file
+   *   The image file being tested.
+   * @param string $op
+   *   The image operation being tested.
    *
    * @return bool
    *   TRUE if the colors differences are within tolerance, FALSE otherwise.
    */
-  protected function colorsAreClose(array $actual, array $expected, $tolerance) {
+  protected function colorsAreClose(array $actual, array $expected, $tolerance, $file, $op) {
     // 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);
+    $this->assertLessThanOrEqual($tolerance, $distance, "Actual: {" . implode(',', $actual) . "}, Expected: {" . implode(',', $expected) . "}, Distance: " . $distance . ", Tolerance: " . $tolerance . ", File: " . $file . ", Operation: " . $op);
     return TRUE;
   }
 
+  /**
+   * Test legacy arguments handling.
+   *
+   * @todo remove in 8.x-3.0.
+   *
+   * @group legacy
+   */
+  public function testArgumentsLegacy() {
+    $this->setUpToolkit('imagemagick');
+
+    // Prepare a copy of test files.
+    $this->getTestFiles('image');
+
+    $image_uri = "public://image-test.png";
+    $image = $this->imageFactory->get($image_uri);
+    if (!$image->isValid()) {
+      $this->fail("Could not load image $image_uri.");
+    }
+
+    // Setup a list of arguments.
+    $image->getToolkit()->addArgument("-resize 100x75!");
+    // Internal argument.
+    $image->getToolkit()->addArgument(">!>INTERNAL");
+    $image->getToolkit()->addArgument("-quality 75");
+    $image->getToolkit()->prependArgument("-hoxi 76");
+
+    // Use methods introduced in 8.x-2.3.
+    $image->getToolkit()->arguments()
+      // Pre source argument.
+      ->add("-density 25", ImagemagickExecArguments::PRE_SOURCE)
+      // Another internal argument.
+      ->add("GATEAU", ImagemagickExecArguments::INTERNAL)
+      // Another pre source argument.
+      ->add("-auchocolat 90", ImagemagickExecArguments::PRE_SOURCE)
+      // Add two arguments with additional info.
+      ->add(
+        "-addz 150",
+        ImagemagickExecArguments::POST_SOURCE,
+        ImagemagickExecArguments::APPEND,
+        [
+          'foo' => 'bar',
+          'qux' => 'der',
+        ]
+      )
+      ->add(
+        "-addz 200",
+        ImagemagickExecArguments::POST_SOURCE,
+        ImagemagickExecArguments::APPEND,
+        [
+          'wey' => 'lod',
+          'foo' => 'bar',
+        ]
+      );
+
+    // Test find arguments skipping identifiers.
+    $this->assertSame([
+      0 => '-hoxi 76',
+      1 => '-resize 100x75!',
+      2 => '>!>INTERNAL',
+      3 => '-quality 75',
+      5 => '>!>GATEAU',
+      7 => '-addz 150',
+      8 => '-addz 200',
+    ], $image->getToolkit()->getArguments());
+    $this->assertSame([2], array_keys($image->getToolkit()->arguments()->find('/^INTERNAL/')));
+    $this->assertSame([5], array_keys($image->getToolkit()->arguments()->find('/^GATEAU/')));
+    $this->assertSame([6], array_keys($image->getToolkit()->arguments()->find('/^\-auchocolat/')));
+    $this->assertSame([7, 8], array_keys($image->getToolkit()->arguments()->find('/^\-addz/')));
+    $this->assertSame([7, 8], array_keys($image->getToolkit()->arguments()->find('/.*/', NULL, ['foo' => 'bar'])));
+    $this->assertSame([], $image->getToolkit()->arguments()->find('/.*/', NULL, ['arw' => 'moo']));
+    $this->assertSame(2, $image->getToolkit()->findArgument('>!>INTERNAL'));
+    $this->assertSame(5, $image->getToolkit()->findArgument('>!>GATEAU'));
+    $this->assertFalse($image->getToolkit()->findArgument('-auchocolat'));
+
+    // Check resulting command line strings.
+    $this->assertSame('-density 25 -auchocolat 90', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
+    $this->assertSame("-hoxi 76 -resize 100x75! -quality 75 -addz 150 -addz 200", $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
+    $this->assertSame("-hoxi 76 -resize 100x75! -quality 75 -addz 150 -addz 200", $image->getToolkit()->getStringForBinary());
+  }
+
+  /**
+   * Test arguments handling.
+   */
+  public function testArguments() {
+    $this->setUpToolkit('imagemagick');
+
+    // Prepare a copy of test files.
+    $this->getTestFiles('image');
+
+    $image_uri = "public://image-test.png";
+    $image = $this->imageFactory->get($image_uri);
+    if (!$image->isValid()) {
+      $this->fail("Could not load image $image_uri.");
+    }
+
+    // Setup a list of arguments.
+    $image->getToolkit()->arguments()
+      ->add("-resize 100x75!")
+      // Internal argument.
+      ->add("INTERNAL", ImagemagickExecArguments::INTERNAL)
+      ->add("-quality 75")
+      // Prepend argument.
+      ->add("-hoxi 76", ImagemagickExecArguments::POST_SOURCE, 0)
+      // Pre source argument.
+      ->add("-density 25", ImagemagickExecArguments::PRE_SOURCE)
+      // Another internal argument.
+      ->add("GATEAU", ImagemagickExecArguments::INTERNAL)
+      // Another pre source argument.
+      ->add("-auchocolat 90", ImagemagickExecArguments::PRE_SOURCE)
+      // Add two arguments with additional info.
+      ->add(
+        "-addz 150",
+        ImagemagickExecArguments::POST_SOURCE,
+        ImagemagickExecArguments::APPEND,
+        [
+          'foo' => 'bar',
+          'qux' => 'der',
+        ]
+      )
+      ->add(
+        "-addz 200",
+        ImagemagickExecArguments::POST_SOURCE,
+        ImagemagickExecArguments::APPEND,
+        [
+          'wey' => 'lod',
+          'foo' => 'bar',
+        ]
+      );
+
+    // Test find arguments skipping identifiers.
+    $this->assertSame([2], array_keys($image->getToolkit()->arguments()->find('/^INTERNAL/')));
+    $this->assertSame([5], array_keys($image->getToolkit()->arguments()->find('/^GATEAU/')));
+    $this->assertSame([6], array_keys($image->getToolkit()->arguments()->find('/^\-auchocolat/')));
+    $this->assertSame([7, 8], array_keys($image->getToolkit()->arguments()->find('/^\-addz/')));
+    $this->assertSame([7, 8], array_keys($image->getToolkit()->arguments()->find('/.*/', NULL, ['foo' => 'bar'])));
+    $this->assertSame([], $image->getToolkit()->arguments()->find('/.*/', NULL, ['arw' => 'moo']));
+
+    // Check resulting command line strings.
+    $this->assertSame('-density 25 -auchocolat 90', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
+    $this->assertSame("-hoxi 76 -resize 100x75! -quality 75 -addz 150 -addz 200", $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
+
+    // Add arguments with a specific index.
+    $image->getToolkit()->arguments()
+      ->add("-ix aa", ImagemagickExecArguments::POST_SOURCE, 4)
+      ->add("-ix bb", ImagemagickExecArguments::POST_SOURCE, 4);
+    $this->assertSame([4, 5], array_keys($image->getToolkit()->arguments()->find('/^\-ix/')));
+    $this->assertSame("-hoxi 76 -resize 100x75! -quality 75 -ix bb -ix aa -addz 150 -addz 200", $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
+
+    // Create a new image and inspect the arguments.
+    $image->createNew(100, 200);
+    $this->assertSame([0], array_keys($image->getToolkit()->arguments()->find('/^./', NULL, ['image_toolkit_operation' => 'create_new'])));
+    $this->assertSame([0], array_keys($image->getToolkit()->arguments()->find('/^./', NULL, ['image_toolkit_operation_plugin_id' => 'imagemagick_create_new'])));
+    $this->assertSame("-size 100x200 xc:transparent", $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
+  }
+
+  /**
+   * Test module arguments alter hook.
+   */
+  public function testArgumentsAlterHook() {
+    $this->setUpToolkit('imagemagick');
+
+    $fmdm = $this->container->get('file_metadata_manager');
+
+    // Change the Advanced Colorspace setting, must be included in the command
+    // line.
+    \Drupal::configFactory()->getEditable('imagemagick.settings')
+      ->set('advanced.colorspace', 'GRAY')
+      ->save();
+
+    // Prepare a copy of test files.
+    $this->getTestFiles('image');
+    $image_uri = "public://image-test.png";
+    $image = $this->imageFactory->get($image_uri);
+    if (!$image->isValid()) {
+      $this->fail("Could not load image $image_uri.");
+    }
+
+    // Check the source colorspace.
+    $this->assertSame('SRGB', $image->getToolkit()->getColorspace());
+
+    // Setup a list of arguments.
+    $image->getToolkit()->arguments()
+      ->add("-resize 100x75!")
+      ->add("-quality 75");
+
+    // Save the derived image.
+    $image->save($image_uri . '.derived');
+
+    // Check expected command line.
+    if (substr(PHP_OS, 0, 3) === 'WIN') {
+      $expected = "-resize 100x75! -quality 75 -colorspace \"GRAY\"";
+    }
+    else {
+      $expected = "-resize 100x75! -quality 75 -colorspace 'GRAY'";
+    }
+    $this->assertSame($expected, $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
+
+    // Check that the colorspace has been actually changed in the file.
+    Cache::InvalidateTags([
+      'config:imagemagick.file_metadata_plugin.imagemagick_identify',
+    ]);
+    $fmdm->release($image_uri . '.derived');
+    $image_md = $fmdm->uri($image_uri . '.derived');
+    $image = $this->imageFactory->get($image_uri . '.derived');
+    $this->assertIdentical(FileMetadataInterface::LOADED_FROM_FILE, $image_md->isMetadataLoaded('imagemagick_identify'));
+    $this->assertSame('GRAY', $image->getToolkit()->getColorspace());
+
+    // Change the Prepend settings, must be included in the command line.
+    // Once before the source image.
+    \Drupal::configFactory()->getEditable('imagemagick.settings')
+      ->set('prepend', '-debug All')
+      ->set('prepend_pre_source', TRUE)
+      ->save();
+    $image = $this->imageFactory->get($image_uri);
+    $image->getToolkit()->arguments()
+      ->add("-resize 100x75!")
+      ->add("-quality 75");
+    $image->save($image_uri . '.derived');
+    if (substr(PHP_OS, 0, 3) === 'WIN') {
+      $expected = "-resize 100x75! -quality 75 -colorspace \"GRAY\"";
+    }
+    else {
+      $expected = "-resize 100x75! -quality 75 -colorspace 'GRAY'";
+    }
+    $this->assertSame('-debug All', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
+    $this->assertSame($expected, $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
+    // Then after the source image.
+    \Drupal::configFactory()->getEditable('imagemagick.settings')
+      ->set('prepend_pre_source', FALSE)
+      ->save();
+    $image = $this->imageFactory->get($image_uri);
+    $image->getToolkit()->arguments()
+      ->add("-resize 100x75!")
+      ->add("-quality 75");
+    $image->save($image_uri . '.derived');
+    if (substr(PHP_OS, 0, 3) === 'WIN') {
+      $expected = "-debug All -resize 100x75! -quality 75 -colorspace \"GRAY\"";
+    }
+    else {
+      $expected = "-debug All -resize 100x75! -quality 75 -colorspace 'GRAY'";
+    }
+    $this->assertSame('', $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::PRE_SOURCE));
+    $this->assertSame($expected, $image->getToolkit()->arguments()->toString(ImagemagickExecArguments::POST_SOURCE));
+  }
+
+  /**
+   * Test missing command on ExecManager.
+   */
+  public function testExecManagerCommandNotFound() {
+    $exec_manager = \Drupal::service('imagemagick.exec_manager');
+    $output = '';
+    $error = '';
+    $expected = substr(PHP_OS, 0, 3) !== 'WIN' ? 127 : 1;
+    $ret = $exec_manager->runOsShell('pinkpanther', '-inspector Clouseau', 'blake', $output, $error);
+    $this->assertEquals($expected, $ret, $error);
+  }
+
+  /**
+   * Test timeout on ExecManager.
+   */
+  public function testExecManagerTimeout() {
+    $exec_manager = \Drupal::service('imagemagick.exec_manager');
+    $output = '';
+    $error = '';
+    $expected = substr(PHP_OS, 0, 3) !== 'WIN' ? 143 : 1;
+    // Set a short timeout (1 sec.) and run a process that is expected to last
+    // longer (10 secs.). Should return a 'terminate' exit code.
+    $exec_manager->setTimeout(1);
+    $ret = $exec_manager->runOsShell('sleep', '10', 'sleep', $output, $error);
+    $this->assertEquals($expected, $ret, $error);
+  }
+
 }
index 1802320a24b7640e9bdde939727d2fb62d2c2d04..58c76bc6669f2beaf3dffd8fff53c9b5483d5fb7 100644 (file)
@@ -124,7 +124,9 @@ function pdf_to_imagefield_entity_presave(Drupal\Core\Entity\EntityInterface $en
 /**
  * Implements hook_imagemagick_arguments_alter().
  */
-function pdf_to_imagefield_imagemagick_arguments_alter(\Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit $toolkit, $command) {
+#function pdf_to_imagefield_imagemagick_arguments_alter(\Drupal\imagemagick\Plugin\ImageToolkit\ImagemagickToolkit $toolkit, $command) {
+function pdf_to_imagefield_imagemagick_arguments_alter(\Drupal\imagemagick\ImagemagickExecArguments $arguments_type, $arguements, $command) {
+
   switch ($command) {
     case 'convert':
       // TODO: For some reason some versions/setups of ImageMagick do not