annotate core/modules/system/src/Plugin/ImageToolkit/GDToolkit.php @ 17:129ea1e6d783

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