wp/wp-includes/class-wp-image-editor-imagick.php
changeset 18 be944660c56a
parent 16 a86126ab1dd4
child 19 3d72ae0968f4
equal deleted inserted replaced
17:34716fd837a4 18:be944660c56a
    69 			'cropimage',
    69 			'cropimage',
    70 			'rotateimage',
    70 			'rotateimage',
    71 			'flipimage',
    71 			'flipimage',
    72 			'flopimage',
    72 			'flopimage',
    73 			'readimage',
    73 			'readimage',
       
    74 			'readimageblob',
    74 		);
    75 		);
    75 
    76 
    76 		// Now, test for deep requirements within Imagick.
    77 		// Now, test for deep requirements within Imagick.
    77 		if ( ! defined( 'imagick::COMPRESSION_JPEG' ) ) {
    78 		if ( ! defined( 'imagick::COMPRESSION_JPEG' ) ) {
    78 			return false;
    79 			return false;
   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 }