Upgraded imagemagick and manually altered pdf to image module to handle changes....
[yaffs-website] / web / modules / contrib / imagemagick / src / Plugin / ImageToolkit / ImagemagickToolkit.php
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;
   }
 
   /**