diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index a72fa4e5b..2ccea8b76 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -10,6 +10,7 @@ - added: example / thumbnail images for templates - added: Style Picker as part of the initial setup wizard - added: CHI cache driver + - added #11498: Gallery: Automatically adjust orientation of images based on EXIF data 7.9.3 - added #11477: No synopsis in asset now means no synopsis in search index diff --git a/lib/WebGUI/Asset/File/GalleryFile/Photo.pm b/lib/WebGUI/Asset/File/GalleryFile/Photo.pm index 3fa02ca54..d2bf05df9 100644 --- a/lib/WebGUI/Asset/File/GalleryFile/Photo.pm +++ b/lib/WebGUI/Asset/File/GalleryFile/Photo.pm @@ -118,7 +118,11 @@ sub applyConstraints { my $parameters = $self->get("parameters"); my $storage = $self->getStorageLocation; my $file = $self->get("filename"); - + + # Adjust orientation based on exif data. Do this before we start to + # generate resolutions so that all images have the correct orientation. + $self->adjustOrientation; + # Make resolutions before fixing image, so that we can get higher quality # resolutions $self->makeResolutions; @@ -130,9 +134,64 @@ sub applyConstraints { $self->generateThumbnail; $self->setSize; $self->updateExifDataFromFile; + $self->SUPER::applyConstraints( $options ); } +#---------------------------------------------------------------------------- + +=head2 adjustOrientation ( ) + +Read orientation information from EXIF data and rotate image if required. +EXIF data is updated to reflect the new orientation of the image. + +=cut + +sub adjustOrientation { + my $self = shift; + my $storage = $self->getStorageLocation; + + # Extract orientation information from EXIF data + my $exifTool = Image::ExifTool->new; + $exifTool->ExtractInfo( $storage->getPath( $self->get('filename') ) ); + my $orientation = $exifTool->GetValue('Orientation', 'ValueConv'); + + # Check whether orientation information is present and transform image if + # required. At the moment we handle only images that need to be rotated by + # (-)90 or 180 deg. Flipping of images is not supported yet. + if ( $orientation ) { + + # We are going to update orientation information before the image is + # rotated. Otherwise we would have to re-extract EXIF data due to + # manipulation by Image Magick. + + # Update orientation information + $exifTool->SetNewValue( 'Exif:Orientation' => 1, Type => 'ValueConv'); + + # Set the following options to make this as robust as possible + $exifTool->Options( 'IgnoreMinorErrors', FixBase => '' ); + # Write updated exif data to disk + $exifTool->WriteInfo( $storage->getPath( $self->get('filename') ) ); + + # Log any errors + my $error = $exifTool->GetValue('Error'); + $self->session->log->error( "Error on updating exif data: $error" ) if $error; + + # Image rotated by 180° + if ( $orientation == 3 || $orientation == 4 ) { + $self->rotate(180); + } + # Image rotated by 90° CCW + elsif ( $orientation == 5 || $orientation == 6 ) { + $self->rotate(90); + } + # Image rotated by 90° CW + elsif ( $orientation == 7 || $orientation == 8 ) { + $self->rotate(-90); + } + } +} + #------------------------------------------------------------------- =head2 generateThumbnail ( ) diff --git a/t/Asset/File/GalleryFile/Photo/adjustOrientation.t b/t/Asset/File/GalleryFile/Photo/adjustOrientation.t new file mode 100644 index 000000000..53581528e --- /dev/null +++ b/t/Asset/File/GalleryFile/Photo/adjustOrientation.t @@ -0,0 +1,133 @@ +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 Plain Black Corporation. +#------------------------------------------------------------------- +# Please read the legal notices (docs/legal.txt) and the license +# (docs/license.txt) that came with this distribution before using +# this software. +#------------------------------------------------------------------- +# http://www.plainblack.com info@plainblack.com +#------------------------------------------------------------------- + +use FindBin; +use strict; +use lib "$FindBin::Bin/../../../../lib"; + +# Test the 'adjustOrientation' method called by 'applyConstraints'. It is +# responsible for rotating JPEG images according to orientation information +# in EXIF data (if present). A number of test images have been created for +# this purpose which are checked based on dimensions and pixel-wise. + +use WebGUI::Test; +use WebGUI::Session; +use WebGUI::Asset::File::GalleryFile::Photo; + +use Image::Magick; +use Test::More; +use Test::Deep; + +#---------------------------------------------------------------------------- +# Init +my $session = WebGUI::Test->session; +my $node = WebGUI::Asset->getImportNode($session); +my $versionTag = WebGUI::VersionTag->getWorking($session); + +# Name version tag and make sure it gets cleaned up +$versionTag->set({name=>"Orientation adjustment test"}); +addToCleanup($versionTag); + +# Create gallery and a single album +my $gallery + = $node->addChild({ + className => "WebGUI::Asset::Wobject::Gallery", + imageResolutions => "1024x768", + }, + undef, + undef, + { + skipAutoCommitWorkflows => 1, + }); +my $album + = $gallery->addChild({ + className => "WebGUI::Asset::Wobject::GalleryAlbum", + }, + undef, + undef, + { + skipAutoCommitWorkflows => 1, + }); + +# Create single photo inside the album +my $photo + = $album->addChild({ + className => "WebGUI::Asset::File::GalleryFile::Photo", + }, + undef, + undef, + { + skipAutoCommitWorkflows => 1, + }); + +# Commit all changes +$versionTag->commit; + +#---------------------------------------------------------------------------- +# Tests +plan tests => 8; + +#---------------------------------------------------------------------------- +# Test adjustment of image with orientation set to 1 + +$photo->setFile( WebGUI::Test->getTestCollateralPath('orientation_1.jpg') ); +my $storage = $photo->getStorageLocation; + +# Check dimensions +cmp_deeply( [ $storage->getSizeInPixels($photo->get('filename')) ], [2, 3], "Check if image with orientation 1 was left as is (based on dimensions)" ); + +# Check single pixel +my $image = new Image::Magick; +$image->Read( $storage->getPath( $photo->get('filename') ) ); +cmp_deeply( [ $image->GetPixel( x=>1, y=>1 ) ], [ 1, 1, 1], "Check if image with orientation 1 was left as is (based on pixel values)"); + +#---------------------------------------------------------------------------- +# Test adjustment of image with orientation set to 3 + +# Attach new image to Photo asset +$photo->setFile( WebGUI::Test->getTestCollateralPath('orientation_3.jpg') ); +my $storage = $photo->getStorageLocation; + +# Check dimensions +cmp_deeply( [ $storage->getSizeInPixels($photo->get('filename')) ], [2, 3], "Check if image with orientation 3 was rotated by 180° (based on dimensions)" ); + +# Check single pixel +$image->Read( $storage->getPath( $photo->get('filename') ) ); +cmp_deeply( [ $image->GetPixel( x=>2, y=>3 ) ], [ 1, 1, 1], "Check if image with orientation 3 was rotated by 180° (based on pixels)"); + + +#---------------------------------------------------------------------------- +# Test adjustment of image with orientation set to 6 + +# Attach new image to Photo asset +$photo->setFile( WebGUI::Test->getTestCollateralPath('orientation_6.jpg') ); +my $storage = $photo->getStorageLocation; + +# Check dimensions +cmp_deeply( [ $storage->getSizeInPixels($photo->get('filename')) ], [3, 2], "Check if image with orientation 6 was rotated by 90° CW (based on dimensions)" ); + +# Check single pixel +$image->Read( $storage->getPath( $photo->get('filename') ) ); +cmp_deeply( [ $image->GetPixel( x=>3, y=>1 ) ], [ 1, 1, 1], "Check if image with orientation 6 was rotated by 90° CW (based on pixels)"); + + +#---------------------------------------------------------------------------- +# Test adjustment of image with orientation set to 8 + +# Attach new image to Photo asset +$photo->setFile( WebGUI::Test->getTestCollateralPath('orientation_8.jpg') ); +my $storage = $photo->getStorageLocation; + +# Check dimensions +cmp_deeply( [ $storage->getSizeInPixels($photo->get('filename')) ], [3, 2], "Check if image with orientation 8 was rotated by 90° CCW (based on dimensions)" ); + +# Check single pixel +$image->Read( $storage->getPath( $photo->get('filename') ) ); +cmp_deeply( [ $image->GetPixel( x=>1, y=>2 ) ], [ 1, 1, 1], "Check if image with orientation 8 was rotated by 90° CCW (based on pixels)"); diff --git a/t/Storage.t b/t/Storage.t index 37c726f9a..b981b6603 100644 --- a/t/Storage.t +++ b/t/Storage.t @@ -19,11 +19,11 @@ use WebGUI::PseudoRequest; use File::Spec; use File::Temp qw/tempdir/; +use Image::Magick; use Test::More; use Test::Deep; use Test::MockObject; use Cwd; -use Data::Dumper; use Path::Class::Dir; my $session = WebGUI::Test->session; @@ -32,7 +32,7 @@ my $cwd = Cwd::cwd(); my ($extensionTests, $fileIconTests) = setupDataDrivenTests($session); -my $numTests = 136; # increment this value for each test you create +my $numTests = 140; # increment this value for each test you create plan tests => $numTests + scalar @{ $extensionTests } + scalar @{ $fileIconTests }; my $uploadDir = $session->config->get('uploadsPath'); @@ -539,6 +539,39 @@ is ($privs, '{"assets":[],"groups":["3","3"],"users":["3"]}', '... correct group is ($privs, '{"assets":["' . $asset->getId . '"],"groups":[],"users":[]}', '... correct asset contents'); } +#################################################### +# +# rotate +# +#################################################### + +# Create new storage for test of 'rotate' method +my $rotateTestStorage = WebGUI::Storage->create($session); +addToCleanup($rotateTestStorage); + +# Add test image from file system +my $file = "rotation_test.png"; +$rotateTestStorage->addFileFromFilesystem( WebGUI::Test->getTestCollateralPath($file) ); + +# Rotate image by 90° CW +$rotateTestStorage->rotate( $file, 90 ); + +# Test based on dimensions +cmp_deeply( [ $rotateTestStorage->getSizeInPixels($file) ], [ 3, 2 ], "rotate: check if image was rotated by 90° CW (based on dimensions)" ); +# Test based on single pixel +my $image = new Image::Magick; +$image->Read( $rotateTestStorage->getPath( $file ) ); +is( $image->GetPixel( x=>3, y=>1 ), 1, "rotate: check if image was rotated by 90° CW (based on pixels)"); + +# Rotate image by 90° CCW +$rotateTestStorage->rotate( $file, -90 ); + +# Test based on dimensions +cmp_deeply( [ $rotateTestStorage->getSizeInPixels($file) ], [ 2, 3 ], "rotate: check if image was rotated by 90° CCW (based on dimensions)" ); +# Test based on single pixel +my $image = new Image::Magick; +$image->Read( $rotateTestStorage->getPath( $file ) ); +is( $image->GetPixel( x=>1, y=>1 ), 1, "rotate: check if image was rotated by 90° CCW (based on pixels)"); #################################################### # diff --git a/t/supporting_collateral/orientation_1.jpg b/t/supporting_collateral/orientation_1.jpg new file mode 100644 index 000000000..b11aac2d1 Binary files /dev/null and b/t/supporting_collateral/orientation_1.jpg differ diff --git a/t/supporting_collateral/orientation_3.jpg b/t/supporting_collateral/orientation_3.jpg new file mode 100644 index 000000000..f63c485ff Binary files /dev/null and b/t/supporting_collateral/orientation_3.jpg differ diff --git a/t/supporting_collateral/orientation_6.jpg b/t/supporting_collateral/orientation_6.jpg new file mode 100644 index 000000000..8a5a3f0cd Binary files /dev/null and b/t/supporting_collateral/orientation_6.jpg differ diff --git a/t/supporting_collateral/orientation_8.jpg b/t/supporting_collateral/orientation_8.jpg new file mode 100644 index 000000000..5049f6bd9 Binary files /dev/null and b/t/supporting_collateral/orientation_8.jpg differ