125 public function load() { |
126 public function load() { |
126 if ( $this->image instanceof Imagick ) { |
127 if ( $this->image instanceof Imagick ) { |
127 return true; |
128 return true; |
128 } |
129 } |
129 |
130 |
130 if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) ) { |
131 if ( ! is_file( $this->file ) && ! wp_is_stream( $this->file ) ) { |
131 return new WP_Error( 'error_loading_image', __( 'File doesn’t exist?' ), $this->file ); |
132 return new WP_Error( 'error_loading_image', __( 'File doesn’t exist?' ), $this->file ); |
132 } |
133 } |
133 |
134 |
134 /* |
135 /* |
135 * Even though Imagick uses less PHP memory than GD, set higher limit |
136 * Even though Imagick uses less PHP memory than GD, set higher limit |
138 wp_raise_memory_limit( 'image' ); |
139 wp_raise_memory_limit( 'image' ); |
139 |
140 |
140 try { |
141 try { |
141 $this->image = new Imagick(); |
142 $this->image = new Imagick(); |
142 $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) ); |
143 $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) ); |
143 $filename = $this->file; |
|
144 |
144 |
145 if ( 'pdf' === $file_extension ) { |
145 if ( 'pdf' === $file_extension ) { |
146 $filename = $this->pdf_setup(); |
146 $pdf_loaded = $this->pdf_load_source(); |
147 } |
147 |
148 |
148 if ( is_wp_error( $pdf_loaded ) ) { |
149 // Reading image after Imagick instantiation because `setResolution` |
149 return $pdf_loaded; |
150 // only applies correctly before the image is read. |
150 } |
151 $this->image->readImage( $filename ); |
151 } else { |
|
152 if ( wp_is_stream( $this->file ) ) { |
|
153 // Due to reports of issues with streams with `Imagick::readImageFile()`, uses `Imagick::readImageBlob()` instead. |
|
154 $this->image->readImageBlob( file_get_contents( $this->file ), $this->file ); |
|
155 } else { |
|
156 $this->image->readImage( $this->file ); |
|
157 } |
|
158 } |
152 |
159 |
153 if ( ! $this->image->valid() ) { |
160 if ( ! $this->image->valid() ) { |
154 return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file ); |
161 return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file ); |
155 } |
162 } |
156 |
163 |
163 } catch ( Exception $e ) { |
170 } catch ( Exception $e ) { |
164 return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); |
171 return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); |
165 } |
172 } |
166 |
173 |
167 $updated_size = $this->update_size(); |
174 $updated_size = $this->update_size(); |
|
175 |
168 if ( is_wp_error( $updated_size ) ) { |
176 if ( is_wp_error( $updated_size ) ) { |
169 return $updated_size; |
177 return $updated_size; |
170 } |
178 } |
171 |
179 |
172 return $this->set_quality(); |
180 return $this->set_quality(); |
187 } else { |
195 } else { |
188 $quality = $this->get_quality(); |
196 $quality = $this->get_quality(); |
189 } |
197 } |
190 |
198 |
191 try { |
199 try { |
192 if ( 'image/jpeg' === $this->mime_type ) { |
200 switch ( $this->mime_type ) { |
193 $this->image->setImageCompressionQuality( $quality ); |
201 case 'image/jpeg': |
194 $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); |
202 $this->image->setImageCompressionQuality( $quality ); |
195 } else { |
203 $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); |
196 $this->image->setImageCompressionQuality( $quality ); |
204 break; |
|
205 case 'image/webp': |
|
206 $webp_info = wp_get_webp_info( $this->file ); |
|
207 |
|
208 if ( 'lossless' === $webp_info['type'] ) { |
|
209 // Use WebP lossless settings. |
|
210 $this->image->setImageCompressionQuality( 100 ); |
|
211 $this->image->setOption( 'webp:lossless', 'true' ); |
|
212 } else { |
|
213 $this->image->setImageCompressionQuality( $quality ); |
|
214 } |
|
215 break; |
|
216 default: |
|
217 $this->image->setImageCompressionQuality( $quality ); |
197 } |
218 } |
198 } catch ( Exception $e ) { |
219 } catch ( Exception $e ) { |
199 return new WP_Error( 'image_quality_error', $e->getMessage() ); |
220 return new WP_Error( 'image_quality_error', $e->getMessage() ); |
200 } |
221 } |
201 |
|
202 return true; |
222 return true; |
203 } |
223 } |
|
224 |
204 |
225 |
205 /** |
226 /** |
206 * Sets or updates current image size. |
227 * Sets or updates current image size. |
207 * |
228 * |
208 * @since 3.5.0 |
229 * @since 3.5.0 |
242 * @since 3.5.0 |
263 * @since 3.5.0 |
243 * |
264 * |
244 * @param int|null $max_w Image width. |
265 * @param int|null $max_w Image width. |
245 * @param int|null $max_h Image height. |
266 * @param int|null $max_h Image height. |
246 * @param bool $crop |
267 * @param bool $crop |
247 * @return bool|WP_Error |
268 * @return true|WP_Error |
248 */ |
269 */ |
249 public function resize( $max_w, $max_h, $crop = false ) { |
270 public function resize( $max_w, $max_h, $crop = false ) { |
250 if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) { |
271 if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) { |
251 return true; |
272 return true; |
252 } |
273 } |
281 * |
302 * |
282 * @param int $dst_w The destination width. |
303 * @param int $dst_w The destination width. |
283 * @param int $dst_h The destination height. |
304 * @param int $dst_h The destination height. |
284 * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'. |
305 * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'. |
285 * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true. |
306 * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true. |
286 * @return bool|WP_Error |
307 * @return void|WP_Error |
287 */ |
308 */ |
288 protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) { |
309 protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) { |
289 $allowed_filters = array( |
310 $allowed_filters = array( |
290 'FILTER_POINT', |
311 'FILTER_POINT', |
291 'FILTER_BOX', |
312 'FILTER_BOX', |
514 * @param int $src_w The width to crop. |
535 * @param int $src_w The width to crop. |
515 * @param int $src_h The height to crop. |
536 * @param int $src_h The height to crop. |
516 * @param int $dst_w Optional. The destination width. |
537 * @param int $dst_w Optional. The destination width. |
517 * @param int $dst_h Optional. The destination height. |
538 * @param int $dst_h Optional. The destination height. |
518 * @param bool $src_abs Optional. If the source crop points are absolute. |
539 * @param bool $src_abs Optional. If the source crop points are absolute. |
519 * @return bool|WP_Error |
540 * @return true|WP_Error |
520 */ |
541 */ |
521 public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { |
542 public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { |
522 if ( $src_abs ) { |
543 if ( $src_abs ) { |
523 $src_w -= $src_x; |
544 $src_w -= $src_x; |
524 $src_h -= $src_y; |
545 $src_h -= $src_y; |
546 return $this->update_size(); |
567 return $this->update_size(); |
547 } |
568 } |
548 } catch ( Exception $e ) { |
569 } catch ( Exception $e ) { |
549 return new WP_Error( 'image_crop_error', $e->getMessage() ); |
570 return new WP_Error( 'image_crop_error', $e->getMessage() ); |
550 } |
571 } |
|
572 |
551 return $this->update_size(); |
573 return $this->update_size(); |
552 } |
574 } |
553 |
575 |
554 /** |
576 /** |
555 * Rotates current image counter-clockwise by $angle. |
577 * Rotates current image counter-clockwise by $angle. |
580 |
602 |
581 $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 ); |
603 $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 ); |
582 } catch ( Exception $e ) { |
604 } catch ( Exception $e ) { |
583 return new WP_Error( 'image_rotate_error', $e->getMessage() ); |
605 return new WP_Error( 'image_rotate_error', $e->getMessage() ); |
584 } |
606 } |
|
607 |
585 return true; |
608 return true; |
586 } |
609 } |
587 |
610 |
588 /** |
611 /** |
589 * Flips current image. |
612 * Flips current image. |
676 try { |
699 try { |
677 // Store initial format. |
700 // Store initial format. |
678 $orig_format = $this->image->getImageFormat(); |
701 $orig_format = $this->image->getImageFormat(); |
679 |
702 |
680 $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); |
703 $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); |
681 $this->make_image( $filename, array( $image, 'writeImage' ), array( $filename ) ); |
704 } catch ( Exception $e ) { |
682 |
705 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); |
|
706 } |
|
707 |
|
708 $write_image_result = $this->write_image( $this->image, $filename ); |
|
709 if ( is_wp_error( $write_image_result ) ) { |
|
710 return $write_image_result; |
|
711 } |
|
712 |
|
713 try { |
683 // Reset original format. |
714 // Reset original format. |
684 $this->image->setImageFormat( $orig_format ); |
715 $this->image->setImageFormat( $orig_format ); |
685 } catch ( Exception $e ) { |
716 } catch ( Exception $e ) { |
686 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); |
717 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); |
687 } |
718 } |
700 'mime-type' => $mime_type, |
731 'mime-type' => $mime_type, |
701 ); |
732 ); |
702 } |
733 } |
703 |
734 |
704 /** |
735 /** |
|
736 * Writes an image to a file or stream. |
|
737 * |
|
738 * @since 5.6.0 |
|
739 * |
|
740 * @param Imagick $image |
|
741 * @param string $filename The destination filename or stream URL. |
|
742 * @return true|WP_Error |
|
743 */ |
|
744 private function write_image( $image, $filename ) { |
|
745 if ( wp_is_stream( $filename ) ) { |
|
746 /* |
|
747 * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead. |
|
748 * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php |
|
749 */ |
|
750 if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) { |
|
751 return new WP_Error( |
|
752 'image_save_error', |
|
753 sprintf( |
|
754 /* translators: %s: PHP function name. */ |
|
755 __( '%s failed while writing image to stream.' ), |
|
756 '<code>file_put_contents()</code>' |
|
757 ), |
|
758 $filename |
|
759 ); |
|
760 } else { |
|
761 return true; |
|
762 } |
|
763 } else { |
|
764 $dirname = dirname( $filename ); |
|
765 |
|
766 if ( ! wp_mkdir_p( $dirname ) ) { |
|
767 return new WP_Error( |
|
768 'image_save_error', |
|
769 sprintf( |
|
770 /* translators: %s: Directory path. */ |
|
771 __( 'Unable to create directory %s. Is its parent directory writable by the server?' ), |
|
772 esc_html( $dirname ) |
|
773 ) |
|
774 ); |
|
775 } |
|
776 |
|
777 try { |
|
778 return $image->writeImage( $filename ); |
|
779 } catch ( Exception $e ) { |
|
780 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); |
|
781 } |
|
782 } |
|
783 } |
|
784 |
|
785 /** |
705 * Streams current image to browser. |
786 * Streams current image to browser. |
706 * |
787 * |
707 * @since 3.5.0 |
788 * @since 3.5.0 |
708 * |
789 * |
709 * @param string $mime_type The mime type of the image. |
790 * @param string $mime_type The mime type of the image. |
710 * @return bool|WP_Error True on success, WP_Error object on failure. |
791 * @return true|WP_Error True on success, WP_Error object on failure. |
711 */ |
792 */ |
712 public function stream( $mime_type = null ) { |
793 public function stream( $mime_type = null ) { |
713 list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type ); |
794 list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type ); |
714 |
795 |
715 try { |
796 try { |
737 * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error. |
818 * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error. |
738 */ |
819 */ |
739 protected function strip_meta() { |
820 protected function strip_meta() { |
740 |
821 |
741 if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) { |
822 if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) { |
742 /* translators: %s: ImageMagick method name. */ |
823 return new WP_Error( |
743 return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), '<code>Imagick::getImageProfiles()</code>' ) ); |
824 'image_strip_meta_error', |
|
825 sprintf( |
|
826 /* translators: %s: ImageMagick method name. */ |
|
827 __( '%s is required to strip image meta.' ), |
|
828 '<code>Imagick::getImageProfiles()</code>' |
|
829 ) |
|
830 ); |
744 } |
831 } |
745 |
832 |
746 if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) { |
833 if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) { |
747 /* translators: %s: ImageMagick method name. */ |
834 return new WP_Error( |
748 return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), '<code>Imagick::removeImageProfile()</code>' ) ); |
835 'image_strip_meta_error', |
|
836 sprintf( |
|
837 /* translators: %s: ImageMagick method name. */ |
|
838 __( '%s is required to strip image meta.' ), |
|
839 '<code>Imagick::removeImageProfile()</code>' |
|
840 ) |
|
841 ); |
749 } |
842 } |
750 |
843 |
751 /* |
844 /* |
752 * Protect a few profiles from being stripped for the following reasons: |
845 * Protect a few profiles from being stripped for the following reasons: |
753 * |
846 * |
791 try { |
884 try { |
792 // By default, PDFs are rendered in a very low resolution. |
885 // By default, PDFs are rendered in a very low resolution. |
793 // We want the thumbnail to be readable, so increase the rendering DPI. |
886 // We want the thumbnail to be readable, so increase the rendering DPI. |
794 $this->image->setResolution( 128, 128 ); |
887 $this->image->setResolution( 128, 128 ); |
795 |
888 |
|
889 // Only load the first page. |
|
890 return $this->file . '[0]'; |
|
891 } catch ( Exception $e ) { |
|
892 return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file ); |
|
893 } |
|
894 } |
|
895 |
|
896 /** |
|
897 * Load the image produced by Ghostscript. |
|
898 * |
|
899 * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files |
|
900 * when `use-cropbox` is set. |
|
901 * |
|
902 * @since 5.6.0 |
|
903 * |
|
904 * @return true|WP_error |
|
905 */ |
|
906 protected function pdf_load_source() { |
|
907 $filename = $this->pdf_setup(); |
|
908 |
|
909 if ( is_wp_error( $filename ) ) { |
|
910 return $filename; |
|
911 } |
|
912 |
|
913 try { |
796 // When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped |
914 // When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped |
797 // area (resulting in unnecessary whitespace) unless the following option is set. |
915 // area (resulting in unnecessary whitespace) unless the following option is set. |
798 $this->image->setOption( 'pdf:use-cropbox', true ); |
916 $this->image->setOption( 'pdf:use-cropbox', true ); |
799 |
917 |
800 // Only load the first page. |
918 // Reading image after Imagick instantiation because `setResolution` |
801 return $this->file . '[0]'; |
919 // only applies correctly before the image is read. |
802 } catch ( Exception $e ) { |
920 $this->image->readImage( $filename ); |
803 return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file ); |
921 } catch ( Exception $e ) { |
804 } |
922 // Attempt to run `gs` without the `use-cropbox` option. See #48853. |
|
923 $this->image->setOption( 'pdf:use-cropbox', false ); |
|
924 |
|
925 $this->image->readImage( $filename ); |
|
926 } |
|
927 |
|
928 return true; |
805 } |
929 } |
806 |
930 |
807 } |
931 } |