diff --git a/HeifImagePlugin.py b/HeifImagePlugin.py index e0b0d7f..81ed65e 100644 --- a/HeifImagePlugin.py +++ b/HeifImagePlugin.py @@ -99,20 +99,20 @@ class HeifImageFile(ImageFile.ImageFile): format = 'HEIF' format_description = "HEIF/HEIC image" - def _open(self): + def _open_heif_file(self, apply_transformations): try: heif_file = pyheif.open( - self.fp, apply_transformations=Transformations is None) + self.fp, apply_transformations=apply_transformations) except HeifError as e: raise SyntaxError(str(e)) _extract_heif_exif(heif_file) - if Transformations is not None: + if apply_transformations: + self._size = heif_file.size + else: heif_file = _rotate_heif_file(heif_file) self._size = heif_file.transformations.crop[2:4] - else: - self._size = heif_file.size if hasattr(self, "_mode"): self._mode = heif_file.mode @@ -121,6 +121,9 @@ def _open(self): # https://pillow.readthedocs.io/en/stable/releasenotes/10.1.0.html#setting-image-mode self.mode = heif_file.mode + self.info.pop('exif', None) + self.info.pop('icc_profile', None) + if heif_file.exif: self.info['exif'] = heif_file.exif @@ -135,20 +138,30 @@ def _open(self): # We need to go deeper... if heif_file.color_profile['type'] in ('rICC', 'prof'): self.info['icc_profile'] = heif_file.color_profile['data'] + return heif_file + def _open(self): self.tile = [] - self.heif_file = heif_file + self.heif_file = self._open_heif_file(Transformations is None) def load(self): heif_file, self.heif_file = self.heif_file, None if heif_file: try: - heif_file = heif_file.load() + try: + heif_file = heif_file.load() + except HeifError as e: + if not (e.code == 4 and e.subcode == 3003): + raise + # Unsupported feature: Unsupported color conversion + # https://github.com/strukturag/libheif/issues/1273 + self.fp.seek(0) + heif_file = self._open_heif_file(True).load() except HeifError as e: + # Ignore EOF error and return blank image otherwise cropped_file = e.code == 7 and e.subcode == 100 if not cropped_file or not ImageFile.LOAD_TRUNCATED_IMAGES: raise - # Ignore EOF error and return blank image otherwise self.load_prepare() diff --git a/tests/images/unreadable-wo-transf.heic b/tests/images/unreadable-wo-transf.heic new file mode 100644 index 0000000..84e5be2 Binary files /dev/null and b/tests/images/unreadable-wo-transf.heic differ diff --git a/tests/images/unreadable-wo-transf.ref.heic b/tests/images/unreadable-wo-transf.ref.heic new file mode 100644 index 0000000..8377667 Binary files /dev/null and b/tests/images/unreadable-wo-transf.ref.heic differ diff --git a/tests/test_transformations.py b/tests/test_transformations.py index 54d4eef..d066a3f 100644 --- a/tests/test_transformations.py +++ b/tests/test_transformations.py @@ -1,18 +1,23 @@ from unittest import mock +import pyheif import pytest from PIL import Image from pyheif import open as pyheif_open from HeifImagePlugin import Transformations -from . import respath +from . import avg_diff, respath -skip_if_no_transformations = pytest.mark.skipif( +skip_no_transformations = pytest.mark.skipif( Transformations is None, reason="pyheif doesn't support transformations") +skip_libheif_not_16 = pytest.mark.skipif( + pyheif.libheif_version() < '1.16.0', + reason="libheif < 1.16.0 can't decode odd sizes") + def open_with_custom_meta(path, *, exif_data=None, exif=None, crop=None, orientation=0): def my_pyheif_open(*args, **kwargs): @@ -47,14 +52,14 @@ def test_no_orientation_and_no_exif(): assert 'exif' not in image.info -@skip_if_no_transformations +@skip_no_transformations def test_empty_exif(): image = open_with_custom_meta(respath('test2.heic'), exif_data=b'', orientation=1) assert 'exif' in image.info assert image.getexif()[274] == 1 -@skip_if_no_transformations +@skip_no_transformations def test_broken_exif(): broken = b'Exif\x00\x00II*\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00' image = open_with_custom_meta(respath('test2.heic'), @@ -63,7 +68,7 @@ def test_broken_exif(): assert image.getexif()[274] == 1 -@skip_if_no_transformations +@skip_no_transformations def test_orientation_and_no_exif(): image = open_with_custom_meta(respath('test2.heic'), orientation=7) @@ -79,7 +84,7 @@ def test_no_orientation_and_exif_with_rotation(): assert image.getexif()[274] == 7 -@skip_if_no_transformations +@skip_no_transformations def test_orientation_and_exif_with_rotation(): # Orientation tag from file should suppress Exif value image = open_with_custom_meta( @@ -89,7 +94,7 @@ def test_orientation_and_exif_with_rotation(): assert image.getexif()[274] == 1 -@skip_if_no_transformations +@skip_no_transformations def test_orientation_and_exif_without_rotation(): image = open_with_custom_meta( respath('test2.heic'), orientation=1, exif={270: "Sample image"}) @@ -98,7 +103,7 @@ def test_orientation_and_exif_without_rotation(): assert image.getexif()[274] == 1 -@skip_if_no_transformations +@skip_no_transformations def test_crop_on_load(): ref_image = Image.open(respath('test2.heic')) assert ref_image.size == (1280, 720) @@ -110,3 +115,14 @@ def test_crop_on_load(): image = open_with_custom_meta(respath('test2.heic'), crop=(99, 33, 512, 256)) assert image.size == (512, 256) assert image.copy() == ref_image.crop((99, 33, 611, 289)) + + +@skip_libheif_not_16 +def test_fallback_to_transforms(): + # Image with 695x472 color and 696x472 alpha with crop + image = Image.open(respath('unreadable-wo-transf.heic')) + assert image.size == (695, 472) + + ref_image = Image.open(respath('unreadable-wo-transf.ref.heic')) + avg_diffs = avg_diff(image, ref_image) + assert max(avg_diffs) <= 0.01