Skip to content
Merged
111 changes: 104 additions & 7 deletions features/media-fix-orientation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand All @@ -122,10 +119,110 @@ 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`
Then STDERR should be:
"""
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.
"""
77 changes: 62 additions & 15 deletions src/Media_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ) ) {
Expand All @@ -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;
Expand Down
Loading