From: Jeff Veit Date: Tue, 13 Nov 2018 18:50:15 +0000 (+0000) Subject: Upgraded imagemagick and manually altered pdf to image module to handle changes.... X-Git-Url: http://www.aleph1.co.uk/gitweb/?p=yaffs-website;a=commitdiff_plain;h=f8fc16ae6b862bef59baaad5d051dd37b7ff11b2 Upgraded imagemagick and manually altered pdf to image module to handle changes. pdf to image may not be complete, and it's not fully tested. --- diff --git a/web/modules/contrib/imagemagick/README.txt b/web/modules/contrib/imagemagick/README.txt index 7fc011ea5..1cb13da0e 100644 --- a/web/modules/contrib/imagemagick/README.txt +++ b/web/modules/contrib/imagemagick/README.txt @@ -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 index 000000000..4e0e46e8c --- /dev/null +++ b/web/modules/contrib/imagemagick/composer.json @@ -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 index 000000000..394780947 --- /dev/null +++ b/web/modules/contrib/imagemagick/config/install/imagemagick.file_metadata_plugin.imagemagick_identify.yml @@ -0,0 +1,7 @@ +configuration: + cache: + override: FALSE + settings: + enabled: TRUE + expiration: 172800 + disallowed_paths: { } diff --git a/web/modules/contrib/imagemagick/config/install/imagemagick.settings.yml b/web/modules/contrib/imagemagick/config/install/imagemagick.settings.yml index d91e5abeb..237593e12 100644 --- a/web/modules/contrib/imagemagick/config/install/imagemagick.settings.yml +++ b/web/modules/contrib/imagemagick/config/install/imagemagick.settings.yml @@ -2,6 +2,7 @@ quality: 75 binaries: 'imagemagick' path_to_binaries: '' prepend: '' +prepend_pre_source: FALSE log_warnings: TRUE debug: FALSE use_identify: TRUE diff --git a/web/modules/contrib/imagemagick/config/schema/imagemagick.schema.yml b/web/modules/contrib/imagemagick/config/schema/imagemagick.schema.yml index a89306424..95df7aee2 100644 --- a/web/modules/contrib/imagemagick/config/schema/imagemagick.schema.yml +++ b/web/modules/contrib/imagemagick/config/schema/imagemagick.schema.yml @@ -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' diff --git a/web/modules/contrib/imagemagick/imagemagick.api.php b/web/modules/contrib/imagemagick/imagemagick.api.php index e11cd26e0..51478cba0 100644 --- a/web/modules/contrib/imagemagick/imagemagick.api.php +++ b/web/modules/contrib/imagemagick/imagemagick.api.php @@ -18,15 +18,15 @@ * 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) { } diff --git a/web/modules/contrib/imagemagick/imagemagick.info.yml b/web/modules/contrib/imagemagick/imagemagick.info.yml index 0c5d35e9c..1481440a6 100644 --- a/web/modules/contrib/imagemagick/imagemagick.info.yml +++ b/web/modules/contrib/imagemagick/imagemagick.info.yml @@ -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 diff --git a/web/modules/contrib/imagemagick/imagemagick.install b/web/modules/contrib/imagemagick/imagemagick.install index 12aa0c05c..1b7800553 100644 --- a/web/modules/contrib/imagemagick/imagemagick.install +++ b/web/modules/contrib/imagemagick/imagemagick.install @@ -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. +} diff --git a/web/modules/contrib/imagemagick/imagemagick.module b/web/modules/contrib/imagemagick/imagemagick.module index e6f0e8b06..13bb01bf0 100644 --- a/web/modules/contrib/imagemagick/imagemagick.module +++ b/web/modules/contrib/imagemagick/imagemagick.module @@ -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); } } diff --git a/web/modules/contrib/imagemagick/imagemagick.services.yml b/web/modules/contrib/imagemagick/imagemagick.services.yml index f5148cdd5..87e2e3f30 100644 --- a/web/modules/contrib/imagemagick/imagemagick.services.yml +++ b/web/modules/contrib/imagemagick/imagemagick.services.yml @@ -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 index 000000000..d15439f4e 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 index 000000000..0666c756f --- /dev/null +++ b/web/modules/contrib/imagemagick/src/ImagemagickExecArguments.php @@ -0,0 +1,634 @@ +!>'; + + /** + * 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 index 000000000..743950670 --- /dev/null +++ b/web/modules/contrib/imagemagick/src/ImagemagickExecManager.php @@ -0,0 +1,541 @@ +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 open_basedir 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:
@raw
executed in @execution_timems', [ + '@suite' => $this->getPackageLabel($id), + '@raw' => print_r($command_line, TRUE), + '@execution_time' => $execution_time, + ]); + if ($output !== '') { + $this->debugMessage('@suite output:
@raw
', [ + '@suite' => $this->getPackageLabel($id), + '@raw' => print_r($output, TRUE), + ]); + } + if ($error !== '') { + $this->debugMessage('@suite error @return_code:
@raw
', [ + '@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 index 000000000..252ef2df3 --- /dev/null +++ b/web/modules/contrib/imagemagick/src/ImagemagickExecManagerInterface.php @@ -0,0 +1,97 @@ +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. * diff --git a/web/modules/contrib/imagemagick/src/ImagemagickFormatMapperInterface.php b/web/modules/contrib/imagemagick/src/ImagemagickFormatMapperInterface.php index e311aed45..97614c2e3 100644 --- a/web/modules/contrib/imagemagick/src/ImagemagickFormatMapperInterface.php +++ b/web/modules/contrib/imagemagick/src/ImagemagickFormatMapperInterface.php @@ -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 index 000000000..bfe457a8c --- /dev/null +++ b/web/modules/contrib/imagemagick/src/ImagemagickMimeTypeMapper.php @@ -0,0 +1,97 @@ +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 index 000000000..d8eb50df2 --- /dev/null +++ b/web/modules/contrib/imagemagick/src/Plugin/FileMetadata/ImagemagickIdentify.php @@ -0,0 +1,278 @@ +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; + } + +} diff --git a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/ImagemagickToolkit.php b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/ImagemagickToolkit.php index 7b5406ff4..f25c74edd 100644 --- a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/ImagemagickToolkit.php +++ b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/ImagemagickToolkit.php @@ -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 (convert, identify, gm, etc.), including the trailing slash/backslash. For example: /usr/bin/ or C:\Program Files\ImageMagick-6.3.4-Q16\.'), ]; // 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' => '
' . implode('
', $version_info) . '
', ]; @@ -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 identify command to parse image files to determine image format and dimensions. If not selected, the PHP getimagesize 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
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('
', 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 'convert -list format'. Note: these are the formats supported by the installed @suite executable, not by the toolkit.

", ['@suite' => $this->getPackageLabel()]), + '#description' => $this->t("Supported image formats returned by executing 'convert -list format'. Note: these are the formats supported by the installed @suite executable, not by the toolkit.

", ['@suite' => $this->getExecManager()->getPackageLabel()]), ]; $form['formats']['list']['list'] = [ '#markup' => "
" . $formats_info . "
", @@ -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('This setting is deprecated and will be removed in the next major release of the Imagemagick module. Leave it enabled to ensure smooth transition.') . ' ' . $this->t('Use the identify command to parse image files to determine image format and dimensions. If not selected, the PHP getimagesize 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 shell 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. -limit or -debug arguments in front of the others when executing the identify and convert 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. -limit or -debug arguments in front of the others when executing the identify and convert 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, 'en_US.UTF-8', should work in most cases. If that is not available on the server, enter another locale. On *nix servers, type 'locale -a' 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, 'en_US.UTF-8', 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('
', 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 'locale -a' on the operating system."), + ]; + $form['exec']['installed_locales']['list'] = [ + '#markup' => "
" . $locales_info . "
", ]; // 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 open_basedir 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('
', $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:
@raw
', [ - '@suite' => $this->getPackageLabel($id), - '@raw' => print_r($command_line, TRUE), - ]); - if ($output !== '') { - $this->debugMessage('@suite output:
@raw
', [ - '@suite' => $this->getPackageLabel($id), - '@raw' => print_r($output, TRUE), - ]); - } - if ($error !== '') { - $this->debugMessage('@suite error @return_code:
@raw
', [ - '@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('
', $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 getimagesize PHP function to parse image files. This functionality will be dropped in the next major release of the Imagemagick module. Go to the Image toolkit 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; } /** diff --git a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Convert.php b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Convert.php index 7015d4a5e..b5865ec01 100644 --- a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Convert.php +++ b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Convert.php @@ -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; } diff --git a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/CreateNew.php b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/CreateNew.php index 1b0c087b7..37636cf7d 100644 --- a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/CreateNew.php +++ b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/CreateNew.php @@ -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; } diff --git a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Crop.php b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Crop.php index eedd1ebca..04f621868 100644 --- a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Crop.php +++ b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Crop.php @@ -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; } diff --git a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Desaturate.php b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Desaturate.php index 571d133ab..cb796523b 100644 --- a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Desaturate.php +++ b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Desaturate.php @@ -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; } diff --git a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/ImagemagickImageToolkitOperationBase.php b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/ImagemagickImageToolkitOperationBase.php index e870245b0..3ec26c771 100644 --- a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/ImagemagickImageToolkitOperationBase.php +++ b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/ImagemagickImageToolkitOperationBase.php @@ -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); + } } diff --git a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Resize.php b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Resize.php index e1c25831d..66e610a97 100644 --- a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Resize.php +++ b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Resize.php @@ -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; } diff --git a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Rotate.php b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Rotate.php index f54eeacab..eab120ac8 100644 --- a/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Rotate.php +++ b/web/modules/contrib/imagemagick/src/Plugin/ImageToolkit/Operation/imagemagick/Rotate.php @@ -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 index fa536cba2..000000000 --- a/web/modules/contrib/imagemagick/src/Todo2311679.php +++ /dev/null @@ -1,43 +0,0 @@ -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 index 000000000..1a5683aea --- /dev/null +++ b/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickFileMetadataTest.php @@ -0,0 +1,1131 @@ +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()); + } + +} diff --git a/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickTest.php b/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickTest.php index 2322581ff..df6af8dab 100644 --- a/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickTest.php +++ b/web/modules/contrib/imagemagick/tests/src/Functional/ToolkitImagemagickTest.php @@ -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); + } + } diff --git a/web/modules/contrib/pdf_to_imagefield/pdf_to_imagefield.module b/web/modules/contrib/pdf_to_imagefield/pdf_to_imagefield.module index 1802320a2..58c76bc66 100644 --- a/web/modules/contrib/pdf_to_imagefield/pdf_to_imagefield.module +++ b/web/modules/contrib/pdf_to_imagefield/pdf_to_imagefield.module @@ -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