Skip to content

Commit ae65315

Browse files
authored
Fix reading RGB and CMYK IPTC images (#9088)
2 parents 61718ea + cfa51ad commit ae65315

File tree

2 files changed

+108
-46
lines changed

2 files changed

+108
-46
lines changed

Tests/test_file_iptc.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,87 @@
22

33
from io import BytesIO
44

5+
import pytest
6+
57
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
68

79
from .helper import assert_image_equal, hopper
810

911
TEST_FILE = "Tests/images/iptc.jpg"
1012

1113

14+
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
15+
def field(tag, value):
16+
return bytes((0x1C,) + tag + (0, len(value))) + value
17+
18+
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
19+
data += field((3, 120), bytes((info.get("compression", 1),)))
20+
if "band" in info:
21+
data += field((3, 65), bytes((info["band"] + 1,)))
22+
data += field((3, 20), b"\x01") # width
23+
data += field((3, 30), b"\x01") # height
24+
data += field(
25+
(8, 10),
26+
bytes((info.get("data", 0),)),
27+
)
28+
29+
return BytesIO(data)
30+
31+
1232
def test_open() -> None:
1333
expected = Image.new("L", (1, 1))
1434

15-
f = BytesIO(
16-
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
17-
b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
18-
)
35+
f = create_iptc_image()
1936
with Image.open(f) as im:
20-
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
37+
assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))]
2138
assert_image_equal(im, expected)
2239

2340
with Image.open(f) as im:
2441
assert im.load() is not None
2542

2643

44+
def test_field_length() -> None:
45+
f = create_iptc_image()
46+
f.seek(28)
47+
f.write(b"\xff")
48+
with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"):
49+
with Image.open(f):
50+
pass
51+
52+
53+
@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK")))
54+
def test_layers(layers: int, mode: str) -> None:
55+
for band in range(-1, layers):
56+
info = {"layers": layers, "component": 1, "data": 5}
57+
if band != -1:
58+
info["band"] = band
59+
f = create_iptc_image(info)
60+
with Image.open(f) as im:
61+
assert im.mode == mode
62+
63+
data = [0] * layers
64+
data[max(band, 0)] = 5
65+
assert im.getpixel((0, 0)) == tuple(data)
66+
67+
68+
def test_unknown_compression() -> None:
69+
f = create_iptc_image({"compression": 2})
70+
with pytest.raises(OSError, match="Unknown IPTC image compression"):
71+
with Image.open(f):
72+
pass
73+
74+
75+
def test_getiptcinfo() -> None:
76+
f = create_iptc_image()
77+
with Image.open(f) as im:
78+
assert IptcImagePlugin.getiptcinfo(im) == {
79+
(3, 60): b"\x01\x00",
80+
(3, 120): b"\x01",
81+
(3, 20): b"\x01",
82+
(3, 30): b"\x01",
83+
}
84+
85+
2786
def test_getiptcinfo_jpg_none() -> None:
2887
# Arrange
2988
with hopper() as im:

src/PIL/IptcImagePlugin.py

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ def _i(c: bytes) -> int:
3434
return i32((b"\0\0\0\0" + c)[-4:])
3535

3636

37-
def _i8(c: int | bytes) -> int:
38-
return c if isinstance(c, int) else c[0]
39-
40-
4137
##
4238
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
4339
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
@@ -100,16 +96,18 @@ def _open(self) -> None:
10096
# mode
10197
layers = self.info[(3, 60)][0]
10298
component = self.info[(3, 60)][1]
103-
if (3, 65) in self.info:
104-
id = self.info[(3, 65)][0] - 1
105-
else:
106-
id = 0
10799
if layers == 1 and not component:
108100
self._mode = "L"
109-
elif layers == 3 and component:
110-
self._mode = "RGB"[id]
111-
elif layers == 4 and component:
112-
self._mode = "CMYK"[id]
101+
band = None
102+
else:
103+
if layers == 3 and component:
104+
self._mode = "RGB"
105+
elif layers == 4 and component:
106+
self._mode = "CMYK"
107+
if (3, 65) in self.info:
108+
band = self.info[(3, 65)][0] - 1
109+
else:
110+
band = 0
113111

114112
# size
115113
self._size = self.getint((3, 20)), self.getint((3, 30))
@@ -124,39 +122,44 @@ def _open(self) -> None:
124122
# tile
125123
if tag == (8, 10):
126124
self.tile = [
127-
ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
125+
ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band))
128126
]
129127

130128
def load(self) -> Image.core.PixelAccess | None:
131-
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
132-
return ImageFile.ImageFile.load(self)
133-
134-
offset, compression = self.tile[0][2:]
135-
136-
self.fp.seek(offset)
137-
138-
# Copy image data to temporary file
139-
o = BytesIO()
140-
if compression == "raw":
141-
# To simplify access to the extracted file,
142-
# prepend a PPM header
143-
o.write(b"P5\n%d %d\n255\n" % self.size)
144-
while True:
145-
type, size = self.field()
146-
if type != (8, 10):
147-
break
148-
while size > 0:
149-
s = self.fp.read(min(size, 8192))
150-
if not s:
129+
if self.tile:
130+
args = self.tile[0].args
131+
assert isinstance(args, tuple)
132+
compression, band = args
133+
134+
self.fp.seek(self.tile[0].offset)
135+
136+
# Copy image data to temporary file
137+
o = BytesIO()
138+
if compression == "raw":
139+
# To simplify access to the extracted file,
140+
# prepend a PPM header
141+
o.write(b"P5\n%d %d\n255\n" % self.size)
142+
while True:
143+
type, size = self.field()
144+
if type != (8, 10):
151145
break
152-
o.write(s)
153-
size -= len(s)
154-
155-
with Image.open(o) as _im:
156-
_im.load()
157-
self.im = _im.im
158-
self.tile = []
159-
return Image.Image.load(self)
146+
while size > 0:
147+
s = self.fp.read(min(size, 8192))
148+
if not s:
149+
break
150+
o.write(s)
151+
size -= len(s)
152+
153+
with Image.open(o) as _im:
154+
if band is not None:
155+
bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
156+
bands[band] = _im
157+
_im = Image.merge(self.mode, bands)
158+
else:
159+
_im.load()
160+
self.im = _im.im
161+
self.tile = []
162+
return ImageFile.ImageFile.load(self)
160163

161164

162165
Image.register_open(IptcImageFile.format, IptcImageFile)

0 commit comments

Comments
 (0)