mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-11-04 09:57:43 +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:
 | 
			
		||||
            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"]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										47
									
								
								src/_avif.c
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								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;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user