Skip to content

Commit cc72287

Browse files
authored
Merge pull request #26 from uploadcare/fix-Unsupported-image
Try reopen heif in case when can't be opened without transformations
2 parents 6b84085 + e1b39f1 commit cc72287

File tree

4 files changed

+64
-18
lines changed

4 files changed

+64
-18
lines changed

HeifImagePlugin.py

+40-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import subprocess
22
import tempfile
33
from copy import copy
4+
from dataclasses import dataclass
45
from weakref import WeakKeyDictionary
56

67
import piexif
@@ -16,6 +17,22 @@
1617
Transformations = None
1718

1819

20+
@dataclass
21+
class LibheifError:
22+
code: int
23+
subcode: int
24+
25+
def __eq__(self, e):
26+
if not isinstance(e, HeifError): # pragma: no cover
27+
return False
28+
return e.code == self.code and e.subcode == self.subcode
29+
30+
31+
class Errors:
32+
end_of_file = LibheifError(7, 100)
33+
unsupported_color_conversion = LibheifError(4, 3003)
34+
35+
1936
ffi = FFI()
2037
_keep_refs = WeakKeyDictionary()
2138
HEIF_ENC_BIN = 'heif-enc'
@@ -27,7 +44,7 @@ def _crop_heif_file(heif):
2744
if crop == (0, 0) + heif.size:
2845
return heif
2946

30-
if heif.mode not in ("L", "RGB", "RGBA"):
47+
if heif.mode not in ("L", "RGB", "RGBA"): # pragma: no cover
3148
raise ValueError("Unknown mode")
3249
pixel_size = len(heif.mode)
3350

@@ -99,20 +116,20 @@ class HeifImageFile(ImageFile.ImageFile):
99116
format = 'HEIF'
100117
format_description = "HEIF/HEIC image"
101118

102-
def _open(self):
119+
def _open_heif_file(self, apply_transformations):
103120
try:
104121
heif_file = pyheif.open(
105-
self.fp, apply_transformations=Transformations is None)
122+
self.fp, apply_transformations=apply_transformations)
106123
except HeifError as e:
107124
raise SyntaxError(str(e))
108125

109126
_extract_heif_exif(heif_file)
110127

111-
if Transformations is not None:
128+
if apply_transformations:
129+
self._size = heif_file.size
130+
else:
112131
heif_file = _rotate_heif_file(heif_file)
113132
self._size = heif_file.transformations.crop[2:4]
114-
else:
115-
self._size = heif_file.size
116133

117134
if hasattr(self, "_mode"):
118135
self._mode = heif_file.mode
@@ -121,6 +138,9 @@ def _open(self):
121138
# https://pillow.readthedocs.io/en/stable/releasenotes/10.1.0.html#setting-image-mode
122139
self.mode = heif_file.mode
123140

141+
self.info.pop('exif', None)
142+
self.info.pop('icc_profile', None)
143+
124144
if heif_file.exif:
125145
self.info['exif'] = heif_file.exif
126146

@@ -135,20 +155,30 @@ def _open(self):
135155
# We need to go deeper...
136156
if heif_file.color_profile['type'] in ('rICC', 'prof'):
137157
self.info['icc_profile'] = heif_file.color_profile['data']
158+
return heif_file
138159

160+
def _open(self):
139161
self.tile = []
140-
self.heif_file = heif_file
162+
self.heif_file = self._open_heif_file(Transformations is None)
141163

142164
def load(self):
143165
heif_file, self.heif_file = self.heif_file, None
144166
if heif_file:
145167
try:
146-
heif_file = heif_file.load()
168+
try:
169+
heif_file = heif_file.load()
170+
except HeifError as e:
171+
if e != Errors.unsupported_color_conversion:
172+
raise
173+
# Unsupported feature: Unsupported color conversion
174+
# https://github.com/strukturag/libheif/issues/1273
175+
self.fp.seek(0)
176+
heif_file = self._open_heif_file(True).load()
147177
except HeifError as e:
148-
cropped_file = e.code == 7 and e.subcode == 100
178+
# Ignore EOF error and return blank image otherwise
179+
cropped_file = e == Errors.end_of_file
149180
if not cropped_file or not ImageFile.LOAD_TRUNCATED_IMAGES:
150181
raise
151-
# Ignore EOF error and return blank image otherwise
152182

153183
self.load_prepare()
154184

71.9 KB
Binary file not shown.
52.1 KB
Binary file not shown.

tests/test_transformations.py

+24-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
from unittest import mock
22

3+
import pyheif
34
import pytest
45
from PIL import Image
56
from pyheif import open as pyheif_open
67

78
from HeifImagePlugin import Transformations
89

9-
from . import respath
10+
from . import avg_diff, respath
1011

1112

12-
skip_if_no_transformations = pytest.mark.skipif(
13+
skip_no_transformations = pytest.mark.skipif(
1314
Transformations is None,
1415
reason="pyheif doesn't support transformations")
1516

17+
skip_libheif_not_16 = pytest.mark.skipif(
18+
pyheif.libheif_version() < '1.16.0',
19+
reason="libheif < 1.16.0 can't decode odd sizes")
20+
1621

1722
def open_with_custom_meta(path, *, exif_data=None, exif=None, crop=None, orientation=0):
1823
def my_pyheif_open(*args, **kwargs):
@@ -47,14 +52,14 @@ def test_no_orientation_and_no_exif():
4752
assert 'exif' not in image.info
4853

4954

50-
@skip_if_no_transformations
55+
@skip_no_transformations
5156
def test_empty_exif():
5257
image = open_with_custom_meta(respath('test2.heic'), exif_data=b'', orientation=1)
5358
assert 'exif' in image.info
5459
assert image.getexif()[274] == 1
5560

5661

57-
@skip_if_no_transformations
62+
@skip_no_transformations
5863
def test_broken_exif():
5964
broken = b'Exif\x00\x00II*\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00'
6065
image = open_with_custom_meta(respath('test2.heic'),
@@ -63,7 +68,7 @@ def test_broken_exif():
6368
assert image.getexif()[274] == 1
6469

6570

66-
@skip_if_no_transformations
71+
@skip_no_transformations
6772
def test_orientation_and_no_exif():
6873
image = open_with_custom_meta(respath('test2.heic'), orientation=7)
6974

@@ -79,7 +84,7 @@ def test_no_orientation_and_exif_with_rotation():
7984
assert image.getexif()[274] == 7
8085

8186

82-
@skip_if_no_transformations
87+
@skip_no_transformations
8388
def test_orientation_and_exif_with_rotation():
8489
# Orientation tag from file should suppress Exif value
8590
image = open_with_custom_meta(
@@ -89,7 +94,7 @@ def test_orientation_and_exif_with_rotation():
8994
assert image.getexif()[274] == 1
9095

9196

92-
@skip_if_no_transformations
97+
@skip_no_transformations
9398
def test_orientation_and_exif_without_rotation():
9499
image = open_with_custom_meta(
95100
respath('test2.heic'), orientation=1, exif={270: "Sample image"})
@@ -98,7 +103,7 @@ def test_orientation_and_exif_without_rotation():
98103
assert image.getexif()[274] == 1
99104

100105

101-
@skip_if_no_transformations
106+
@skip_no_transformations
102107
def test_crop_on_load():
103108
ref_image = Image.open(respath('test2.heic'))
104109
assert ref_image.size == (1280, 720)
@@ -110,3 +115,14 @@ def test_crop_on_load():
110115
image = open_with_custom_meta(respath('test2.heic'), crop=(99, 33, 512, 256))
111116
assert image.size == (512, 256)
112117
assert image.copy() == ref_image.crop((99, 33, 611, 289))
118+
119+
120+
@skip_libheif_not_16
121+
def test_fallback_to_transforms():
122+
# Image with 695x472 color and 696x472 alpha with crop
123+
image = Image.open(respath('unreadable-wo-transf.heic'))
124+
assert image.size == (695, 472)
125+
126+
ref_image = Image.open(respath('unreadable-wo-transf.ref.heic'))
127+
avg_diffs = avg_diff(image, ref_image)
128+
assert max(avg_diffs) <= 0.01

0 commit comments

Comments
 (0)