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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autorotate images based on EXIF metadata #52

Merged
merged 8 commits into from Jul 2, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .appveyor.yml
Expand Up @@ -9,7 +9,7 @@ environment:
PHPCI_COMPOSER: C:\tools\phpci\composer

cache:
- '%PHPCI_CACHE%'
- '%PHPCI_CACHE% -> appveyor.yml'
ausi marked this conversation as resolved.
Show resolved Hide resolved

init:
- SET PATH=%PHPCI_PHP%;%PHPCI_COMPOSER%;%PATH%
Expand All @@ -33,6 +33,7 @@ install:
- IF %PHP%==0 echo extension=php_gd2.dll >> php.ini
- IF %PHP%==0 echo extension=php_intl.dll >> php.ini
- IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini
- IF %PHP%==0 echo extension=php_exif.dll >> php.ini
- IF %PHP%==0 echo extension=php_openssl.dll >> php.ini
- IF %PHP%==0 (composer --version) ELSE (composer self-update)
- cd %APPVEYOR_BUILD_FOLDER%
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Expand Up @@ -26,6 +26,9 @@
"phpstan/phpstan-phpunit": "^0.10",
"phpunit/phpunit": "^7.5 || ^8.0"
},
"suggest": {
"ext-exif": "To support EXIF auto-rotation"
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
Expand Down
50 changes: 42 additions & 8 deletions src/Image.php
Expand Up @@ -106,24 +106,35 @@ public function getUrl(string $rootDir, string $prefix = ''): string
public function getDimensions(): ImageDimensionsInterface
{
if (null === $this->dimensions) {
// Try getSvgSize() or native getimagesize() for better performance
// Try getSvgSize() or native exif_read_data()/getimagesize() for better performance
if ($this->imagine instanceof SvgImagine) {
$size = $this->getSvgSize();

if (null !== $size) {
$this->dimensions = new ImageDimensions($size);
}
} else {
$size = @getimagesize($this->path);

if (!empty($size[0]) && !empty($size[1])) {
$this->dimensions = new ImageDimensions(new Box($size[0], $size[1]));
}
} elseif (
\function_exists('exif_read_data')
&& ($exif = @exif_read_data($this->path, 'COMPUTED,IFD0'))
&& !empty($exif['COMPUTED']['Width'])
&& !empty($exif['COMPUTED']['Height'])
) {
$orientation = $exif['Orientation'] ?? ImageDimensionsInterface::ORIENTATION_NORMAL;
$size = $this->fixOrientation(new Box($exif['COMPUTED']['Width'], $exif['COMPUTED']['Height']), $orientation);
$this->dimensions = new ImageDimensions($size, null, null, $orientation);
} elseif (
($size = @getimagesize($this->path))
&& !empty($size[0]) && !empty($size[1])
) {
$this->dimensions = new ImageDimensions(new Box($size[0], $size[1]));
}

// Fall back to Imagine
if (null === $this->dimensions) {
$this->dimensions = new ImageDimensions($this->imagine->open($this->path)->getSize());
$imagineImage = $this->imagine->open($this->path);
$orientation = $imagineImage->metadata()->get('ifd0.Orientation') ?? ImageDimensionsInterface::ORIENTATION_NORMAL;
$size = $this->fixOrientation($imagineImage->getSize(), $orientation);
$this->dimensions = new ImageDimensions($size, null, null, $orientation);
}
}

Expand All @@ -148,6 +159,29 @@ public function setImportantPart(ImportantPartInterface $importantPart = null):
return $this;
}

/**
* Swaps width and height for (-/+)90 degree rotated orientations.
*/
private function fixOrientation(BoxInterface $size, int $orientation): BoxInterface
{
if (
\in_array(
$orientation,
[
ImageDimensionsInterface::ORIENTATION_NORMAL_90,
ImageDimensionsInterface::ORIENTATION_NORMAL_270,
ImageDimensionsInterface::ORIENTATION_MIRROR_90,
ImageDimensionsInterface::ORIENTATION_MIRROR_270,
],
true
)
) {
return new Box($size->getHeight(), $size->getWidth());
}

return $size;
}

/**
* Reads the SVG image file partially and returns the size of it.
*
Expand Down
20 changes: 19 additions & 1 deletion src/ImageDimensions.php
Expand Up @@ -23,6 +23,11 @@ class ImageDimensions implements ImageDimensionsInterface
*/
private $size;

/**
* @var int
*/
private $orientation;

/**
* @var bool
*/
Expand All @@ -33,8 +38,12 @@ class ImageDimensions implements ImageDimensionsInterface
*/
private $undefined;

public function __construct(BoxInterface $size, bool $relative = null, bool $undefined = null)
public function __construct(BoxInterface $size, bool $relative = null, bool $undefined = null, int $orientation = self::ORIENTATION_NORMAL)
{
if ($orientation < 1 || $orientation > 8) {
throw new \InvalidArgumentException('Orientation must be one of the ImageDimensionsInterface::ORIENTATION_* constants');
}

if (null === $relative) {
$relative = $size instanceof RelativeBoxInterface;
}
Expand All @@ -44,6 +53,7 @@ public function __construct(BoxInterface $size, bool $relative = null, bool $und
}

$this->size = $size;
$this->orientation = $orientation;
$this->relative = $relative;
$this->undefined = $undefined;
}
Expand All @@ -56,6 +66,14 @@ public function getSize(): BoxInterface
return $this->size;
}

/**
* {@inheritdoc}
*/
public function getOrientation(): int
{
return $this->orientation;
}

/**
* {@inheritdoc}
*/
Expand Down
19 changes: 19 additions & 0 deletions src/ImageDimensionsInterface.php
Expand Up @@ -16,11 +16,30 @@

interface ImageDimensionsInterface
{
/**
* Exif 2.32 orientation attribute (tag 274).
*
* @see <http://www.cipa.jp/std/documents/e/DC-008-Translation-2019-E.pdf>
*/
public const ORIENTATION_NORMAL = 1;
public const ORIENTATION_NORMAL_90 = 6;
public const ORIENTATION_NORMAL_180 = 3;
public const ORIENTATION_NORMAL_270 = 8;
public const ORIENTATION_MIRROR = 2;
public const ORIENTATION_MIRROR_90 = 7;
public const ORIENTATION_MIRROR_180 = 4;
ausi marked this conversation as resolved.
Show resolved Hide resolved
public const ORIENTATION_MIRROR_270 = 5;

/**
* Returns the size.
*/
public function getSize(): BoxInterface;

/**
* Returns the orientation flag.
*/
public function getOrientation(): int;

/**
* Returns the relative flag.
*/
Expand Down
23 changes: 23 additions & 0 deletions src/ResizeOptions.php
Expand Up @@ -31,6 +31,11 @@ class ResizeOptions implements ResizeOptionsInterface
*/
private $bypassCache = false;

/**
* @var bool
*/
private $forceReEncoding = false;
ausi marked this conversation as resolved.
Show resolved Hide resolved

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -88,4 +93,22 @@ public function setBypassCache(bool $bypassCache): ResizeOptionsInterface

return $this;
}

/**
* {@inheritdoc}
*/
public function getForceReEncoding(): bool
{
return $this->forceReEncoding;
}

/**
* {@inheritdoc}
*/
public function setForceReEncoding(bool $forceReEncoding): ResizeOptionsInterface
{
$this->forceReEncoding = $forceReEncoding;

return $this;
}
}
10 changes: 10 additions & 0 deletions src/ResizeOptionsInterface.php
Expand Up @@ -43,4 +43,14 @@ public function getBypassCache(): bool;
* Sets the bypass cache flag.
*/
public function setBypassCache(bool $bypassCache): self;

/**
* Returns the force re-encoding flag.
*/
public function getForceReEncoding(): bool;

/**
* Sets the force re-encoding flag.
*/
public function setForceReEncoding(bool $forceReEncoding): self;
}
24 changes: 19 additions & 5 deletions src/Resizer.php
Expand Up @@ -13,6 +13,7 @@
namespace Contao\Image;

use Imagine\Exception\RuntimeException as ImagineRuntimeException;
use Imagine\Filter\Basic\Autorotate;
use Imagine\Image\Palette\RGB;
use Symfony\Component\Filesystem\Filesystem;
use Webmozart\PathUtil\Path;
Expand Down Expand Up @@ -58,7 +59,11 @@ public function __construct(string $cacheDir, ResizeCalculatorInterface $calcula
*/
public function resize(ImageInterface $image, ResizeConfigurationInterface $config, ResizeOptionsInterface $options): ImageInterface
{
if ($config->isEmpty() || $image->getDimensions()->isUndefined()) {
if (
($config->isEmpty() || $image->getDimensions()->isUndefined())
&& ImageDimensionsInterface::ORIENTATION_NORMAL === $image->getDimensions()->getOrientation()
&& !$options->getForceReEncoding()
) {
$image = $this->createImage($image, $image->getPath());
} else {
$image = $this->processResize($image, $config, $options);
Expand Down Expand Up @@ -87,9 +92,13 @@ protected function executeResize(ImageInterface $image, ResizeCoordinatesInterfa

$imagineOptions = $options->getImagineOptions();

$imagineImage = $image
->getImagine()
->open($image->getPath())
$imagineImage = $image->getImagine()->open($image->getPath());

if (ImageDimensionsInterface::ORIENTATION_NORMAL !== $image->getDimensions()->getOrientation()) {
(new Autorotate())->apply($imagineImage);
}

$imagineImage
->resize($coordinates->getSize())
->crop($coordinates->getCropStart(), $coordinates->getCropSize())
->usePalette(new RGB())
Expand Down Expand Up @@ -137,7 +146,12 @@ protected function processResize(ImageInterface $image, ResizeConfigurationInter
$coordinates = $this->calculator->calculate($config, $image->getDimensions(), $image->getImportantPart());

// Skip resizing if it would have no effect
if (!$image->getDimensions()->isRelative() && $coordinates->isEqualTo($image->getDimensions()->getSize())) {
if (
!$image->getDimensions()->isRelative()
&& ImageDimensionsInterface::ORIENTATION_NORMAL === $image->getDimensions()->getOrientation()
&& $coordinates->isEqualTo($image->getDimensions()->getSize())
&& !$options->getForceReEncoding()
) {
return $this->createImage($image, $image->getPath());
}

Expand Down
13 changes: 13 additions & 0 deletions tests/ImageDimensionsTest.php
Expand Up @@ -13,6 +13,7 @@
namespace Contao\Image\Tests;

use Contao\Image\ImageDimensions;
use Contao\Image\ImageDimensionsInterface;
use Contao\ImagineSvg\RelativeBoxInterface;
use Contao\ImagineSvg\UndefinedBoxInterface;
use Imagine\Image\BoxInterface;
Expand All @@ -28,6 +29,18 @@ public function testGetSize(): void
$this->assertSame($size, $dimensions->getSize());
}

public function testGetOrientation(): void
{
$size = $this->createMock(BoxInterface::class);
$dimensions = new ImageDimensions($size, null, null, ImageDimensionsInterface::ORIENTATION_NORMAL_90);

$this->assertSame(ImageDimensionsInterface::ORIENTATION_NORMAL_90, $dimensions->getOrientation());

$this->expectException('InvalidArgumentException');

new ImageDimensions($size, null, null, 0);
}

public function testIsRelative(): void
{
$size = $this->createMock(BoxInterface::class);
Expand Down