fix: set exif orientation from irot/imir when decoding AVIF

This commit is contained in:
Frankie Dintino 2024-12-08 22:00:34 -05:00
parent de4c6c1976
commit 524d802eda
No known key found for this signature in database
GPG Key ID: 97E295AACFBABD9E
11 changed files with 110 additions and 10 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -10,6 +10,7 @@ from io import BytesIO
from pathlib import Path from pathlib import Path
from struct import unpack from struct import unpack
from typing import Any from typing import Any
from unittest import mock
import pytest import pytest
@ -329,17 +330,29 @@ class TestFileAvif:
exif = im.getexif() exif = im.getexif()
assert exif[274] == 3 assert exif[274] == 3
@pytest.mark.parametrize("bytes", [True, False]) @pytest.mark.parametrize("bytes,orientation", [(True, 1), (False, 2)])
def test_exif_save(self, tmp_path: Path, bytes: bool) -> None: 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 = Image.Exif()
exif[274] = 1 exif[274] = orientation
exif_data = exif.tobytes() exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im: with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif") test_file = str(tmp_path / "temp.avif")
im.save(test_file, exif=exif_data if bytes else exif) im.save(test_file, exif=exif_data if bytes else exif)
with Image.open(test_file) as reloaded: 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: def test_exif_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im: with Image.open(TEST_AVIF_FILE) as im:
@ -347,6 +360,35 @@ class TestFileAvif:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
im.save(test_file, exif=b"invalid") 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: def test_xmp(self) -> None:
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
xmp = im.info["xmp"] xmp = im.info["xmp"]

View File

@ -86,7 +86,9 @@ class AvifImageFile(ImageFile.ImageFile):
) )
# Get info from decoder # 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._size = width, height
self.n_frames = n_frames self.n_frames = n_frames
self.is_animated = self.n_frames > 1 self.is_animated = self.n_frames > 1
@ -99,6 +101,16 @@ class AvifImageFile(ImageFile.ImageFile):
if xmp: if xmp:
self.info["xmp"] = 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: def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
@ -176,9 +188,14 @@ def _save(
else: else:
exif_data = Image.Exif() exif_data = Image.Exif()
exif_data.load(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: else:
exif_orientation = 1 exif_orientation = 0
xmp = info.get("xmp") xmp = info.get("xmp")

View File

@ -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 static void
exif_orientation_to_irot_imir(avifImage *image, int orientation) { exif_orientation_to_irot_imir(avifImage *image, int orientation) {
const avifTransformFlags otherFlags = const avifTransformFlags otherFlags =
@ -485,7 +523,9 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
return NULL; 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->image = image;
self->frame_index = -1; self->frame_index = -1;
@ -806,14 +846,15 @@ _decoder_get_info(AvifDecoderObject *self) {
} }
ret = Py_BuildValue( ret = Py_BuildValue(
"IIIsSSS", "IIIsSSSI",
image->width, image->width,
image->height, image->height,
decoder->imageCount, decoder->imageCount,
self->mode, self->mode,
NULL == icc ? Py_None : icc, NULL == icc ? Py_None : icc,
NULL == exif ? Py_None : exif, NULL == exif ? Py_None : exif,
NULL == xmp ? Py_None : xmp NULL == xmp ? Py_None : xmp,
irot_imir_to_exif_orientation(image)
); );
Py_XDECREF(xmp); Py_XDECREF(xmp);