Skip to content

Commit

Permalink
fix: set exif orientation from irot/imir when decoding AVIF
Browse files Browse the repository at this point in the history
  • Loading branch information
fdintino committed Dec 9, 2024
1 parent de4c6c1 commit 524d802
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 10 deletions.
Binary file added Tests/images/avif/rot0mir0.avif
Binary file not shown.
Binary file added Tests/images/avif/rot0mir1.avif
Binary file not shown.
Binary file added Tests/images/avif/rot1mir0.avif
Binary file not shown.
Binary file added Tests/images/avif/rot1mir1.avif
Binary file not shown.
Binary file added Tests/images/avif/rot2mir0.avif
Binary file not shown.
Binary file added Tests/images/avif/rot2mir1.avif
Binary file not shown.
Binary file added Tests/images/avif/rot3mir0.avif
Binary file not shown.
Binary file added Tests/images/avif/rot3mir1.avif
Binary file not shown.
50 changes: 46 additions & 4 deletions Tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pathlib import Path
from struct import unpack
from typing import Any
from unittest import mock

import pytest

Expand Down Expand Up @@ -329,24 +330,65 @@ def test_exif(self) -> None:
exif = im.getexif()
assert exif[274] == 3

@pytest.mark.parametrize("bytes", [True, False])
def test_exif_save(self, tmp_path: Path, bytes: bool) -> None:
@pytest.mark.parametrize("bytes,orientation", [(True, 1), (False, 2)])
def test_exif_save(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
bytes: bool,
orientation: int,
) -> None:
mock_avif_encoder = mock.Mock(wraps=_avif.AvifEncoder)
monkeypatch.setattr(_avif, "AvifEncoder", mock_avif_encoder)
exif = Image.Exif()
exif[274] = 1
exif[274] = orientation
exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, exif=exif_data if bytes else exif)

with Image.open(test_file) as reloaded:
assert reloaded.info["exif"] == exif_data
if orientation == 1:
assert "exif" not in reloaded.info
else:
assert reloaded.info["exif"] == exif_data
mock_avif_encoder.mock_calls[0].args[16:17] == (b"", orientation)

def test_exif_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(SyntaxError):
im.save(test_file, exif=b"invalid")

@pytest.mark.parametrize(
"rot,mir,exif_orientation",
[
(0, 0, 4),
(0, 1, 2),
(1, 0, 5),
(1, 1, 7),
(2, 0, 2),
(2, 1, 4),
(3, 0, 7),
(3, 1, 5),
],
)
def test_rot_mir_exif(
self, rot: int, mir: int, exif_orientation: int, tmp_path: Path
) -> None:
with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im:
exif = im.info["exif"]
test_file = str(tmp_path / "temp.avif")
im.save(test_file, exif=exif)

exif_data = Image.Exif()
exif_data.load(exif)
assert exif_data[274] == exif_orientation
with Image.open(test_file) as reloaded:
exif_data = Image.Exif()
exif_data.load(reloaded.info["exif"])
assert exif_data[274] == exif_orientation

def test_xmp(self) -> None:
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
xmp = im.info["xmp"]
Expand Down
23 changes: 20 additions & 3 deletions src/PIL/AvifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def _open(self) -> None:
)

# Get info from decoder
width, height, n_frames, mode, icc, exif, xmp = self._decoder.get_info()
width, height, n_frames, mode, icc, exif, xmp, exif_orientation = (
self._decoder.get_info()
)
self._size = width, height
self.n_frames = n_frames
self.is_animated = self.n_frames > 1
Expand All @@ -99,6 +101,16 @@ def _open(self) -> None:
if xmp:
self.info["xmp"] = xmp

if exif_orientation != 1 or exif is not None:
exif_data = Image.Exif()
orig_orientation = 1
if exif is not None:
exif_data.load(exif)
orig_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
if exif_orientation != orig_orientation:
exif_data[ExifTags.Base.Orientation] = exif_orientation
self.info["exif"] = exif_data.tobytes()

def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
Expand Down Expand Up @@ -176,9 +188,14 @@ def _save(
else:
exif_data = Image.Exif()
exif_data.load(exif)
exif_orientation = exif_data.pop(ExifTags.Base.Orientation, 1)
exif_orientation = exif_data.pop(ExifTags.Base.Orientation, 0)
if exif_orientation != 0:
if len(exif_data):
exif = exif_data.tobytes()
else:
exif = None
else:
exif_orientation = 1
exif_orientation = 0

xmp = info.get("xmp")

Expand Down
47 changes: 44 additions & 3 deletions src/_avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,44 @@ exc_type_for_avif_result(avifResult result) {
}
}

static uint8_t
irot_imir_to_exif_orientation(const avifImage *image) {
#if AVIF_VERSION_MAJOR >= 1
uint8_t axis = image->imir.axis;
#else
uint8_t axis = image->imir.mode;
#endif
uint8_t angle = image->irot.angle;
int irot = !!(image->transformFlags & AVIF_TRANSFORM_IROT);
int imir = !!(image->transformFlags & AVIF_TRANSFORM_IMIR);
if (irot && angle == 1) {
if (imir) {
return axis ? 7 // 90 degrees anti-clockwise then swap left and right.
: 5; // 90 degrees anti-clockwise then swap top and bottom.
}
return 6; // 90 degrees anti-clockwise.
}
if (irot && angle == 2) {
if (imir) {
return axis ? 4 // 180 degrees anti-clockwise then swap left and right.
: 2; // 180 degrees anti-clockwise then swap top and bottom.
}
return 3; // 180 degrees anti-clockwise.
}
if (irot && angle == 3) {
if (imir) {
return axis ? 5 // 270 degrees anti-clockwise then swap left and right.
: 7; // 270 degrees anti-clockwise then swap top and bottom.
}
return 8; // 270 degrees anti-clockwise.
}
if (imir) {
return axis ? 2 // Swap left and right.
: 4; // Swap top and bottom.
}
return 1; // Default orientation ("top-left", no-op).
}

static void
exif_orientation_to_irot_imir(avifImage *image, int orientation) {
const avifTransformFlags otherFlags =
Expand Down Expand Up @@ -485,7 +523,9 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
return NULL;
}
}
exif_orientation_to_irot_imir(image, exif_orientation);
if (exif_orientation > 0) {
exif_orientation_to_irot_imir(image, exif_orientation);
}

self->image = image;
self->frame_index = -1;
Expand Down Expand Up @@ -806,14 +846,15 @@ _decoder_get_info(AvifDecoderObject *self) {
}

ret = Py_BuildValue(
"IIIsSSS",
"IIIsSSSI",
image->width,
image->height,
decoder->imageCount,
self->mode,
NULL == icc ? Py_None : icc,
NULL == exif ? Py_None : exif,
NULL == xmp ? Py_None : xmp
NULL == xmp ? Py_None : xmp,
irot_imir_to_exif_orientation(image)
);

Py_XDECREF(xmp);
Expand Down

0 comments on commit 524d802

Please sign in to comment.