mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-06 14:40:51 +03:00
fix: set exif orientation from irot/imir when decoding AVIF
This commit is contained in:
parent
de4c6c1976
commit
524d802eda
BIN
Tests/images/avif/rot0mir0.avif
Normal file
BIN
Tests/images/avif/rot0mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot0mir1.avif
Normal file
BIN
Tests/images/avif/rot0mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot1mir0.avif
Normal file
BIN
Tests/images/avif/rot1mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot1mir1.avif
Normal file
BIN
Tests/images/avif/rot1mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot2mir0.avif
Normal file
BIN
Tests/images/avif/rot2mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot2mir1.avif
Normal file
BIN
Tests/images/avif/rot2mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot3mir0.avif
Normal file
BIN
Tests/images/avif/rot3mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot3mir1.avif
Normal file
BIN
Tests/images/avif/rot3mir1.avif
Normal file
Binary file not shown.
|
@ -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:
|
||||
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"]
|
||||
|
|
|
@ -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_orientation = 1
|
||||
exif = None
|
||||
else:
|
||||
exif_orientation = 0
|
||||
|
||||
xmp = info.get("xmp")
|
||||
|
||||
|
|
45
src/_avif.c
45
src/_avif.c
|
@ -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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
|
Loading…
Reference in New Issue
Block a user