...] * : One or more IDs of the attachments to regenerate. * * [--image_size=] * : Name of the image size to regenerate. Only thumbnails of this image size will be regenerated, thumbnails of other image sizes will not. * * [--skip-delete] * : Skip deletion of the original thumbnails. If your thumbnails are linked from sources outside your control, it's likely best to leave them around. Defaults to false. * * [--only-missing] * : Only generate thumbnails for images missing image sizes. * * [--delete-unknown] * : Only delete thumbnails for old unregistered image sizes. * * [--yes] * : Answer yes to the confirmation message. Confirmation only shows when no IDs passed as arguments. * * ## EXAMPLES * * # Regenerate thumbnails for given attachment IDs. * $ wp media regenerate 123 124 125 * Found 3 images to regenerate. * 1/3 Regenerated thumbnails for "Vertical Image" (ID 123). * 2/3 Regenerated thumbnails for "Horizontal Image" (ID 124). * 3/3 Regenerated thumbnails for "Beautiful Picture" (ID 125). * Success: Regenerated 3 of 3 images. * * # Regenerate all thumbnails, without confirmation. * $ wp media regenerate --yes * Found 3 images to regenerate. * 1/3 Regenerated thumbnails for "Sydney Harbor Bridge" (ID 760). * 2/3 Regenerated thumbnails for "Boardwalk" (ID 757). * 3/3 Regenerated thumbnails for "Sunburst Over River" (ID 756). * Success: Regenerated 3 of 3 images. * * # Re-generate all thumbnails that have IDs between 1000 and 2000. * $ seq 1000 2000 | xargs wp media regenerate * Found 4 images to regenerate. * 1/4 Regenerated thumbnails for "Vertical Featured Image" (ID 1027). * 2/4 Regenerated thumbnails for "Horizontal Featured Image" (ID 1022). * 3/4 Regenerated thumbnails for "Unicorn Wallpaper" (ID 1045). * 4/4 Regenerated thumbnails for "I Am Worth Loving Wallpaper" (ID 1023). * Success: Regenerated 4 of 4 images. * * # Re-generate only the thumbnails of "large" image size for all images. * $ wp media regenerate --image_size=large * Do you really want to regenerate the "large" image size for all images? [y/n] y * Found 3 images to regenerate. * 1/3 Regenerated "large" thumbnail for "Sydney Harbor Bridge" (ID 760). * 2/3 No "large" thumbnail regeneration needed for "Boardwalk" (ID 757). * 3/3 Regenerated "large" thumbnail for "Sunburst Over River" (ID 756). * Success: Regenerated 3 of 3 images. */ public function regenerate( $args, $assoc_args = array() ) { $assoc_args = wp_parse_args( $assoc_args, [ 'image_size' => '' ] ); $image_size = $assoc_args['image_size']; if ( $image_size && ! in_array( $image_size, get_intermediate_image_sizes(), true ) ) { WP_CLI::error( sprintf( 'Unknown image size "%s".', $image_size ) ); } if ( empty( $args ) ) { if ( $image_size ) { WP_CLI::confirm( sprintf( 'Do you really want to regenerate the "%s" image size for all images?', $image_size ), $assoc_args ); } else { WP_CLI::confirm( 'Do you really want to regenerate all images?', $assoc_args ); } } $skip_delete = Utils\get_flag_value( $assoc_args, 'skip-delete' ); $only_missing = Utils\get_flag_value( $assoc_args, 'only-missing' ); if ( $only_missing ) { $skip_delete = true; } $delete_unknown = Utils\get_flag_value( $assoc_args, 'delete-unknown' ); if ( $delete_unknown ) { $skip_delete = false; } $additional_mime_types = array(); if ( Utils\wp_version_compare( '4.7', '>=' ) ) { $additional_mime_types[] = 'application/pdf'; } $images = $this->get_images( $args, $additional_mime_types ); $count = $images->post_count; if ( ! $count ) { WP_CLI::warning( 'No images found.' ); return; } WP_CLI::log( sprintf( 'Found %1$d %2$s to regenerate.', $count, _n( 'image', 'images', $count ) ) ); if ( $image_size ) { $image_size_filters = $this->add_image_size_filters( $image_size ); } $number = 0; $successes = 0; $errors = 0; $skips = 0; foreach ( $images->posts as $post_id ) { ++$number; if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) { Utils\wp_clear_object_cache(); } $this->process_regeneration( $post_id, $skip_delete, $only_missing, $delete_unknown, $image_size, $number . '/' . $count, $successes, $errors, $skips ); } if ( $image_size ) { $this->remove_image_size_filters( $image_size_filters ); } Utils\report_batch_operation_results( 'image', 'regenerate', $count, $successes, $errors, $skips ); } /** * Creates attachments from local files or URLs. * * ## OPTIONS * * ... * : Path to file or files to be imported. Supports the glob(3) capabilities of the current shell. * If file is recognized as a URL (for example, with a scheme of http or ftp), the file will be * downloaded to a temp file before being sideloaded. * * [--post_id=] * : ID of the post to attach the imported files to. * * [--post_name=] * : Name of the post to attach the imported files to. * * [--file_name=] * : Attachment name (post_name field). * * [--title=] * : Attachment title (post title field). * * [--caption=<caption>] * : Caption for attachment (post excerpt field). * * [--alt=<alt_text>] * : Alt text for image (saved as post meta). * * [--desc=<description>] * : "Description" field (post content) of attachment post. * * [--skip-copy] * : If set, media files (local only) are imported to the library but not moved on disk. * File names will not be run through wp_unique_filename() with this set. * * [--preserve-filetime] * : Use the file modified time as the post published & modified dates. * Remote files will always use the current time. * * [--featured_image] * : If set, set the imported image as the Featured Image of the post it is attached to. * * [--porcelain[=<field>]] * : Output a single field for each imported image. Defaults to attachment ID when used as flag. * --- * options: * - url * --- * * ## EXAMPLES * * # Import all jpgs in the current user's "Pictures" directory, not attached to any post. * $ wp media import ~/Pictures/**\/*.jpg * Imported file '/home/person/Pictures/landscape-photo.jpg' as attachment ID 1751. * Imported file '/home/person/Pictures/fashion-icon.jpg' as attachment ID 1752. * Success: Imported 2 of 2 items. * * # Import a local image and set it to be the post thumbnail for a post. * $ wp media import ~/Downloads/image.png --post_id=123 --title="A downloaded picture" --featured_image * Imported file '/home/person/Downloads/image.png' as attachment ID 1753 and attached to post 123 as featured image. * Success: Imported 1 of 1 images. * * # Import a local image, but set it as the featured image for all posts. * # 1. Import the image and get its attachment ID. * # 2. Assign the attachment ID as the featured image for all posts. * $ ATTACHMENT_ID="$(wp media import ~/Downloads/image.png --porcelain)" * $ wp post list --post_type=post --format=ids | xargs -d ' ' -I % wp post meta add % _thumbnail_id $ATTACHMENT_ID * Success: Added custom field. * Success: Added custom field. * * # Import an image from the web. * $ wp media import http://s.wordpress.org/style/images/wp-header-logo.png --title='The WordPress logo' --alt="Semantic personal publishing" * Imported file 'http://s.wordpress.org/style/images/wp-header-logo.png' as attachment ID 1755. * Success: Imported 1 of 1 images. * * # Get the URL for an attachment after import. * $ wp media import http://s.wordpress.org/style/images/wp-header-logo.png --porcelain | xargs -I {} wp post list --post__in={} --field=url --post_type=attachment * http://wordpress-develop.dev/wp-header-logo/ */ public function import( $args, $assoc_args = array() ) { $assoc_args = wp_parse_args( $assoc_args, array( 'file_name' => '', 'title' => '', 'caption' => '', 'alt' => '', 'desc' => '', 'post_name' => '', ) ); // Assume the most generic term $noun = 'item'; // Current site's timezone offset. $gmt_offset = get_option( 'gmt_offset' ); // Use the noun `image` when sure the media file is an image if ( Utils\get_flag_value( $assoc_args, 'featured_image' ) || $assoc_args['alt'] ) { $noun = 'image'; } $porcelain = Utils\get_flag_value( $assoc_args, 'porcelain' ); if ( is_string( $porcelain ) && ! in_array( $porcelain, array( 'url' ), true ) ) { WP_CLI::error( sprintf( 'Invalid value for <porcelain>: %s. Expected flag or \'url\'.', $porcelain ) ); } if ( isset( $assoc_args['post_id'] ) ) { if ( ! get_post( $assoc_args['post_id'] ) ) { WP_CLI::warning( 'Invalid --post_id' ); $assoc_args['post_id'] = false; } } else { $assoc_args['post_id'] = false; } $number = 0; $successes = 0; $errors = 0; foreach ( $args as $file ) { ++$number; if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) { Utils\wp_clear_object_cache(); } // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url. $is_file_remote = function_exists( 'wp_parse_url' ) ? wp_parse_url( $file, PHP_URL_HOST ) : parse_url( $file, PHP_URL_HOST ); $orig_filename = $file; $file_time = ''; if ( empty( $is_file_remote ) ) { if ( ! file_exists( $file ) ) { WP_CLI::warning( "Unable to import file '$file'. Reason: File doesn't exist." ); ++$errors; continue; } if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) { $tempfile = $file; } else { $tempfile = $this->make_copy( $file ); } $name = Utils\basename( $file ); if ( Utils\get_flag_value( $assoc_args, 'preserve-filetime' ) ) { $file_time = @filemtime( $file ); } } else { $tempfile = download_url( $file ); if ( is_wp_error( $tempfile ) ) { WP_CLI::warning( sprintf( "Unable to import file '%s'. Reason: %s", $file, implode( ', ', $tempfile->get_error_messages() ) ) ); ++$errors; continue; } $name = strtok( Utils\basename( $file ), '?' ); } if ( ! empty( $assoc_args['file_name'] ) ) { $image_name = $this->get_image_name( $name, $assoc_args['file_name'] ); $name = ! empty( $image_name ) ? $image_name : $name; } $file_array = array( 'tmp_name' => $tempfile, 'name' => $name, ); $post_array = array( 'post_title' => $assoc_args['title'], 'post_excerpt' => $assoc_args['caption'], 'post_content' => $assoc_args['desc'], 'post_name' => $assoc_args['post_name'], ); if ( ! empty( $file_time ) ) { $post_array['post_date'] = gmdate( 'Y-m-d H:i:s', $file_time + ( $gmt_offset * HOUR_IN_SECONDS ) ); $post_array['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $file_time ); $post_array['post_modified'] = gmdate( 'Y-m-d H:i:s', $file_time + ( $gmt_offset * HOUR_IN_SECONDS ) ); $post_array['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $file_time ); } $post_array = wp_slash( $post_array ); // use image exif/iptc data for title and caption defaults if possible if ( empty( $post_array['post_title'] ) || empty( $post_array['post_excerpt'] ) ) { // @codingStandardsIgnoreStart $image_meta = @wp_read_image_metadata( $tempfile ); // @codingStandardsIgnoreEnd if ( ! empty( $image_meta ) ) { if ( empty( $post_array['post_title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { $post_array['post_title'] = $image_meta['title']; } if ( empty( $post_array['post_excerpt'] ) && trim( $image_meta['caption'] ) ) { $post_array['post_excerpt'] = $image_meta['caption']; } } } if ( empty( $post_array['post_title'] ) ) { $post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Utils\basename( $file ) ); } if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) { $wp_filetype = wp_check_filetype( $file, null ); $post_array['post_mime_type'] = $wp_filetype['type']; $post_array['post_status'] = 'inherit'; $success = wp_insert_attachment( $post_array, $file, $assoc_args['post_id'] ); if ( is_wp_error( $success ) ) { WP_CLI::warning( sprintf( "Unable to insert file '%s'. Reason: %s", $orig_filename, implode( ', ', $success->get_error_messages() ) ) ); ++$errors; continue; } wp_update_attachment_metadata( $success, wp_generate_attachment_metadata( $success, $file ) ); } else { // Deletes the temporary file. $success = media_handle_sideload( $file_array, $assoc_args['post_id'], $assoc_args['title'], $post_array ); if ( is_wp_error( $success ) ) { WP_CLI::warning( sprintf( "Unable to import file '%s'. Reason: %s", $orig_filename, implode( ', ', $success->get_error_messages() ) ) ); ++$errors; continue; } } // Set alt text if ( $assoc_args['alt'] ) { update_post_meta( $success, '_wp_attachment_image_alt', wp_slash( $assoc_args['alt'] ) ); } // Set as featured image, if --post_id and --featured_image are set if ( $assoc_args['post_id'] && Utils\get_flag_value( $assoc_args, 'featured_image' ) ) { update_post_meta( $assoc_args['post_id'], '_thumbnail_id', $success ); } $attachment_success_text = ''; if ( $assoc_args['file_name'] ) { $attachment_success_text .= " with file name {$name}"; } if ( $assoc_args['post_id'] ) { $attachment_success_text = " and attached to post {$assoc_args['post_id']}"; if ( Utils\get_flag_value( $assoc_args, 'featured_image' ) ) { $attachment_success_text .= ' as featured image'; } } if ( $porcelain ) { if ( 'url' === strtolower( $porcelain ) ) { $file_location = $this->get_real_attachment_url( $success ); WP_CLI::line( $file_location ); } else { WP_CLI::line( $success ); } } else { WP_CLI::log( sprintf( "Imported file '%s' as attachment ID %d%s.", $orig_filename, $success, $attachment_success_text ) ); } ++$successes; } // Report the result of the operation if ( ! Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { Utils\report_batch_operation_results( $noun, 'import', count( $args ), $successes, $errors ); } elseif ( $errors ) { WP_CLI::halt( 1 ); } } /** * Lists image sizes registered with WordPress. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a specific format * --- * default: table * options: * - table * - json * - csv * - yaml * - count * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each image size: * * name * * width * * height * * crop * * ratio * * ## EXAMPLES * * # List all registered image sizes * $ wp media image-size * +---------------------------+-------+--------+-------+-------+ * | name | width | height | crop | ratio | * +---------------------------+-------+--------+-------+-------+ * | full | | | N/A | N/A | * | twentyfourteen-full-width | 1038 | 576 | hard | 173:96| * | large | 1024 | 1024 | soft | N/A | * | medium_large | 768 | 0 | soft | N/A | * | medium | 300 | 300 | soft | N/A | * | thumbnail | 150 | 150 | hard | 1:1 | * +---------------------------+-------+--------+-------+-------+ * * @subcommand image-size */ public function image_size( $args, $assoc_args ) { $assoc_args = array_merge( array( 'fields' => 'name,width,height,crop,ratio', ), $assoc_args ); $sizes = $this->get_registered_image_sizes(); usort( $sizes, function ( $a, $b ) { if ( $a['width'] === $b['width'] ) { return 0; } return ( $a['width'] < $b['width'] ) ? 1 : -1; } ); array_unshift( $sizes, array( 'name' => 'full', 'width' => '', 'height' => '', 'crop' => 'N/A', 'ratio' => 'N/A', ) ); WP_CLI\Utils\format_items( $assoc_args['format'], $sizes, explode( ',', $assoc_args['fields'] ) ); } private function get_ratio( $width, $height ) { if ( 0 === $height ) { return "0:{$width}"; } if ( 0 === $width ) { return "{$height}:0"; } $gcd = $this->gcd( $width, $height ); $width_ratio = $width / $gcd; $height_ratio = $height / $gcd; return "{$width_ratio}:{$height_ratio}"; } private function gcd( $num1, $num2 ) { while ( 0 !== $num2 ) { $t = $num1 % $num2; $num1 = $num2; $num2 = $t; } return $num1; } // wp_tempnam() inexplicably forces a .tmp extension, which spoils MIME type detection private function make_copy( $path ) { $dir = get_temp_dir(); $filename = Utils\basename( $path ); if ( empty( $filename ) ) { $filename = time(); } $filename = $dir . wp_unique_filename( $dir, $filename ); if ( ! copy( $path, $filename ) ) { WP_CLI::error( "Could not create temporary file for $path." ); } return $filename; } private function process_regeneration( $id, $skip_delete, $only_missing, $delete_unknown, $image_size, $progress, &$successes, &$errors, &$skips ) { $title = get_the_title( $id ); if ( '' === $title ) { // If audio or video cover art then the id is the sub attachment id, which has no title. if ( metadata_exists( 'post', $id, '_cover_hash' ) ) { // Unfortunately the only way to get the attachment title would be to do a non-indexed query against the meta value of `_thumbnail_id`. So don't. $att_desc = sprintf( 'cover attachment (ID %d)', $id ); } else { $att_desc = sprintf( '"(no title)" (ID %d)', $id ); } } else { $att_desc = sprintf( '"%1$s" (ID %2$d)', $title, $id ); } $thumbnail_desc = $image_size ? sprintf( '"%s" thumbnail', $image_size ) : 'thumbnail'; $fullsizepath = $this->get_attached_file( $id ); if ( false === $fullsizepath || ! file_exists( $fullsizepath ) ) { WP_CLI::warning( "Can't find $att_desc." ); ++$errors; return; } $is_pdf = 'application/pdf' === get_post_mime_type( $id ); $original_meta = wp_get_attachment_metadata( $id ); if ( $delete_unknown ) { $this->delete_unknown_image_sizes( $id, $fullsizepath ); WP_CLI::log( "$progress Deleted unknown image sizes for $att_desc." ); ++$successes; return; } $needs_regeneration = $this->needs_regeneration( $id, $fullsizepath, $is_pdf, $image_size, $skip_delete, $skip_it ); if ( $skip_it ) { WP_CLI::log( "$progress Skipped $thumbnail_desc regeneration for $att_desc." ); ++$skips; return; } if ( $only_missing && ! $needs_regeneration ) { WP_CLI::log( "$progress No $thumbnail_desc regeneration needed for $att_desc." ); ++$successes; return; } $metadata = wp_generate_attachment_metadata( $id, $fullsizepath ); // Note it's possible for no metadata to be generated for PDFs if restricted to a specific image size. if ( empty( $metadata ) && ! ( $is_pdf && $image_size ) ) { WP_CLI::warning( sprintf( 'No metadata. (ID %d)', $id ) ); WP_CLI::log( "$progress Couldn't regenerate thumbnails for $att_desc." ); ++$errors; return; } // On read error, we might only get the filesize returned and nothing else. if ( 1 === count( $metadata ) && array_key_exists( 'filesize', $metadata ) && ! ( $is_pdf && $image_size ) ) { WP_CLI::warning( sprintf( 'Read error while retrieving metadata. (ID %d)', $id ) ); WP_CLI::log( "$progress Couldn't regenerate thumbnails for $att_desc." ); ++$errors; return; } if ( $image_size ) { if ( $this->update_attachment_metadata_for_image_size( $id, $metadata, $image_size, $original_meta ) ) { WP_CLI::log( "$progress Regenerated $thumbnail_desc for $att_desc." ); } else { WP_CLI::log( "$progress No $thumbnail_desc regeneration needed for $att_desc." ); } } else { wp_update_attachment_metadata( $id, $metadata ); WP_CLI::log( "$progress Regenerated thumbnails for $att_desc." ); } ++$successes; } private function remove_old_images( $metadata, $fullsizepath, $image_size ) { if ( empty( $metadata['sizes'] ) ) { return; } if ( $image_size ) { if ( empty( $metadata['sizes'][ $image_size ] ) ) { return; } $metadata['sizes'] = array( $image_size => $metadata['sizes'][ $image_size ] ); } $dir_path = dirname( $fullsizepath ) . '/'; foreach ( $metadata['sizes'] as $size_info ) { $intermediate_path = $dir_path . $size_info['file']; if ( $intermediate_path === $fullsizepath ) { continue; } if ( file_exists( $intermediate_path ) ) { unlink( $intermediate_path ); } } } private function needs_regeneration( $att_id, $fullsizepath, $is_pdf, $image_size, $skip_delete, &$skip_it ) { // Assume not skipping. $skip_it = false; // Note: zero-length string returned if no metadata, for instance if PDF or non-standard image (eg an SVG). $metadata = wp_get_attachment_metadata( $att_id ); $image_sizes = $this->get_intermediate_image_sizes_for_attachment( $fullsizepath, $is_pdf, $metadata, $att_id ); // First check if no applicable editor currently available (non-destructive - ie old thumbnails not removed). if ( is_wp_error( $image_sizes ) && 'image_no_editor' === $image_sizes->get_error_code() ) { // Warn unless PDF or non-standard image. if ( ! $is_pdf && is_array( $metadata ) && ! empty( $metadata['sizes'] ) ) { WP_CLI::warning( sprintf( '%s (ID %d)', $image_sizes->get_error_message(), $att_id ) ); } $skip_it = true; return false; } // If uploaded when applicable image editor such as Imagick unavailable, the metadata or sizes metadata may not exist. if ( ! is_array( $metadata ) ) { $metadata = array(); } // If set `$metadata['sizes']` should be array but explicitly check as following code depends on it. if ( ! isset( $metadata['sizes'] ) || ! is_array( $metadata['sizes'] ) ) { $metadata['sizes'] = array(); } // Remove any old thumbnails (so now destructive). if ( ! $skip_delete ) { $this->remove_old_images( $metadata, $fullsizepath, $image_size ); } // Check for any other error (such as load error) apart from no editor available. if ( is_wp_error( $image_sizes ) ) { // Warn but assume it may be possible to regenerate and allow processing to continue and possibly fail. WP_CLI::warning( sprintf( '%s (ID %d)', $image_sizes->get_error_message(), $att_id ) ); return true; } // Have sizes - check whether they're new ones or they've changed. Note that an attachment can have no sizes if it's on or below the thumbnail threshold. if ( $image_size ) { if ( empty( $image_sizes[ $image_size ] ) ) { return false; } if ( empty( $metadata['sizes'][ $image_size ] ) ) { return true; } $metadata['sizes'] = array( $image_size => $metadata['sizes'][ $image_size ] ); } if ( $this->image_sizes_differ( $image_sizes, $metadata['sizes'] ) ) { return true; } $dir_path = dirname( $fullsizepath ) . '/'; // Check that the thumbnail files exist. foreach ( $metadata['sizes'] as $size_info ) { $intermediate_path = $dir_path . $size_info['file']; if ( $intermediate_path === $fullsizepath ) { continue; } if ( ! file_exists( $intermediate_path ) ) { return true; } } return false; } // Whether there's new image sizes or the width/height of existing image sizes have changed. private function image_sizes_differ( $image_sizes, $meta_sizes ) { // Check if have new image size(s). if ( array_diff( array_keys( $image_sizes ), array_keys( $meta_sizes ) ) ) { return true; } // Check if image sizes have changed. foreach ( $image_sizes as $name => $image_size ) { if ( $image_size['width'] !== $meta_sizes[ $name ]['width'] || $image_size['height'] !== $meta_sizes[ $name ]['height'] ) { return true; } } return false; } /** * Returns image sizes for a given attachment. * * Like WP's get_intermediate_image_sizes(), but removes sizes that won't be generated for a particular attachment due to it being on or below their thresholds, * and returns associative array with size name => width/height entries, resolved to crop values if applicable. * * @param string $fullsizepath Filepath of the attachment * @param bool $is_pdf Whether it is a PDF. * @param array $metadata Attachment metadata. * @param int $att_id Attachment ID. * * @return array|WP_Error Image sizes on success, WP_Error instance otherwise. */ private function get_intermediate_image_sizes_for_attachment( $fullsizepath, $is_pdf, $metadata, $att_id ) { // Need to get width, height of attachment for image_resize_dimensions(). $editor = wp_get_image_editor( $fullsizepath ); if ( is_wp_error( $editor ) ) { return $editor; } $result = $editor->load(); if ( is_wp_error( $result ) ) { unset( $editor ); return $result; } list( $width, $height ) = array_values( $editor->get_size() ); unset( $editor ); $sizes = array(); foreach ( $this->get_intermediate_sizes( $is_pdf, $metadata, $att_id ) as $name => $size ) { // Need to check destination and original width or height differ before calling image_resize_dimensions(), otherwise it will return non-false. $dims = image_resize_dimensions( $width, $height, $size['width'], $size['height'], $size['crop'] ); if ( ( $width !== $size['width'] || $height !== $size['height'] ) && $dims ) { list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; $sizes[ $name ] = array( 'width' => $dst_w, 'height' => $dst_h, ); } } return $sizes; } // Like WP's get_intermediate_image_sizes(), but returns associative array with name => size info entries (and caters for PDFs also). private function get_intermediate_sizes( $is_pdf, $metadata, $att_id ) { if ( $is_pdf ) { // Copied from wp_generate_attachment_metadata() in "wp-admin/includes/image.php". $fallback_sizes = array( 'thumbnail', 'medium', 'large', ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $intermediate_image_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata ); } else { $intermediate_image_sizes = get_intermediate_image_sizes(); } // Adapted from wp_generate_attachment_metadata() in "wp-admin/includes/image.php". if ( function_exists( 'wp_get_additional_image_sizes' ) ) { $_wp_additional_image_sizes = wp_get_additional_image_sizes(); } else { // For WP < 4.7.0. global $_wp_additional_image_sizes; if ( ! $_wp_additional_image_sizes ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Used as a fallback for WordPress version less than 4.7.0 as function wp_get_additional_image_sizes didn't exist then. $_wp_additional_image_sizes = array(); } } $sizes = array(); foreach ( $intermediate_image_sizes as $s ) { if ( isset( $_wp_additional_image_sizes[ $s ]['width'] ) ) { $sizes[ $s ]['width'] = (int) $_wp_additional_image_sizes[ $s ]['width']; } else { $sizes[ $s ]['width'] = (int) get_option( "{$s}_size_w" ); } if ( isset( $_wp_additional_image_sizes[ $s ]['height'] ) ) { $sizes[ $s ]['height'] = (int) $_wp_additional_image_sizes[ $s ]['height']; } else { $sizes[ $s ]['height'] = (int) get_option( "{$s}_size_h" ); } if ( isset( $_wp_additional_image_sizes[ $s ]['crop'] ) ) { $sizes[ $s ]['crop'] = (bool) $_wp_additional_image_sizes[ $s ]['crop']; // Force PDF thumbnails to be soft crops. } elseif ( $is_pdf && 'thumbnail' === $s ) { $sizes[ $s ]['crop'] = false; } else { $sizes[ $s ]['crop'] = (bool) get_option( "{$s}_crop" ); } } // Check here that not PDF (as filter not applied in core if is) and `$metadata` is array (as may not be and filter only applied in core when is). if ( ! $is_pdf && is_array( $metadata ) ) { if ( Utils\wp_version_compare( '5.3', '>=' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $sizes = apply_filters( 'intermediate_image_sizes_advanced', $sizes, $metadata, $att_id ); } else { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $sizes = apply_filters( 'intermediate_image_sizes_advanced', $sizes, $metadata ); } } return $sizes; } // Add filters to only process a particular intermediate image size in wp_generate_attachment_metadata(). private function add_image_size_filters( $image_size ) { $image_size_filters = array(); // For images. $image_size_filters['intermediate_image_sizes_advanced'] = function ( $sizes ) use ( $image_size ) { // $sizes is associative array of name => size info entries. if ( isset( $sizes[ $image_size ] ) ) { return array( $image_size => $sizes[ $image_size ] ); } return array(); }; // For PDF previews. $image_size_filters['fallback_intermediate_image_sizes'] = function ( $fallback_sizes ) use ( $image_size ) { // $fallback_sizes is indexed array of size names. if ( in_array( $image_size, $fallback_sizes, true ) ) { return array( $image_size ); } return array(); }; foreach ( $image_size_filters as $name => $filter ) { add_filter( $name, $filter, PHP_INT_MAX ); } return $image_size_filters; } // Remove above intermediate image size filters. private function remove_image_size_filters( $image_size_filters ) { foreach ( $image_size_filters as $name => $filter ) { remove_filter( $name, $filter, PHP_INT_MAX ); } } // Update attachment sizes metadata just for a particular intermediate image size. private function update_attachment_metadata_for_image_size( $id, $new_metadata, $image_size, $metadata ) { if ( ! is_array( $metadata ) ) { return false; } // If have metadata for image_size. if ( ! empty( $new_metadata['sizes'][ $image_size ] ) ) { $metadata['sizes'][ $image_size ] = $new_metadata['sizes'][ $image_size ]; wp_update_attachment_metadata( $id, $metadata ); return true; } // Else remove unused metadata if any. if ( ! empty( $metadata['sizes'][ $image_size ] ) ) { unset( $metadata['sizes'][ $image_size ] ); wp_update_attachment_metadata( $id, $metadata ); // Treat removing unused metadata as no change. } return false; } /** * Get images from the installation. * * @param array $args The query arguments to use. Optional. * @param array $additional_mime_types The additional mime types to search for. Optional. * * @return WP_Query The query result. */ private function get_images( $args = array(), $additional_mime_types = array() ) { $mime_types = array_merge( array( 'image' ), $additional_mime_types ); $query_args = array( 'post_type' => 'attachment', 'post__in' => $args, 'post_mime_type' => $mime_types, 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ); return new WP_Query( $query_args ); } /** * Get all the registered image sizes along with their dimensions. * * @return array $image_sizes The image sizes */ private function get_registered_image_sizes() { $image_sizes = array(); $all_sizes = $this->wp_get_registered_image_subsizes(); foreach ( $all_sizes as $size => $size_args ) { $crop = filter_var( $size_args['crop'], FILTER_VALIDATE_BOOLEAN ); $image_sizes[] = array( 'name' => $size, 'width' => $size_args['width'], 'height' => $size_args['height'], 'crop' => empty( $crop ) || is_array( $size_args['crop'] ) ? 'soft' : 'hard', 'ratio' => empty( $crop ) || is_array( $size_args['crop'] ) ? 'N/A' : $this->get_ratio( $size_args['width'], $size_args['height'] ), ); } return $image_sizes; } /** * Returns a normalized list of all currently registered image sub-sizes. * * If exists, uses output of wp_get_registered_image_subsizes() function (introduced in WP 5.3). * Definition of this method is modified version of core function wp_get_registered_image_subsizes(). * * @global array $_wp_additional_image_sizes * * @return array[] Associative array of arrays of image sub-size information, keyed by image size name. */ private function wp_get_registered_image_subsizes() { if ( Utils\wp_version_compare( '5.3', '>=' ) ) { return wp_get_registered_image_subsizes(); } global $_wp_additional_image_sizes; $additional_sizes = $_wp_additional_image_sizes ? $_wp_additional_image_sizes : array(); $all_sizes = array(); foreach ( get_intermediate_image_sizes() as $size_name ) { $size_data = array( 'width' => 0, 'height' => 0, 'crop' => false, ); if ( isset( $additional_sizes[ $size_name ]['width'] ) ) { // For sizes added by plugins and themes. $size_data['width'] = (int) $additional_sizes[ $size_name ]['width']; } else { // For default sizes set in options. $size_data['width'] = (int) get_option( "{$size_name}_size_w" ); } if ( isset( $additional_sizes[ $size_name ]['height'] ) ) { $size_data['height'] = (int) $additional_sizes[ $size_name ]['height']; } else { $size_data['height'] = (int) get_option( "{$size_name}_size_h" ); } if ( empty( $size_data['width'] ) && empty( $size_data['height'] ) ) { // This size isn't set. continue; } if ( isset( $additional_sizes[ $size_name ]['crop'] ) ) { $size_data['crop'] = $additional_sizes[ $size_name ]['crop']; } else { $size_data['crop'] = get_option( "{$size_name}_crop" ); } if ( ! is_array( $size_data['crop'] ) || empty( $size_data['crop'] ) ) { $size_data['crop'] = (bool) $size_data['crop']; } $all_sizes[ $size_name ] = $size_data; } return $all_sizes; } /** * Fix image orientation for one or more attachments. * * ## OPTIONS * * [<attachment-id>...] * : One or more IDs of the attachments to regenerate. * * [--dry-run] * : Check images needing orientation without performing the operation. * * ## EXAMPLES * * # Fix orientation for all images. * $ wp media fix-orientation * 1/3 Fixing orientation for "Landscape_4" (ID 62). * 2/3 Fixing orientation for "Landscape_3" (ID 61). * 3/3 Fixing orientation for "Landscape_2" (ID 60). * Success: Fixed 3 of 3 images. * * # Fix orientation dry run. * $ wp media fix-orientation 63 --dry-run * 1/1 "Portrait_6" (ID 63) will be affected. * Success: 1 of 1 image will be affected. * * # Fix orientation for specific images. * $ wp media fix-orientation 63 * 1/1 Fixing orientation for "Portrait_6" (ID 63). * Success: Fixed 1 of 1 images. * * @subcommand fix-orientation */ public function fix_orientation( $args, $assoc_args ) { // EXIF is required to read image metadata for orientation. if ( ! extension_loaded( 'exif' ) ) { WP_CLI::error( "'EXIF' extension is not loaded, it is required for this operation." ); } elseif ( ! function_exists( 'exif_read_data' ) ) { WP_CLI::error( "Function 'exif_read_data' does not exist, it is required for this operation." ); } $images = $this->get_images( $args ); $count = $images->post_count; $dry_run = Utils\get_flag_value( $assoc_args, 'dry-run' ); if ( ! $count ) { WP_CLI::error( 'No images found.' ); } $number = 0; $successes = 0; $errors = 0; foreach ( $images->posts as $post_id ) { ++$number; if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) { Utils\wp_clear_object_cache(); } $this->process_orientation_fix( $post_id, "{$number}/{$count}", $successes, $errors, $dry_run ); } if ( Utils\get_flag_value( $assoc_args, 'dry-run' ) ) { WP_CLI::success( sprintf( '%s of %s %s will be affected.', $successes, $count, Utils\pluralize( 'image', $count ) ) ); } else { Utils\report_batch_operation_results( 'image', 'fix', $count, $successes, $errors ); } } /** * Perform orientation fix on attachments. * * @param int $id Attachment Id. * @param string $progress Current progress string. * @param int $successes Count of success in current operation. * @param int $errors Count of errors in current operation. * @param bool $dry_run Is this a dry run? */ private function process_orientation_fix( $id, $progress, &$successes, &$errors, $dry_run ) { $title = get_the_title( $id ); if ( '' === $title ) { // If audio or video cover art then the id is the sub attachment id, which has no title. if ( metadata_exists( 'post', $id, '_cover_hash' ) ) { // Unfortunately the only way to get the attachment title would be to do a non-indexed query against the meta value of `_thumbnail_id`. So don't. $att_desc = sprintf( 'cover attachment (ID %d)', $id ); } else { $att_desc = sprintf( '"(no title)" (ID %d)', $id ); } } else { $att_desc = sprintf( '"%1$s" (ID %2$d)', $title, $id ); } $full_size_path = $this->get_attached_file( $id ); if ( false === $full_size_path || ! file_exists( $full_size_path ) ) { WP_CLI::warning( "Can't find {$att_desc}." ); ++$errors; return; } // Get current metadata of the attachment. $metadata = wp_generate_attachment_metadata( $id, $full_size_path ); $image_meta = ! empty( $metadata['image_meta'] ) ? $metadata['image_meta'] : []; if ( isset( $image_meta['orientation'] ) && absint( $image_meta['orientation'] ) > 1 ) { if ( ! $dry_run ) { WP_CLI::log( "{$progress} Fixing orientation for {$att_desc}." ); if ( false !== $this->flip_rotate_image( $id, $metadata, $image_meta, $full_size_path ) ) { ++$successes; } else { ++$errors; WP_CLI::log( "Couldn't fix orientation for {$att_desc}." ); } } else { WP_CLI::log( "{$progress} {$att_desc} will be affected." ); ++$successes; } } else { WP_CLI::log( "{$progress} No orientation fix required for {$att_desc}." ); } } /** * Perform image rotate operations on the image. * * @param int $id Attachment Id. * @param array $metadata Attachment Metadata. * @param array $image_meta `image_meta` information for the attachment. * @param string $full_size_path Path to original image. * * @return bool Whether the image rotation operation succeeded. */ private function flip_rotate_image( $id, $metadata, $image_meta, $full_size_path ) { $editor = wp_get_image_editor( $full_size_path ); if ( ! is_wp_error( $editor ) ) { $operations = $this->calculate_transformation( (int) $image_meta['orientation'] ); // Rotate image if required. if ( true === $operations['rotate'] ) { $editor->rotate( $operations['degree'] ); } // Flip image if required. if ( false !== $operations['flip'] ) { $editor->flip( $operations['flip'][0], $operations['flip'][1] ); } // Save the image and generate metadata. $editor->save( $full_size_path ); $metadata = wp_generate_attachment_metadata( $id, $full_size_path ); $image_meta = empty( $metadata['image_meta'] ) ? [] : $metadata['image_meta']; // Update attachment metadata with newly generated data. wp_update_attachment_metadata( $id, $metadata ); if ( isset( $image_meta['orientation'] ) && absint( $image_meta['orientation'] ) === 0 ) { return true; } } return false; } /** * Return array of operations to be done for provided orientation value. * * @param int $orientation EXIF orientation value. * * @return array */ private function calculate_transformation( $orientation ) { $rotate = false; $flip = false; $degree = 0; switch ( $orientation ) { case 2: $flip = [ false, true ]; // $flip image along given axis [ horizontal, vertical ] break; case 3: $flip = [ true, true ]; break; case 4: $flip = [ true, false ]; break; case 5: $degree = -90; $rotate = true; $flip = [ false, true ]; break; case 6: $degree = -90; $rotate = true; break; case 7: $degree = 90; $rotate = true; $flip = [ false, true ]; break; case 8: $degree = 90; $rotate = true; break; default: $degree = 0; $rotate = true; break; } return [ 'flip' => $flip, 'degree' => $degree, 'rotate' => $rotate, ]; } /** * Add compatibility indirection to get_attached_file(). * * In WordPress 5.3, behavior changed to account for automatic resizing of * big image files. * * @see https://core.trac.wordpress.org/ticket/47873 * * @param int $attachment_id ID of the attachment to get the filepath for. * @return string|false Filepath of the attachment, or false if not found. */ private function get_attached_file( $attachment_id ) { if ( function_exists( 'wp_get_original_image_path' ) ) { $filepath = wp_get_original_image_path( $attachment_id ); if ( false !== $filepath ) { return $filepath; } } return get_attached_file( $attachment_id ); } /** * Image-friendly alternative to wp_get_attachment_url(). Will return the full size URL of an image instead of the `-scaled` version. * * In WordPress 5.3, behavior changed to account for automatic resizing of * big image files. * * @see https://core.trac.wordpress.org/ticket/47873 * * @param int $attachment_id ID of the attachment to get the URL for. * @return string|false URL of the attachment, or false if not found. */ private function get_real_attachment_url( $attachment_id ) { if ( function_exists( 'wp_get_original_image_url' ) ) { $url = wp_get_original_image_url( $attachment_id ); if ( false !== $url ) { return $url; } } return wp_get_attachment_url( $attachment_id ); } /** * Create image slug based on user input slug. * Add basename extension to slug. * * @param string $basename Default slu of image. * @param string $slug User input slug. * * @return string Image slug with extension. */ private function get_image_name( $basename, $slug ) { $extension = pathinfo( $basename, PATHINFO_EXTENSION ); return $slug . '.' . $extension; } /** * Removes files for unknown/unregistered image sizes. * * Similar to {@see self::remove_old_images} but also updates metadata afterwards. * * @param int $id Attachment ID. * @param string $fullsizepath Filepath of the attachment. * * @return void */ private function delete_unknown_image_sizes( $id, $fullsizepath ) { $original_meta = wp_get_attachment_metadata( $id ); $image_sizes = wp_list_pluck( $this->get_registered_image_sizes(), 'name' ); $dir_path = dirname( $fullsizepath ) . '/'; $sizes_to_delete = array(); if ( isset( $original_meta['sizes'] ) ) { foreach ( $original_meta['sizes'] as $size_name => $size_meta ) { if ( 'full' === $size_name ) { continue; } if ( ! in_array( $size_name, $image_sizes, true ) ) { $intermediate_path = $dir_path . $size_meta['file']; if ( $intermediate_path === $fullsizepath ) { continue; } if ( file_exists( $intermediate_path ) ) { unlink( $intermediate_path ); } $sizes_to_delete[] = $size_name; } } foreach ( $sizes_to_delete as $size_name ) { unset( $original_meta['sizes'][ $size_name ] ); } } wp_update_attachment_metadata( $id, $original_meta ); } }