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