3 namespace Drupal\imagemagick\Plugin\ImageToolkit;
5 use Drupal\Component\Render\FormattableMarkup;
6 use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
7 use Drupal\Component\Serialization\Yaml;
8 use Drupal\Component\Utility\Html;
9 use Drupal\Component\Utility\Unicode;
10 use Drupal\Core\Config\ConfigFactoryInterface;
11 use Drupal\Core\Extension\ModuleHandlerInterface;
12 use Drupal\Core\Form\FormStateInterface;
13 use Drupal\Core\ImageToolkit\ImageToolkitBase;
14 use Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface;
16 use Drupal\imagemagick\ImagemagickFormatMapperInterface;
17 use Psr\Log\LoggerInterface;
18 use Symfony\Component\DependencyInjection\ContainerInterface;
21 * Provides ImageMagick integration toolkit for image manipulation.
25 * title = @Translation("ImageMagick image toolkit")
28 class ImagemagickToolkit extends ImageToolkitBase {
31 * Whether we are running on Windows OS.
38 * The module handler service.
40 * @var \Drupal\Core\Extension\ModuleHandlerInterface
42 protected $moduleHandler;
45 * The format mapper service.
47 * @var \Drupal\imagemagick\ImagemagickFormatMapperInterface
49 protected $formatMapper;
59 * The array of command line arguments to be used by 'convert'.
63 protected $arguments = [];
66 * The width of the image.
73 * The height of the image.
80 * The number of frames of the image, for multi-frame images (e.g. GIF).
87 * The local filesystem path to the source image file.
91 protected $sourceLocalPath = '';
94 * The source image format.
98 protected $sourceFormat = '';
101 * Keeps a copy of source image EXIF information.
105 protected $exifInfo = [];
108 * The image destination URI/path on saving.
112 protected $destination = NULL;
115 * The local filesystem path to the image destination.
119 protected $destinationLocalPath = '';
122 * The image destination format on saving.
126 protected $destinationFormat = '';
129 * Constructs an ImagemagickToolkit object.
131 * @param array $configuration
132 * A configuration array containing information about the plugin instance.
133 * @param string $plugin_id
134 * The plugin_id for the plugin instance.
135 * @param array $plugin_definition
136 * The plugin implementation definition.
137 * @param \Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface $operation_manager
138 * The toolkit operation manager.
139 * @param \Psr\Log\LoggerInterface $logger
141 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
142 * The config factory.
143 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
144 * The module handler service.
145 * @param \Drupal\imagemagick\ImagemagickFormatMapperInterface $format_mapper
146 * The format mapper service.
147 * @param string $app_root
150 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) {
151 parent::__construct($configuration, $plugin_id, $plugin_definition, $operation_manager, $logger, $config_factory);
152 $this->moduleHandler = $module_handler;
153 $this->formatMapper = $format_mapper;
154 $this->appRoot = $app_root;
155 $this->isWindows = substr(PHP_OS, 0, 3) === 'WIN';
161 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
166 $container->get('image.toolkit.operation.manager'),
167 $container->get('logger.channel.image'),
168 $container->get('config.factory'),
169 $container->get('module_handler'),
170 $container->get('imagemagick.format_mapper'),
171 $container->get('app.root')
178 public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
179 $config = $this->configFactory->getEditable('imagemagick.settings');
181 $form['imagemagick'] = [
182 '#markup' => $this->t("<a href=':im-url'>ImageMagick</a> and <a href=':gm-url'>GraphicsMagick</a> are stand-alone packages for image manipulation. At least one of them must be installed on the server, and you need to know where it is located. Consult your server administrator or hosting provider for details.", [
183 ':im-url' => 'http://www.imagemagick.org',
184 ':gm-url' => 'http://www.graphicsmagick.org',
189 '#title' => $this->t('Image quality'),
194 '#default_value' => $config->get('quality'),
195 '#field_suffix' => '%',
196 '#description' => $this->t('Define the image quality of processed images. Ranges from 0 to 100. Higher values mean better image quality but bigger files.'),
199 // Graphics suite to use.
201 '#type' => 'details',
203 '#collapsible' => FALSE,
204 '#title' => $this->t('Graphics package'),
207 'imagemagick' => $this->getPackageLabel('imagemagick'),
208 'graphicsmagick' => $this->getPackageLabel('graphicsmagick'),
210 $form['suite']['binaries'] = [
212 '#title' => $this->t('Suite'),
213 '#default_value' => $this->getPackage(),
214 '#options' => $options,
216 '#description' => $this->t("Select the graphics package to use."),
219 $form['suite']['path_to_binaries'] = [
220 '#type' => 'textfield',
221 '#title' => $this->t('Path to the package executables'),
222 '#default_value' => $config->get('path_to_binaries'),
223 '#required' => FALSE,
224 '#description' => $this->t('If needed, the path to the package executables (<kbd>convert</kbd>, <kbd>identify</kbd>, <kbd>gm</kbd>, etc.), <b>including</b> the trailing slash/backslash. For example: <kbd>/usr/bin/</kbd> or <kbd>C:\Program Files\ImageMagick-6.3.4-Q16\</kbd>.'),
226 // Version information.
227 $status = $this->checkPath($config->get('path_to_binaries'));
228 if (empty($status['errors'])) {
229 $version_info = explode("\n", preg_replace('/\r/', '', Html::escape($status['output'])));
232 $version_info = $status['errors'];
234 $form['suite']['version'] = [
235 '#type' => 'details',
236 '#collapsible' => TRUE,
237 '#collapsed' => TRUE,
238 '#title' => $this->t('Version information'),
239 '#description' => '<pre>' . implode('<br />', $version_info) . '</pre>',
244 '#type' => 'details',
246 '#collapsible' => FALSE,
247 '#title' => $this->t('Image formats'),
249 // Use 'identify' command.
250 $form['formats']['use_identify'] = [
251 '#type' => 'checkbox',
252 '#title' => $this->t('Use "identify"'),
253 '#default_value' => $config->get('use_identify'),
254 '#description' => $this->t('Use the <kbd>identify</kbd> command to parse image files to determine image format and dimensions. If not selected, the PHP <kbd>getimagesize</kbd> function will be used, BUT this will limit the image formats supported by the toolkit.'),
256 // Image formats enabled in the toolkit.
257 $form['formats']['enabled'] = [
259 '#title' => $this->t('Enabled images'),
260 '#description' => $this->t("@suite formats: %formats<br />Image file extensions: %extensions", [
261 '%formats' => implode(', ', $this->formatMapper->getEnabledFormats()),
262 '%extensions' => Unicode::strtolower(implode(', ', static::getSupportedExtensions())),
263 '@suite' => $this->getPackageLabel(),
266 // Image formats map.
267 $form['formats']['mapping'] = [
268 '#type' => 'details',
269 '#collapsible' => TRUE,
270 '#collapsed' => TRUE,
271 '#title' => $this->t('Enable/disable image formats'),
272 '#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"),
274 $form['formats']['mapping']['image_formats'] = [
275 '#type' => 'textarea',
277 '#default_value' => Yaml::encode($config->get('image_formats')),
279 // Image formats supported by the package.
280 if (empty($status['errors'])) {
281 $this->addArgument('-list format');
282 $this->imagemagickExec('convert', $output);
283 $this->resetArguments();
284 $formats_info = implode('<br />', explode("\n", preg_replace('/\r/', '', Html::escape($output))));
285 $form['formats']['list'] = [
286 '#type' => 'details',
287 '#collapsible' => TRUE,
288 '#collapsed' => TRUE,
289 '#title' => $this->t('Format list'),
290 '#description' => $this->t("Supported image formats returned by executing <kbd>'convert -list format'</kbd>. <b>Note:</b> these are the formats supported by the installed @suite executable, <b>not</b> by the toolkit.<br /><br />", ['@suite' => $this->getPackageLabel()]),
292 $form['formats']['list']['list'] = [
293 '#markup' => "<pre>" . $formats_info . "</pre>",
297 // Execution options.
299 '#type' => 'details',
301 '#collapsible' => FALSE,
302 '#title' => $this->t('Execution options'),
304 // Prepend arguments.
305 $form['exec']['prepend'] = [
306 '#type' => 'textfield',
307 '#title' => $this->t('Prepend arguments'),
308 '#default_value' => $config->get('prepend'),
309 '#required' => FALSE,
310 '#description' => $this->t('Use this to add e.g. <kbd>-limit</kbd> or <kbd>-debug</kbd> arguments in front of the others when executing the <kbd>identify</kbd> and <kbd>convert</kbd> commands.'),
313 $form['exec']['locale'] = [
314 '#type' => 'textfield',
315 '#title' => $this->t('Locale'),
316 '#default_value' => $config->get('locale'),
317 '#required' => FALSE,
318 '#description' => $this->t("The locale to be used to prepare the command passed to executables. The default, <kbd>'en_US.UTF-8'</kbd>, should work in most cases. If that is not available on the server, enter another locale. On *nix servers, type <kbd>'locale -a'</kbd> in a shell window to see a list of all locales available."),
321 $form['exec']['log_warnings'] = [
322 '#type' => 'checkbox',
323 '#title' => $this->t('Log warnings'),
324 '#default_value' => $config->get('log_warnings'),
325 '#description' => $this->t('Log a warning entry in the watchdog when the execution of a command returns with a non-zero code, but no error message.'),
328 $form['exec']['debug'] = [
329 '#type' => 'checkbox',
330 '#title' => $this->t('Display debugging information'),
331 '#default_value' => $config->get('debug'),
332 '#description' => $this->t('Shows commands and their output to users with the %permission permission.', [
333 '%permission' => $this->t('Administer site configuration'),
337 // Advanced image settings.
338 $form['advanced'] = [
339 '#type' => 'details',
340 '#collapsible' => TRUE,
341 '#collapsed' => TRUE,
342 '#title' => $this->t('Advanced image settings'),
344 $form['advanced']['density'] = [
345 '#type' => 'checkbox',
346 '#title' => $this->t('Change image resolution to 72 ppi'),
347 '#default_value' => $config->get('advanced.density'),
348 '#return_value' => 72,
349 '#description' => $this->t("Resamples the image <a href=':help-url'>density</a> to a resolution of 72 pixels per inch, the default for web images. Does not affect the pixel size or quality.", [
350 ':help-url' => 'http://www.imagemagick.org/script/command-line-options.php#density',
353 $form['advanced']['colorspace'] = [
355 '#title' => $this->t('Convert colorspace'),
356 '#default_value' => $config->get('advanced.colorspace'),
358 'RGB' => $this->t('RGB'),
359 'sRGB' => $this->t('sRGB'),
360 'GRAY' => $this->t('Gray'),
363 '#empty_option' => $this->t('- Original -'),
364 '#description' => $this->t("Converts processed images to the specified <a href=':help-url'>colorspace</a>. The color profile option overrides this setting.", [
365 ':help-url' => 'http://www.imagemagick.org/script/command-line-options.php#colorspace',
369 ':input[name="imagemagick[advanced][profile]"]' => ['value' => ''],
373 $form['advanced']['profile'] = [
374 '#type' => 'textfield',
375 '#title' => $this->t('Color profile path'),
376 '#default_value' => $config->get('advanced.profile'),
377 '#description' => $this->t("The path to a <a href=':help-url'>color profile</a> file that all processed images will be converted to. Leave blank to disable. Use a <a href=':color-url'>sRGB profile</a> to correct the display of professional images and photography.", [
378 ':help-url' => 'http://www.imagemagick.org/script/command-line-options.php#profile',
379 ':color-url' => 'http://www.color.org/profiles.html',
387 * Gets the binaries package in use.
389 * @param string $package
390 * (optional) Force the graphics package.
393 * The default package ('imagemagick'|'graphicsmagick'), or the $package
396 public function getPackage($package = NULL) {
397 if ($package === NULL) {
398 $package = $this->configFactory->get('imagemagick.settings')->get('binaries');
404 * Gets a translated label of the binaries package in use.
406 * @param string $package
407 * (optional) Force the package.
410 * A translated label of the binaries package in use, or the $package
413 public function getPackageLabel($package = NULL) {
414 switch ($this->getPackage($package)) {
416 return $this->t('ImageMagick');
418 case 'graphicsmagick':
419 return $this->t('GraphicsMagick');
428 * Verifies file path of the executable binary by checking its version.
430 * @param string $path
431 * The user-submitted file path to the convert binary.
432 * @param string $package
433 * (optional) The graphics package to use.
436 * An associative array containing:
437 * - output: The shell output of 'convert -version', if any.
438 * - errors: A list of error messages indicating if the executable could
439 * not be found or executed.
441 public function checkPath($path, $package = NULL) {
447 // Execute gm or convert based on settings.
448 $package = $package ?: $this->getPackage();
449 $binary = $package === 'imagemagick' ? 'convert' : 'gm';
450 $executable = $this->getExecutable($binary, $path);
452 // If a path is given, we check whether the binary exists and can be
455 // Check whether the given file exists.
456 if (!is_file($executable)) {
457 $status['errors'][] = $this->t('The @suite executable %file does not exist.', ['@suite' => $this->getPackageLabel($package), '%file' => $executable]);
459 // If it exists, check whether we can execute it.
460 elseif (!is_executable($executable)) {
461 $status['errors'][] = $this->t('The @suite file %file is not executable.', ['@suite' => $this->getPackageLabel($package), '%file' => $executable]);
465 // In case of errors, check for open_basedir restrictions.
466 if ($status['errors'] && ($open_basedir = ini_get('open_basedir'))) {
467 $status['errors'][] = $this->t('The PHP <a href=":php-url">open_basedir</a> security restriction is set to %open-basedir, which may prevent to locate the @suite executable.', [
468 '@suite' => $this->getPackageLabel($package),
469 '%open-basedir' => $open_basedir,
470 ':php-url' => 'http://php.net/manual/en/ini.core.php#ini.open-basedir',
474 // Unless we had errors so far, try to invoke convert.
475 if (!$status['errors']) {
477 $this->runOsShell($executable, '-version', $package, $status['output'], $error);
479 // $error normally needs check_plain(), but file system errors on
480 // Windows use a unknown encoding. check_plain() would eliminate the
482 $status['errors'][] = $error;
492 public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
494 // Check that the format map contains valid YAML.
495 $image_formats = Yaml::decode($form_state->getValue(['imagemagick', 'formats', 'mapping', 'image_formats']));
496 // Validate the enabled image formats.
497 $errors = $this->formatMapper->validateMap($image_formats);
499 $form_state->setErrorByName('imagemagick][formats][mapping][image_formats', new FormattableMarkup("<pre>@errors</pre>", ['@errors' => Yaml::encode($errors)]));
502 catch (InvalidDataTypeException $e) {
503 // Invalid YAML detected, show details.
504 $form_state->setErrorByName('imagemagick][formats][mapping][image_formats', $this->t("YAML syntax error: @error", ['@error' => $e->getMessage()]));
506 // Validate the binaries path only if this toolkit is selected, otherwise
507 // it will prevent the entire image toolkit selection form from being
509 if ($form_state->getValue(['image_toolkit']) === 'imagemagick') {
510 $status = $this->checkPath($form_state->getValue(['imagemagick', 'suite', 'path_to_binaries']), $form_state->getValue(['imagemagick', 'suite', 'binaries']));
511 if ($status['errors']) {
512 $form_state->setErrorByName('imagemagick][suite][path_to_binaries', new FormattableMarkup(implode('<br />', $status['errors']), []));
520 public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
521 $this->configFactory->getEditable('imagemagick.settings')
522 ->set('quality', $form_state->getValue(['imagemagick', 'quality']))
523 ->set('binaries', $form_state->getValue(['imagemagick', 'suite', 'binaries']))
524 ->set('path_to_binaries', $form_state->getValue(['imagemagick', 'suite', 'path_to_binaries']))
525 ->set('use_identify', $form_state->getValue(['imagemagick', 'formats', 'use_identify']))
526 ->set('image_formats', Yaml::decode($form_state->getValue(['imagemagick', 'formats', 'mapping', 'image_formats'])))
527 ->set('prepend', $form_state->getValue(['imagemagick', 'exec', 'prepend']))
528 ->set('locale', $form_state->getValue(['imagemagick', 'exec', 'locale']))
529 ->set('log_warnings', (bool) $form_state->getValue(['imagemagick', 'exec', 'log_warnings']))
530 ->set('debug', $form_state->getValue(['imagemagick', 'exec', 'debug']))
531 ->set('advanced.density', $form_state->getValue(['imagemagick', 'advanced', 'density']))
532 ->set('advanced.colorspace', $form_state->getValue(['imagemagick', 'advanced', 'colorspace']))
533 ->set('advanced.profile', $form_state->getValue(['imagemagick', 'advanced', 'profile']))
540 public function isValid() {
541 return ((bool) $this->getMimeType());
545 * Gets the local filesystem path to the image file.
550 public function getSourceLocalPath() {
551 return $this->sourceLocalPath;
555 * Sets the local filesystem path to the image file.
557 * @param string $path
562 public function setSourceLocalPath($path) {
563 $this->sourceLocalPath = $path;
568 * Gets the source image format.
571 * The source image format.
573 public function getSourceFormat() {
574 return $this->sourceFormat;
578 * Sets the source image format.
580 * @param string $format
585 public function setSourceFormat($format) {
586 $this->sourceFormat = $this->formatMapper->isFormatEnabled($format) ? $format : '';
591 * Sets the source image format from an image file extension.
593 * @param string $extension
594 * The image file extension.
598 public function setSourceFormatFromExtension($extension) {
599 $format = $this->formatMapper->getFormatFromExtension($extension);
600 $this->sourceFormat = $format ?: '';
605 * Gets the source EXIF orientation.
608 * The source EXIF orientation.
610 public function getExifOrientation() {
611 if (empty($this->exifInfo)) {
612 $this->parseExifData();
614 return isset($this->exifInfo['Orientation']) ? $this->exifInfo['Orientation'] : NULL;
618 * Sets the source EXIF orientation.
620 * @param integer|null $exif_orientation
621 * The EXIF orientation.
625 public function setExifOrientation($exif_orientation) {
626 $this->exifInfo['Orientation'] = !empty($exif_orientation) ? ((int) $exif_orientation !== 0 ? (int) $exif_orientation : NULL) : NULL;
631 * Gets the source image number of frames.
634 * The number of frames of the image.
636 public function getFrames() {
637 return $this->frames;
641 * Sets the source image number of frames.
643 * @param integer|null $frames
644 * The number of frames of the image.
648 public function setFrames($frames) {
649 $this->frames = $frames;
654 * Gets the image destination URI/path on saving.
657 * The image destination URI/path.
659 public function getDestination() {
660 return $this->destination;
664 * Sets the image destination URI/path on saving.
666 * @param string $destination
667 * The image destination URI/path.
671 public function setDestination($destination) {
672 $this->destination = $destination;
677 * Gets the local filesystem path to the destination image file.
682 public function getDestinationLocalPath() {
683 return $this->destinationLocalPath;
687 * Sets the local filesystem path to the destination image file.
689 * @param string $path
694 public function setDestinationLocalPath($path) {
695 $this->destinationLocalPath = $path;
700 * Gets the image destination format.
702 * When set, it is passed to the convert binary in the syntax
703 * "[format]:[destination]", where [format] is a string denoting an
704 * ImageMagick's image format.
707 * The image destination format.
709 public function getDestinationFormat() {
710 return $this->destinationFormat;
714 * Sets the image destination format.
716 * When set, it is passed to the convert binary in the syntax
717 * "[format]:[destination]", where [format] is a string denoting an
718 * ImageMagick's image format.
720 * @param string $format
721 * The image destination format.
725 public function setDestinationFormat($format) {
726 $this->destinationFormat = $this->formatMapper->isFormatEnabled($format) ? $format : '';
731 * Sets the image destination format from an image file extension.
733 * When set, it is passed to the convert binary in the syntax
734 * "[format]:[destination]", where [format] is a string denoting an
735 * ImageMagick's image format.
737 * @param string $extension
738 * The destination image file extension.
742 public function setDestinationFormatFromExtension($extension) {
743 $format = $this->formatMapper->getFormatFromExtension($extension);
744 $this->destinationFormat = $format ?: '';
751 public function getWidth() {
763 public function setWidth($width) {
764 $this->width = $width;
771 public function getHeight() {
772 return $this->height;
783 public function setHeight($height) {
784 $this->height = $height;
791 public function getMimeType() {
792 return $this->formatMapper->getMimeTypeFromFormat($this->getSourceFormat());
796 * Gets the command line arguments for the binary.
799 * The array of command line arguments.
801 public function getArguments() {
802 return $this->arguments ?: [];
806 * Adds a command line argument.
809 * The command line argument to be added.
813 public function addArgument($arg) {
814 $this->arguments[] = $arg;
819 * Prepends a command line argument.
822 * The command line argument to be prepended.
826 public function prependArgument($arg) {
827 array_unshift($this->arguments, $arg);
832 * Finds if a command line argument exists.
835 * The command line argument to be found.
838 * Returns the array key for the argument if it is found in the array,
841 public function findArgument($arg) {
842 foreach ($this->getArguments() as $i => $a) {
843 if (strpos($a, $arg) === 0) {
851 * Removes a command line argument.
854 * The index of the command line argument to be removed.
858 public function removeArgument($index) {
859 if (isset($this->arguments[$index])) {
860 unset($this->arguments[$index]);
866 * Resets the command line arguments.
870 public function resetArguments() {
871 $this->arguments = [];
876 * Returns the count of command line arguments.
880 public function countArguments() {
881 return count($this->arguments);
887 * PHP escapeshellarg() drops non-ascii characters, this is a replacement.
889 * Stop-gap replacement until core issue #1561214 has been solved. Solution
890 * proposed in #1502924-8.
892 * PHP escapeshellarg() on Windows also drops % (percentage sign) characters.
893 * We prevent this by replacing it with a pattern that should be highly
894 * unlikely to appear in the string itself and does not contain any
895 * "dangerous" character at all (very wide definition of dangerous). After
896 * escaping we replace that pattern back with a % character.
899 * The string to escape.
902 * An escaped string for use in the ::imagemagickExec method.
904 public function escapeShellArg($arg) {
905 static $percentage_sign_replace_pattern = '1357902468IMAGEMAGICKPERCENTSIGNPATTERN8642097531';
907 // Put the configured locale in a static to avoid multiple config get calls
908 // in the same request.
909 static $config_locale;
911 if (!isset($config_locale)) {
912 $config_locale = $this->configFactory->get('imagemagick.settings')->get('locale');
913 if (empty($config_locale)) {
914 $config_locale = FALSE;
918 if ($this->isWindows) {
919 // Temporarily replace % characters.
920 $arg = str_replace('%', $percentage_sign_replace_pattern, $arg);
923 // If no locale specified in config, return with standard.
924 if ($config_locale === FALSE) {
925 $arg_escaped = escapeshellarg($arg);
928 // Get the current locale.
929 $current_locale = setlocale(LC_CTYPE, 0);
930 if ($current_locale != $config_locale) {
931 // Temporarily swap the current locale with the configured one.
932 setlocale(LC_CTYPE, $config_locale);
933 $arg_escaped = escapeshellarg($arg);
934 setlocale(LC_CTYPE, $current_locale);
937 $arg_escaped = escapeshellarg($arg);
941 // Get our % characters back.
942 if ($this->isWindows) {
943 $arg_escaped = str_replace($percentage_sign_replace_pattern, '%', $arg_escaped);
952 public function save($destination) {
953 $this->setDestination($destination);
954 if ($ret = $this->convert()) {
955 // Allow modules to alter the destination file.
956 $this->moduleHandler->alter('imagemagick_post_save', $this);
957 // Reset local path to allow saving to other file.
958 $this->setDestinationLocalPath('');
966 public function parseFile() {
967 // Allow modules to alter the source file.
968 $this->moduleHandler->alter('imagemagick_pre_parse_file', $this);
969 if ($this->configFactory->get('imagemagick.settings')->get('use_identify')) {
970 return $this->parseFileViaIdentify();
973 return $this->parseFileViaGetImageSize();
978 * Parses the image file using the 'identify' executable.
981 * TRUE if the file could be found and is an image, FALSE otherwise.
983 protected function parseFileViaIdentify() {
984 // Prepare the -format argument according to the graphics package in use.
985 switch ($this->getPackage()) {
987 $this->addArgument('-format ' . $this->escapeShellArg("format:%[magick]|width:%[width]|height:%[height]|exif_orientation:%[EXIF:Orientation]\\n"));
990 case 'graphicsmagick':
991 $this->addArgument('-format ' . $this->escapeShellArg("format:%m|width:%w|height:%h|exif_orientation:%[EXIF:Orientation]\\n"));
996 if ($identify_output = $this->identify()) {
997 $frames = explode("\n", $identify_output);
999 // Remove empty items at the end of the array.
1000 while (empty($frames[count($frames) - 1])) {
1004 // If remaining items are more than one, we have a multi-frame image.
1005 if (count($frames) > 1) {
1006 $this->setFrames(count($frames));
1009 // Take information from the first frame.
1010 $info = explode('|', $frames[0]);
1012 foreach ($info as $item) {
1013 list($key, $value) = explode(':', $item);
1014 $data[trim($key)] = trim($value);
1016 $format = isset($data['format']) ? $data['format'] : NULL;
1017 if ($this->formatMapper->isFormatEnabled($format)) {
1019 ->setSourceFormat($format)
1020 ->setWidth((int) $data['width'])
1021 ->setHeight((int) $data['height'])
1022 ->setExifOrientation($data['exif_orientation']);
1030 * Parses the image file using the PHP getimagesize() function.
1033 * TRUE if the file could be found and is an image, FALSE otherwise.
1035 protected function parseFileViaGetImageSize() {
1036 if ($data = @getimagesize($this->getSourceLocalPath())) {
1037 $format = $this->formatMapper->getFormatFromExtension(image_type_to_extension($data[2], FALSE));
1040 ->setSourceFormat($format)
1041 ->setWidth($data[0])
1042 ->setHeight($data[1]);
1050 * Parses the image file EXIF data using the PHP read_exif_data() function.
1054 protected function parseExifData() {
1056 // Test to see if EXIF is supported by the image format.
1057 $mime_type = $this->getMimeType();
1058 if (!in_array($mime_type, ['image/jpeg', 'image/tiff'])) {
1059 // Not an EXIF enabled image.
1062 $local_path = $this->getSourceLocalPath();
1063 if ($continue && empty($local_path)) {
1064 // No file path available. Most likely a new image from scratch.
1067 if ($continue && !function_exists('exif_read_data')) {
1068 // No PHP EXIF extension enabled, return.
1069 $this->logger->error('The PHP EXIF extension is not installed. The \'imagemagick\' toolkit is unable to automatically determine image orientation.');
1072 if ($continue && ($exif_data = @exif_read_data($this->getSourceLocalPath()))) {
1073 $this->exifInfo = $exif_data;
1076 $this->setExifOrientation(NULL);
1081 * Calls the identify executable on the specified file.
1084 * TRUE if the file could be identified, FALSE otherwise.
1086 protected function identify() {
1087 // Allow modules to alter the command line parameters.
1088 $command = 'identify';
1089 $this->moduleHandler->alter('imagemagick_arguments', $this, $command);
1091 // Executes the command.
1093 $ret = $this->imagemagickExec($command, $output);
1094 $this->resetArguments();
1095 return ($ret === TRUE) ? $output : FALSE;
1099 * Calls the convert executable with the specified arguments.
1102 * TRUE if the file could be converted, FALSE otherwise.
1104 protected function convert() {
1105 // Allow modules to alter the command line parameters.
1106 $command = 'convert';
1107 $this->moduleHandler->alter('imagemagick_arguments', $this, $command);
1109 // Executes the command.
1110 return $this->imagemagickExec($command) === TRUE ? file_exists($this->getDestinationLocalPath()) : FALSE;
1114 * Executes the convert executable as shell command.
1116 * @param string $command
1117 * The executable to run.
1118 * @param string $command_args
1119 * A string containing arguments to pass to the command, which must have
1120 * been passed through $this->escapeShellArg() already.
1121 * @param string &$output
1122 * (optional) A variable to assign the shell stdout to, passed by reference.
1123 * @param string &$error
1124 * (optional) A variable to assign the shell stderr to, passed by reference.
1125 * @param string $path
1126 * (optional) A custom file path to the executable binary.
1129 * The return value depends on the shell command result:
1130 * - Boolean TRUE if the command succeeded.
1131 * - Boolean FALSE if the shell process could not be executed.
1132 * - Error exit status code integer returned by the executable.
1134 protected function imagemagickExec($command, &$output = NULL, &$error = NULL, $path = NULL) {
1137 $binary = $this->getPackage() === 'imagemagick' ? 'convert' : 'gm';
1141 $binary = $this->getPackage() === 'imagemagick' ? 'identify' : 'gm';
1145 $cmd = $this->getExecutable($binary, $path);
1147 if ($source_path = $this->getSourceLocalPath()) {
1148 $source_path = $this->escapeShellArg($source_path);
1151 if ($destination_path = $this->getDestinationLocalPath()) {
1152 $destination_path = $this->escapeShellArg($destination_path);
1153 // If the format of the derivative image has to be changed, concatenate
1154 // the new image format and the destination path, delimited by a colon.
1155 // @see http://www.imagemagick.org/script/command-line-processing.php#output
1156 if (($format = $this->getDestinationFormat()) !== '') {
1157 $destination_path = $format . ':' . $destination_path;
1163 switch($this->getPackage()) {
1165 // ImageMagick syntax:
1166 // identify [arguments] source
1167 $cmdline = implode(' ', $this->getArguments()) . ' ' . $source_path;
1170 case 'graphicsmagick':
1171 // GraphicsMagick syntax:
1172 // gm identify [arguments] source
1173 $cmdline = 'identify ' . implode(' ', $this->getArguments()) . ' ' . $source_path;
1180 switch($this->getPackage()) {
1182 // ImageMagick syntax:
1183 // convert input [arguments] output
1184 // @see http://www.imagemagick.org/Usage/basics/#cmdline
1185 $cmdline = $source_path . ' ' . implode(' ', $this->getArguments()) . ' ' . $destination_path;
1188 case 'graphicsmagick':
1189 // GraphicsMagick syntax:
1190 // gm convert [arguments] input output
1191 // @see http://www.graphicsmagick.org/GraphicsMagick.html
1192 $cmdline = 'convert ' . implode(' ', $this->getArguments()) . ' ' . $source_path . ' ' . $destination_path;
1200 $return_code = $this->runOsShell($cmd, $cmdline, $this->getPackage(), $output, $error);
1202 if ($return_code !== FALSE) {
1203 // If the executable returned a non-zero code, log to the watchdog.
1204 if ($return_code != 0) {
1205 if ($error === '') {
1206 // If there is no error message, and allowed in config, log a
1208 if ($this->configFactory->get('imagemagick.settings')->get('log_warnings') === TRUE) {
1209 $this->logger->warning("@suite returned with code @code [command: @command @cmdline]", [
1210 '@suite' => $this->getPackageLabel(),
1211 '@code' => $return_code,
1213 '@cmdline' => $cmdline,
1218 // Log $error with context information.
1219 $this->logger->error("@suite error @code: @error [command: @command @cmdline]", [
1220 '@suite' => $this->getPackageLabel(),
1221 '@code' => $return_code,
1224 '@cmdline' => $cmdline,
1227 // Executable exited with an error code, return it.
1228 return $return_code;
1231 // The shell command was executed successfully.
1234 // The shell command could not be executed.
1239 * Executes a command on the operating system.
1241 * @param string $command
1242 * The command to run.
1243 * @param string $arguments
1244 * The arguments of the command to run.
1246 * An identifier for the process to be spawned on the operating system.
1247 * @param string &$output
1248 * (optional) A variable to assign the shell stdout to, passed by
1250 * @param string &$error
1251 * (optional) A variable to assign the shell stderr to, passed by
1255 * The operating system returned code, or FALSE if it was not possible to
1256 * execute the command.
1258 protected function runOsShell($command, $arguments, $id, &$output = NULL, &$error = NULL) {
1259 if ($this->isWindows) {
1260 // Use Window's start command with the /B flag to make the process run in
1261 // the background and avoid a shell command line window from showing up.
1262 // @see http://us3.php.net/manual/en/function.exec.php#56599
1263 // Use /D to run the command from PHP's current working directory so the
1264 // file paths don't have to be absolute.
1265 $command = 'start "' . $id . '" /D ' . $this->escapeShellArg($this->appRoot) . ' /B ' . $this->escapeShellArg($command);
1267 $command_line = $command . ' ' . $arguments;
1269 // Executes the command on the OS via proc_open().
1279 if ($h = proc_open($command_line, $descriptors, $pipes, $this->appRoot)) {
1281 while (!feof($pipes[1])) {
1282 $output .= fgets($pipes[1]);
1284 $output = utf8_encode($output);
1286 while (!feof($pipes[2])) {
1287 $error .= fgets($pipes[2]);
1289 $error = utf8_encode($error);
1293 $return_code = proc_close($h);
1296 $return_code = FALSE;
1299 // Process debugging information if required.
1300 if ($this->configFactory->get('imagemagick.settings')->get('debug')) {
1301 $this->debugMessage('@suite command: <pre>@raw</pre>', [
1302 '@suite' => $this->getPackageLabel($id),
1303 '@raw' => print_r($command_line, TRUE),
1305 if ($output !== '') {
1306 $this->debugMessage('@suite output: <pre>@raw</pre>', [
1307 '@suite' => $this->getPackageLabel($id),
1308 '@raw' => print_r($output, TRUE),
1311 if ($error !== '') {
1312 $this->debugMessage('@suite error @return_code: <pre>@raw</pre>', [
1313 '@suite' => $this->getPackageLabel($id),
1314 '@return_code' => $return_code,
1315 '@raw' => print_r($error, TRUE),
1320 return $return_code;
1324 * Logs a debug message, and shows it on the screen for authorized users.
1326 * @param string $message
1327 * The debug message.
1328 * @param string[] $context
1329 * Context information.
1331 public function debugMessage($message, array $context) {
1332 $this->logger->debug($message, $context);
1333 if (\Drupal::currentUser()->hasPermission('administer site configuration')) {
1334 // Strips raw text longer than 10 lines to optimize displaying.
1335 if (isset($context['@raw'])) {
1336 $raw = explode("\n", $context['@raw']);
1337 if (count($raw) > 10) {
1339 for ($i = 0; $i < 9; $i++) {
1342 $tmp[] = (string) $this->t('[Further text stripped. The watchdog log has the full text.]');
1343 $context['@raw'] = implode("\n", $tmp);
1346 drupal_set_message($this->t($message, $context), 'status', TRUE);
1351 * Returns the full path to the executable.
1353 * @param string $binary
1354 * The program to execute, typically 'convert', 'identify' or 'gm'.
1355 * @param string $path
1356 * (optional) A custom path to the folder of the executable. When left
1357 * empty, the setting imagemagick.settings.path_to_binaries is taken.
1360 * The full path to the executable.
1362 public function getExecutable($binary, $path = NULL) {
1363 // $path is only passed from the validation of the image toolkit form, on
1364 // which the path to convert is configured. @see ::checkPath()
1365 if (!isset($path)) {
1366 $path = $this->configFactory->get('imagemagick.settings')->get('path_to_binaries');
1369 $executable = $binary;
1370 if ($this->isWindows) {
1371 $executable .= '.exe';
1374 return $path . $executable;
1380 public function getRequirements() {
1381 $reported_info = [];
1382 if (stripos(ini_get('disable_functions'), 'proc_open') !== FALSE) {
1383 // proc_open() is disabled.
1384 $severity = REQUIREMENT_ERROR;
1385 $reported_info[] = $this->t("The <a href=':proc_open_url'>proc_open()</a> PHP function is disabled. It must be enabled for the toolkit to work. Edit the <a href=':disable_functions_url'>disable_functions</a> entry in your php.ini file, or consult your hosting provider.", [
1386 ':proc_open_url' => 'http://php.net/manual/en/function.proc-open.php',
1387 ':disable_functions_url' => 'http://php.net/manual/en/ini.core.php#ini.disable-functions',
1391 $status = $this->checkPath($this->configFactory->get('imagemagick.settings')->get('path_to_binaries'));
1392 if (!empty($status['errors'])) {
1393 // Can not execute 'convert'.
1394 $severity = REQUIREMENT_ERROR;
1395 foreach ($status['errors'] as $error) {
1396 $reported_info[] = $error;
1398 $reported_info[] = $this->t('Go to the <a href=":url">Image toolkit</a> page to configure the toolkit.', [':url' => Url::fromRoute('system.image_toolkit_settings')->toString()]);
1401 // No errors, report the version information.
1402 $severity = REQUIREMENT_INFO;
1403 $version_info = explode("\n", preg_replace('/\r/', '', Html::escape($status['output'])));
1404 $more_info_available = FALSE;
1405 foreach ($version_info as $key => $item) {
1406 if (stripos($item, 'feature') !== FALSE || $key > 4) {
1407 $more_info_available = TRUE;
1411 $reported_info[] = $item;
1413 if ($more_info_available) {
1414 $reported_info[] = $this->t('To display more information, go to the <a href=":url">Image toolkit</a> page, and expand the \'Version information\' section.', [':url' => Url::fromRoute('system.image_toolkit_settings')->toString()]);
1416 $reported_info[] = '';
1417 $reported_info[] = $this->t("Enabled image file extensions: %extensions", [
1418 '%extensions' => Unicode::strtolower(implode(', ', static::getSupportedExtensions())),
1424 'title' => $this->t('ImageMagick'),
1426 '#markup' => implode('<br />', $reported_info),
1428 'severity' => $severity,
1436 public static function isAvailable() {
1443 public static function getSupportedExtensions() {
1444 return \Drupal::service('imagemagick.format_mapper')->getEnabledExtensions();