100 |
100 |
101 if ( ! $imagick_extension ) { |
101 if ( ! $imagick_extension ) { |
102 return false; |
102 return false; |
103 } |
103 } |
104 |
104 |
105 // setIteratorIndex is optional unless mime is an animated format. |
105 /* |
106 // Here, we just say no if you are missing it and aren't loading a jpeg. |
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 */ |
107 if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) { |
109 if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) { |
108 return false; |
110 return false; |
109 } |
111 } |
110 |
112 |
111 try { |
113 try { |
164 // Select the first frame to handle animated images properly. |
166 // Select the first frame to handle animated images properly. |
165 if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) { |
167 if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) { |
166 $this->image->setIteratorIndex( 0 ); |
168 $this->image->setIteratorIndex( 0 ); |
167 } |
169 } |
168 |
170 |
|
171 if ( 'pdf' === $file_extension ) { |
|
172 $this->remove_pdf_alpha_channel(); |
|
173 } |
|
174 |
169 $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() ); |
175 $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() ); |
170 } catch ( Exception $e ) { |
176 } catch ( Exception $e ) { |
171 return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); |
177 return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); |
172 } |
178 } |
173 |
179 |
211 $this->image->setOption( 'webp:lossless', 'true' ); |
217 $this->image->setOption( 'webp:lossless', 'true' ); |
212 } else { |
218 } else { |
213 $this->image->setImageCompressionQuality( $quality ); |
219 $this->image->setImageCompressionQuality( $quality ); |
214 } |
220 } |
215 break; |
221 break; |
|
222 case 'image/avif': |
216 default: |
223 default: |
217 $this->image->setImageCompressionQuality( $quality ); |
224 $this->image->setImageCompressionQuality( $quality ); |
218 } |
225 } |
219 } catch ( Exception $e ) { |
226 } catch ( Exception $e ) { |
220 return new WP_Error( 'image_quality_error', $e->getMessage() ); |
227 return new WP_Error( 'image_quality_error', $e->getMessage() ); |
248 |
255 |
249 if ( ! $height ) { |
256 if ( ! $height ) { |
250 $height = $size['height']; |
257 $height = $size['height']; |
251 } |
258 } |
252 |
259 |
|
260 /* |
|
261 * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF images |
|
262 * are properly sized without affecting previous `getImageGeometry` behavior. |
|
263 */ |
|
264 if ( ( ! $width || ! $height ) && 'image/avif' === $this->mime_type ) { |
|
265 $size = wp_getimagesize( $this->file ); |
|
266 $width = $size[0]; |
|
267 $height = $size[1]; |
|
268 } |
|
269 |
253 return parent::update_size( $width, $height ); |
270 return parent::update_size( $width, $height ); |
|
271 } |
|
272 |
|
273 /** |
|
274 * Sets Imagick time limit. |
|
275 * |
|
276 * Depending on configuration, Imagick processing may take time. |
|
277 * |
|
278 * Multiple problems exist if PHP times out before ImageMagick completed: |
|
279 * 1. Temporary files aren't cleaned by ImageMagick garbage collection. |
|
280 * 2. No clear error is provided. |
|
281 * 3. The cause of such timeout can be hard to pinpoint. |
|
282 * |
|
283 * This function, which is expected to be run before heavy image routines, resolves |
|
284 * point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it is set. |
|
285 * |
|
286 * However seems it introduces more problems than it fixes, |
|
287 * see https://core.trac.wordpress.org/ticket/58202. |
|
288 * |
|
289 * Note: |
|
290 * - Imagick resource exhaustion does not issue catchable exceptions (yet). |
|
291 * See https://github.com/Imagick/imagick/issues/333. |
|
292 * - The resource limit is not saved/restored. It applies to subsequent |
|
293 * image operations within the time of the HTTP request. |
|
294 * |
|
295 * @since 6.2.0 |
|
296 * @since 6.3.0 This method was deprecated. |
|
297 * |
|
298 * @return int|null The new limit on success, null on failure. |
|
299 */ |
|
300 public static function set_imagick_time_limit() { |
|
301 _deprecated_function( __METHOD__, '6.3.0' ); |
|
302 |
|
303 if ( ! defined( 'Imagick::RESOURCETYPE_TIME' ) ) { |
|
304 return null; |
|
305 } |
|
306 |
|
307 // Returns PHP_FLOAT_MAX if unset. |
|
308 $imagick_timeout = Imagick::getResourceLimit( Imagick::RESOURCETYPE_TIME ); |
|
309 |
|
310 // Convert to an integer, keeping in mind that: 0 === (int) PHP_FLOAT_MAX. |
|
311 $imagick_timeout = $imagick_timeout > PHP_INT_MAX ? PHP_INT_MAX : (int) $imagick_timeout; |
|
312 |
|
313 $php_timeout = (int) ini_get( 'max_execution_time' ); |
|
314 |
|
315 if ( $php_timeout > 1 && $php_timeout < $imagick_timeout ) { |
|
316 $limit = (float) 0.8 * $php_timeout; |
|
317 Imagick::setResourceLimit( Imagick::RESOURCETYPE_TIME, $limit ); |
|
318 |
|
319 return $limit; |
|
320 } |
254 } |
321 } |
255 |
322 |
256 /** |
323 /** |
257 * Resizes current image. |
324 * Resizes current image. |
258 * |
325 * |
260 * If one of the two is set to null, the resize will |
327 * If one of the two is set to null, the resize will |
261 * maintain aspect ratio according to the provided dimension. |
328 * maintain aspect ratio according to the provided dimension. |
262 * |
329 * |
263 * @since 3.5.0 |
330 * @since 3.5.0 |
264 * |
331 * |
265 * @param int|null $max_w Image width. |
332 * @param int|null $max_w Image width. |
266 * @param int|null $max_h Image height. |
333 * @param int|null $max_h Image height. |
267 * @param bool $crop |
334 * @param bool|array $crop { |
|
335 * Optional. Image cropping behavior. If false, the image will be scaled (default). |
|
336 * If true, image will be cropped to the specified dimensions using center positions. |
|
337 * If an array, the image will be cropped using the array to specify the crop location: |
|
338 * |
|
339 * @type string $0 The x crop position. Accepts 'left' 'center', or 'right'. |
|
340 * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'. |
|
341 * } |
268 * @return true|WP_Error |
342 * @return true|WP_Error |
269 */ |
343 */ |
270 public function resize( $max_w, $max_h, $crop = false ) { |
344 public function resize( $max_w, $max_h, $crop = false ) { |
271 if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) { |
345 if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) { |
272 return true; |
346 return true; |
273 } |
347 } |
274 |
348 |
275 $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop ); |
349 $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop ); |
276 if ( ! $dims ) { |
350 if ( ! $dims ) { |
413 if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) { |
487 if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) { |
414 if ( 8 < $this->image->getImageDepth() ) { |
488 if ( 8 < $this->image->getImageDepth() ) { |
415 $this->image->setImageDepth( 8 ); |
489 $this->image->setImageDepth( 8 ); |
416 } |
490 } |
417 } |
491 } |
418 |
|
419 if ( is_callable( array( $this->image, 'setInterlaceScheme' ) ) && defined( 'Imagick::INTERLACE_NO' ) ) { |
|
420 $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); |
|
421 } |
|
422 } catch ( Exception $e ) { |
492 } catch ( Exception $e ) { |
423 return new WP_Error( 'image_resize_error', $e->getMessage() ); |
493 return new WP_Error( 'image_resize_error', $e->getMessage() ); |
424 } |
494 } |
425 } |
495 } |
426 |
496 |
445 * maintain aspect ratio according to the provided dimension. |
515 * maintain aspect ratio according to the provided dimension. |
446 * |
516 * |
447 * @type array ...$0 { |
517 * @type array ...$0 { |
448 * Array of height, width values, and whether to crop. |
518 * Array of height, width values, and whether to crop. |
449 * |
519 * |
450 * @type int $width Image width. Optional if `$height` is specified. |
520 * @type int $width Image width. Optional if `$height` is specified. |
451 * @type int $height Image height. Optional if `$width` is specified. |
521 * @type int $height Image height. Optional if `$width` is specified. |
452 * @type bool $crop Optional. Whether to crop the image. Default false. |
522 * @type bool|array $crop Optional. Whether to crop the image. Default false. |
453 * } |
523 * } |
454 * } |
524 * } |
455 * @return array An array of resized images' metadata by size. |
525 * @return array An array of resized images' metadata by size. |
456 */ |
526 */ |
457 public function multi_resize( $sizes ) { |
527 public function multi_resize( $sizes ) { |
474 * @since 5.3.0 |
544 * @since 5.3.0 |
475 * |
545 * |
476 * @param array $size_data { |
546 * @param array $size_data { |
477 * Array of size data. |
547 * Array of size data. |
478 * |
548 * |
479 * @type int $width The maximum width in pixels. |
549 * @type int $width The maximum width in pixels. |
480 * @type int $height The maximum height in pixels. |
550 * @type int $height The maximum height in pixels. |
481 * @type bool $crop Whether to crop the image to exact dimensions. |
551 * @type bool|array $crop Whether to crop the image to exact dimensions. |
482 * } |
552 * } |
483 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta, |
553 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta, |
484 * WP_Error object on error. |
554 * WP_Error object on error. |
485 */ |
555 */ |
486 public function make_subsize( $size_data ) { |
556 public function make_subsize( $size_data ) { |
499 $size_data['height'] = null; |
569 $size_data['height'] = null; |
500 } |
570 } |
501 |
571 |
502 if ( ! isset( $size_data['crop'] ) ) { |
572 if ( ! isset( $size_data['crop'] ) ) { |
503 $size_data['crop'] = false; |
573 $size_data['crop'] = false; |
|
574 } |
|
575 |
|
576 if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) { |
|
577 return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) ); |
504 } |
578 } |
505 |
579 |
506 $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] ); |
580 $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] ); |
507 |
581 |
508 if ( is_wp_error( $resized ) ) { |
582 if ( is_wp_error( $resized ) ) { |
548 try { |
622 try { |
549 $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); |
623 $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); |
550 $this->image->setImagePage( $src_w, $src_h, 0, 0 ); |
624 $this->image->setImagePage( $src_w, $src_h, 0, 0 ); |
551 |
625 |
552 if ( $dst_w || $dst_h ) { |
626 if ( $dst_w || $dst_h ) { |
553 // If destination width/height isn't specified, |
627 /* |
554 // use same as width/height from source. |
628 * If destination width/height isn't specified, |
|
629 * use same as width/height from source. |
|
630 */ |
555 if ( ! $dst_w ) { |
631 if ( ! $dst_w ) { |
556 $dst_w = $src_w; |
632 $dst_w = $src_w; |
557 } |
633 } |
558 if ( ! $dst_h ) { |
634 if ( ! $dst_h ) { |
559 $dst_h = $src_h; |
635 $dst_h = $src_h; |
659 |
735 |
660 /** |
736 /** |
661 * Saves current image to file. |
737 * Saves current image to file. |
662 * |
738 * |
663 * @since 3.5.0 |
739 * @since 3.5.0 |
|
740 * @since 6.0.0 The `$filesize` value was added to the returned array. |
664 * |
741 * |
665 * @param string $destfilename Optional. Destination filename. Default null. |
742 * @param string $destfilename Optional. Destination filename. Default null. |
666 * @param string $mime_type Optional. The mime-type. Default null. |
743 * @param string $mime_type Optional. The mime-type. Default null. |
667 * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string} |
744 * @return array|WP_Error { |
|
745 * Array on success or WP_Error if the file failed to save. |
|
746 * |
|
747 * @type string $path Path to the image file. |
|
748 * @type string $file Name of the image file. |
|
749 * @type int $width Image width. |
|
750 * @type int $height Image height. |
|
751 * @type string $mime-type The mime type of the image. |
|
752 * @type int $filesize File size of the image. |
|
753 * } |
668 */ |
754 */ |
669 public function save( $destfilename = null, $mime_type = null ) { |
755 public function save( $destfilename = null, $mime_type = null ) { |
670 $saved = $this->_save( $this->image, $destfilename, $mime_type ); |
756 $saved = $this->_save( $this->image, $destfilename, $mime_type ); |
671 |
757 |
672 if ( ! is_wp_error( $saved ) ) { |
758 if ( ! is_wp_error( $saved ) ) { |
682 |
768 |
683 return $saved; |
769 return $saved; |
684 } |
770 } |
685 |
771 |
686 /** |
772 /** |
|
773 * Removes PDF alpha after it's been read. |
|
774 * |
|
775 * @since 6.4.0 |
|
776 */ |
|
777 protected function remove_pdf_alpha_channel() { |
|
778 $version = Imagick::getVersion(); |
|
779 // Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5. |
|
780 if ( $version['versionNumber'] >= 0x675 ) { |
|
781 try { |
|
782 // Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2. |
|
783 $this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 ); |
|
784 } catch ( Exception $e ) { |
|
785 return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() ); |
|
786 } |
|
787 } |
|
788 } |
|
789 |
|
790 /** |
|
791 * @since 3.5.0 |
|
792 * @since 6.0.0 The `$filesize` value was added to the returned array. |
|
793 * |
687 * @param Imagick $image |
794 * @param Imagick $image |
688 * @param string $filename |
795 * @param string $filename |
689 * @param string $mime_type |
796 * @param string $mime_type |
690 * @return array|WP_Error |
797 * @return array|WP_Error { |
|
798 * Array on success or WP_Error if the file failed to save. |
|
799 * |
|
800 * @type string $path Path to the image file. |
|
801 * @type string $file Name of the image file. |
|
802 * @type int $width Image width. |
|
803 * @type int $height Image height. |
|
804 * @type string $mime-type The mime type of the image. |
|
805 * @type int $filesize File size of the image. |
|
806 * } |
691 */ |
807 */ |
692 protected function _save( $image, $filename = null, $mime_type = null ) { |
808 protected function _save( $image, $filename = null, $mime_type = null ) { |
693 list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type ); |
809 list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type ); |
694 |
810 |
695 if ( ! $filename ) { |
811 if ( ! $filename ) { |
701 $orig_format = $this->image->getImageFormat(); |
817 $orig_format = $this->image->getImageFormat(); |
702 |
818 |
703 $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); |
819 $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); |
704 } catch ( Exception $e ) { |
820 } catch ( Exception $e ) { |
705 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); |
821 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); |
|
822 } |
|
823 |
|
824 if ( method_exists( $this->image, 'setInterlaceScheme' ) |
|
825 && method_exists( $this->image, 'getInterlaceScheme' ) |
|
826 && defined( 'Imagick::INTERLACE_PLANE' ) |
|
827 ) { |
|
828 $orig_interlace = $this->image->getInterlaceScheme(); |
|
829 |
|
830 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ |
|
831 if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) { |
|
832 $this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output. |
|
833 } else { |
|
834 $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output. |
|
835 } |
706 } |
836 } |
707 |
837 |
708 $write_image_result = $this->write_image( $this->image, $filename ); |
838 $write_image_result = $this->write_image( $this->image, $filename ); |
709 if ( is_wp_error( $write_image_result ) ) { |
839 if ( is_wp_error( $write_image_result ) ) { |
710 return $write_image_result; |
840 return $write_image_result; |
711 } |
841 } |
712 |
842 |
713 try { |
843 try { |
714 // Reset original format. |
844 // Reset original format. |
715 $this->image->setImageFormat( $orig_format ); |
845 $this->image->setImageFormat( $orig_format ); |
|
846 |
|
847 if ( isset( $orig_interlace ) ) { |
|
848 $this->image->setInterlaceScheme( $orig_interlace ); |
|
849 } |
716 } catch ( Exception $e ) { |
850 } catch ( Exception $e ) { |
717 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); |
851 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); |
718 } |
852 } |
719 |
853 |
720 // Set correct file permissions. |
854 // Set correct file permissions. |
881 * |
1015 * |
882 * @return string|WP_Error File to load or WP_Error on failure. |
1016 * @return string|WP_Error File to load or WP_Error on failure. |
883 */ |
1017 */ |
884 protected function pdf_setup() { |
1018 protected function pdf_setup() { |
885 try { |
1019 try { |
886 // By default, PDFs are rendered in a very low resolution. |
1020 /* |
887 // We want the thumbnail to be readable, so increase the rendering DPI. |
1021 * By default, PDFs are rendered in a very low resolution. |
|
1022 * We want the thumbnail to be readable, so increase the rendering DPI. |
|
1023 */ |
888 $this->image->setResolution( 128, 128 ); |
1024 $this->image->setResolution( 128, 128 ); |
889 |
1025 |
890 // Only load the first page. |
1026 // Only load the first page. |
891 return $this->file . '[0]'; |
1027 return $this->file . '[0]'; |
892 } catch ( Exception $e ) { |
1028 } catch ( Exception $e ) { |
910 if ( is_wp_error( $filename ) ) { |
1046 if ( is_wp_error( $filename ) ) { |
911 return $filename; |
1047 return $filename; |
912 } |
1048 } |
913 |
1049 |
914 try { |
1050 try { |
915 // When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped |
1051 /* |
916 // area (resulting in unnecessary whitespace) unless the following option is set. |
1052 * When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped |
|
1053 * area (resulting in unnecessary whitespace) unless the following option is set. |
|
1054 */ |
917 $this->image->setOption( 'pdf:use-cropbox', true ); |
1055 $this->image->setOption( 'pdf:use-cropbox', true ); |
918 |
1056 |
919 // Reading image after Imagick instantiation because `setResolution` |
1057 /* |
920 // only applies correctly before the image is read. |
1058 * Reading image after Imagick instantiation because `setResolution` |
|
1059 * only applies correctly before the image is read. |
|
1060 */ |
921 $this->image->readImage( $filename ); |
1061 $this->image->readImage( $filename ); |
922 } catch ( Exception $e ) { |
1062 } catch ( Exception $e ) { |
923 // Attempt to run `gs` without the `use-cropbox` option. See #48853. |
1063 // Attempt to run `gs` without the `use-cropbox` option. See #48853. |
924 $this->image->setOption( 'pdf:use-cropbox', false ); |
1064 $this->image->setOption( 'pdf:use-cropbox', false ); |
925 |
1065 |
926 $this->image->readImage( $filename ); |
1066 $this->image->readImage( $filename ); |
927 } |
1067 } |
928 |
1068 |
929 return true; |
1069 return true; |
930 } |
1070 } |
931 |
|
932 } |
1071 } |