<?php /** * WordPress Imagick Image Editor * * @package WordPress * @subpackage Image_Editor */ /** * WordPress Image Editor Class for Image Manipulation through Imagick PHP Module * * @since 3.5.0 * * @see WP_Image_Editor */ class WP_Image_Editor_Imagick extends WP_Image_Editor { /** * Imagick object. * * @var Imagick */ protected $image; public function __destruct() { if ( $this->image instanceof Imagick ) { // We don't need the original in memory anymore. $this->image->clear(); $this->image->destroy(); } } /** * Checks to see if current environment supports Imagick. * * We require Imagick 2.2.0 or greater, based on whether the queryFormats() * method can be called statically. * * @since 3.5.0 * * @param array $args * @return bool */ public static function test( $args = array() ) { // First, test Imagick's extension and classes. if ( ! extension_loaded( 'imagick' ) || ! class_exists( 'Imagick', false ) || ! class_exists( 'ImagickPixel', false ) ) { return false; } if ( version_compare( phpversion( 'imagick' ), '2.2.0', '<' ) ) { return false; } $required_methods = array( 'clear', 'destroy', 'valid', 'getimage', 'writeimage', 'getimageblob', 'getimagegeometry', 'getimageformat', 'setimageformat', 'setimagecompression', 'setimagecompressionquality', 'setimagepage', 'setoption', 'scaleimage', 'cropimage', 'rotateimage', 'flipimage', 'flopimage', 'readimage', 'readimageblob', ); // Now, test for deep requirements within Imagick. if ( ! defined( 'imagick::COMPRESSION_JPEG' ) ) { return false; } $class_methods = array_map( 'strtolower', get_class_methods( 'Imagick' ) ); if ( array_diff( $required_methods, $class_methods ) ) { return false; } return true; } /** * Checks to see if editor supports the mime-type specified. * * @since 3.5.0 * * @param string $mime_type * @return bool */ public static function supports_mime_type( $mime_type ) { $imagick_extension = strtoupper( self::get_extension( $mime_type ) ); if ( ! $imagick_extension ) { return false; } /* * setIteratorIndex is optional unless mime is an animated format. * Here, we just say no if you are missing it and aren't loading a jpeg. */ if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) { return false; } try { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged return ( (bool) @Imagick::queryFormats( $imagick_extension ) ); } catch ( Exception $e ) { return false; } } /** * Loads image from $this->file into new Imagick Object. * * @since 3.5.0 * * @return true|WP_Error True if loaded; WP_Error on failure. */ public function load() { if ( $this->image instanceof Imagick ) { return true; } if ( ! is_file( $this->file ) && ! wp_is_stream( $this->file ) ) { return new WP_Error( 'error_loading_image', __( 'File does not exist?' ), $this->file ); } /* * Even though Imagick uses less PHP memory than GD, set higher limit * for users that have low PHP.ini limits. */ wp_raise_memory_limit( 'image' ); try { $this->image = new Imagick(); $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) ); if ( 'pdf' === $file_extension ) { $pdf_loaded = $this->pdf_load_source(); if ( is_wp_error( $pdf_loaded ) ) { return $pdf_loaded; } } else { if ( wp_is_stream( $this->file ) ) { // Due to reports of issues with streams with `Imagick::readImageFile()`, uses `Imagick::readImageBlob()` instead. $this->image->readImageBlob( file_get_contents( $this->file ), $this->file ); } else { $this->image->readImage( $this->file ); } } if ( ! $this->image->valid() ) { return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file ); } // Select the first frame to handle animated images properly. if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) { $this->image->setIteratorIndex( 0 ); } if ( 'pdf' === $file_extension ) { $this->remove_pdf_alpha_channel(); } $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() ); } catch ( Exception $e ) { return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); } $updated_size = $this->update_size(); if ( is_wp_error( $updated_size ) ) { return $updated_size; } return $this->set_quality(); } /** * Sets Image Compression quality on a 1-100% scale. * * @since 3.5.0 * * @param int $quality Compression Quality. Range: [1,100] * @return true|WP_Error True if set successfully; WP_Error on failure. */ public function set_quality( $quality = null ) { $quality_result = parent::set_quality( $quality ); if ( is_wp_error( $quality_result ) ) { return $quality_result; } else { $quality = $this->get_quality(); } try { switch ( $this->mime_type ) { case 'image/jpeg': $this->image->setImageCompressionQuality( $quality ); $this->image->setCompressionQuality( $quality ); $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); break; case 'image/webp': $webp_info = wp_get_webp_info( $this->file ); if ( 'lossless' === $webp_info['type'] ) { // Use WebP lossless settings. $this->image->setImageCompressionQuality( 100 ); $this->image->setCompressionQuality( 100 ); $this->image->setOption( 'webp:lossless', 'true' ); parent::set_quality( 100 ); } else { $this->image->setImageCompressionQuality( $quality ); $this->image->setCompressionQuality( $quality ); } break; case 'image/avif': // Set the AVIF encoder to work faster, with minimal impact on image size. $this->image->setOption( 'heic:speed', 7 ); $this->image->setImageCompressionQuality( $quality ); $this->image->setCompressionQuality( $quality ); break; default: $this->image->setImageCompressionQuality( $quality ); $this->image->setCompressionQuality( $quality ); } } catch ( Exception $e ) { return new WP_Error( 'image_quality_error', $e->getMessage() ); } return true; } /** * Sets or updates current image size. * * @since 3.5.0 * * @param int $width * @param int $height * @return true|WP_Error */ protected function update_size( $width = null, $height = null ) { $size = null; if ( ! $width || ! $height ) { try { $size = $this->image->getImageGeometry(); } catch ( Exception $e ) { return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file ); } } if ( ! $width ) { $width = $size['width']; } if ( ! $height ) { $height = $size['height']; } /* * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF and HEIC images * are properly sized without affecting previous `getImageGeometry` behavior. */ if ( ( ! $width || ! $height ) && ( 'image/avif' === $this->mime_type || wp_is_heic_image_mime_type( $this->mime_type ) ) ) { $size = wp_getimagesize( $this->file ); $width = $size[0]; $height = $size[1]; } return parent::update_size( $width, $height ); } /** * Sets Imagick time limit. * * Depending on configuration, Imagick processing may take time. * * Multiple problems exist if PHP times out before ImageMagick completed: * 1. Temporary files aren't cleaned by ImageMagick garbage collection. * 2. No clear error is provided. * 3. The cause of such timeout can be hard to pinpoint. * * This function, which is expected to be run before heavy image routines, resolves * point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it is set. * * However seems it introduces more problems than it fixes, * see https://core.trac.wordpress.org/ticket/58202. * * Note: * - Imagick resource exhaustion does not issue catchable exceptions (yet). * See https://github.com/Imagick/imagick/issues/333. * - The resource limit is not saved/restored. It applies to subsequent