image.php (36989B)
1 <?php 2 /** 3 * File contains all the administration image manipulation functions. 4 * 5 * @package WordPress 6 * @subpackage Administration 7 */ 8 9 /** 10 * Crops an image to a given size. 11 * 12 * @since 2.1.0 13 * 14 * @param string|int $src The source file or Attachment ID. 15 * @param int $src_x The start x position to crop from. 16 * @param int $src_y The start y position to crop from. 17 * @param int $src_w The width to crop. 18 * @param int $src_h The height to crop. 19 * @param int $dst_w The destination width. 20 * @param int $dst_h The destination height. 21 * @param bool|false $src_abs Optional. If the source crop points are absolute. 22 * @param string|false $dst_file Optional. The destination file to write to. 23 * @return string|WP_Error New filepath on success, WP_Error on failure. 24 */ 25 function wp_crop_image( $src, $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs = false, $dst_file = false ) { 26 $src_file = $src; 27 if ( is_numeric( $src ) ) { // Handle int as attachment ID. 28 $src_file = get_attached_file( $src ); 29 30 if ( ! file_exists( $src_file ) ) { 31 // If the file doesn't exist, attempt a URL fopen on the src link. 32 // This can occur with certain file replication plugins. 33 $src = _load_image_to_edit_path( $src, 'full' ); 34 } else { 35 $src = $src_file; 36 } 37 } 38 39 $editor = wp_get_image_editor( $src ); 40 if ( is_wp_error( $editor ) ) { 41 return $editor; 42 } 43 44 $src = $editor->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs ); 45 if ( is_wp_error( $src ) ) { 46 return $src; 47 } 48 49 if ( ! $dst_file ) { 50 $dst_file = str_replace( wp_basename( $src_file ), 'cropped-' . wp_basename( $src_file ), $src_file ); 51 } 52 53 /* 54 * The directory containing the original file may no longer exist when 55 * using a replication plugin. 56 */ 57 wp_mkdir_p( dirname( $dst_file ) ); 58 59 $dst_file = dirname( $dst_file ) . '/' . wp_unique_filename( dirname( $dst_file ), wp_basename( $dst_file ) ); 60 61 $result = $editor->save( $dst_file ); 62 if ( is_wp_error( $result ) ) { 63 return $result; 64 } 65 66 return $dst_file; 67 } 68 69 /** 70 * Compare the existing image sub-sizes (as saved in the attachment meta) 71 * to the currently registered image sub-sizes, and return the difference. 72 * 73 * Registered sub-sizes that are larger than the image are skipped. 74 * 75 * @since 5.3.0 76 * 77 * @param int $attachment_id The image attachment post ID. 78 * @return array An array of the image sub-sizes that are currently defined but don't exist for this image. 79 */ 80 function wp_get_missing_image_subsizes( $attachment_id ) { 81 if ( ! wp_attachment_is_image( $attachment_id ) ) { 82 return array(); 83 } 84 85 $registered_sizes = wp_get_registered_image_subsizes(); 86 $image_meta = wp_get_attachment_metadata( $attachment_id ); 87 88 // Meta error? 89 if ( empty( $image_meta ) ) { 90 return $registered_sizes; 91 } 92 93 // Use the originally uploaded image dimensions as full_width and full_height. 94 if ( ! empty( $image_meta['original_image'] ) ) { 95 $image_file = wp_get_original_image_path( $attachment_id ); 96 $imagesize = wp_getimagesize( $image_file ); 97 } 98 99 if ( ! empty( $imagesize ) ) { 100 $full_width = $imagesize[0]; 101 $full_height = $imagesize[1]; 102 } else { 103 $full_width = (int) $image_meta['width']; 104 $full_height = (int) $image_meta['height']; 105 } 106 107 $possible_sizes = array(); 108 109 // Skip registered sizes that are too large for the uploaded image. 110 foreach ( $registered_sizes as $size_name => $size_data ) { 111 if ( image_resize_dimensions( $full_width, $full_height, $size_data['width'], $size_data['height'], $size_data['crop'] ) ) { 112 $possible_sizes[ $size_name ] = $size_data; 113 } 114 } 115 116 if ( empty( $image_meta['sizes'] ) ) { 117 $image_meta['sizes'] = array(); 118 } 119 120 /* 121 * Remove sizes that already exist. Only checks for matching "size names". 122 * It is possible that the dimensions for a particular size name have changed. 123 * For example the user has changed the values on the Settings -> Media screen. 124 * However we keep the old sub-sizes with the previous dimensions 125 * as the image may have been used in an older post. 126 */ 127 $missing_sizes = array_diff_key( $possible_sizes, $image_meta['sizes'] ); 128 129 /** 130 * Filters the array of missing image sub-sizes for an uploaded image. 131 * 132 * @since 5.3.0 133 * 134 * @param array $missing_sizes Array with the missing image sub-sizes. 135 * @param array $image_meta The image meta data. 136 * @param int $attachment_id The image attachment post ID. 137 */ 138 return apply_filters( 'wp_get_missing_image_subsizes', $missing_sizes, $image_meta, $attachment_id ); 139 } 140 141 /** 142 * If any of the currently registered image sub-sizes are missing, 143 * create them and update the image meta data. 144 * 145 * @since 5.3.0 146 * 147 * @param int $attachment_id The image attachment post ID. 148 * @return array|WP_Error The updated image meta data array or WP_Error object 149 * if both the image meta and the attached file are missing. 150 */ 151 function wp_update_image_subsizes( $attachment_id ) { 152 $image_meta = wp_get_attachment_metadata( $attachment_id ); 153 $image_file = wp_get_original_image_path( $attachment_id ); 154 155 if ( empty( $image_meta ) || ! is_array( $image_meta ) ) { 156 // Previously failed upload? 157 // If there is an uploaded file, make all sub-sizes and generate all of the attachment meta. 158 if ( ! empty( $image_file ) ) { 159 $image_meta = wp_create_image_subsizes( $image_file, $attachment_id ); 160 } else { 161 return new WP_Error( 'invalid_attachment', __( 'The attached file cannot be found.' ) ); 162 } 163 } else { 164 $missing_sizes = wp_get_missing_image_subsizes( $attachment_id ); 165 166 if ( empty( $missing_sizes ) ) { 167 return $image_meta; 168 } 169 170 // This also updates the image meta. 171 $image_meta = _wp_make_subsizes( $missing_sizes, $image_file, $image_meta, $attachment_id ); 172 } 173 174 /** This filter is documented in wp-admin/includes/image.php */ 175 $image_meta = apply_filters( 'wp_generate_attachment_metadata', $image_meta, $attachment_id, 'update' ); 176 177 // Save the updated metadata. 178 wp_update_attachment_metadata( $attachment_id, $image_meta ); 179 180 return $image_meta; 181 } 182 183 /** 184 * Updates the attached file and image meta data when the original image was edited. 185 * 186 * @since 5.3.0 187 * @access private 188 * 189 * @param array $saved_data The data returned from WP_Image_Editor after successfully saving an image. 190 * @param string $original_file Path to the original file. 191 * @param array $image_meta The image meta data. 192 * @param int $attachment_id The attachment post ID. 193 * @return array The updated image meta data. 194 */ 195 function _wp_image_meta_replace_original( $saved_data, $original_file, $image_meta, $attachment_id ) { 196 $new_file = $saved_data['path']; 197 198 // Update the attached file meta. 199 update_attached_file( $attachment_id, $new_file ); 200 201 // Width and height of the new image. 202 $image_meta['width'] = $saved_data['width']; 203 $image_meta['height'] = $saved_data['height']; 204 205 // Make the file path relative to the upload dir. 206 $image_meta['file'] = _wp_relative_upload_path( $new_file ); 207 208 // Store the original image file name in image_meta. 209 $image_meta['original_image'] = wp_basename( $original_file ); 210 211 return $image_meta; 212 } 213 214 /** 215 * Creates image sub-sizes, adds the new data to the image meta `sizes` array, and updates the image metadata. 216 * 217 * Intended for use after an image is uploaded. Saves/updates the image metadata after each 218 * sub-size is created. If there was an error, it is added to the returned image metadata array. 219 * 220 * @since 5.3.0 221 * 222 * @param string $file Full path to the image file. 223 * @param int $attachment_id Attachment Id to process. 224 * @return array The image attachment meta data. 225 */ 226 function wp_create_image_subsizes( $file, $attachment_id ) { 227 $imagesize = wp_getimagesize( $file ); 228 229 if ( empty( $imagesize ) ) { 230 // File is not an image. 231 return array(); 232 } 233 234 // Default image meta. 235 $image_meta = array( 236 'width' => $imagesize[0], 237 'height' => $imagesize[1], 238 'file' => _wp_relative_upload_path( $file ), 239 'sizes' => array(), 240 ); 241 242 // Fetch additional metadata from EXIF/IPTC. 243 $exif_meta = wp_read_image_metadata( $file ); 244 245 if ( $exif_meta ) { 246 $image_meta['image_meta'] = $exif_meta; 247 } 248 249 // Do not scale (large) PNG images. May result in sub-sizes that have greater file size than the original. See #48736. 250 if ( 'image/png' !== $imagesize['mime'] ) { 251 252 /** 253 * Filters the "BIG image" threshold value. 254 * 255 * If the original image width or height is above the threshold, it will be scaled down. The threshold is 256 * used as max width and max height. The scaled down image will be used as the largest available size, including 257 * the `_wp_attached_file` post meta value. 258 * 259 * Returning `false` from the filter callback will disable the scaling. 260 * 261 * @since 5.3.0 262 * 263 * @param int $threshold The threshold value in pixels. Default 2560. 264 * @param array $imagesize { 265 * Indexed array of the image width and height in pixels. 266 * 267 * @type int $0 The image width. 268 * @type int $1 The image height. 269 * } 270 * @param string $file Full path to the uploaded image file. 271 * @param int $attachment_id Attachment post ID. 272 */ 273 $threshold = (int) apply_filters( 'big_image_size_threshold', 2560, $imagesize, $file, $attachment_id ); 274 275 // If the original image's dimensions are over the threshold, 276 // scale the image and use it as the "full" size. 277 if ( $threshold && ( $image_meta['width'] > $threshold || $image_meta['height'] > $threshold ) ) { 278 $editor = wp_get_image_editor( $file ); 279 280 if ( is_wp_error( $editor ) ) { 281 // This image cannot be edited. 282 return $image_meta; 283 } 284 285 // Resize the image. 286 $resized = $editor->resize( $threshold, $threshold ); 287 $rotated = null; 288 289 // If there is EXIF data, rotate according to EXIF Orientation. 290 if ( ! is_wp_error( $resized ) && is_array( $exif_meta ) ) { 291 $resized = $editor->maybe_exif_rotate(); 292 $rotated = $resized; 293 } 294 295 if ( ! is_wp_error( $resized ) ) { 296 // Append "-scaled" to the image file name. It will look like "my_image-scaled.jpg". 297 // This doesn't affect the sub-sizes names as they are generated from the original image (for best quality). 298 $saved = $editor->save( $editor->generate_filename( 'scaled' ) ); 299 300 if ( ! is_wp_error( $saved ) ) { 301 $image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id ); 302 303 // If the image was rotated update the stored EXIF data. 304 if ( true === $rotated && ! empty( $image_meta['image_meta']['orientation'] ) ) { 305 $image_meta['image_meta']['orientation'] = 1; 306 } 307 } else { 308 // TODO: Log errors. 309 } 310 } else { 311 // TODO: Log errors. 312 } 313 } elseif ( ! empty( $exif_meta['orientation'] ) && 1 !== (int) $exif_meta['orientation'] ) { 314 // Rotate the whole original image if there is EXIF data and "orientation" is not 1. 315 316 $editor = wp_get_image_editor( $file ); 317 318 if ( is_wp_error( $editor ) ) { 319 // This image cannot be edited. 320 return $image_meta; 321 } 322 323 // Rotate the image. 324 $rotated = $editor->maybe_exif_rotate(); 325 326 if ( true === $rotated ) { 327 // Append `-rotated` to the image file name. 328 $saved = $editor->save( $editor->generate_filename( 'rotated' ) ); 329 330 if ( ! is_wp_error( $saved ) ) { 331 $image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id ); 332 333 // Update the stored EXIF data. 334 if ( ! empty( $image_meta['image_meta']['orientation'] ) ) { 335 $image_meta['image_meta']['orientation'] = 1; 336 } 337 } else { 338 // TODO: Log errors. 339 } 340 } 341 } 342 } 343 344 /* 345 * Initial save of the new metadata. 346 * At this point the file was uploaded and moved to the uploads directory 347 * but the image sub-sizes haven't been created yet and the `sizes` array is empty. 348 */ 349 wp_update_attachment_metadata( $attachment_id, $image_meta ); 350 351 $new_sizes = wp_get_registered_image_subsizes(); 352 353 /** 354 * Filters the image sizes automatically generated when uploading an image. 355 * 356 * @since 2.9.0 357 * @since 4.4.0 Added the `$image_meta` argument. 358 * @since 5.3.0 Added the `$attachment_id` argument. 359 * 360 * @param array $new_sizes Associative array of image sizes to be created. 361 * @param array $image_meta The image meta data: width, height, file, sizes, etc. 362 * @param int $attachment_id The attachment post ID for the image. 363 */ 364 $new_sizes = apply_filters( 'intermediate_image_sizes_advanced', $new_sizes, $image_meta, $attachment_id ); 365 366 return _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ); 367 } 368 369 /** 370 * Low-level function to create image sub-sizes. 371 * 372 * Updates the image meta after each sub-size is created. 373 * Errors are stored in the returned image metadata array. 374 * 375 * @since 5.3.0 376 * @access private 377 * 378 * @param array $new_sizes Array defining what sizes to create. 379 * @param string $file Full path to the image file. 380 * @param array $image_meta The attachment meta data array. 381 * @param int $attachment_id Attachment Id to process. 382 * @return array The attachment meta data with updated `sizes` array. Includes an array of errors encountered while resizing. 383 */ 384 function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) { 385 if ( empty( $image_meta ) || ! is_array( $image_meta ) ) { 386 // Not an image attachment. 387 return array(); 388 } 389 390 // Check if any of the new sizes already exist. 391 if ( isset( $image_meta['sizes'] ) && is_array( $image_meta['sizes'] ) ) { 392 foreach ( $image_meta['sizes'] as $size_name => $size_meta ) { 393 /* 394 * Only checks "size name" so we don't override existing images even if the dimensions 395 * don't match the currently defined size with the same name. 396 * To change the behavior, unset changed/mismatched sizes in the `sizes` array in image meta. 397 */ 398 if ( array_key_exists( $size_name, $new_sizes ) ) { 399 unset( $new_sizes[ $size_name ] ); 400 } 401 } 402 } else { 403 $image_meta['sizes'] = array(); 404 } 405 406 if ( empty( $new_sizes ) ) { 407 // Nothing to do... 408 return $image_meta; 409 } 410 411 /* 412 * Sort the image sub-sizes in order of priority when creating them. 413 * This ensures there is an appropriate sub-size the user can access immediately 414 * even when there was an error and not all sub-sizes were created. 415 */ 416 $priority = array( 417 'medium' => null, 418 'large' => null, 419 'thumbnail' => null, 420 'medium_large' => null, 421 ); 422 423 $new_sizes = array_filter( array_merge( $priority, $new_sizes ) ); 424 425 $editor = wp_get_image_editor( $file ); 426 427 if ( is_wp_error( $editor ) ) { 428 // The image cannot be edited. 429 return $image_meta; 430 } 431 432 // If stored EXIF data exists, rotate the source image before creating sub-sizes. 433 if ( ! empty( $image_meta['image_meta'] ) ) { 434 $rotated = $editor->maybe_exif_rotate(); 435 436 if ( is_wp_error( $rotated ) ) { 437 // TODO: Log errors. 438 } 439 } 440 441 if ( method_exists( $editor, 'make_subsize' ) ) { 442 foreach ( $new_sizes as $new_size_name => $new_size_data ) { 443 $new_size_meta = $editor->make_subsize( $new_size_data ); 444 445 if ( is_wp_error( $new_size_meta ) ) { 446 // TODO: Log errors. 447 } else { 448 // Save the size meta value. 449 $image_meta['sizes'][ $new_size_name ] = $new_size_meta; 450 wp_update_attachment_metadata( $attachment_id, $image_meta ); 451 } 452 } 453 } else { 454 // Fall back to `$editor->multi_resize()`. 455 $created_sizes = $editor->multi_resize( $new_sizes ); 456 457 if ( ! empty( $created_sizes ) ) { 458 $image_meta['sizes'] = array_merge( $image_meta['sizes'], $created_sizes ); 459 wp_update_attachment_metadata( $attachment_id, $image_meta ); 460 } 461 } 462 463 return $image_meta; 464 } 465 466 /** 467 * Generate attachment meta data and create image sub-sizes for images. 468 * 469 * @since 2.1.0 470 * 471 * @param int $attachment_id Attachment Id to process. 472 * @param string $file Filepath of the Attached image. 473 * @return array Metadata for attachment. 474 */ 475 function wp_generate_attachment_metadata( $attachment_id, $file ) { 476 $attachment = get_post( $attachment_id ); 477 478 $metadata = array(); 479 $support = false; 480 $mime_type = get_post_mime_type( $attachment ); 481 482 if ( preg_match( '!^image/!', $mime_type ) && file_is_displayable_image( $file ) ) { 483 // Make thumbnails and other intermediate sizes. 484 $metadata = wp_create_image_subsizes( $file, $attachment_id ); 485 } elseif ( wp_attachment_is( 'video', $attachment ) ) { 486 $metadata = wp_read_video_metadata( $file ); 487 $support = current_theme_supports( 'post-thumbnails', 'attachment:video' ) || post_type_supports( 'attachment:video', 'thumbnail' ); 488 } elseif ( wp_attachment_is( 'audio', $attachment ) ) { 489 $metadata = wp_read_audio_metadata( $file ); 490 $support = current_theme_supports( 'post-thumbnails', 'attachment:audio' ) || post_type_supports( 'attachment:audio', 'thumbnail' ); 491 } 492 493 /* 494 * wp_read_video_metadata() and wp_read_audio_metadata() return `false` 495 * if the attachment does not exist in the local filesystem, 496 * so make sure to convert the value to an array. 497 */ 498 if ( ! is_array( $metadata ) ) { 499 $metadata = array(); 500 } 501 502 if ( $support && ! empty( $metadata['image']['data'] ) ) { 503 // Check for existing cover. 504 $hash = md5( $metadata['image']['data'] ); 505 $posts = get_posts( 506 array( 507 'fields' => 'ids', 508 'post_type' => 'attachment', 509 'post_mime_type' => $metadata['image']['mime'], 510 'post_status' => 'inherit', 511 'posts_per_page' => 1, 512 'meta_key' => '_cover_hash', 513 'meta_value' => $hash, 514 ) 515 ); 516 $exists = reset( $posts ); 517 518 if ( ! empty( $exists ) ) { 519 update_post_meta( $attachment_id, '_thumbnail_id', $exists ); 520 } else { 521 $ext = '.jpg'; 522 switch ( $metadata['image']['mime'] ) { 523 case 'image/gif': 524 $ext = '.gif'; 525 break; 526 case 'image/png': 527 $ext = '.png'; 528 break; 529 case 'image/webp': 530 $ext = '.webp'; 531 break; 532 } 533 $basename = str_replace( '.', '-', wp_basename( $file ) ) . '-image' . $ext; 534 $uploaded = wp_upload_bits( $basename, '', $metadata['image']['data'] ); 535 if ( false === $uploaded['error'] ) { 536 $image_attachment = array( 537 'post_mime_type' => $metadata['image']['mime'], 538 'post_type' => 'attachment', 539 'post_content' => '', 540 ); 541 /** 542 * Filters the parameters for the attachment thumbnail creation. 543 * 544 * @since 3.9.0 545 * 546 * @param array $image_attachment An array of parameters to create the thumbnail. 547 * @param array $metadata Current attachment metadata. 548 * @param array $uploaded { 549 * Information about the newly-uploaded file. 550 * 551 * @type string $file Filename of the newly-uploaded file. 552 * @type string $url URL of the uploaded file. 553 * @type string $type File type. 554 * } 555 */ 556 $image_attachment = apply_filters( 'attachment_thumbnail_args', $image_attachment, $metadata, $uploaded ); 557 558 $sub_attachment_id = wp_insert_attachment( $image_attachment, $uploaded['file'] ); 559 add_post_meta( $sub_attachment_id, '_cover_hash', $hash ); 560 $attach_data = wp_generate_attachment_metadata( $sub_attachment_id, $uploaded['file'] ); 561 wp_update_attachment_metadata( $sub_attachment_id, $attach_data ); 562 update_post_meta( $attachment_id, '_thumbnail_id', $sub_attachment_id ); 563 } 564 } 565 } elseif ( 'application/pdf' === $mime_type ) { 566 // Try to create image thumbnails for PDFs. 567 568 $fallback_sizes = array( 569 'thumbnail', 570 'medium', 571 'large', 572 ); 573 574 /** 575 * Filters the image sizes generated for non-image mime types. 576 * 577 * @since 4.7.0 578 * 579 * @param string[] $fallback_sizes An array of image size names. 580 * @param array $metadata Current attachment metadata. 581 */ 582 $fallback_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata ); 583 584 $registered_sizes = wp_get_registered_image_subsizes(); 585 $merged_sizes = array_intersect_key( $registered_sizes, array_flip( $fallback_sizes ) ); 586 587 // Force thumbnails to be soft crops. 588 if ( isset( $merged_sizes['thumbnail'] ) && is_array( $merged_sizes['thumbnail'] ) ) { 589 $merged_sizes['thumbnail']['crop'] = false; 590 } 591 592 // Only load PDFs in an image editor if we're processing sizes. 593 if ( ! empty( $merged_sizes ) ) { 594 $editor = wp_get_image_editor( $file ); 595 596 if ( ! is_wp_error( $editor ) ) { // No support for this type of file. 597 /* 598 * PDFs may have the same file filename as JPEGs. 599 * Ensure the PDF preview image does not overwrite any JPEG images that already exist. 600 */ 601 $dirname = dirname( $file ) . '/'; 602 $ext = '.' . pathinfo( $file, PATHINFO_EXTENSION ); 603 $preview_file = $dirname . wp_unique_filename( $dirname, wp_basename( $file, $ext ) . '-pdf.jpg' ); 604 605 $uploaded = $editor->save( $preview_file, 'image/jpeg' ); 606 unset( $editor ); 607 608 // Resize based on the full size image, rather than the source. 609 if ( ! is_wp_error( $uploaded ) ) { 610 $image_file = $uploaded['path']; 611 unset( $uploaded['path'] ); 612 613 $metadata['sizes'] = array( 614 'full' => $uploaded, 615 ); 616 617 // Save the meta data before any image post-processing errors could happen. 618 wp_update_attachment_metadata( $attachment_id, $metadata ); 619 620 // Create sub-sizes saving the image meta after each. 621 $metadata = _wp_make_subsizes( $merged_sizes, $image_file, $metadata, $attachment_id ); 622 } 623 } 624 } 625 } 626 627 // Remove the blob of binary data from the array. 628 unset( $metadata['image']['data'] ); 629 630 /** 631 * Filters the generated attachment meta data. 632 * 633 * @since 2.1.0 634 * @since 5.3.0 The `$context` parameter was added. 635 * 636 * @param array $metadata An array of attachment meta data. 637 * @param int $attachment_id Current attachment ID. 638 * @param string $context Additional context. Can be 'create' when metadata was initially created for new attachment 639 * or 'update' when the metadata was updated. 640 */ 641 return apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'create' ); 642 } 643 644 /** 645 * Convert a fraction string to a decimal. 646 * 647 * @since 2.5.0 648 * 649 * @param string $str 650 * @return int|float 651 */ 652 function wp_exif_frac2dec( $str ) { 653 if ( false === strpos( $str, '/' ) ) { 654 return $str; 655 } 656 657 list( $numerator, $denominator ) = explode( '/', $str ); 658 if ( ! empty( $denominator ) ) { 659 return $numerator / $denominator; 660 } 661 return $str; 662 } 663 664 /** 665 * Convert the exif date format to a unix timestamp. 666 * 667 * @since 2.5.0 668 * 669 * @param string $str 670 * @return int 671 */ 672 function wp_exif_date2ts( $str ) { 673 list( $date, $time ) = explode( ' ', trim( $str ) ); 674 list( $y, $m, $d ) = explode( ':', $date ); 675 676 return strtotime( "{$y}-{$m}-{$d} {$time}" ); 677 } 678 679 /** 680 * Get extended image metadata, exif or iptc as available. 681 * 682 * Retrieves the EXIF metadata aperture, credit, camera, caption, copyright, iso 683 * created_timestamp, focal_length, shutter_speed, and title. 684 * 685 * The IPTC metadata that is retrieved is APP13, credit, byline, created date 686 * and time, caption, copyright, and title. Also includes FNumber, Model, 687 * DateTimeDigitized, FocalLength, ISOSpeedRatings, and ExposureTime. 688 * 689 * @todo Try other exif libraries if available. 690 * @since 2.5.0 691 * 692 * @param string $file 693 * @return array|false Image metadata array on success, false on failure. 694 */ 695 function wp_read_image_metadata( $file ) { 696 if ( ! file_exists( $file ) ) { 697 return false; 698 } 699 700 list( , , $image_type ) = wp_getimagesize( $file ); 701 702 /* 703 * EXIF contains a bunch of data we'll probably never need formatted in ways 704 * that are difficult to use. We'll normalize it and just extract the fields 705 * that are likely to be useful. Fractions and numbers are converted to 706 * floats, dates to unix timestamps, and everything else to strings. 707 */ 708 $meta = array( 709 'aperture' => 0, 710 'credit' => '', 711 'camera' => '', 712 'caption' => '', 713 'created_timestamp' => 0, 714 'copyright' => '', 715 'focal_length' => 0, 716 'iso' => 0, 717 'shutter_speed' => 0, 718 'title' => '', 719 'orientation' => 0, 720 'keywords' => array(), 721 ); 722 723 $iptc = array(); 724 $info = array(); 725 /* 726 * Read IPTC first, since it might contain data not available in exif such 727 * as caption, description etc. 728 */ 729 if ( is_callable( 'iptcparse' ) ) { 730 wp_getimagesize( $file, $info ); 731 732 if ( ! empty( $info['APP13'] ) ) { 733 // Don't silence errors when in debug mode, unless running unit tests. 734 if ( defined( 'WP_DEBUG' ) && WP_DEBUG 735 && ! defined( 'WP_RUN_CORE_TESTS' ) 736 ) { 737 $iptc = iptcparse( $info['APP13'] ); 738 } else { 739 // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silencing notice and warning is intentional. See https://core.trac.wordpress.org/ticket/42480 740 $iptc = @iptcparse( $info['APP13'] ); 741 } 742 743 // Headline, "A brief synopsis of the caption". 744 if ( ! empty( $iptc['2#105'][0] ) ) { 745 $meta['title'] = trim( $iptc['2#105'][0] ); 746 /* 747 * Title, "Many use the Title field to store the filename of the image, 748 * though the field may be used in many ways". 749 */ 750 } elseif ( ! empty( $iptc['2#005'][0] ) ) { 751 $meta['title'] = trim( $iptc['2#005'][0] ); 752 } 753 754 if ( ! empty( $iptc['2#120'][0] ) ) { // Description / legacy caption. 755 $caption = trim( $iptc['2#120'][0] ); 756 757 mbstring_binary_safe_encoding(); 758 $caption_length = strlen( $caption ); 759 reset_mbstring_encoding(); 760 761 if ( empty( $meta['title'] ) && $caption_length < 80 ) { 762 // Assume the title is stored in 2:120 if it's short. 763 $meta['title'] = $caption; 764 } 765 766 $meta['caption'] = $caption; 767 } 768 769 if ( ! empty( $iptc['2#110'][0] ) ) { // Credit. 770 $meta['credit'] = trim( $iptc['2#110'][0] ); 771 } elseif ( ! empty( $iptc['2#080'][0] ) ) { // Creator / legacy byline. 772 $meta['credit'] = trim( $iptc['2#080'][0] ); 773 } 774 775 if ( ! empty( $iptc['2#055'][0] ) && ! empty( $iptc['2#060'][0] ) ) { // Created date and time. 776 $meta['created_timestamp'] = strtotime( $iptc['2#055'][0] . ' ' . $iptc['2#060'][0] ); 777 } 778 779 if ( ! empty( $iptc['2#116'][0] ) ) { // Copyright. 780 $meta['copyright'] = trim( $iptc['2#116'][0] ); 781 } 782 783 if ( ! empty( $iptc['2#025'][0] ) ) { // Keywords array. 784 $meta['keywords'] = array_values( $iptc['2#025'] ); 785 } 786 } 787 } 788 789 $exif = array(); 790 791 /** 792 * Filters the image types to check for exif data. 793 * 794 * @since 2.5.0 795 * 796 * @param array $image_types Image types to check for exif data. 797 */ 798 $exif_image_types = apply_filters( 'wp_read_image_metadata_types', array( IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM ) ); 799 800 if ( is_callable( 'exif_read_data' ) && in_array( $image_type, $exif_image_types, true ) ) { 801 // Don't silence errors when in debug mode, unless running unit tests. 802 if ( defined( 'WP_DEBUG' ) && WP_DEBUG 803 && ! defined( 'WP_RUN_CORE_TESTS' ) 804 ) { 805 $exif = exif_read_data( $file ); 806 } else { 807 // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silencing notice and warning is intentional. See https://core.trac.wordpress.org/ticket/42480 808 $exif = @exif_read_data( $file ); 809 } 810 811 if ( ! empty( $exif['ImageDescription'] ) ) { 812 mbstring_binary_safe_encoding(); 813 $description_length = strlen( $exif['ImageDescription'] ); 814 reset_mbstring_encoding(); 815 816 if ( empty( $meta['title'] ) && $description_length < 80 ) { 817 // Assume the title is stored in ImageDescription. 818 $meta['title'] = trim( $exif['ImageDescription'] ); 819 } 820 821 if ( empty( $meta['caption'] ) && ! empty( $exif['COMPUTED']['UserComment'] ) ) { 822 $meta['caption'] = trim( $exif['COMPUTED']['UserComment'] ); 823 } 824 825 if ( empty( $meta['caption'] ) ) { 826 $meta['caption'] = trim( $exif['ImageDescription'] ); 827 } 828 } elseif ( empty( $meta['caption'] ) && ! empty( $exif['Comments'] ) ) { 829 $meta['caption'] = trim( $exif['Comments'] ); 830 } 831 832 if ( empty( $meta['credit'] ) ) { 833 if ( ! empty( $exif['Artist'] ) ) { 834 $meta['credit'] = trim( $exif['Artist'] ); 835 } elseif ( ! empty( $exif['Author'] ) ) { 836 $meta['credit'] = trim( $exif['Author'] ); 837 } 838 } 839 840 if ( empty( $meta['copyright'] ) && ! empty( $exif['Copyright'] ) ) { 841 $meta['copyright'] = trim( $exif['Copyright'] ); 842 } 843 if ( ! empty( $exif['FNumber'] ) ) { 844 $meta['aperture'] = round( wp_exif_frac2dec( $exif['FNumber'] ), 2 ); 845 } 846 if ( ! empty( $exif['Model'] ) ) { 847 $meta['camera'] = trim( $exif['Model'] ); 848 } 849 if ( empty( $meta['created_timestamp'] ) && ! empty( $exif['DateTimeDigitized'] ) ) { 850 $meta['created_timestamp'] = wp_exif_date2ts( $exif['DateTimeDigitized'] ); 851 } 852 if ( ! empty( $exif['FocalLength'] ) ) { 853 $meta['focal_length'] = (string) wp_exif_frac2dec( $exif['FocalLength'] ); 854 } 855 if ( ! empty( $exif['ISOSpeedRatings'] ) ) { 856 $meta['iso'] = is_array( $exif['ISOSpeedRatings'] ) ? reset( $exif['ISOSpeedRatings'] ) : $exif['ISOSpeedRatings']; 857 $meta['iso'] = trim( $meta['iso'] ); 858 } 859 if ( ! empty( $exif['ExposureTime'] ) ) { 860 $meta['shutter_speed'] = (string) wp_exif_frac2dec( $exif['ExposureTime'] ); 861 } 862 if ( ! empty( $exif['Orientation'] ) ) { 863 $meta['orientation'] = $exif['Orientation']; 864 } 865 } 866 867 foreach ( array( 'title', 'caption', 'credit', 'copyright', 'camera', 'iso' ) as $key ) { 868 if ( $meta[ $key ] && ! seems_utf8( $meta[ $key ] ) ) { 869 $meta[ $key ] = utf8_encode( $meta[ $key ] ); 870 } 871 } 872 873 foreach ( $meta['keywords'] as $key => $keyword ) { 874 if ( ! seems_utf8( $keyword ) ) { 875 $meta['keywords'][ $key ] = utf8_encode( $keyword ); 876 } 877 } 878 879 $meta = wp_kses_post_deep( $meta ); 880 881 /** 882 * Filters the array of meta data read from an image's exif data. 883 * 884 * @since 2.5.0 885 * @since 4.4.0 The `$iptc` parameter was added. 886 * @since 5.0.0 The `$exif` parameter was added. 887 * 888 * @param array $meta Image meta data. 889 * @param string $file Path to image file. 890 * @param int $image_type Type of image, one of the `IMAGETYPE_XXX` constants. 891 * @param array $iptc IPTC data. 892 * @param array $exif EXIF data. 893 */ 894 return apply_filters( 'wp_read_image_metadata', $meta, $file, $image_type, $iptc, $exif ); 895 896 } 897 898 /** 899 * Validate that file is an image. 900 * 901 * @since 2.5.0 902 * 903 * @param string $path File path to test if valid image. 904 * @return bool True if valid image, false if not valid image. 905 */ 906 function file_is_valid_image( $path ) { 907 $size = wp_getimagesize( $path ); 908 return ! empty( $size ); 909 } 910 911 /** 912 * Validate that file is suitable for displaying within a web page. 913 * 914 * @since 2.5.0 915 * 916 * @param string $path File path to test. 917 * @return bool True if suitable, false if not suitable. 918 */ 919 function file_is_displayable_image( $path ) { 920 $displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO, IMAGETYPE_WEBP ); // phpcs:ignore PHPCompatibility.Constants.NewConstants.imagetype_webpFound 921 922 $info = wp_getimagesize( $path ); 923 if ( empty( $info ) ) { 924 $result = false; 925 } elseif ( ! in_array( $info[2], $displayable_image_types, true ) ) { 926 $result = false; 927 } else { 928 $result = true; 929 } 930 931 /** 932 * Filters whether the current image is displayable in the browser. 933 * 934 * @since 2.5.0 935 * 936 * @param bool $result Whether the image can be displayed. Default true. 937 * @param string $path Path to the image. 938 */ 939 return apply_filters( 'file_is_displayable_image', $result, $path ); 940 } 941 942 /** 943 * Load an image resource for editing. 944 * 945 * @since 2.9.0 946 * 947 * @param int $attachment_id Attachment ID. 948 * @param string $mime_type Image mime type. 949 * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array 950 * of width and height values in pixels (in that order). Default 'full'. 951 * @return resource|GdImage|false The resulting image resource or GdImage instance on success, 952 * false on failure. 953 */ 954 function load_image_to_edit( $attachment_id, $mime_type, $size = 'full' ) { 955 $filepath = _load_image_to_edit_path( $attachment_id, $size ); 956 if ( empty( $filepath ) ) { 957 return false; 958 } 959 960 switch ( $mime_type ) { 961 case 'image/jpeg': 962 $image = imagecreatefromjpeg( $filepath ); 963 break; 964 case 'image/png': 965 $image = imagecreatefrompng( $filepath ); 966 break; 967 case 'image/gif': 968 $image = imagecreatefromgif( $filepath ); 969 break; 970 case 'image/webp': 971 $image = false; 972 if ( function_exists( 'imagecreatefromwebp' ) ) { 973 $image = imagecreatefromwebp( $filepath ); 974 } 975 break; 976 default: 977 $image = false; 978 break; 979 } 980 981 if ( is_gd_image( $image ) ) { 982 /** 983 * Filters the current image being loaded for editing. 984 * 985 * @since 2.9.0 986 * 987 * @param resource|GdImage $image Current image. 988 * @param int $attachment_id Attachment ID. 989 * @param string|int[] $size Requested image size. Can be any registered image size name, or 990 * an array of width and height values in pixels (in that order). 991 */ 992 $image = apply_filters( 'load_image_to_edit', $image, $attachment_id, $size ); 993 994 if ( function_exists( 'imagealphablending' ) && function_exists( 'imagesavealpha' ) ) { 995 imagealphablending( $image, false ); 996 imagesavealpha( $image, true ); 997 } 998 } 999 1000 return $image; 1001 } 1002 1003 /** 1004 * Retrieve the path or URL of an attachment's attached file. 1005 * 1006 * If the attached file is not present on the local filesystem (usually due to replication plugins), 1007 * then the URL of the file is returned if `allow_url_fopen` is supported. 1008 * 1009 * @since 3.4.0 1010 * @access private 1011 * 1012 * @param int $attachment_id Attachment ID. 1013 * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array 1014 * of width and height values in pixels (in that order). Default 'full'. 1015 * @return string|false File path or URL on success, false on failure. 1016 */ 1017 function _load_image_to_edit_path( $attachment_id, $size = 'full' ) { 1018 $filepath = get_attached_file( $attachment_id ); 1019 1020 if ( $filepath && file_exists( $filepath ) ) { 1021 if ( 'full' !== $size ) { 1022 $data = image_get_intermediate_size( $attachment_id, $size ); 1023 1024 if ( $data ) { 1025 $filepath = path_join( dirname( $filepath ), $data['file'] ); 1026 1027 /** 1028 * Filters the path to an attachment's file when editing the image. 1029 * 1030 * The filter is evaluated for all image sizes except 'full'. 1031 * 1032 * @since 3.1.0 1033 * 1034 * @param string $path Path to the current image. 1035 * @param int $attachment_id Attachment ID. 1036 * @param string|int[] $size Requested image size. Can be any registered image size name, or 1037 * an array of width and height values in pixels (in that order). 1038 */ 1039 $filepath = apply_filters( 'load_image_to_edit_filesystempath', $filepath, $attachment_id, $size ); 1040 } 1041 } 1042 } elseif ( function_exists( 'fopen' ) && ini_get( 'allow_url_fopen' ) ) { 1043 /** 1044 * Filters the path to an attachment's URL when editing the image. 1045 * 1046 * The filter is only evaluated if the file isn't stored locally and `allow_url_fopen` is enabled on the server. 1047 * 1048 * @since 3.1.0 1049 * 1050 * @param string|false $image_url Current image URL. 1051 * @param int $attachment_id Attachment ID. 1052 * @param string|int[] $size Requested image size. Can be any registered image size name, or 1053 * an array of width and height values in pixels (in that order). 1054 */ 1055 $filepath = apply_filters( 'load_image_to_edit_attachmenturl', wp_get_attachment_url( $attachment_id ), $attachment_id, $size ); 1056 } 1057 1058 /** 1059 * Filters the returned path or URL of the current image. 1060 * 1061 * @since 2.9.0 1062 * 1063 * @param string|false $filepath File path or URL to current image, or false. 1064 * @param int $attachment_id Attachment ID. 1065 * @param string|int[] $size Requested image size. Can be any registered image size name, or 1066 * an array of width and height values in pixels (in that order). 1067 */ 1068 return apply_filters( 'load_image_to_edit_path', $filepath, $attachment_id, $size ); 1069 } 1070 1071 /** 1072 * Copy an existing image file. 1073 * 1074 * @since 3.4.0 1075 * @access private 1076 * 1077 * @param int $attachment_id Attachment ID. 1078 * @return string|false New file path on success, false on failure. 1079 */ 1080 function _copy_image_file( $attachment_id ) { 1081 $dst_file = get_attached_file( $attachment_id ); 1082 $src_file = $dst_file; 1083 1084 if ( ! file_exists( $src_file ) ) { 1085 $src_file = _load_image_to_edit_path( $attachment_id ); 1086 } 1087 1088 if ( $src_file ) { 1089 $dst_file = str_replace( wp_basename( $dst_file ), 'copy-' . wp_basename( $dst_file ), $dst_file ); 1090 $dst_file = dirname( $dst_file ) . '/' . wp_unique_filename( dirname( $dst_file ), wp_basename( $dst_file ) ); 1091 1092 /* 1093 * The directory containing the original file may no longer 1094 * exist when using a replication plugin. 1095 */ 1096 wp_mkdir_p( dirname( $dst_file ) ); 1097 1098 if ( ! copy( $src_file, $dst_file ) ) { 1099 $dst_file = false; 1100 } 1101 } else { 1102 $dst_file = false; 1103 } 1104 1105 return $dst_file; 1106 }