diff --git a/features/media-fix-orientation.feature b/features/media-fix-orientation.feature index ce3cbed8..d02fd3d3 100644 --- a/features/media-fix-orientation.feature +++ b/features/media-fix-orientation.feature @@ -11,9 +11,8 @@ Feature: Fix WordPress attachments orientation Error: No images found. """ - # On WP 4.9 tests this results in "Couldn't fix orientation". - # Todo: Revisit this test and improve or potentially remove it if useless. - @require-extension-exif @require-wp-4.0 @less-than-wp-4.9 + # On WP 5.3+, images are auto-rotated by WordPress during import, so fix-orientation reports them as already fixed. + @require-extension-exif @require-wp-4.0 @less-than-wp-5.3 Scenario: Fix orientation for all images Given download: | path | url | @@ -96,10 +95,8 @@ Feature: Fix WordPress attachments orientation Success: Images already fixed. """ - # On newer versions (5.3+) the image is already considered fixed. - # On WP 4.9 tests this results in "Couldn't fix orientation". - # Todo: Revisit this test and improve or potentially remove it if useless. - @require-extension-exif @require-wp-4.0 @less-than-wp-4.9 + # On WP 5.3+, images are auto-rotated by WordPress during import, so fix-orientation reports them as already fixed. + @require-extension-exif @require-wp-4.0 @less-than-wp-5.3 Scenario: Fix orientation for single image Given download: | path | url | @@ -122,6 +119,52 @@ Feature: Fix WordPress attachments orientation Success: Image already fixed. """ + # This specifically tests the Imagick flip-only path (orientations 2, 4) where + # WP_Image_Editor_Imagick::flip() does not update the EXIF orientation tag, requiring + # explicit metadata normalization after the fix. + @require-extension-exif @require-extension-imagick @require-wp-4.0 @less-than-wp-5.3 + Scenario: Fix flip-only orientation with Imagick + Given download: + | path | url | + | {CACHE_DIR}/landscape-2.jpg | https://raw.githubusercontent.com/thrijith/test-images/master/Landscape_2.jpg | + | {CACHE_DIR}/portrait-4.jpg | https://raw.githubusercontent.com/thrijith/test-images/master/Portrait_4.jpg | + And I run `wp option update uploads_use_yearmonth_folders 0` + + When I run `wp media import {CACHE_DIR}/landscape-2.jpg --title="Landscape Two" --porcelain` + Then save STDOUT as {LANDSCAPE_TWO} + + When I run `wp media import {CACHE_DIR}/portrait-4.jpg --title="Portrait Four" --porcelain` + Then save STDOUT as {PORTRAIT_FOUR} + + When I run `wp media fix-orientation` + Then STDOUT should contain: + """ + Fixing orientation for "Landscape Two" (ID {LANDSCAPE_TWO}). + """ + And STDOUT should contain: + """ + Fixing orientation for "Portrait Four" (ID {PORTRAIT_FOUR}). + """ + And STDOUT should contain: + """ + Success: Fixed 2 of 2 images. + """ + + # Verify that a second run reports no fix required (metadata normalized after save). + When I run `wp media fix-orientation` + Then STDOUT should contain: + """ + No orientation fix required for "Landscape Two" (ID {LANDSCAPE_TWO}). + """ + And STDOUT should contain: + """ + No orientation fix required for "Portrait Four" (ID {PORTRAIT_FOUR}). + """ + And STDOUT should contain: + """ + Success: Images already fixed. + """ + @require-wp-4.0 Scenario: Fix orientation for non existent image When I try `wp media fix-orientation 9999` @@ -129,3 +172,57 @@ Feature: Fix WordPress attachments orientation """ Error: No images found. """ + + @require-extension-exif @require-wp-5.3 + Scenario: Fix orientation for all images already auto-rotated by WordPress + Given download: + | path | url | + | {CACHE_DIR}/landscape-2.jpg | https://raw.githubusercontent.com/thrijith/test-images/master/Landscape_2.jpg | + | {CACHE_DIR}/landscape-5.jpg | https://raw.githubusercontent.com/thrijith/test-images/master/Landscape_5.jpg | + | {CACHE_DIR}/portrait-4.jpg | https://raw.githubusercontent.com/thrijith/test-images/master/Portrait_4.jpg | + And I run `wp option update uploads_use_yearmonth_folders 0` + + When I run `wp media import {CACHE_DIR}/landscape-2.jpg --title="Landscape Two" --porcelain` + Then save STDOUT as {LANDSCAPE_TWO} + + When I run `wp media import {CACHE_DIR}/landscape-5.jpg --title="Landscape Five" --porcelain` + Then save STDOUT as {LANDSCAPE_FIVE} + + When I run `wp media import {CACHE_DIR}/portrait-4.jpg --title="Portrait Four" --porcelain` + Then save STDOUT as {PORTRAIT_FOUR} + + When I run `wp media fix-orientation` + Then STDOUT should contain: + """ + No orientation fix required for "Portrait Four" (ID {PORTRAIT_FOUR}). + """ + + And STDOUT should contain: + """ + No orientation fix required for "Landscape Five" (ID {LANDSCAPE_FIVE}). + """ + + And STDOUT should contain: + """ + No orientation fix required for "Landscape Two" (ID {LANDSCAPE_TWO}). + """ + + And STDOUT should contain: + """ + Success: Images already fixed. + """ + + @require-extension-exif @require-wp-5.3 + Scenario: Fix orientation for single image already auto-rotated by WordPress + Given download: + | path | url | + | {CACHE_DIR}/portrait-6.jpg | https://raw.githubusercontent.com/thrijith/test-images/master/Portrait_6.jpg | + When I run `wp media import {CACHE_DIR}/portrait-6.jpg --title="Portrait Six" --porcelain` + Then save STDOUT as {PORTRAIT_SIX} + + When I run `wp media fix-orientation {PORTRAIT_SIX}` + Then STDOUT should be: + """ + 1/1 No orientation fix required for "Portrait Six" (ID {PORTRAIT_SIX}). + Success: Image already fixed. + """ diff --git a/src/Media_Command.php b/src/Media_Command.php index eaa49ccf..6fe25421 100644 --- a/src/Media_Command.php +++ b/src/Media_Command.php @@ -1462,14 +1462,48 @@ private function process_orientation_fix( $id, $progress, &$successes, &$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'] : []; + // Get current metadata of the attachment from the database. + $metadata = wp_get_attachment_metadata( $id ); + $image_meta = is_array( $metadata ) && ! empty( $metadata['image_meta'] ) ? $metadata['image_meta'] : []; - if ( isset( $image_meta['orientation'] ) && absint( $image_meta['orientation'] ) > 1 ) { + // Determine orientation from DB metadata first. + $orientation = isset( $image_meta['orientation'] ) ? absint( $image_meta['orientation'] ) : 0; + + if ( $orientation > 1 ) { + // DB shows orientation > 1, but WP 5.3+ may have already auto-rotated the image + // on import (via wp_maybe_exif_rotate()), storing the original EXIF value before + // rotating. On WP < 5.3 this behavior does not occur, so skip the extra EXIF read. + if ( Utils\wp_version_compare( '5.3', '>=' ) ) { + // Verify against the file's current EXIF: if it is <= 1 the image is already + // correctly oriented and no fix is needed. + $file_image_meta = wp_read_image_metadata( $full_size_path ); + if ( is_array( $file_image_meta ) && isset( $file_image_meta['orientation'] ) ) { + $raw_orientation = $file_image_meta['orientation']; + $file_orientation = is_scalar( $raw_orientation ) ? absint( $raw_orientation ) : 0; + if ( $file_orientation <= 1 ) { + $orientation = $file_orientation; + } + } + } + } elseif ( empty( $image_meta ) || ! isset( $image_meta['orientation'] ) ) { + // DB has no orientation data at all (stale/absent metadata). Fall back to reading + // from the file's EXIF so the command still works for such attachments. + $file_image_meta = wp_read_image_metadata( $full_size_path ); + if ( is_array( $file_image_meta ) && isset( $file_image_meta['orientation'] ) ) { + $raw_orientation = $file_image_meta['orientation']; + $file_orientation = is_scalar( $raw_orientation ) ? absint( $raw_orientation ) : 0; + if ( $file_orientation > 1 ) { + // Merge file-based metadata so flip_rotate_image() has the orientation. + $image_meta = array_merge( $image_meta, $file_image_meta ); + $orientation = $file_orientation; + } + } + } + + if ( $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 ) ) { + if ( false !== $this->flip_rotate_image( $id, $image_meta, $full_size_path ) ) { ++$successes; } else { ++$errors; @@ -1488,13 +1522,12 @@ private function process_orientation_fix( $id, $progress, &$successes, &$errors, * 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 ) { + private function flip_rotate_image( $id, $image_meta, $full_size_path ) { $editor = wp_get_image_editor( $full_size_path ); if ( ! is_wp_error( $editor ) ) { @@ -1510,17 +1543,31 @@ private function flip_rotate_image( $id, $metadata, $image_meta, $full_size_path $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']; + $saved = $editor->save( $full_size_path ); - // Update attachment metadata with newly generated data. - wp_update_attachment_metadata( $id, $metadata ); + if ( is_wp_error( $saved ) ) { + return false; + } - if ( isset( $image_meta['orientation'] ) && absint( $image_meta['orientation'] ) === 0 ) { - return true; + // Regenerate attachment metadata after the corrected image is saved. + $metadata = wp_generate_attachment_metadata( $id, $full_size_path ); + + if ( empty( $metadata ) ) { + return false; + } + + // Normalize the stored orientation to prevent re-detection on subsequent runs. + // WP_Image_Editor_Imagick::flip() does not reset the EXIF orientation tag in the + // file, so the file may still report a non-normal orientation even though the pixels + // have been corrected. Forcing orientation to 0 in the stored metadata ensures the + // next run reports "No orientation fix required". + if ( isset( $metadata['image_meta']['orientation'] ) ) { + $metadata['image_meta']['orientation'] = 0; } + + wp_update_attachment_metadata( $id, $metadata ); + + return true; } return false;