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 struct import unpack
from typing import Any
from unittest import mock
import pytest
@ -329,17 +330,29 @@ class TestFileAvif:
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:
@ -347,6 +360,35 @@ class TestFileAvif:
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"]

View File

@ -86,7 +86,9 @@ class AvifImageFile(ImageFile.ImageFile):
)
# 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
@ -99,6 +101,16 @@ class AvifImageFile(ImageFile.ImageFile):
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
@ -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")

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
exif_orientation_to_irot_imir(avifImage *image, int orientation) {
const avifTransformFlags otherFlags =
@ -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;
@ -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);