Diff: STRATO-apps/wordpress_03/app/wp-includes/class-wp-image-editor-imagick.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + <?php
2 + /**
3 + * WordPress Imagick Image Editor
4 + *
5 + * @package WordPress
6 + * @subpackage Image_Editor
7 + */
8 +
9 + /**
10 + * WordPress Image Editor Class for Image Manipulation through Imagick PHP Module
11 + *
12 + * @since 3.5.0
13 + *
14 + * @see WP_Image_Editor
15 + */
16 + class WP_Image_Editor_Imagick extends WP_Image_Editor {
17 + /**
18 + * Imagick object.
19 + *
20 + * @var Imagick
21 + */
22 + protected $image;
23 +
24 + public function __destruct() {
25 + if ( $this->image instanceof Imagick ) {
26 + // We don't need the original in memory anymore.
27 + $this->image->clear();
28 + $this->image->destroy();
29 + }
30 + }
31 +
32 + /**
33 + * Checks to see if current environment supports Imagick.
34 + *
35 + * We require Imagick 2.2.0 or greater, based on whether the queryFormats()
36 + * method can be called statically.
37 + *
38 + * @since 3.5.0
39 + *
40 + * @param array $args
41 + * @return bool
42 + */
43 + public static function test( $args = array() ) {
44 +
45 + // First, test Imagick's extension and classes.
46 + if ( ! extension_loaded( 'imagick' ) || ! class_exists( 'Imagick', false ) || ! class_exists( 'ImagickPixel', false ) ) {
47 + return false;
48 + }
49 +
50 + if ( version_compare( phpversion( 'imagick' ), '2.2.0', '<' ) ) {
51 + return false;
52 + }
53 +
54 + $required_methods = array(
55 + 'clear',
56 + 'destroy',
57 + 'valid',
58 + 'getimage',
59 + 'writeimage',
60 + 'getimageblob',
61 + 'getimagegeometry',
62 + 'getimageformat',
63 + 'setimageformat',
64 + 'setimagecompression',
65 + 'setimagecompressionquality',
66 + 'setimagepage',
67 + 'setoption',
68 + 'scaleimage',
69 + 'cropimage',
70 + 'rotateimage',
71 + 'flipimage',
72 + 'flopimage',
73 + 'readimage',
74 + 'readimageblob',
75 + );
76 +
77 + // Now, test for deep requirements within Imagick.
78 + if ( ! defined( 'imagick::COMPRESSION_JPEG' ) ) {
79 + return false;
80 + }
81 +
82 + $class_methods = array_map( 'strtolower', get_class_methods( 'Imagick' ) );
83 + if ( array_diff( $required_methods, $class_methods ) ) {
84 + return false;
85 + }
86 +
87 + return true;
88 + }
89 +
90 + /**
91 + * Checks to see if editor supports the mime-type specified.
92 + *
93 + * @since 3.5.0
94 + *
95 + * @param string $mime_type
96 + * @return bool
97 + */
98 + public static function supports_mime_type( $mime_type ) {
99 + $imagick_extension = strtoupper( self::get_extension( $mime_type ) );
100 +
101 + if ( ! $imagick_extension ) {
102 + return false;
103 + }
104 +
105 + /*
106 + * setIteratorIndex is optional unless mime is an animated format.
107 + * Here, we just say no if you are missing it and aren't loading a jpeg.
108 + */
109 + if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) {
110 + return false;
111 + }
112 +
113 + try {
114 + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
115 + return ( (bool) @Imagick::queryFormats( $imagick_extension ) );
116 + } catch ( Exception $e ) {
117 + return false;
118 + }
119 + }
120 +
121 + /**
122 + * Loads image from $this->file into new Imagick Object.
123 + *
124 + * @since 3.5.0
125 + *
126 + * @return true|WP_Error True if loaded; WP_Error on failure.
127 + */
128 + public function load() {
129 + if ( $this->image instanceof Imagick ) {
130 + return true;
131 + }
132 +
133 + if ( ! is_file( $this->file ) && ! wp_is_stream( $this->file ) ) {
134 + return new WP_Error( 'error_loading_image', __( 'File does not exist?' ), $this->file );
135 + }
136 +
137 + /*
138 + * Even though Imagick uses less PHP memory than GD, set higher limit
139 + * for users that have low PHP.ini limits.
140 + */
141 + wp_raise_memory_limit( 'image' );
142 +
143 + try {
144 + $this->image = new Imagick();
145 + $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) );
146 +
147 + if ( 'pdf' === $file_extension ) {
148 + $pdf_loaded = $this->pdf_load_source();
149 +
150 + if ( is_wp_error( $pdf_loaded ) ) {
151 + return $pdf_loaded;
152 + }
153 + } else {
154 + if ( wp_is_stream( $this->file ) ) {
155 + // Due to reports of issues with streams with `Imagick::readImageFile()`, uses `Imagick::readImageBlob()` instead.
156 + $this->image->readImageBlob( file_get_contents( $this->file ), $this->file );
157 + } else {
158 + $this->image->readImage( $this->file );
159 + }
160 + }
161 +
162 + if ( ! $this->image->valid() ) {
163 + return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
164 + }
165 +
166 + // Select the first frame to handle animated images properly.
167 + if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) {
168 + $this->image->setIteratorIndex( 0 );
169 + }
170 +
171 + if ( 'pdf' === $file_extension ) {
172 + $this->remove_pdf_alpha_channel();
173 + }
174 +
175 + $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() );
176 + } catch ( Exception $e ) {
177 + return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
178 + }
179 +
180 + $updated_size = $this->update_size();
181 +
182 + if ( is_wp_error( $updated_size ) ) {
183 + return $updated_size;
184 + }
185 +
186 + return $this->set_quality();
187 + }
188 +
189 + /**
190 + * Sets Image Compression quality on a 1-100% scale.
191 + *
192 + * @since 3.5.0
193 + * @since 6.8.0 The `$dims` parameter was added.
194 + *
195 + * @param int $quality Compression Quality. Range: [1,100]
196 + * @param array $dims Optional. Image dimensions array with 'width' and 'height' keys.
197 + * @return true|WP_Error True if set successfully; WP_Error on failure.
198 + */
199 + public function set_quality( $quality = null, $dims = array() ) {
200 + $quality_result = parent::set_quality( $quality, $dims );
201 + if ( is_wp_error( $quality_result ) ) {
202 + return $quality_result;
203 + } else {
204 + $quality = $this->get_quality();
205 + }
206 +
207 + try {
208 + switch ( $this->mime_type ) {
209 + case 'image/jpeg':
210 + $this->image->setImageCompressionQuality( $quality );
211 + $this->image->setCompressionQuality( $quality );
212 + $this->image->setImageCompression( imagick::COMPRESSION_JPEG );
213 + break;
214 + case 'image/webp':
215 + $webp_info = wp_get_webp_info( $this->file );
216 +
217 + if ( 'lossless' === $webp_info['type'] ) {
218 + // Use WebP lossless settings.
219 + $this->image->setImageCompressionQuality( 100 );
220 + $this->image->setCompressionQuality( 100 );
221 + $this->image->setOption( 'webp:lossless', 'true' );
222 + parent::set_quality( 100 );
223 + } else {
224 + $this->image->setImageCompressionQuality( $quality );
225 + $this->image->setCompressionQuality( $quality );
226 + }
227 + break;
228 + case 'image/avif':
229 + // Set the AVIF encoder to work faster, with minimal impact on image size.
230 + $this->image->setOption( 'heic:speed', 7 );
231 + $this->image->setImageCompressionQuality( $quality );
232 + $this->image->setCompressionQuality( $quality );
233 + break;
234 + default:
235 + $this->image->setImageCompressionQuality( $quality );
236 + $this->image->setCompressionQuality( $quality );
237 + }
238 + } catch ( Exception $e ) {
239 + return new WP_Error( 'image_quality_error', $e->getMessage() );
240 + }
241 + return true;
242 + }
243 +
244 +
245 + /**
246 + * Sets or updates current image size.
247 + *
248 + * @since 3.5.0
249 + *
250 + * @param int $width
251 + * @param int $height
252 + * @return true|WP_Error
253 + */
254 + protected function update_size( $width = null, $height = null ) {
255 + $size = null;
256 + if ( ! $width || ! $height ) {
257 + try {
258 + $size = $this->image->getImageGeometry();
259 + } catch ( Exception $e ) {
260 + return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file );
261 + }
262 + }
263 +
264 + if ( ! $width ) {
265 + $width = $size['width'];
266 + }
267 +
268 + if ( ! $height ) {
269 + $height = $size['height'];
270 + }
271 +
272 + /*
273 + * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF and HEIC images
274 + * are properly sized without affecting previous `getImageGeometry` behavior.
275 + */
276 + if ( ( ! $width || ! $height ) && ( 'image/avif' === $this->mime_type || wp_is_heic_image_mime_type( $this->mime_type ) ) ) {
277 + $size = wp_getimagesize( $this->file );
278 + $width = $size[0];
279 + $height = $size[1];
280 + }
281 +
282 + return parent::update_size( $width, $height );
283 + }
284 +
285 + /**
286 + * Sets Imagick time limit.
287 + *
288 + * Depending on configuration, Imagick processing may take time.
289 + *
290 + * Multiple problems exist if PHP times out before ImageMagick completed:
291 + * 1. Temporary files aren't cleaned by ImageMagick garbage collection.
292 + * 2. No clear error is provided.
293 + * 3. The cause of such timeout can be hard to pinpoint.
294 + *
295 + * This function, which is expected to be run before heavy image routines, resolves
296 + * point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it is set.
297 + *
298 + * However seems it introduces more problems than it fixes,
299 + * see https://core.trac.wordpress.org/ticket/58202.
300 + *
301 + * Note:
302 + * - Imagick resource exhaustion does not issue catchable exceptions (yet).
303 + * See https://github.com/Imagick/imagick/issues/333.
304 + * - The resource limit is not saved/restored. It applies to subsequent
305 + * image operations within the time of the HTTP request.
306 + *
307 + * @since 6.2.0
308 + * @deprecated 6.3.0 No longer used in core.
309 + *
310 + * @return int|null The new limit on success, null on failure.
311 + */
312 + public static function set_imagick_time_limit() {
313 + _deprecated_function( __METHOD__, '6.3.0' );
314 +
315 + if ( ! defined( 'Imagick::RESOURCETYPE_TIME' ) ) {
316 + return null;
317 + }
318 +
319 + // Returns PHP_FLOAT_MAX if unset.
320 + $imagick_timeout = Imagick::getResourceLimit( Imagick::RESOURCETYPE_TIME );
321 +
322 + // Convert to an integer, keeping in mind that: 0 === (int) PHP_FLOAT_MAX.
323 + $imagick_timeout = $imagick_timeout > PHP_INT_MAX ? PHP_INT_MAX : (int) $imagick_timeout;
324 +
325 + $php_timeout = (int) ini_get( 'max_execution_time' );
326 +
327 + if ( $php_timeout > 1 && $php_timeout < $imagick_timeout ) {
328 + $limit = (float) 0.8 * $php_timeout;
329 + Imagick::setResourceLimit( Imagick::RESOURCETYPE_TIME, $limit );
330 +
331 + return $limit;
332 + }
333 + }
334 +
335 + /**
336 + * Resizes current image.
337 + *
338 + * At minimum, either a height or width must be provided.
339 + * If one of the two is set to null, the resize will
340 + * maintain aspect ratio according to the provided dimension.
341 + *
342 + * @since 3.5.0
343 + *
344 + * @param int|null $max_w Image width.
345 + * @param int|null $max_h Image height.
346 + * @param bool|array $crop {
347 + * Optional. Image cropping behavior. If false, the image will be scaled (default).
348 + * If true, image will be cropped to the specified dimensions using center positions.
349 + * If an array, the image will be cropped using the array to specify the crop location:
350 + *
351 + * @type string $0 The x crop position. Accepts 'left', 'center', or 'right'.
352 + * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'.
353 + * }
354 + * @return true|WP_Error
355 + */
356 + public function resize( $max_w, $max_h, $crop = false ) {
357 + if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) {
358 + return true;
359 + }
360 +
361 + $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
362 + if ( ! $dims ) {
363 + return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) );
364 + }
365 +
366 + list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
367 +
368 + if ( $crop ) {
369 + return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
370 + }
371 +
372 + $this->set_quality(
373 + null,
374 + array(
375 + 'width' => $dst_w,
376 + 'height' => $dst_h,
377 + )
378 + );
379 +
380 + // Execute the resize.
381 + $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
382 + if ( is_wp_error( $thumb_result ) ) {
383 + return $thumb_result;
384 + }
385 +
386 + return $this->update_size( $dst_w, $dst_h );
387 + }
388 +
389 + /**
390 + * Efficiently resize the current image
391 + *
392 + * This is a WordPress specific implementation of Imagick::thumbnailImage(),
393 + * which resizes an image to given dimensions and removes any associated profiles.
394 + *
395 + * @since 4.5.0
396 + *
397 + * @param int $dst_w The destination width.
398 + * @param int $dst_h The destination height.
399 + * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'.
400 + * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true.
401 + * @return void|WP_Error
402 + */
403 + protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) {
404 + $allowed_filters = array(
405 + 'FILTER_POINT',
406 + 'FILTER_BOX',
407 + 'FILTER_TRIANGLE',
408 + 'FILTER_HERMITE',
409 + 'FILTER_HANNING',
410 + 'FILTER_HAMMING',
411 + 'FILTER_BLACKMAN',
412 + 'FILTER_GAUSSIAN',
413 + 'FILTER_QUADRATIC',
414 + 'FILTER_CUBIC',
415 + 'FILTER_CATROM',
416 + 'FILTER_MITCHELL',
417 + 'FILTER_LANCZOS',
418 + 'FILTER_BESSEL',
419 + 'FILTER_SINC',
420 + );
421 +
422 + /**
423 + * Set the filter value if '$filter_name' name is in the allowed list and the related
424 + * Imagick constant is defined or fall back to the default filter.
425 + */
426 + if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) {
427 + $filter = constant( 'Imagick::' . $filter_name );
428 + } else {
429 + $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false;
430 + }
431 +
432 + /**
433 + * Filters whether to strip metadata from images when they're resized.
434 + *
435 + * This filter only applies when resizing using the Imagick editor since GD
436 + * always strips profiles by default.
437 + *
438 + * @since 4.5.0
439 + *
440 + * @param bool $strip_meta Whether to strip image metadata during resizing. Default true.
441 + */
442 + if ( apply_filters( 'image_strip_meta', $strip_meta ) ) {
443 + $this->strip_meta(); // Fail silently if not supported.
444 + }
445 +
446 + try {
447 + /**
448 + * Special handling for certain types of PNG images:
449 + * 1. For PNG images, we need to specify compression settings and remove unneeded chunks.
450 + * 2. For indexed PNG images, the number of colors must not exceed 256.
451 + * 3. For indexed PNG images with an alpha channel, the tRNS chunk must be preserved.
452 + * 4. For indexed PNG images with true alpha transparency (an alpha channel > 1 bit), we need to avoid saving
453 + * the image using ImageMagick's 'png8' format, because that supports only binary (1 bit) transparency.
454 + *
455 + * For #4 we want to check whether the image has a 1-bit alpha channel before resizing, because resizing
456 + * may cause the number of alpha values to multiply due to antialiasing. If the original image had only a
457 + * 1-bit alpha channel, then a 1-bit alpha channel should be good enough for the resized images.
458 + *
459 + * Perform all the necessary checks before resizing the image and store the results in variables for later use.
460 + */
461 + $is_png = false;
462 + $is_indexed_png = false;
463 + $is_indexed_png_with_alpha_channel = false;
464 + $is_indexed_png_with_true_alpha_transparency = false;
465 +
466 + if ( 'image/png' === $this->mime_type ) {
467 + $is_png = true;
468 +
469 + if (
470 + is_callable( array( $this->image, 'getImageProperty' ) )
471 + && '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' )
472 + ) {
473 + $is_indexed_png = true;
474 +
475 + if (
476 + is_callable( array( $this->image, 'getImageAlphaChannel' ) )
477 + && $this->image->getImageAlphaChannel()
478 + ) {
479 + $is_indexed_png_with_alpha_channel = true;
480 +
481 + if (
482 + is_callable( array( $this->image, 'getImageChannelDepth' ) )
483 + && defined( 'Imagick::CHANNEL_ALPHA' )
484 + && 1 < $this->image->getImageChannelDepth( Imagick::CHANNEL_ALPHA )
485 + ) {
486 + $is_indexed_png_with_true_alpha_transparency = true;
487 + }
488 + }
489 + }
490 + }
491 +
492 + /*
493 + * To be more efficient, resample large images to 5x the destination size before resizing
494 + * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111),
495 + * unless we would be resampling to a scale smaller than 128x128.
496 + */
497 + if ( is_callable( array( $this->image, 'sampleImage' ) ) ) {
498 + $resize_ratio = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] );
499 + $sample_factor = 5;
500 +
501 + if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) {
502 + $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor );
503 + }
504 + }
505 +
506 + /*
507 + * Use resizeImage() when it's available and a valid filter value is set.
508 + * Otherwise, fall back to the scaleImage() method for resizing, which
509 + * results in better image quality over resizeImage() with default filter
510 + * settings and retains backward compatibility with pre 4.5 functionality.
511 + */
512 + if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) {
513 + $this->image->setOption( 'filter:support', '2.0' );
514 + $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 );
515 + } else {
516 + $this->image->scaleImage( $dst_w, $dst_h );
517 + }
518 +
519 + // Set appropriate quality settings after resizing.
520 + if ( 'image/jpeg' === $this->mime_type ) {
521 + if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) {
522 + $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 );
523 + }
524 +
525 + $this->image->setOption( 'jpeg:fancy-upsampling', 'off' );
526 + }
527 +
528 + if ( $is_png ) {
529 + $this->image->setOption( 'png:compression-filter', '5' );
530 + $this->image->setOption( 'png:compression-level', '9' );
531 + $this->image->setOption( 'png:compression-strategy', '1' );
532 +
533 + // Indexed PNG files get some additional handling.
534 + // See #63448 for details.
535 + if ( $is_indexed_png ) {
536 +
537 + // Check for an alpha channel.
538 + if ( $is_indexed_png_with_alpha_channel ) {
539 + $this->image->setOption( 'png:include-chunk', 'tRNS' );
540 + } else {
541 + $this->image->setOption( 'png:exclude-chunk', 'all' );
542 + }
543 +
544 + $this->image->quantizeImage( 256, $this->image->getColorspace(), 0, false, false );
545 +
546 + /*
547 + * If the colorspace is 'gray', use the png8 format to ensure it stays indexed.
548 + * ImageMagick tends to save grayscale images as grayscale PNGs rather than indexed PNGs,
549 + * even though grayscale PNGs usually have considerably larger file sizes.
550 + * But we can force ImageMagick to save the image as an indexed PNG instead,
551 + * by telling it to use png8 format.
552 + *
553 + * Note that we need to first call quantizeImage() before checking getImageColorspace(),
554 + * because only after calling quantizeImage() will the colorspace be COLORSPACE_GRAY for grayscale images
555 + * (and we have not found any other way to identify grayscale images).
556 + *
557 + * We need to avoid forcing indexed format for images with true alpha transparency,
558 + * because ImageMagick does not support saving an image with true alpha transparency as an indexed PNG.
559 + */
560 + if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() && ! $is_indexed_png_with_true_alpha_transparency ) {
561 + // Set the image format to Indexed PNG.
562 + $this->image->setOption( 'png:format', 'png8' );
563 + }
564 + } else {
565 + $this->image->setOption( 'png:exclude-chunk', 'all' );
566 + }
567 + }
568 +
569 + /*
570 + * If alpha channel is not defined, set it opaque.
571 + *
572 + * Note that Imagick::getImageAlphaChannel() is only available if Imagick
573 + * has been compiled against ImageMagick version 6.4.0 or newer.
574 + */
575 + if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) )
576 + && is_callable( array( $this->image, 'setImageAlphaChannel' ) )
577 + && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' )
578 + && defined( 'Imagick::ALPHACHANNEL_OPAQUE' )
579 + ) {
580 + if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) {
581 + $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE );
582 + }
583 + }
584 +
585 + // Limit the bit depth of resized images.
586 + if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) {
587 + /**
588 + * Filters the maximum bit depth of resized images.
589 + *
590 + * This filter only applies when resizing using the Imagick editor since GD
591 + * does not support getting or setting bit depth.
592 + *
593 + * Use this to adjust the maximum bit depth of resized images.
594 + *
595 + * @since 6.8.0
596 + *
597 + * @param int $max_depth The maximum bit depth. Default is the input depth.
598 + * @param int $image_depth The bit depth of the original image.
599 + */
600 + $max_depth = apply_filters( 'image_max_bit_depth', $this->image->getImageDepth(), $this->image->getImageDepth() );
601 + $this->image->setImageDepth( $max_depth );
602 + }
603 + } catch ( Exception $e ) {
604 + return new WP_Error( 'image_resize_error', $e->getMessage() );
605 + }
606 + }
607 +
608 + /**
609 + * Create multiple smaller images from a single source.
610 + *
611 + * Attempts to create all sub-sizes and returns the meta data at the end. This
612 + * may result in the server running out of resources. When it fails there may be few
613 + * "orphaned" images left over as the meta data is never returned and saved.
614 + *
615 + * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates
616 + * the new images one at a time and allows for the meta data to be saved after
617 + * each new image is created.
618 + *
619 + * @since 3.5.0
620 + *
621 + * @param array $sizes {
622 + * An array of image size data arrays.
623 + *
624 + * Either a height or width must be provided.
625 + * If one of the two is set to null, the resize will
626 + * maintain aspect ratio according to the provided dimension.
627 + *
628 + * @type array ...$0 {
629 + * Array of height, width values, and whether to crop.
630 + *
631 + * @type int $width Image width. Optional if `$height` is specified.
632 + * @type int $height Image height. Optional if `$width` is specified.
633 + * @type bool|array $crop Optional. Whether to crop the image. Default false.
634 + * }
635 + * }
636 + * @return array An array of resized images' metadata by size.
637 + */
638 + public function multi_resize( $sizes ) {
639 + $metadata = array();
640 +
641 + foreach ( $sizes as $size => $size_data ) {
642 + $meta = $this->make_subsize( $size_data );
643 +
644 + if ( ! is_wp_error( $meta ) ) {
645 + $metadata[ $size ] = $meta;
646 + }
647 + }
648 +
649 + return $metadata;
650 + }
651 +
652 + /**
653 + * Create an image sub-size and return the image meta data value for it.
654 + *
655 + * @since 5.3.0
656 + *
657 + * @param array $size_data {
658 + * Array of size data.
659 + *
660 + * @type int $width The maximum width in pixels.
661 + * @type int $height The maximum height in pixels.
662 + * @type bool|array $crop Whether to crop the image to exact dimensions.
663 + * }
664 + * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta,
665 + * WP_Error object on error.
666 + */
667 + public function make_subsize( $size_data ) {
668 + if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
669 + return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
670 + }
671 +
672 + $orig_size = $this->size;
673 + $orig_image = $this->image->getImage();
674 +
675 + if ( ! isset( $size_data['width'] ) ) {
676 + $size_data['width'] = null;
677 + }
678 +
679 + if ( ! isset( $size_data['height'] ) ) {
680 + $size_data['height'] = null;
681 + }
682 +
683 + if ( ! isset( $size_data['crop'] ) ) {
684 + $size_data['crop'] = false;
685 + }
686 +
687 + if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) {
688 + return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) );
689 + }
690 +
691 + $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
692 +
693 + if ( is_wp_error( $resized ) ) {
694 + $saved = $resized;
695 + } else {
696 + $saved = $this->_save( $this->image );
697 +
698 + $this->image->clear();
699 + $this->image->destroy();
700 + $this->image = null;
701 + }
702 +
703 + $this->size = $orig_size;
704 + $this->image = $orig_image;
705 +
706 + if ( ! is_wp_error( $saved ) ) {
707 + unset( $saved['path'] );
708 + }
709 +
710 + return $saved;
711 + }
712 +
713 + /**
714 + * Crops Image.
715 + *
716 + * @since 3.5.0
717 + *
718 + * @param int $src_x The start x position to crop from.
719 + * @param int $src_y The start y position to crop from.
720 + * @param int $src_w The width to crop.
721 + * @param int $src_h The height to crop.
722 + * @param int $dst_w Optional. The destination width.
723 + * @param int $dst_h Optional. The destination height.
724 + * @param bool $src_abs Optional. If the source crop points are absolute.
725 + * @return true|WP_Error
726 + */
727 + public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
728 + if ( $src_abs ) {
729 + $src_w -= $src_x;
730 + $src_h -= $src_y;
731 + }
732 +
733 + try {
734 + $this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
735 + $this->image->setImagePage( $src_w, $src_h, 0, 0 );
736 +
737 + if ( $dst_w || $dst_h ) {
738 + /*
739 + * If destination width/height isn't specified,
740 + * use same as width/height from source.
741 + */
742 + if ( ! $dst_w ) {
743 + $dst_w = $src_w;
744 + }
745 + if ( ! $dst_h ) {
746 + $dst_h = $src_h;
747 + }
748 +
749 + $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
750 + if ( is_wp_error( $thumb_result ) ) {
751 + return $thumb_result;
752 + }
753 +
754 + return $this->update_size();
755 + }
756 + } catch ( Exception $e ) {
757 + return new WP_Error( 'image_crop_error', $e->getMessage() );
758 + }
759 +
760 + return $this->update_size();
761 + }
762 +
763 + /**
764 + * Rotates current image counter-clockwise by $angle.
765 + *
766 + * @since 3.5.0
767 + *
768 + * @param float $angle
769 + * @return true|WP_Error
770 + */
771 + public function rotate( $angle ) {
772 + /**
773 + * $angle is 360-$angle because Imagick rotates clockwise
774 + * (GD rotates counter-clockwise)
775 + */
776 + try {
777 + $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle );
778 +
779 + // Normalize EXIF orientation data so that display is consistent across devices.
780 + if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
781 + $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
782 + }
783 +
784 + // Since this changes the dimensions of the image, update the size.
785 + $result = $this->update_size();
786 + if ( is_wp_error( $result ) ) {
787 + return $result;
788 + }
789 +
790 + $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 );
791 + } catch ( Exception $e ) {
792 + return new WP_Error( 'image_rotate_error', $e->getMessage() );
793 + }
794 +
795 + return true;
796 + }
797 +
798 + /**
799 + * Flips current image.
800 + *
801 + * @since 3.5.0
802 + *
803 + * @param bool $horz Flip along Horizontal Axis
804 + * @param bool $vert Flip along Vertical Axis
805 + * @return true|WP_Error
806 + */
807 + public function flip( $horz, $vert ) {
808 + try {
809 + if ( $horz ) {
810 + $this->image->flipImage();
811 + }
812 +
813 + if ( $vert ) {
814 + $this->image->flopImage();
815 + }
816 +
817 + // Normalize EXIF orientation data so that display is consistent across devices.
818 + if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
819 + $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
820 + }
821 + } catch ( Exception $e ) {
822 + return new WP_Error( 'image_flip_error', $e->getMessage() );
823 + }
824 +
825 + return true;
826 + }
827 +
828 + /**
829 + * Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
830 + *
831 + * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only
832 + * if EXIF Orientation can be reset afterwards.
833 + *
834 + * @since 5.3.0
835 + *
836 + * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation.
837 + * WP_Error if error while rotating.
838 + */
839 + public function maybe_exif_rotate() {
840 + if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
841 + return parent::maybe_exif_rotate();
842 + } else {
843 + return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) );
844 + }
845 + }
846 +
847 + /**
848 + * Saves current image to file.
849 + *
850 + * @since 3.5.0
851 + * @since 6.0.0 The `$filesize` value was added to the returned array.
852 + *
853 + * @param string $destfilename Optional. Destination filename. Default null.
854 + * @param string $mime_type Optional. The mime-type. Default null.
855 + * @return array|WP_Error {
856 + * Array on success or WP_Error if the file failed to save.
857 + *
858 + * @type string $path Path to the image file.
859 + * @type string $file Name of the image file.
860 + * @type int $width Image width.
861 + * @type int $height Image height.
862 + * @type string $mime-type The mime type of the image.
863 + * @type int $filesize File size of the image.
864 + * }
865 + */
866 + public function save( $destfilename = null, $mime_type = null ) {
867 + $saved = $this->_save( $this->image, $destfilename, $mime_type );
868 +
869 + if ( ! is_wp_error( $saved ) ) {
870 + $this->file = $saved['path'];
871 + $this->mime_type = $saved['mime-type'];
872 +
873 + try {
874 + $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) );
875 + } catch ( Exception $e ) {
876 + return new WP_Error( 'image_save_error', $e->getMessage(), $this->file );
877 + }
878 + }
879 +
880 + return $saved;
881 + }
882 +
883 + /**
884 + * Removes PDF alpha after it's been read.
885 + *
886 + * @since 6.4.0
887 + */
888 + protected function remove_pdf_alpha_channel() {
889 + $version = Imagick::getVersion();
890 + // Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5.
891 + if ( $version['versionNumber'] >= 0x675 ) {
892 + try {
893 + // Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2.
894 + $this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 );
895 + } catch ( Exception $e ) {
896 + return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() );
897 + }
898 + }
899 + }
900 +
901 + /**
902 + * @since 3.5.0
903 + * @since 6.0.0 The `$filesize` value was added to the returned array.
904 + *
905 + * @param Imagick $image
906 + * @param string $filename
907 + * @param string $mime_type
908 + * @return array|WP_Error {
909 + * Array on success or WP_Error if the file failed to save.
910 + *
911 + * @type string $path Path to the image file.
912 + * @type string $file Name of the image file.
913 + * @type int $width Image width.
914 + * @type int $height Image height.
915 + * @type string $mime-type The mime type of the image.
916 + * @type int $filesize File size of the image.
917 + * }
918 + */
919 + protected function _save( $image, $filename = null, $mime_type = null ) {
920 + list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
921 +
922 + if ( ! $filename ) {
923 + $filename = $this->generate_filename( null, null, $extension );
924 + }
925 +
926 + try {
927 + // Store initial format.
928 + $orig_format = $this->image->getImageFormat();
929 +
930 + $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
931 + } catch ( Exception $e ) {
932 + return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
933 + }
934 +
935 + if ( method_exists( $this->image, 'setInterlaceScheme' )
936 + && method_exists( $this->image, 'getInterlaceScheme' )
937 + && defined( 'Imagick::INTERLACE_PLANE' )
938 + ) {
939 + $orig_interlace = $this->image->getInterlaceScheme();
940 +
941 + /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
942 + if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) {
943 + $this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output.
944 + } else {
945 + $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output.
946 + }
947 + }
948 +
949 + $write_image_result = $this->write_image( $this->image, $filename );
950 + if ( is_wp_error( $write_image_result ) ) {
951 + return $write_image_result;
952 + }
953 +
954 + try {
955 + // Reset original format.
956 + $this->image->setImageFormat( $orig_format );
957 +
958 + if ( isset( $orig_interlace ) ) {
959 + $this->image->setInterlaceScheme( $orig_interlace );
960 + }
961 + } catch ( Exception $e ) {
962 + return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
963 + }
964 +
965 + // Set correct file permissions.
966 + $stat = stat( dirname( $filename ) );
967 + $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits.
968 + chmod( $filename, $perms );
969 +
970 + return array(
971 + 'path' => $filename,
972 + /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
973 + 'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
974 + 'width' => $this->size['width'],
975 + 'height' => $this->size['height'],
976 + 'mime-type' => $mime_type,
977 + 'filesize' => wp_filesize( $filename ),
978 + );
979 + }
980 +
981 + /**
982 + * Writes an image to a file or stream.
983 + *
984 + * @since 5.6.0
985 + *
986 + * @param Imagick $image
987 + * @param string $filename The destination filename or stream URL.
988 + * @return true|WP_Error
989 + */
990 + private function write_image( $image, $filename ) {
991 + if ( wp_is_stream( $filename ) ) {
992 + /*
993 + * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead.
994 + * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php
995 + */
996 + if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) {
997 + return new WP_Error(
998 + 'image_save_error',
999 + sprintf(
1000 + /* translators: %s: PHP function name. */
1001 + __( '%s failed while writing image to stream.' ),
1002 + '<code>file_put_contents()</code>'
1003 + ),
1004 + $filename
1005 + );
1006 + } else {
1007 + return true;
1008 + }
1009 + } else {
1010 + $dirname = dirname( $filename );
1011 +
1012 + if ( ! wp_mkdir_p( $dirname ) ) {
1013 + return new WP_Error(
1014 + 'image_save_error',
1015 + sprintf(
1016 + /* translators: %s: Directory path. */
1017 + __( 'Unable to create directory %s. Is its parent directory writable by the server?' ),
1018 + esc_html( $dirname )
1019 + )
1020 + );
1021 + }
1022 +
1023 + try {
1024 + return $image->writeImage( $filename );
1025 + } catch ( Exception $e ) {
1026 + return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
1027 + }
1028 + }
1029 + }
1030 +
1031 + /**
1032 + * Streams current image to browser.
1033 + *
1034 + * @since 3.5.0
1035 + *
1036 + * @param string $mime_type The mime type of the image.
1037 + * @return true|WP_Error True on success, WP_Error object on failure.
1038 + */
1039 + public function stream( $mime_type = null ) {
1040 + list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
1041 +
1042 + try {
1043 + // Temporarily change format for stream.
1044 + $this->image->setImageFormat( strtoupper( $extension ) );
1045 +
1046 + // Output stream of image content.
1047 + header( "Content-Type: $mime_type" );
1048 + print $this->image->getImageBlob();
1049 +
1050 + // Reset image to original format.
1051 + $this->image->setImageFormat( $this->get_extension( $this->mime_type ) );
1052 + } catch ( Exception $e ) {
1053 + return new WP_Error( 'image_stream_error', $e->getMessage() );
1054 + }
1055 +
1056 + return true;
1057 + }
1058 +
1059 + /**
1060 + * Strips all image meta except color profiles from an image.
1061 + *
1062 + * @since 4.5.0
1063 + *
1064 + * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error.
1065 + */
1066 + protected function strip_meta() {
1067 +
1068 + if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) {
1069 + return new WP_Error(
1070 + 'image_strip_meta_error',
1071 + sprintf(
1072 + /* translators: %s: ImageMagick method name. */
1073 + __( '%s is required to strip image meta.' ),
1074 + '<code>Imagick::getImageProfiles()</code>'
1075 + )
1076 + );
1077 + }
1078 +
1079 + if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) {
1080 + return new WP_Error(
1081 + 'image_strip_meta_error',
1082 + sprintf(
1083 + /* translators: %s: ImageMagick method name. */
1084 + __( '%s is required to strip image meta.' ),
1085 + '<code>Imagick::removeImageProfile()</code>'
1086 + )
1087 + );
1088 + }
1089 +
1090 + /*
1091 + * Protect a few profiles from being stripped for the following reasons:
1092 + *
1093 + * - icc: Color profile information
1094 + * - icm: Color profile information
1095 + * - iptc: Copyright data
1096 + * - exif: Orientation data
1097 + * - xmp: Rights usage data
1098 + */
1099 + $protected_profiles = array(
1100 + 'icc',
1101 + 'icm',
1102 + 'iptc',
1103 + 'exif',
1104 + 'xmp',
1105 + );
1106 +
1107 + try {
1108 + // Strip profiles.
1109 + foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) {
1110 + if ( ! in_array( $key, $protected_profiles, true ) ) {
1111 + $this->image->removeImageProfile( $key );
1112 + }
1113 + }
1114 + } catch ( Exception $e ) {
1115 + return new WP_Error( 'image_strip_meta_error', $e->getMessage() );
1116 + }
1117 +
1118 + return true;
1119 + }
1120 +
1121 + /**
1122 + * Sets up Imagick for PDF processing.
1123 + * Increases rendering DPI and only loads first page.
1124 + *
1125 + * @since 4.7.0
1126 + *
1127 + * @return string|WP_Error File to load or WP_Error on failure.
1128 + */
1129 + protected function pdf_setup() {
1130 + try {
1131 + /*
1132 + * By default, PDFs are rendered in a very low resolution.
1133 + * We want the thumbnail to be readable, so increase the rendering DPI.
1134 + */
1135 + $this->image->setResolution( 128, 128 );
1136 +
1137 + // Only load the first page.
1138 + return $this->file . '[0]';
1139 + } catch ( Exception $e ) {
1140 + return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file );
1141 + }
1142 + }
1143 +
1144 + /**
1145 + * Load the image produced by Ghostscript.
1146 + *
1147 + * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files
1148 + * when `use-cropbox` is set.
1149 + *
1150 + * @since 5.6.0
1151 + *
1152 + * @return true|WP_Error
1153 + */
1154 + protected function pdf_load_source() {
1155 + $filename = $this->pdf_setup();
1156 +
1157 + if ( is_wp_error( $filename ) ) {
1158 + return $filename;
1159 + }
1160 +
1161 + try {
1162 + /*
1163 + * When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
1164 + * area (resulting in unnecessary whitespace) unless the following option is set.
1165 + */
1166 + $this->image->setOption( 'pdf:use-cropbox', true );
1167 +
1168 + /*
1169 + * Reading image after Imagick instantiation because `setResolution`
1170 + * only applies correctly before the image is read.
1171 + */
1172 + $this->image->readImage( $filename );
1173 + } catch ( Exception $e ) {
1174 + // Attempt to run `gs` without the `use-cropbox` option. See #48853.
1175 + $this->image->setOption( 'pdf:use-cropbox', false );
1176 +
1177 + $this->image->readImage( $filename );
1178 + }
1179 +
1180 + return true;
1181 + }
1182 + }
1183 +