7b5406ff43f14cb90dd3388b52854012e28c77c2
[yaffs-website] / web / modules / contrib / imagemagick / src / Plugin / ImageToolkit / ImagemagickToolkit.php
1 <?php
2
3 namespace Drupal\imagemagick\Plugin\ImageToolkit;
4
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;
15 use Drupal\Core\Url;
16 use Drupal\imagemagick\ImagemagickFormatMapperInterface;
17 use Psr\Log\LoggerInterface;
18 use Symfony\Component\DependencyInjection\ContainerInterface;
19
20 /**
21  * Provides ImageMagick integration toolkit for image manipulation.
22  *
23  * @ImageToolkit(
24  *   id = "imagemagick",
25  *   title = @Translation("ImageMagick image toolkit")
26  * )
27  */
28 class ImagemagickToolkit extends ImageToolkitBase {
29
30   /**
31    * Whether we are running on Windows OS.
32    *
33    * @var bool
34    */
35   protected $isWindows;
36
37   /**
38    * The module handler service.
39    *
40    * @var \Drupal\Core\Extension\ModuleHandlerInterface
41    */
42   protected $moduleHandler;
43
44   /**
45    * The format mapper service.
46    *
47    * @var \Drupal\imagemagick\ImagemagickFormatMapperInterface
48    */
49   protected $formatMapper;
50
51   /**
52    * The app root.
53    *
54    * @var string
55    */
56   protected $appRoot;
57
58   /**
59    * The array of command line arguments to be used by 'convert'.
60    *
61    * @var string[]
62    */
63   protected $arguments = [];
64
65   /**
66    * The width of the image.
67    *
68    * @var int
69    */
70   protected $width;
71
72   /**
73    * The height of the image.
74    *
75    * @var int
76    */
77   protected $height;
78
79   /**
80    * The number of frames of the image, for multi-frame images (e.g. GIF).
81    *
82    * @var int
83    */
84   protected $frames;
85
86   /**
87    * The local filesystem path to the source image file.
88    *
89    * @var string
90    */
91   protected $sourceLocalPath = '';
92
93   /**
94    * The source image format.
95    *
96    * @var string
97    */
98   protected $sourceFormat = '';
99
100   /**
101    * Keeps a copy of source image EXIF information.
102    *
103    * @var array
104    */
105   protected $exifInfo = [];
106
107   /**
108    * The image destination URI/path on saving.
109    *
110    * @var string
111    */
112   protected $destination = NULL;
113
114   /**
115    * The local filesystem path to the image destination.
116    *
117    * @var string
118    */
119   protected $destinationLocalPath = '';
120
121   /**
122    * The image destination format on saving.
123    *
124    * @var string
125    */
126   protected $destinationFormat = '';
127
128   /**
129    * Constructs an ImagemagickToolkit object.
130    *
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
140    *   A logger instance.
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
148    *   The app root.
149    */
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';
156   }
157
158   /**
159    * {@inheritdoc}
160    */
161   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
162     return new static(
163       $configuration,
164       $plugin_id,
165       $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')
172     );
173   }
174
175   /**
176    * {@inheritdoc}
177    */
178   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
179     $config = $this->configFactory->getEditable('imagemagick.settings');
180
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',
185       ]),
186     ];
187     $form['quality'] = [
188       '#type' => 'number',
189       '#title' => $this->t('Image quality'),
190       '#size' => 10,
191       '#min' => 0,
192       '#max' => 100,
193       '#maxlength' => 3,
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.'),
197     ];
198
199     // Graphics suite to use.
200     $form['suite'] = [
201       '#type' => 'details',
202       '#open' => TRUE,
203       '#collapsible' => FALSE,
204       '#title' => $this->t('Graphics package'),
205     ];
206     $options = [
207       'imagemagick' => $this->getPackageLabel('imagemagick'),
208       'graphicsmagick' => $this->getPackageLabel('graphicsmagick'),
209     ];
210     $form['suite']['binaries'] = [
211       '#type' => 'radios',
212       '#title' => $this->t('Suite'),
213       '#default_value' => $this->getPackage(),
214       '#options' => $options,
215       '#required' => TRUE,
216       '#description' => $this->t("Select the graphics package to use."),
217     ];
218     // Path to binaries.
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>.'),
225     ];
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'])));
230     }
231     else {
232       $version_info = $status['errors'];
233     }
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>',
240     ];
241
242     // Image formats.
243     $form['formats'] = [
244       '#type' => 'details',
245       '#open' => TRUE,
246       '#collapsible' => FALSE,
247       '#title' => $this->t('Image formats'),
248     ];
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.'),
255     ];
256     // Image formats enabled in the toolkit.
257     $form['formats']['enabled'] = [
258       '#type' => 'item',
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(),
264       ]),
265     ];
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"),
273     ];
274     $form['formats']['mapping']['image_formats'] = [
275       '#type' => 'textarea',
276       '#rows' => 15,
277       '#default_value' => Yaml::encode($config->get('image_formats')),
278     ];
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()]),
291       ];
292       $form['formats']['list']['list'] = [
293         '#markup' => "<pre>" . $formats_info . "</pre>",
294       ];
295     }
296
297     // Execution options.
298     $form['exec'] = [
299       '#type' => 'details',
300       '#open' => TRUE,
301       '#collapsible' => FALSE,
302       '#title' => $this->t('Execution options'),
303     ];
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.'),
311     ];
312     // Locale.
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."),
319     ];
320     // Log warnings.
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.'),
326     ];
327     // Debugging.
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'),
334       ]),
335     ];
336
337     // Advanced image settings.
338     $form['advanced'] = [
339       '#type' => 'details',
340       '#collapsible' => TRUE,
341       '#collapsed' => TRUE,
342       '#title' => $this->t('Advanced image settings'),
343     ];
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',
351       ]),
352     ];
353     $form['advanced']['colorspace'] = [
354       '#type' => 'select',
355       '#title' => $this->t('Convert colorspace'),
356       '#default_value' => $config->get('advanced.colorspace'),
357       '#options' => [
358         'RGB' => $this->t('RGB'),
359         'sRGB' => $this->t('sRGB'),
360         'GRAY' => $this->t('Gray'),
361       ],
362       '#empty_value' => 0,
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',
366       ]),
367       '#states' => [
368         'enabled' => [
369           ':input[name="imagemagick[advanced][profile]"]' => ['value' => ''],
370         ],
371       ],
372     ];
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',
380       ]),
381     ];
382
383     return $form;
384   }
385
386   /**
387    * Gets the binaries package in use.
388    *
389    * @param string $package
390    *   (optional) Force the graphics package.
391    *
392    * @return string
393    *   The default package ('imagemagick'|'graphicsmagick'), or the $package
394    *   argument.
395    */
396   public function getPackage($package = NULL) {
397     if ($package === NULL) {
398       $package = $this->configFactory->get('imagemagick.settings')->get('binaries');
399     }
400     return $package;
401   }
402
403   /**
404    * Gets a translated label of the binaries package in use.
405    *
406    * @param string $package
407    *   (optional) Force the package.
408    *
409    * @return string
410    *   A translated label of the binaries package in use, or the $package
411    *   argument.
412    */
413   public function getPackageLabel($package = NULL) {
414     switch ($this->getPackage($package)) {
415       case 'imagemagick':
416         return $this->t('ImageMagick');
417
418       case 'graphicsmagick':
419         return $this->t('GraphicsMagick');
420
421       default:
422         return $package;
423
424     }
425   }
426
427   /**
428    * Verifies file path of the executable binary by checking its version.
429    *
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.
434    *
435    * @return array
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.
440    */
441   public function checkPath($path, $package = NULL) {
442     $status = [
443       'output' => '',
444       'errors' => [],
445     ];
446
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);
451
452     // If a path is given, we check whether the binary exists and can be
453     // invoked.
454     if (!empty($path)) {
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]);
458       }
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]);
462       }
463     }
464
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',
471       ]);
472     }
473
474     // Unless we had errors so far, try to invoke convert.
475     if (!$status['errors']) {
476       $error = NULL;
477       $this->runOsShell($executable, '-version', $package, $status['output'], $error);
478       if ($error !== '') {
479         // $error normally needs check_plain(), but file system errors on
480         // Windows use a unknown encoding. check_plain() would eliminate the
481         // entire string.
482         $status['errors'][] = $error;
483       }
484     }
485
486     return $status;
487   }
488
489   /**
490    * {@inheritdoc}
491    */
492   public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
493     try {
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);
498       if ($errors) {
499         $form_state->setErrorByName('imagemagick][formats][mapping][image_formats', new FormattableMarkup("<pre>@errors</pre>", ['@errors' => Yaml::encode($errors)]));
500       }
501     }
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()]));
505     }
506     // Validate the binaries path only if this toolkit is selected, otherwise
507     // it will prevent the entire image toolkit selection form from being
508     // submitted.
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']), []));
513       }
514     }
515   }
516
517   /**
518    * {@inheritdoc}
519    */
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']))
534       ->save();
535   }
536
537   /**
538    * {@inheritdoc}
539    */
540   public function isValid() {
541     return ((bool) $this->getMimeType());
542   }
543
544   /**
545    * Gets the local filesystem path to the image file.
546    *
547    * @return string
548    *   A filesystem path.
549    */
550   public function getSourceLocalPath() {
551     return $this->sourceLocalPath;
552   }
553
554   /**
555    * Sets the local filesystem path to the image file.
556    *
557    * @param string $path
558    *   A filesystem path.
559    *
560    * @return $this
561    */
562   public function setSourceLocalPath($path) {
563     $this->sourceLocalPath = $path;
564     return $this;
565   }
566
567   /**
568    * Gets the source image format.
569    *
570    * @return string
571    *   The source image format.
572    */
573   public function getSourceFormat() {
574     return $this->sourceFormat;
575   }
576
577   /**
578    * Sets the source image format.
579    *
580    * @param string $format
581    *   The image format.
582    *
583    * @return $this
584    */
585   public function setSourceFormat($format) {
586     $this->sourceFormat = $this->formatMapper->isFormatEnabled($format) ? $format : '';
587     return $this;
588   }
589
590   /**
591    * Sets the source image format from an image file extension.
592    *
593    * @param string $extension
594    *   The image file extension.
595    *
596    * @return $this
597    */
598   public function setSourceFormatFromExtension($extension) {
599     $format = $this->formatMapper->getFormatFromExtension($extension);
600     $this->sourceFormat = $format ?: '';
601     return $this;
602   }
603
604   /**
605    * Gets the source EXIF orientation.
606    *
607    * @return integer
608    *   The source EXIF orientation.
609    */
610   public function getExifOrientation() {
611     if (empty($this->exifInfo)) {
612       $this->parseExifData();
613     }
614     return isset($this->exifInfo['Orientation']) ? $this->exifInfo['Orientation'] : NULL;
615   }
616
617   /**
618    * Sets the source EXIF orientation.
619    *
620    * @param integer|null $exif_orientation
621    *   The EXIF orientation.
622    *
623    * @return $this
624    */
625   public function setExifOrientation($exif_orientation) {
626     $this->exifInfo['Orientation'] = !empty($exif_orientation) ? ((int) $exif_orientation !== 0 ? (int) $exif_orientation : NULL) : NULL;
627     return $this;
628   }
629
630   /**
631    * Gets the source image number of frames.
632    *
633    * @return integer
634    *   The number of frames of the image.
635    */
636   public function getFrames() {
637     return $this->frames;
638   }
639
640   /**
641    * Sets the source image number of frames.
642    *
643    * @param integer|null $frames
644    *   The number of frames of the image.
645    *
646    * @return $this
647    */
648   public function setFrames($frames) {
649     $this->frames = $frames;
650     return $this;
651   }
652
653   /**
654    * Gets the image destination URI/path on saving.
655    *
656    * @return string
657    *   The image destination URI/path.
658    */
659   public function getDestination() {
660     return $this->destination;
661   }
662
663   /**
664    * Sets the image destination URI/path on saving.
665    *
666    * @param string $destination
667    *   The image destination URI/path.
668    *
669    * @return $this
670    */
671   public function setDestination($destination) {
672     $this->destination = $destination;
673     return $this;
674   }
675
676   /**
677    * Gets the local filesystem path to the destination image file.
678    *
679    * @return string
680    *   A filesystem path.
681    */
682   public function getDestinationLocalPath() {
683     return $this->destinationLocalPath;
684   }
685
686   /**
687    * Sets the local filesystem path to the destination image file.
688    *
689    * @param string $path
690    *   A filesystem path.
691    *
692    * @return $this
693    */
694   public function setDestinationLocalPath($path) {
695     $this->destinationLocalPath = $path;
696     return $this;
697   }
698
699   /**
700    * Gets the image destination format.
701    *
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.
705    *
706    * @return string
707    *   The image destination format.
708    */
709   public function getDestinationFormat() {
710     return $this->destinationFormat;
711   }
712
713   /**
714    * Sets the image destination format.
715    *
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.
719    *
720    * @param string $format
721    *   The image destination format.
722    *
723    * @return $this
724    */
725   public function setDestinationFormat($format) {
726     $this->destinationFormat = $this->formatMapper->isFormatEnabled($format) ? $format : '';
727     return $this;
728   }
729
730   /**
731    * Sets the image destination format from an image file extension.
732    *
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.
736    *
737    * @param string $extension
738    *   The destination image file extension.
739    *
740    * @return $this
741    */
742   public function setDestinationFormatFromExtension($extension) {
743     $format = $this->formatMapper->getFormatFromExtension($extension);
744     $this->destinationFormat = $format ?: '';
745     return $this;
746   }
747
748   /**
749    * {@inheritdoc}
750    */
751   public function getWidth() {
752     return $this->width;
753   }
754
755   /**
756    * Sets image width.
757    *
758    * @param int $width
759    *   The image width.
760    *
761    * @return $this
762    */
763   public function setWidth($width) {
764     $this->width = $width;
765     return $this;
766   }
767
768   /**
769    * {@inheritdoc}
770    */
771   public function getHeight() {
772     return $this->height;
773   }
774
775   /**
776    * Sets image height.
777    *
778    * @param int $height
779    *   The image height.
780    *
781    * @return $this
782    */
783   public function setHeight($height) {
784     $this->height = $height;
785     return $this;
786   }
787
788   /**
789    * {@inheritdoc}
790    */
791   public function getMimeType() {
792     return $this->formatMapper->getMimeTypeFromFormat($this->getSourceFormat());
793   }
794
795   /**
796    * Gets the command line arguments for the binary.
797    *
798    * @return string[]
799    *   The array of command line arguments.
800    */
801   public function getArguments() {
802     return $this->arguments ?: [];
803   }
804
805   /**
806    * Adds a command line argument.
807    *
808    * @param string $arg
809    *   The command line argument to be added.
810    *
811    * @return $this
812    */
813   public function addArgument($arg) {
814     $this->arguments[] = $arg;
815     return $this;
816   }
817
818   /**
819    * Prepends a command line argument.
820    *
821    * @param string $arg
822    *   The command line argument to be prepended.
823    *
824    * @return $this
825    */
826   public function prependArgument($arg) {
827     array_unshift($this->arguments, $arg);
828     return $this;
829   }
830
831   /**
832    * Finds if a command line argument exists.
833    *
834    * @param string $arg
835    *   The command line argument to be found.
836    *
837    * @return bool
838    *   Returns the array key for the argument if it is found in the array,
839    *   FALSE otherwise.
840    */
841   public function findArgument($arg) {
842     foreach ($this->getArguments() as $i => $a) {
843       if (strpos($a, $arg) === 0) {
844         return $i;
845       }
846     }
847     return FALSE;
848   }
849
850   /**
851    * Removes a command line argument.
852    *
853    * @param int $index
854    *   The index of the command line argument to be removed.
855    *
856    * @return $this
857    */
858   public function removeArgument($index) {
859     if (isset($this->arguments[$index])) {
860       unset($this->arguments[$index]);
861     }
862     return $this;
863   }
864
865   /**
866    * Resets the command line arguments.
867    *
868    * @return $this
869    */
870   public function resetArguments() {
871     $this->arguments = [];
872     return $this;
873   }
874
875   /**
876    * Returns the count of command line arguments.
877    *
878    * @return $this
879    */
880   public function countArguments() {
881     return count($this->arguments);
882   }
883
884   /**
885    * Escapes a string.
886    *
887    * PHP escapeshellarg() drops non-ascii characters, this is a replacement.
888    *
889    * Stop-gap replacement until core issue #1561214 has been solved. Solution
890    * proposed in #1502924-8.
891    *
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.
897    *
898    * @param string $arg
899    *   The string to escape.
900    *
901    * @return string
902    *   An escaped string for use in the ::imagemagickExec method.
903    */
904   public function escapeShellArg($arg) {
905     static $percentage_sign_replace_pattern = '1357902468IMAGEMAGICKPERCENTSIGNPATTERN8642097531';
906
907     // Put the configured locale in a static to avoid multiple config get calls
908     // in the same request.
909     static $config_locale;
910
911     if (!isset($config_locale)) {
912       $config_locale = $this->configFactory->get('imagemagick.settings')->get('locale');
913       if (empty($config_locale)) {
914         $config_locale = FALSE;
915       }
916     }
917
918     if ($this->isWindows) {
919       // Temporarily replace % characters.
920       $arg = str_replace('%', $percentage_sign_replace_pattern, $arg);
921     }
922
923     // If no locale specified in config, return with standard.
924     if ($config_locale === FALSE) {
925       $arg_escaped = escapeshellarg($arg);
926     }
927     else {
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);
935       }
936       else {
937         $arg_escaped = escapeshellarg($arg);
938       }
939     }
940
941     // Get our % characters back.
942     if ($this->isWindows) {
943       $arg_escaped = str_replace($percentage_sign_replace_pattern, '%', $arg_escaped);
944     }
945
946     return $arg_escaped;
947   }
948
949   /**
950    * {@inheritdoc}
951    */
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('');
959     }
960     return $ret;
961   }
962
963   /**
964    * {@inheritdoc}
965    */
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();
971     }
972     else {
973       return $this->parseFileViaGetImageSize();
974     }
975   }
976
977   /**
978    * Parses the image file using the 'identify' executable.
979    *
980    * @return bool
981    *   TRUE if the file could be found and is an image, FALSE otherwise.
982    */
983   protected function parseFileViaIdentify() {
984     // Prepare the -format argument according to the graphics package in use.
985     switch ($this->getPackage()) {
986       case 'imagemagick':
987         $this->addArgument('-format ' . $this->escapeShellArg("format:%[magick]|width:%[width]|height:%[height]|exif_orientation:%[EXIF:Orientation]\\n"));
988         break;
989
990       case 'graphicsmagick':
991         $this->addArgument('-format ' . $this->escapeShellArg("format:%m|width:%w|height:%h|exif_orientation:%[EXIF:Orientation]\\n"));
992         break;
993
994     }
995
996     if ($identify_output = $this->identify()) {
997       $frames = explode("\n", $identify_output);
998
999       // Remove empty items at the end of the array.
1000       while (empty($frames[count($frames) - 1])) {
1001         array_pop($frames);
1002       }
1003
1004       // If remaining items are more than one, we have a multi-frame image.
1005       if (count($frames) > 1) {
1006         $this->setFrames(count($frames));
1007       }
1008
1009       // Take information from the first frame.
1010       $info = explode('|', $frames[0]);
1011       $data = [];
1012       foreach ($info as $item) {
1013         list($key, $value) = explode(':', $item);
1014         $data[trim($key)] = trim($value);
1015       }
1016       $format = isset($data['format']) ? $data['format'] : NULL;
1017       if ($this->formatMapper->isFormatEnabled($format)) {
1018         $this
1019           ->setSourceFormat($format)
1020           ->setWidth((int) $data['width'])
1021           ->setHeight((int) $data['height'])
1022           ->setExifOrientation($data['exif_orientation']);
1023         return TRUE;
1024       }
1025     }
1026     return FALSE;
1027   }
1028
1029   /**
1030    * Parses the image file using the PHP getimagesize() function.
1031    *
1032    * @return bool
1033    *   TRUE if the file could be found and is an image, FALSE otherwise.
1034    */
1035   protected function parseFileViaGetImageSize() {
1036     if ($data = @getimagesize($this->getSourceLocalPath())) {
1037       $format = $this->formatMapper->getFormatFromExtension(image_type_to_extension($data[2], FALSE));
1038       if ($format) {
1039         $this
1040           ->setSourceFormat($format)
1041           ->setWidth($data[0])
1042           ->setHeight($data[1]);
1043         return TRUE;
1044       }
1045     };
1046     return FALSE;
1047   }
1048
1049   /**
1050    * Parses the image file EXIF data using the PHP read_exif_data() function.
1051    *
1052    * @return $this
1053    */
1054   protected function parseExifData() {
1055     $continue = TRUE;
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.
1060       $continue = FALSE;
1061     }
1062     $local_path = $this->getSourceLocalPath();
1063     if ($continue && empty($local_path)) {
1064       // No file path available. Most likely a new image from scratch.
1065       $continue = FALSE;
1066     }
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.');
1070       $continue = FALSE;
1071     }
1072     if ($continue && ($exif_data = @exif_read_data($this->getSourceLocalPath()))) {
1073       $this->exifInfo = $exif_data;
1074       return $this;
1075     }
1076     $this->setExifOrientation(NULL);
1077     return $this;
1078   }
1079
1080   /**
1081    * Calls the identify executable on the specified file.
1082    *
1083    * @return bool
1084    *   TRUE if the file could be identified, FALSE otherwise.
1085    */
1086   protected function identify() {
1087     // Allow modules to alter the command line parameters.
1088     $command = 'identify';
1089     $this->moduleHandler->alter('imagemagick_arguments', $this, $command);
1090
1091     // Executes the command.
1092     $output = NULL;
1093     $ret = $this->imagemagickExec($command, $output);
1094     $this->resetArguments();
1095     return ($ret === TRUE) ? $output : FALSE;
1096   }
1097
1098   /**
1099    * Calls the convert executable with the specified arguments.
1100    *
1101    * @return bool
1102    *   TRUE if the file could be converted, FALSE otherwise.
1103    */
1104   protected function convert() {
1105     // Allow modules to alter the command line parameters.
1106     $command = 'convert';
1107     $this->moduleHandler->alter('imagemagick_arguments', $this, $command);
1108
1109     // Executes the command.
1110     return $this->imagemagickExec($command) === TRUE ? file_exists($this->getDestinationLocalPath()) : FALSE;
1111   }
1112
1113   /**
1114    * Executes the convert executable as shell command.
1115    *
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.
1127    *
1128    * @return mixed
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.
1133    */
1134   protected function imagemagickExec($command, &$output = NULL, &$error = NULL, $path = NULL) {
1135     switch ($command) {
1136       case 'convert':
1137         $binary = $this->getPackage() === 'imagemagick' ? 'convert' : 'gm';
1138         break;
1139
1140       case 'identify':
1141         $binary = $this->getPackage() === 'imagemagick' ? 'identify' : 'gm';
1142         break;
1143
1144     }
1145     $cmd = $this->getExecutable($binary, $path);
1146
1147     if ($source_path = $this->getSourceLocalPath()) {
1148       $source_path = $this->escapeShellArg($source_path);
1149     }
1150
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;
1158       }
1159     }
1160
1161     switch($command) {
1162       case 'identify':
1163         switch($this->getPackage()) {
1164           case 'imagemagick':
1165             // ImageMagick syntax:
1166             // identify [arguments] source
1167             $cmdline = implode(' ', $this->getArguments()) . ' ' . $source_path;
1168             break;
1169
1170           case 'graphicsmagick':
1171             // GraphicsMagick syntax:
1172             // gm identify [arguments] source
1173             $cmdline = 'identify ' . implode(' ', $this->getArguments()) . ' ' . $source_path;
1174             break;
1175
1176         }
1177         break;
1178
1179       case 'convert':
1180         switch($this->getPackage()) {
1181           case 'imagemagick':
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;
1186             break;
1187
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;
1193             break;
1194
1195         }
1196         break;
1197
1198     }
1199
1200     $return_code = $this->runOsShell($cmd, $cmdline, $this->getPackage(), $output, $error);
1201
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
1207           // warning.
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,
1212               '@command' => $cmd,
1213               '@cmdline' => $cmdline,
1214             ]);
1215           }
1216         }
1217         else {
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,
1222             '@error' => $error,
1223             '@command' => $cmd,
1224             '@cmdline' => $cmdline,
1225           ]);
1226         }
1227         // Executable exited with an error code, return it.
1228         return $return_code;
1229       }
1230
1231       // The shell command was executed successfully.
1232       return TRUE;
1233     }
1234     // The shell command could not be executed.
1235     return FALSE;
1236   }
1237
1238   /**
1239    * Executes a command on the operating system.
1240    *
1241    * @param string $command
1242    *   The command to run.
1243    * @param string $arguments
1244    *   The arguments of the command to run.
1245    * @param string $id
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
1249    *   reference.
1250    * @param string &$error
1251    *   (optional) A variable to assign the shell stderr to, passed by
1252    *   reference.
1253    *
1254    * @return int|bool
1255    *   The operating system returned code, or FALSE if it was not possible to
1256    *   execute the command.
1257    */
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);
1266     }
1267     $command_line = $command . ' ' . $arguments;
1268
1269     // Executes the command on the OS via proc_open().
1270     $descriptors = [
1271       // This is stdin.
1272       0 => ['pipe', 'r'],
1273       // This is stdout.
1274       1 => ['pipe', 'w'],
1275       // This is stderr.
1276       2 => ['pipe', 'w'],
1277     ];
1278
1279     if ($h = proc_open($command_line, $descriptors, $pipes, $this->appRoot)) {
1280       $output = '';
1281       while (!feof($pipes[1])) {
1282         $output .= fgets($pipes[1]);
1283       }
1284       $output = utf8_encode($output);
1285       $error = '';
1286       while (!feof($pipes[2])) {
1287         $error .= fgets($pipes[2]);
1288       }
1289       $error = utf8_encode($error);
1290       fclose($pipes[0]);
1291       fclose($pipes[1]);
1292       fclose($pipes[2]);
1293       $return_code = proc_close($h);
1294     }
1295     else {
1296       $return_code = FALSE;
1297     }
1298
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),
1304       ]);
1305       if ($output !== '') {
1306         $this->debugMessage('@suite output: <pre>@raw</pre>', [
1307           '@suite' => $this->getPackageLabel($id),
1308           '@raw' => print_r($output, TRUE),
1309         ]);
1310       }
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),
1316         ]);
1317       }
1318     }
1319
1320     return $return_code;
1321   }
1322
1323   /**
1324    * Logs a debug message, and shows it on the screen for authorized users.
1325    *
1326    * @param string $message
1327    *   The debug message.
1328    * @param string[] $context
1329    *   Context information.
1330    */
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) {
1338           $tmp = [];
1339           for ($i = 0; $i < 9; $i++) {
1340             $tmp[] = $raw[$i];
1341           }
1342           $tmp[] = (string) $this->t('[Further text stripped. The watchdog log has the full text.]');
1343           $context['@raw'] = implode("\n", $tmp);
1344         }
1345       }
1346       drupal_set_message($this->t($message, $context), 'status', TRUE);
1347     }
1348   }
1349
1350   /**
1351    * Returns the full path to the executable.
1352    *
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.
1358    *
1359    * @return string
1360    *   The full path to the executable.
1361    */
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');
1367     }
1368
1369     $executable = $binary;
1370     if ($this->isWindows) {
1371       $executable .= '.exe';
1372     }
1373
1374     return $path . $executable;
1375   }
1376
1377   /**
1378    * {@inheritdoc}
1379    */
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',
1388       ]);
1389     }
1390     else {
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;
1397         }
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()]);
1399       }
1400       else {
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;
1408             break;
1409
1410           }
1411           $reported_info[] = $item;
1412         }
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()]);
1415         }
1416         $reported_info[] = '';
1417         $reported_info[] = $this->t("Enabled image file extensions: %extensions", [
1418           '%extensions' => Unicode::strtolower(implode(', ', static::getSupportedExtensions())),
1419         ]);
1420       }
1421     }
1422     return [
1423       'imagemagick' => [
1424         'title' => $this->t('ImageMagick'),
1425         'description' => [
1426           '#markup' => implode('<br />', $reported_info),
1427         ],
1428         'severity' => $severity,
1429       ],
1430     ];
1431   }
1432
1433   /**
1434    * {@inheritdoc}
1435    */
1436   public static function isAvailable() {
1437     return TRUE;
1438   }
1439
1440   /**
1441    * {@inheritdoc}
1442    */
1443   public static function getSupportedExtensions() {
1444     return \Drupal::service('imagemagick.format_mapper')->getEnabledExtensions();
1445   }
1446
1447 }