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