| [ 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 536 /** 537 * Resizes current image. 538 * 539 * At minimum, either a height or width must be provided. 540 * If one of the two is set to null, the resize will 541 * maintain aspect ratio according to the provided dimension. 542 * 543 * @since 3.5.0 544 * 545 * @param int|null $max_w Image width. 546 * @param int|null $max_h Image height. 547 * @param bool|array $crop { 548 * Optional. Image cropping behavior. If false, the image will be scaled (default). 549 * If true, image will be cropped to the specified dimensions using center positions. 550 * If an array, the image will be cropped using the array to specify the crop location: 551 * 552 * @type string $0 The x crop position. Accepts 'left', 'center', or 'right'. 553 * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'. 554 * } 555 * @return true|WP_Error 556 */ 557 public function resize( $max_w, $max_h, $crop = false ) { 558 if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) { 559 return true; 560 } 561 562 $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop ); 563 if ( ! $dims ) { 564 return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) ); 565 } 566 567 list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; 568 569 if ( $crop ) { 570 return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h ); 571 } 572 573 $this->set_quality( 574 null, 575 array( 576 'width' => $dst_w, 577 'height' => $dst_h, 578 ) 579 ); 580 581 // Execute the resize. 582 $thumb_result = $this->thumbnail_image( $dst_w, $dst_h ); 583 if ( is_wp_error( $thumb_result ) ) { 584 return $thumb_result; 585 } 586 587 return $this->update_size( $dst_w, $dst_h ); 588 } 589 590 /** 591 * Efficiently resize the current image 592 * 593 * This is a WordPress specific implementation of Imagick::thumbnailImage(), 594 * which resizes an image to given dimensions and removes any associated profiles. 595 * 596 * @since 4.5.0 597 * 598 * @param int $dst_w The destination width. 599 * @param int $dst_h The destination height. 600 * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'. 601 * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true. 602 * @return void|WP_Error 603 */ 604 protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) { 605 $allowed_filters = array( 606 'FILTER_POINT', 607 'FILTER_BOX', 608 'FILTER_TRIANGLE', 609 'FILTER_HERMITE', 610 'FILTER_HANNING', 611 'FILTER_HAMMING', 612 'FILTER_BLACKMAN', 613 'FILTER_GAUSSIAN', 614 'FILTER_QUADRATIC', 615 'FILTER_CUBIC', 616 'FILTER_CATROM', 617 'FILTER_MITCHELL', 618 'FILTER_LANCZOS', 619 'FILTER_BESSEL', 620 'FILTER_SINC', 621 ); 622 623 /** 624 * Set the filter value if '$filter_name' name is in the allowed list and the related 625 * Imagick constant is defined or fall back to the default filter. 626 */ 627 if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) { 628 $filter = constant( 'Imagick::' . $filter_name ); 629 } else { 630 $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false; 631 } 632 633 /** 634 * Filters whether to strip metadata from images when they're resized. 635 * 636 * This filter only applies when resizing using the Imagick editor since GD 637 * always strips profiles by default. 638 * 639 * @since 4.5.0 640 * 641 * @param bool $strip_meta Whether to strip image metadata during resizing. Default true. 642 */ 643 if ( apply_filters( 'image_strip_meta', $strip_meta ) ) { 644 $this->strip_meta(); // Fail silently if not supported. 645 } 646 647 try { 648 /** 649 * Special handling for certain types of PNG images: 650 * 1. For PNG images, we need to specify compression settings and remove unneeded chunks. 651 * 2. For indexed PNG images, the number of colors must not exceed 256. 652 * 3. For indexed PNG images with an alpha channel, the tRNS chunk must be preserved. 653 * 4. For indexed PNG images with true alpha transparency (an alpha channel > 1 bit), we need to avoid saving 654 * the image using ImageMagick's 'png8' format, because that supports only binary (1 bit) transparency. 655 * 656 * For #4 we want to check whether the image has a 1-bit alpha channel before resizing, because resizing 657 * may cause the number of alpha values to multiply due to antialiasing. If the original image had only a 658 * 1-bit alpha channel, then a 1-bit alpha channel should be good enough for the resized images. 659 * 660 * Perform all the necessary checks before resizing the image and store the results in variables for later use. 661 */ 662 $is_png = false; 663 $is_indexed_png = false; 664 $is_indexed_png_with_alpha_channel = false; 665 $is_indexed_png_with_true_alpha_transparency = false; 666 667 if ( 'image/png' === $this->mime_type ) { 668 $is_png = true; 669 670 if ( 671 is_callable( array( $this->image, 'getImageProperty' ) ) 672 && '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' ) 673 ) { 674 $is_indexed_png = true; 675 676 if ( 677 is_callable( array( $this->image, 'getImageAlphaChannel' ) ) 678 && $this->image->getImageAlphaChannel() 679 ) { 680 $is_indexed_png_with_alpha_channel = true; 681 682 if ( 683 is_callable( array( $this->image, 'getImageChannelDepth' ) ) 684 && defined( 'Imagick::CHANNEL_ALPHA' ) 685 && 1 < $this->image->getImageChannelDepth( Imagick::CHANNEL_ALPHA ) 686 ) { 687 $is_indexed_png_with_true_alpha_transparency = true; 688 } 689 } 690 } 691 } 692 693 /* 694 * To be more efficient, resample large images to 5x the destination size before resizing 695 * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111), 696 * unless we would be resampling to a scale smaller than 128x128. 697 */ 698 if ( is_callable( array( $this->image, 'sampleImage' ) ) ) { 699 $resize_ratio = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] ); 700 $sample_factor = 5; 701 702 if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) { 703 $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor ); 704 } 705 } 706 707 /* 708 * Use resizeImage() when it's available and a valid filter value is set. 709 * Otherwise, fall back to the scaleImage() method for resizing, which 710 * results in better image quality over resizeImage() with default filter 711 * settings and retains backward compatibility with pre 4.5 functionality. 712 */ 713 if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) { 714 $this->image->setOption( 'filter:support', '2.0' ); 715 $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 ); 716 } else { 717 $this->image->scaleImage( $dst_w, $dst_h ); 718 } 719 720 // Set appropriate quality settings after resizing. 721 if ( 'image/jpeg' === $this->mime_type ) { 722 if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) { 723 $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 ); 724 } 725 726 $this->image->setOption( 'jpeg:fancy-upsampling', 'off' ); 727 } 728 729 if ( $is_png ) { 730 $this->image->setOption( 'png:compression-filter', '5' ); 731 $this->image->setOption( 'png:compression-level', '9' ); 732 $this->image->setOption( 'png:compression-strategy', '1' ); 733 734 // Indexed PNG files get some additional handling. 735 // See #63448 for details. 736 if ( $is_indexed_png ) { 737 738 // Check for an alpha channel. 739 if ( $is_indexed_png_with_alpha_channel ) { 740 $this->image->setOption( 'png:include-chunk', 'tRNS' ); 741 } else { 742 $this->image->setOption( 'png:exclude-chunk', 'all' ); 743 } 744 745 $this->image->quantizeImage( 256, $this->image->getColorspace(), 0, false, false ); 746 747 /* 748 * If the colorspace is 'gray', use the png8 format to ensure it stays indexed. 749 * ImageMagick tends to save grayscale images as grayscale PNGs rather than indexed PNGs, 750 * even though grayscale PNGs usually have considerably larger file sizes. 751 * But we can force ImageMagick to save the image as an indexed PNG instead, 752 * by telling it to use png8 format. 753 * 754 * Note that we need to first call quantizeImage() before checking getImageColorspace(), 755 * because only after calling quantizeImage() will the colorspace be COLORSPACE_GRAY for grayscale images 756 * (and we have not found any other way to identify grayscale images). 757 * 758 * We need to avoid forcing indexed format for images with true alpha transparency, 759 * because ImageMagick does not support saving an image with true alpha transparency as an indexed PNG. 760 */ 761 if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() && ! $is_indexed_png_with_true_alpha_transparency ) { 762 // Set the image format to Indexed PNG. 763 $this->image->setOption( 'png:format', 'png8' ); 764 } 765 } else { 766 $this->image->setOption( 'png:exclude-chunk', 'all' ); 767 } 768 } 769 770 /* 771 * If alpha channel is not defined, set it opaque. 772 * 773 * Note that Imagick::getImageAlphaChannel() is only available if Imagick 774 * has been compiled against ImageMagick version 6.4.0 or newer. 775 */ 776 if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) ) 777 && is_callable( array( $this->image, 'setImageAlphaChannel' ) ) 778 && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' ) 779 && defined( 'Imagick::ALPHACHANNEL_OPAQUE' ) 780 ) { 781 if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) { 782 $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE ); 783 } 784 } 785 786 // Limit the bit depth of resized images. 787 if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) { 788 /** 789 * Filters the maximum bit depth of resized images. 790 * 791 * This filter only applies when resizing using the Imagick editor since GD 792 * does not support getting or setting bit depth. 793 * 794 * Use this to adjust the maximum bit depth of resized images. 795 * 796 * @since 6.8.0 797 * 798 * @param int $max_depth The maximum bit depth. Default is the input depth. 799 * @param int $image_depth The bit depth of the original image. 800 */ 801 $max_depth = apply_filters( 'image_max_bit_depth', $this->image->getImageDepth(), $this->image->getImageDepth() ); 802 $this->image->setImageDepth( $max_depth ); 803 } 804 } catch ( Exception $e ) { 805 return new WP_Error( 'image_resize_error', $e->getMessage() ); 806 } 807 } 808 809 /** 810 * Create multiple smaller images from a single source. 811 * 812 * Attempts to create all sub-sizes and returns the meta data at the end. This 813 * may result in the server running out of resources. When it fails there may be few 814 * "orphaned" images left over as the meta data is never returned and saved. 815 * 816 * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates 817 * the new images one at a time and allows for the meta data to be saved after 818 * each new image is created. 819 * 820 * @since 3.5.0 821 * 822 * @param array $sizes { 823 * An array of image size data arrays. 824 * 825 * Either a height or width must be provided. 826 * If one of the two is set to null, the resize will 827 * maintain aspect ratio according to the provided dimension. 828 * 829 * @type array ...$0 { 830 * Array of height, width values, and whether to crop. 831 * 832 * @type int $width Image width. Optional if `$height` is specified. 833 * @type int $height Image height. Optional if `$width` is specified. 834 * @type bool|array $crop Optional. Whether to crop the image. Default false. 835 * } 836 * } 837 * @return array An array of resized images' metadata by size. 838 */ 839 public function multi_resize( $sizes ) { 840 $metadata = array(); 841 842 foreach ( $sizes as $size => $size_data ) { 843 $meta = $this->make_subsize( $size_data ); 844 845 if ( ! is_wp_error( $meta ) ) { 846 $metadata[ $size ] = $meta; 847 } 848 } 849 850 return $metadata; 851 } 852 853 /** 854 * Create an image sub-size and return the image meta data value for it. 855 * 856 * @since 5.3.0 857 * 858 * @param array $size_data { 859 * Array of size data. 860 * 861 * @type int $width The maximum width in pixels. 862 * @type int $height The maximum height in pixels. 863 * @type bool|array $crop Whether to crop the image to exact dimensions. 864 * } 865 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta, 866 * WP_Error object on error. 867 */ 868 public function make_subsize( $size_data ) { 869 if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { 870 return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) ); 871 } 872 873 $orig_size = $this->size; 874 $orig_image = $this->image->getImage(); 875 876 if ( ! isset( $size_data['width'] ) ) { 877 $size_data['width'] = null; 878 } 879 880 if ( ! isset( $size_data['height'] ) ) { 881 $size_data['height'] = null; 882 } 883 884 if ( ! isset( $size_data['crop'] ) ) { 885 $size_data['crop'] = false; 886 } 887 888 if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) { 889 return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) ); 890 } 891 892 $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] ); 893 894 if ( is_wp_error( $resized ) ) { 895 $saved = $resized; 896 } else { 897 $saved = $this->_save( $this->image ); 898 899 $this->image->clear(); 900 $this->image->destroy(); 901 $this->image = null; 902 } 903 904 $this->size = $orig_size; 905 $this->image = $orig_image; 906 907 if ( ! is_wp_error( $saved ) ) { 908 unset( $saved['path'] ); 909 } 910 911 return $saved; 912 } 913 914 /** 915 * Crops Image. 916 * 917 * @since 3.5.0 918 * 919 * @param int $src_x The start x position to crop from. 920 * @param int $src_y The start y position to crop from. 921 * @param int $src_w The width to crop. 922 * @param int $src_h The height to crop. 923 * @param int $dst_w Optional. The destination width. 924 * @param int $dst_h Optional. The destination height. 925 * @param bool $src_abs Optional. If the source crop points are absolute. 926 * @return true|WP_Error 927 */ 928 public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { 929 if ( $src_abs ) { 930 $src_w -= $src_x; 931 $src_h -= $src_y; 932 } 933 934 try { 935 $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); 936 $this->image->setImagePage( $src_w, $src_h, 0, 0 ); 937 938 if ( $dst_w || $dst_h ) { 939 /* 940 * If destination width/height isn't specified, 941 * use same as width/height from source. 942 */ 943 if ( ! $dst_w ) { 944 $dst_w = $src_w; 945 } 946 if ( ! $dst_h ) { 947 $dst_h = $src_h; 948 } 949 950 $thumb_result = $this->thumbnail_image( $dst_w, $dst_h ); 951 if ( is_wp_error( $thumb_result ) ) { 952 return $thumb_result; 953 } 954 955 return $this->update_size(); 956 } 957 } catch ( Exception $e ) { 958 return new WP_Error( 'image_crop_error', $e->getMessage() ); 959 } 960 961 return $this->update_size(); 962 } 963 964 /** 965 * Rotates current image counter-clockwise by $angle. 966 * 967 * @since 3.5.0 968 * 969 * @param float $angle 970 * @return true|WP_Error 971 */ 972 public function rotate( $angle ) { 973 /** 974 * $angle is 360-$angle because Imagick rotates clockwise 975 * (GD rotates counter-clockwise) 976 */ 977 try { 978 $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle ); 979 980 // Normalize EXIF orientation data so that display is consistent across devices. 981 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 982 $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT ); 983 } 984 985 // Since this changes the dimensions of the image, update the size. 986 $result = $this->update_size(); 987 if ( is_wp_error( $result ) ) { 988 return $result; 989 } 990 991 $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 ); 992 } catch ( Exception $e ) { 993 return new WP_Error( 'image_rotate_error', $e->getMessage() ); 994 } 995 996 return true; 997 } 998 999 /** 1000 * Flips current image. 1001 * 1002 * @since 3.5.0 1003 * 1004 * @param bool $horz Flip along Horizontal Axis 1005 * @param bool $vert Flip along Vertical Axis 1006 * @return true|WP_Error 1007 */ 1008 public function flip( $horz, $vert ) { 1009 try { 1010 if ( $horz ) { 1011 $this->image->flipImage(); 1012 } 1013 1014 if ( $vert ) { 1015 $this->image->flopImage(); 1016 } 1017 1018 // Normalize EXIF orientation data so that display is consistent across devices. 1019 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 1020 $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT ); 1021 } 1022 } catch ( Exception $e ) { 1023 return new WP_Error( 'image_flip_error', $e->getMessage() ); 1024 } 1025 1026 return true; 1027 } 1028 1029 /** 1030 * Check if a JPEG image has EXIF Orientation tag and rotate it if needed. 1031 * 1032 * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only 1033 * if EXIF Orientation can be reset afterwards. 1034 * 1035 * @since 5.3.0 1036 * 1037 * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation. 1038 * WP_Error if error while rotating. 1039 */ 1040 public function maybe_exif_rotate() { 1041 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 1042 return parent::maybe_exif_rotate(); 1043 } else { 1044 return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) ); 1045 } 1046 } 1047 1048 /** 1049 * Saves current image to file. 1050 * 1051 * @since 3.5.0 1052 * @since 6.0.0 The `$filesize` value was added to the returned array. 1053 * 1054 * @param string $destfilename Optional. Destination filename. Default null. 1055 * @param string $mime_type Optional. The mime-type. Default null. 1056 * @return array|WP_Error { 1057 * Array on success or WP_Error if the file failed to save. 1058 * 1059 * @type string $path Path to the image file. 1060 * @type string $file Name of the image file. 1061 * @type int $width Image width. 1062 * @type int $height Image height. 1063 * @type string $mime-type The mime type of the image. 1064 * @type int $filesize File size of the image. 1065 * } 1066 */ 1067 public function save( $destfilename = null, $mime_type = null ) { 1068 $saved = $this->_save( $this->image, $destfilename, $mime_type ); 1069 1070 if ( ! is_wp_error( $saved ) ) { 1071 $this->file = $saved['path']; 1072 $this->mime_type = $saved['mime-type']; 1073 1074 try { 1075 $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) ); 1076 } catch ( Exception $e ) { 1077 return new WP_Error( 'image_save_error', $e->getMessage(), $this->file ); 1078 } 1079 } 1080 1081 return $saved; 1082 } 1083 1084 /** 1085 * Removes PDF alpha after it's been read. 1086 * 1087 * @since 6.4.0 1088 */ 1089 protected function remove_pdf_alpha_channel() { 1090 $version = Imagick::getVersion(); 1091 // Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5. 1092 if ( $version['versionNumber'] >= 0x675 ) { 1093 try { 1094 // Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2. 1095 $this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 ); 1096 } catch ( Exception $e ) { 1097 return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() ); 1098 } 1099 } 1100 } 1101 1102 /** 1103 * @since 3.5.0 1104 * @since 6.0.0 The `$filesize` value was added to the returned array. 1105 * 1106 * @param Imagick $image 1107 * @param string $filename 1108 * @param string $mime_type 1109 * @return array|WP_Error { 1110 * Array on success or WP_Error if the file failed to save. 1111 * 1112 * @type string $path Path to the image file. 1113 * @type string $file Name of the image file. 1114 * @type int $width Image width. 1115 * @type int $height Image height. 1116 * @type string $mime-type The mime type of the image. 1117 * @type int $filesize File size of the image. 1118 * } 1119 */ 1120 protected function _save( $image, $filename = null, $mime_type = null ) { 1121 list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type ); 1122 1123 if ( ! $filename ) { 1124 $filename = $this->generate_filename( null, null, $extension ); 1125 } 1126 1127 try { 1128 // Store initial format. 1129 $orig_format = $this->image->getImageFormat(); 1130 1131 $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); 1132 } catch ( Exception $e ) { 1133 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 1134 } 1135 1136 if ( method_exists( $this->image, 'setInterlaceScheme' ) 1137 && method_exists( $this->image, 'getInterlaceScheme' ) 1138 && defined( 'Imagick::INTERLACE_PLANE' ) 1139 ) { 1140 $orig_interlace = $this->image->getInterlaceScheme(); 1141 1142 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ 1143 if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) { 1144 $this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output. 1145 } else { 1146 $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output. 1147 } 1148 } 1149 1150 $write_image_result = $this->write_image( $this->image, $filename ); 1151 if ( is_wp_error( $write_image_result ) ) { 1152 return $write_image_result; 1153 } 1154 1155 try { 1156 // Reset original format. 1157 $this->image->setImageFormat( $orig_format ); 1158 1159 if ( isset( $orig_interlace ) ) { 1160 $this->image->setInterlaceScheme( $orig_interlace ); 1161 } 1162 } catch ( Exception $e ) { 1163 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 1164 } 1165 1166 // Set correct file permissions. 1167 $stat = stat( dirname( $filename ) ); 1168 $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits. 1169 chmod( $filename, $perms ); 1170 1171 return array( 1172 'path' => $filename, 1173 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ 1174 'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ), 1175 'width' => $this->size['width'], 1176 'height' => $this->size['height'], 1177 'mime-type' => $mime_type, 1178 'filesize' => wp_filesize( $filename ), 1179 ); 1180 } 1181 1182 /** 1183 * Writes an image to a file or stream. 1184 * 1185 * @since 5.6.0 1186 * 1187 * @param Imagick $image 1188 * @param string $filename The destination filename or stream URL. 1189 * @return true|WP_Error 1190 */ 1191 private function write_image( $image, $filename ) { 1192 if ( wp_is_stream( $filename ) ) { 1193 /* 1194 * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead. 1195 * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php 1196 */ 1197 if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) { 1198 return new WP_Error( 1199 'image_save_error', 1200 sprintf( 1201 /* translators: %s: PHP function name. */ 1202 __( '%s failed while writing image to stream.' ), 1203 '<code>file_put_contents()</code>' 1204 ), 1205 $filename 1206 ); 1207 } else { 1208 return true; 1209 } 1210 } else { 1211 $dirname = dirname( $filename ); 1212 1213 if ( ! wp_mkdir_p( $dirname ) ) { 1214 return new WP_Error( 1215 'image_save_error', 1216 sprintf( 1217 /* translators: %s: Directory path. */ 1218 __( 'Unable to create directory %s. Is its parent directory writable by the server?' ), 1219 esc_html( $dirname ) 1220 ) 1221 ); 1222 } 1223 1224 try { 1225 return $image->writeImage( $filename ); 1226 } catch ( Exception $e ) { 1227 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 1228 } 1229 } 1230 } 1231 1232 /** 1233 * Streams current image to browser. 1234 * 1235 * @since 3.5.0 1236 * 1237 * @param string $mime_type The mime type of the image. 1238 * @return true|WP_Error True on success, WP_Error object on failure. 1239 */ 1240 public function stream( $mime_type = null ) { 1241 list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type ); 1242 1243 try { 1244 // Temporarily change format for stream. 1245 $this->image->setImageFormat( strtoupper( $extension ) ); 1246 1247 // Output stream of image content. 1248 header( "Content-Type: $mime_type" ); 1249 print $this->image->getImageBlob(); 1250 1251 // Reset image to original format. 1252 $this->image->setImageFormat( $this->get_extension( $this->mime_type ) ); 1253 } catch ( Exception $e ) { 1254 return new WP_Error( 'image_stream_error', $e->getMessage() ); 1255 } 1256 1257 return true; 1258 } 1259 1260 /** 1261 * Strips all image meta except color profiles from an image. 1262 * 1263 * @since 4.5.0 1264 * 1265 * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error. 1266 */ 1267 protected function strip_meta() { 1268 1269 if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) { 1270 return new WP_Error( 1271 'image_strip_meta_error', 1272 sprintf( 1273 /* translators: %s: ImageMagick method name. */ 1274 __( '%s is required to strip image meta.' ), 1275 '<code>Imagick::getImageProfiles()</code>' 1276 ) 1277 ); 1278 } 1279 1280 if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) { 1281 return new WP_Error( 1282 'image_strip_meta_error', 1283 sprintf( 1284 /* translators: %s: ImageMagick method name. */ 1285 __( '%s is required to strip image meta.' ), 1286 '<code>Imagick::removeImageProfile()</code>' 1287 ) 1288 ); 1289 } 1290 1291 /* 1292 * Protect a few profiles from being stripped for the following reasons: 1293 * 1294 * - icc: Color profile information 1295 * - icm: Color profile information 1296 * - iptc: Copyright data 1297 * - exif: Orientation data 1298 * - xmp: Rights usage data 1299 */ 1300 $protected_profiles = array( 1301 'icc', 1302 'icm', 1303 'iptc', 1304 'exif', 1305 'xmp', 1306 ); 1307 1308 try { 1309 // Strip profiles. 1310 foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) { 1311 if ( ! in_array( $key, $protected_profiles, true ) ) { 1312 $this->image->removeImageProfile( $key ); 1313 } 1314 } 1315 } catch ( Exception $e ) { 1316 return new WP_Error( 'image_strip_meta_error', $e->getMessage() ); 1317 } 1318 1319 return true; 1320 } 1321 1322 /** 1323 * Sets up Imagick for PDF processing. 1324 * Increases rendering DPI and only loads first page. 1325 * 1326 * @since 4.7.0 1327 * 1328 * @return string|WP_Error File to load or WP_Error on failure. 1329 */ 1330 protected function pdf_setup() { 1331 try { 1332 /* 1333 * By default, PDFs are rendered in a very low resolution. 1334 * We want the thumbnail to be readable, so increase the rendering DPI. 1335 */ 1336 $this->image->setResolution( 128, 128 ); 1337 1338 // Only load the first page. 1339 return $this->file . '[0]'; 1340 } catch ( Exception $e ) { 1341 return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file ); 1342 } 1343 } 1344 1345 /** 1346 * Load the image produced by Ghostscript. 1347 * 1348 * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files 1349 * when `use-cropbox` is set. 1350 * 1351 * @since 5.6.0 1352 * 1353 * @return true|WP_Error 1354 */ 1355 protected function pdf_load_source() { 1356 $filename = $this->pdf_setup(); 1357 1358 if ( is_wp_error( $filename ) ) { 1359 return $filename; 1360 } 1361 1362 foreach ( array( 'true', 'false' ) as $use_cropbox ) { 1363 try { 1364 /** 1365 * When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped 1366 * area (resulting in unnecessary whitespace) unless the following option is set. 1367 * 1368 * However, it sometimes fails, so if that happens, run it without the option. 1369 * 1370 * @ticket 48853 1371 */ 1372 $this->image->setOption( 'pdf:use-cropbox', $use_cropbox ); 1373 1374 /* 1375 * Reading image after Imagick instantiation because `setResolution` 1376 * only applies correctly before the image is read. 1377 */ 1378 if ( is_string( $this->stream_file_data ) ) { 1379 $this->image->setFilename( 'PDF:unknown.pdf[0]' ); 1380 $this->image->readImageBlob( $this->stream_file_data, $this->image_given_name ); 1381 } else { 1382 $this->image->readImage( $filename ); 1383 } 1384 1385 return true; 1386 } catch ( Exception $e ) { 1387 continue; 1388 } 1389 } 1390 1391 return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file ); 1392 } 1393 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Aug 26 08:20:24 2026 | Cross-referenced by PHPXref |