Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exif orientation tag related issues and how to solve them #2695

Open
adamkiss opened this issue Jul 7, 2020 · 15 comments
Open

Exif orientation tag related issues and how to solve them #2695

adamkiss opened this issue Jul 7, 2020 · 15 comments
Labels
type: bug 🐛 Is a bug; fixes a bug

Comments

@adamkiss
Copy link
Contributor

adamkiss commented Jul 7, 2020

Describe the bug

If an image includes EXIF orientation code, both GD and ImageMagick behave wildly inconsistently: It might be anything from resizing with dimensions switched, but correct orientation (i.e. thumb(400, 300) creates a 300x400 image), through stretched images, etc.

To Reproduce
Steps to reproduce the behavior:

  1. Go to any image field
  2. Test any of the images available here: https://github.com/recurser/exif-orientation-examples
  3. See the bug

Screenshots

These are screenshots from one of the many seemingly unrelated problems, in this case it's from flokosiol/kirby-focus#50

79700605-29bac500-8297-11ea-8a36-381342cafa9a

79700610-36d7b400-8297-11ea-9724-a3888179f734

Additional context

Few of the solutions found on the internet deal with this by basically "burning the orientation in" and removing the exif orientation tag on upload.

Some examples are in this forum thread, my own looks like this:

<?php

use Kirby\Cms\App;
use Kirby\Toolkit\Str;

App::plugin('adamkiss/fix-orientation', [
	'hooks' => [
		'file.create:after' => function ($file) {
			if (in_array(Str::lower($file->extension()), ['jpg', 'jpeg'])) {
				$filename = $file->root();
				$image = imagecreatefromjpeg($filename);
				$exif = exif_read_data($filename);

				if (!empty($exif['Orientation'])) {
					switch ($exif['Orientation']) {
						case 3:
							$image = imagerotate($image, 180, 0);
							break;

						case 6:
							$image = imagerotate($image, -90, 0);
							break;

						case 8:
							$image = imagerotate($image, 90, 0);
							break;
					}
				}
				imagejpeg($image, $filename);
			}
		}
	]
]);

One problem with this is that the "fix" changes picture files, which might not be always applicable, so maybe configurable option "burn in exif orientation", with default state either backwards compatible (off) or forward problem solving (on) :)

@afbora afbora added the type: bug 🐛 Is a bug; fixes a bug label Jul 7, 2020
@lukasbestle
Copy link
Member

lukasbestle commented Jul 8, 2020

Several users have reported this issue in multiple different variations (for example #1926, I can't find the other issues on the fly).

A new idea: Given that images that are served to the visitor are basically always run through the thumb engine (using the original images in full resolution doesn't make sense for most use-cases), the only place that really needs fixing is the thumb engine itself. It could apply the rotation on a temporary image file before and then pass that rotated image through the actual thumb library. Additionally, we would need to ensure that our other thumb-related classes (e.g. the Image\Dimensions class) all base their calculations on the EXIF data instead of on the actual image size.

Advantage: The original image would be unchanged, but thumbs would be generated correctly.

@adamkiss
Copy link
Contributor Author

@lukasbestle Do the "resize" and "crop" methods use the thumbs class? Because it would be bad if the redults were incosistent, IMO.

@lukasbestle
Copy link
Member

Yes, they do!

@distantnative
Copy link
Member

Would be good to check if GD and ImageMagick fixed their false behaviour in the meantime.

@distantnative distantnative added the needs: information ❓ Requires more information to proceed label Dec 22, 2021
@lukasbestle
Copy link
Member

lukasbestle commented Dec 23, 2021

Don't think it's false behavior in the drivers actually. They just ignore EXIF information and resize/crop exactly like we tell them to on a pixel level.

@lukasbestle lukasbestle removed the needs: information ❓ Requires more information to proceed label Dec 23, 2021
@silllli
Copy link
Contributor

silllli commented Feb 2, 2022

Since I changed from the Gd to the ImageMagick driver in my latest project, I have problems with this. The image orientation is read correctly (due to the -auto-orient option) from the EXIF data, but the resize() function of the ImageMagick driver uses the file’s original dimensions, squeezing rotated images into wrong dimensions. Shouldn’t width and height be flipped according to rotation information in the EXIF data?

return '-thumbnail ' . escapeshellarg(sprintf('%sx%s!', $options['width'], $options['height']));

Something like this:

$image = new Image($file);

$flip = false;
$problematicOrientations = [6, 8];

// check if orientation was set in EXIF data
if (array_key_exists('Orientation', $image->exif()->data())) {
  $fileOrientation = $image->exif()->data()['Orientation'];
  
  // check if a rotated orientation was set
  if (in_array($fileOrientation, $problematicOrientations)) $flip = true;
}

$width = $flip ? $options['height'] : $options['width'];
$height = $flip ? $options['width'] : $options['height'];

return '-thumbnail ' . escapeshellarg(sprintf('%dx%d!', $width, $height)); 

@lukasbestle
Copy link
Member

@silllli Thanks for sharing. So you're saying that you didn't have these problems when using GD?

@silllli
Copy link
Contributor

silllli commented Feb 5, 2022

@silllli Thanks for sharing. So you're saying that you didn't have these problems when using GD?

Exactly. I noticed it with one image, which was fine before I started using the ImageMagick driver. I switched back to Gd to be sure and the thumbnail was created correctly again.

@Jayshua
Copy link

Jayshua commented Jul 18, 2022

@silllli Thanks for sharing. So you're saying that you didn't have these problems when using GD?

Exactly. I noticed it with one image, which was fine before I started using the ImageMagick driver. I switched back to Gd to be sure and the thumbnail was created correctly again.

I had the opposite issue - the ImageMagick driver worked but the GD driver swapped the width/height of the crop. I looked at the source for the GD driver in GdLib.php and found that the image is resized before it is oriented. Presumably resize doesn't consider the EXIF orientation. The crop is applied correctly if I swap the ->autoOrient() and ->resize() calls so that the image is oriented before resizing.

// kirby/src/Image/Darkroom/GdLib.php:35

- $image = $this->resize($image, $options);
- $image = $this->autoOrient($image, $options);

+ $image = $this->autoOrient($image, $options);
+ $image = $this->resize($image, $options);

To confirm, I checked the ImageMagick driver and found it behaves the same way, except the order of operations is already correct.

// kirby/src/Image/Darkroom/ImageMagick.php:136

$command[] = $this->autoOrient($file, $options);
$command[] = $this->resize($file, $options);

If I swap those two lines so that the resize is applied before the orientation, ImageMagick crops incorrectly just like the GD driver.

Here's the image I used. Assuming Github didn't strip EXIF data from it, it should have an Orientation tag set to Rotate 90 CW.

img_1045

@lukasbestle
Copy link
Member

Thanks for sharing. Reading the diff from the GD driver it makes a lot of sense. But now I'm confused why GD has worked for @silllli but not ImageMagick. Maybe we should build a collection of different test images that fail in one or the other driver. Ideal of course would be rather small files that we can then include in our unit tests as test fixtures.

@fabianmichael
Copy link
Contributor

Here is another example image where Kirby/GD/ImageMagic does not recognize EXIF rotation correctly, provided by one of my clients:

img_3510

@rasteiner
Copy link
Contributor

Has someone tried fixing the Dimensions class first? In the end the drivers do what the Dimensions class says.

A fix would be something like:

	/**
	 * Detect the dimensions for an image file
	 */
	public static function forImage(string $root): static
	{
		if (file_exists($root) === false) {
			return new static(0, 0);
		}

		// create Image to read exif Orientation data
		$image = new Image($root);
		$orientation = $image->exif()->data()['Orientation'] ?? 1;

		// orientation 1 is normal, 2 - 4 are flipped, 5 - 8 are rotated
		if($orientation < 5) {
			list($width, $height) = getimagesize($root);
		} else {
			list($height, $width) = getimagesize($root);
		}

		return new static($width ?? 0, $height ?? 1);
	}

If you want test images, here are images in all possible orientations (actually also a 0 orientation which technically doesn't exist, afaik): https://github.com/recurser/exif-orientation-examples

@rasteiner
Copy link
Contributor

I've also made these: https://github.com/rasteiner/exif-orientation-test-images
The idea was to have images that are simple to automatically test, hence the 4 colored rects...

You can test the test images by putting them into a Kirby installation and see that for images 5, 6, 7 and 8 the panel lies to you:
image

@distantnative
Copy link
Member

With @rasteiner's test images, what seems to work for me is a combination:

// Kirby\Image\Darkroom\GdLib::process()

+ $image = $this->autoOrient($image, $options);
+ $image = $this->resize($image, $options);
- $image = $this->resize($image, $options);
- $image = $this->autoOrient($image, $options);
// Kirby\Image\Dimensions

public static function forImage(string $root): static
{
	if (file_exists($root) === false) {
		return new static(0, 0);
	}

	// create Image to read exif Orientation data
	$image 		 = new Image($root);
	$orientation = $image->exif()->data()['Orientation'] ?? 1;

	// orientation 1 is normal, 2 - 4 are flipped, 5 - 8 are rotated
	if($orientation < 5) {
		[$width, $height] = getimagesize($root);
	} else {
		[$height, $width] = getimagesize($root);
	}

	return new static($width ?? 0, $height ?? 1);
}

@distantnative
Copy link
Member

I've started a PR. Would appreciate help to add unit tests with proper small test images for both libraries #5071

@distantnative distantnative linked a pull request Feb 25, 2023 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug 🐛 Is a bug; fixes a bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants