Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / system / src / Plugin / ImageToolkit / GDToolkit.php
1 <?php
2
3 namespace Drupal\system\Plugin\ImageToolkit;
4
5 use Drupal\Component\Utility\Color;
6 use Drupal\Core\Config\ConfigFactoryInterface;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\ImageToolkit\ImageToolkitBase;
9 use Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface;
10 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
11 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
12 use Psr\Log\LoggerInterface;
13 use Symfony\Component\DependencyInjection\ContainerInterface;
14
15 /**
16  * Defines the GD2 toolkit for image manipulation within Drupal.
17  *
18  * @ImageToolkit(
19  *   id = "gd",
20  *   title = @Translation("GD2 image manipulation toolkit")
21  * )
22  */
23 class GDToolkit extends ImageToolkitBase {
24
25   /**
26    * A GD image resource.
27    *
28    * @var resource|null
29    */
30   protected $resource = NULL;
31
32   /**
33    * Image type represented by a PHP IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG).
34    *
35    * @var int
36    */
37   protected $type;
38
39   /**
40    * Image information from a file, available prior to loading the GD resource.
41    *
42    * This contains a copy of the array returned by executing getimagesize()
43    * on the image file when the image object is instantiated. It gets reset
44    * to NULL as soon as the GD resource is loaded.
45    *
46    * @var array|null
47    *
48    * @see \Drupal\system\Plugin\ImageToolkit\GDToolkit::parseFile()
49    * @see \Drupal\system\Plugin\ImageToolkit\GDToolkit::setResource()
50    * @see http://php.net/manual/function.getimagesize.php
51    */
52   protected $preLoadInfo = NULL;
53
54   /**
55    * The StreamWrapper manager.
56    *
57    * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
58    */
59   protected $streamWrapperManager;
60
61   /**
62    * Constructs a GDToolkit object.
63    *
64    * @param array $configuration
65    *   A configuration array containing information about the plugin instance.
66    * @param string $plugin_id
67    *   The plugin_id for the plugin instance.
68    * @param array $plugin_definition
69    *   The plugin implementation definition.
70    * @param \Drupal\Core\ImageToolkit\ImageToolkitOperationManagerInterface $operation_manager
71    *   The toolkit operation manager.
72    * @param \Psr\Log\LoggerInterface $logger
73    *   A logger instance.
74    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
75    *   The config factory.
76    * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
77    *   The StreamWrapper manager.
78    */
79   public function __construct(array $configuration, $plugin_id, array $plugin_definition, ImageToolkitOperationManagerInterface $operation_manager, LoggerInterface $logger, ConfigFactoryInterface $config_factory, StreamWrapperManagerInterface $stream_wrapper_manager) {
80     parent::__construct($configuration, $plugin_id, $plugin_definition, $operation_manager, $logger, $config_factory);
81     $this->streamWrapperManager = $stream_wrapper_manager;
82   }
83
84   /**
85    * Destructs a GDToolkit object.
86    *
87    * Frees memory associated with a GD image resource.
88    */
89   public function __destruct() {
90     if (is_resource($this->resource)) {
91       imagedestroy($this->resource);
92     }
93   }
94
95   /**
96    * {@inheritdoc}
97    */
98   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
99     return new static(
100       $configuration,
101       $plugin_id,
102       $plugin_definition,
103       $container->get('image.toolkit.operation.manager'),
104       $container->get('logger.channel.image'),
105       $container->get('config.factory'),
106       $container->get('stream_wrapper_manager')
107     );
108   }
109
110   /**
111    * Sets the GD image resource.
112    *
113    * @param resource $resource
114    *   The GD image resource.
115    *
116    * @return \Drupal\system\Plugin\ImageToolkit\GDToolkit
117    *   An instance of the current toolkit object.
118    */
119   public function setResource($resource) {
120     if (!is_resource($resource) || get_resource_type($resource) != 'gd') {
121       throw new \InvalidArgumentException('Invalid resource argument');
122     }
123     $this->preLoadInfo = NULL;
124     $this->resource = $resource;
125     return $this;
126   }
127
128   /**
129    * Retrieves the GD image resource.
130    *
131    * @return resource|null
132    *   The GD image resource, or NULL if not available.
133    */
134   public function getResource() {
135     if (!is_resource($this->resource)) {
136       $this->load();
137     }
138     return $this->resource;
139   }
140
141   /**
142    * {@inheritdoc}
143    */
144   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
145     $form['image_jpeg_quality'] = [
146       '#type' => 'number',
147       '#title' => t('JPEG quality'),
148       '#description' => t('Define the image quality for JPEG manipulations. Ranges from 0 to 100. Higher values mean better image quality but bigger files.'),
149       '#min' => 0,
150       '#max' => 100,
151       '#default_value' => $this->configFactory->getEditable('system.image.gd')->get('jpeg_quality', FALSE),
152       '#field_suffix' => t('%'),
153     ];
154     return $form;
155   }
156
157   /**
158    * {@inheritdoc}
159    */
160   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
161     $this->configFactory->getEditable('system.image.gd')
162       ->set('jpeg_quality', $form_state->getValue(['gd', 'image_jpeg_quality']))
163       ->save();
164   }
165
166   /**
167    * Loads a GD resource from a file.
168    *
169    * @return bool
170    *   TRUE or FALSE, based on success.
171    */
172   protected function load() {
173     // Return immediately if the image file is not valid.
174     if (!$this->isValid()) {
175       return FALSE;
176     }
177
178     $function = 'imagecreatefrom' . image_type_to_extension($this->getType(), FALSE);
179     if (function_exists($function) && $resource = $function($this->getSource())) {
180       $this->setResource($resource);
181       if (imageistruecolor($resource)) {
182         return TRUE;
183       }
184       else {
185         // Convert indexed images to truecolor, copying the image to a new
186         // truecolor resource, so that filters work correctly and don't result
187         // in unnecessary dither.
188         $data = [
189           'width' => imagesx($resource),
190           'height' => imagesy($resource),
191           'extension' => image_type_to_extension($this->getType(), FALSE),
192           'transparent_color' => $this->getTransparentColor(),
193           'is_temp' => TRUE,
194         ];
195         if ($this->apply('create_new', $data)) {
196           imagecopy($this->getResource(), $resource, 0, 0, 0, 0, imagesx($resource), imagesy($resource));
197           imagedestroy($resource);
198         }
199       }
200       return (bool) $this->getResource();
201     }
202     return FALSE;
203   }
204
205   /**
206    * {@inheritdoc}
207    */
208   public function isValid() {
209     return ((bool) $this->preLoadInfo || (bool) $this->resource);
210   }
211
212   /**
213    * {@inheritdoc}
214    */
215   public function save($destination) {
216     $scheme = file_uri_scheme($destination);
217     // Work around lack of stream wrapper support in imagejpeg() and imagepng().
218     if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
219       // If destination is not local, save image to temporary local file.
220       $local_wrappers = $this->streamWrapperManager->getWrappers(StreamWrapperInterface::LOCAL);
221       if (!isset($local_wrappers[$scheme])) {
222         $permanent_destination = $destination;
223         $destination = drupal_tempnam('temporary://', 'gd_');
224       }
225       // Convert stream wrapper URI to normal path.
226       $destination = \Drupal::service('file_system')->realpath($destination);
227     }
228
229     $function = 'image' . image_type_to_extension($this->getType(), FALSE);
230     if (!function_exists($function)) {
231       return FALSE;
232     }
233     if ($this->getType() == IMAGETYPE_JPEG) {
234       $success = $function($this->getResource(), $destination, $this->configFactory->get('system.image.gd')->get('jpeg_quality'));
235     }
236     else {
237       // Always save PNG images with full transparency.
238       if ($this->getType() == IMAGETYPE_PNG) {
239         imagealphablending($this->getResource(), FALSE);
240         imagesavealpha($this->getResource(), TRUE);
241       }
242       $success = $function($this->getResource(), $destination);
243     }
244     // Move temporary local file to remote destination.
245     if (isset($permanent_destination) && $success) {
246       return (bool) file_unmanaged_move($destination, $permanent_destination, FILE_EXISTS_REPLACE);
247     }
248     return $success;
249   }
250
251   /**
252    * {@inheritdoc}
253    */
254   public function parseFile() {
255     $data = @getimagesize($this->getSource());
256     if ($data && in_array($data[2], static::supportedTypes())) {
257       $this->setType($data[2]);
258       $this->preLoadInfo = $data;
259       return TRUE;
260     }
261     return FALSE;
262   }
263
264   /**
265    * Gets the color set for transparency in GIF images.
266    *
267    * @return string|null
268    *   A color string like '#rrggbb', or NULL if not set or not relevant.
269    */
270   public function getTransparentColor() {
271     if (!$this->getResource() || $this->getType() != IMAGETYPE_GIF) {
272       return NULL;
273     }
274     // Find out if a transparent color is set, will return -1 if no
275     // transparent color has been defined in the image.
276     $transparent = imagecolortransparent($this->getResource());
277     if ($transparent >= 0) {
278       // Find out the number of colors in the image palette. It will be 0 for
279       // truecolor images.
280       $palette_size = imagecolorstotal($this->getResource());
281       if ($palette_size == 0 || $transparent < $palette_size) {
282         // Return the transparent color, either if it is a truecolor image
283         // or if the transparent color is part of the palette.
284         // Since the index of the transparent color is a property of the
285         // image rather than of the palette, it is possible that an image
286         // could be created with this index set outside the palette size.
287         // (see http://stackoverflow.com/a/3898007).
288         $rgb = imagecolorsforindex($this->getResource(), $transparent);
289         unset($rgb['alpha']);
290         return Color::rgbToHex($rgb);
291       }
292     }
293     return NULL;
294   }
295
296   /**
297    * {@inheritdoc}
298    */
299   public function getWidth() {
300     if ($this->preLoadInfo) {
301       return $this->preLoadInfo[0];
302     }
303     elseif ($res = $this->getResource()) {
304       return imagesx($res);
305     }
306     else {
307       return NULL;
308     }
309   }
310
311   /**
312    * {@inheritdoc}
313    */
314   public function getHeight() {
315     if ($this->preLoadInfo) {
316       return $this->preLoadInfo[1];
317     }
318     elseif ($res = $this->getResource()) {
319       return imagesy($res);
320     }
321     else {
322       return NULL;
323     }
324   }
325
326   /**
327    * Gets the PHP type of the image.
328    *
329    * @return int
330    *   The image type represented by a PHP IMAGETYPE_* constant (e.g.
331    *   IMAGETYPE_JPEG).
332    */
333   public function getType() {
334     return $this->type;
335   }
336
337   /**
338    * Sets the PHP type of the image.
339    *
340    * @param int $type
341    *   The image type represented by a PHP IMAGETYPE_* constant (e.g.
342    *   IMAGETYPE_JPEG).
343    *
344    * @return $this
345    */
346   public function setType($type) {
347     if (in_array($type, static::supportedTypes())) {
348       $this->type = $type;
349     }
350     return $this;
351   }
352
353   /**
354    * {@inheritdoc}
355    */
356   public function getMimeType() {
357     return $this->getType() ? image_type_to_mime_type($this->getType()) : '';
358   }
359
360   /**
361    * {@inheritdoc}
362    */
363   public function getRequirements() {
364     $requirements = [];
365
366     $info = gd_info();
367     $requirements['version'] = [
368       'title' => t('GD library'),
369       'value' => $info['GD Version'],
370     ];
371
372     // Check for filter and rotate support.
373     if (!function_exists('imagefilter') || !function_exists('imagerotate')) {
374       $requirements['version']['severity'] = REQUIREMENT_WARNING;
375       $requirements['version']['description'] = t('The GD Library for PHP is enabled, but was compiled without support for functions used by the rotate and desaturate effects. It was probably compiled using the official GD libraries from http://www.libgd.org instead of the GD library bundled with PHP. You should recompile PHP --with-gd using the bundled GD library. See <a href="http://php.net/manual/book.image.php">the PHP manual</a>.');
376     }
377
378     return $requirements;
379   }
380
381   /**
382    * {@inheritdoc}
383    */
384   public static function isAvailable() {
385     // GD2 support is available.
386     return function_exists('imagegd2');
387   }
388
389   /**
390    * {@inheritdoc}
391    */
392   public static function getSupportedExtensions() {
393     $extensions = [];
394     foreach (static::supportedTypes() as $image_type) {
395       // @todo Automatically fetch possible extensions for each mime type.
396       // @see https://www.drupal.org/node/2311679
397       $extension = mb_strtolower(image_type_to_extension($image_type, FALSE));
398       $extensions[] = $extension;
399       // Add some known similar extensions.
400       if ($extension === 'jpeg') {
401         $extensions[] = 'jpg';
402         $extensions[] = 'jpe';
403       }
404     }
405     return $extensions;
406   }
407
408   /**
409    * Returns the IMAGETYPE_xxx constant for the given extension.
410    *
411    * This is the reverse of the image_type_to_extension() function.
412    *
413    * @param string $extension
414    *   The extension to get the IMAGETYPE_xxx constant for.
415    *
416    * @return int
417    *   The IMAGETYPE_xxx constant for the given extension, or IMAGETYPE_UNKNOWN
418    *   for unsupported extensions.
419    *
420    * @see image_type_to_extension()
421    */
422   public function extensionToImageType($extension) {
423     if (in_array($extension, ['jpe', 'jpg'])) {
424       $extension = 'jpeg';
425     }
426     foreach ($this->supportedTypes() as $type) {
427       if (image_type_to_extension($type, FALSE) === $extension) {
428         return $type;
429       }
430     }
431     return IMAGETYPE_UNKNOWN;
432   }
433
434   /**
435    * Returns a list of image types supported by the toolkit.
436    *
437    * @return array
438    *   An array of available image types. An image type is represented by a PHP
439    *   IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG, IMAGETYPE_PNG, etc.).
440    */
441   protected static function supportedTypes() {
442     return [IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF];
443   }
444
445 }