1#
2# The Python Imaging Library
3# $Id$
4#
5# JPEG2000 file handling
6#
7# History:
8# 2014-03-12 ajh Created
9# 2021-06-30 rogermb Extract dpi information from the 'resc' header box
10#
11# Copyright (c) 2014 Coriolis Systems Limited
12# Copyright (c) 2014 Alastair Houghton
13#
14# See the README file for information on usage and redistribution.
15#
16from __future__ import annotations
17
18import io
19import os
20import struct
21from typing import cast
22
23from . import Image, ImageFile, ImagePalette, _binary
24
25TYPE_CHECKING = False
26if TYPE_CHECKING:
27 from collections.abc import Callable
28 from typing import IO
29
30
31class BoxReader:
32 """
33 A small helper class to read fields stored in JPEG2000 header boxes
34 and to easily step into and read sub-boxes.
35 """
36
37 def __init__(self, fp: IO[bytes], length: int = -1) -> None:
38 self.fp = fp
39 self.has_length = length >= 0
40 self.length = length
41 self.remaining_in_box = -1
42
43 def _can_read(self, num_bytes: int) -> bool:
44 if self.has_length and self.fp.tell() + num_bytes > self.length:
45 # Outside box: ensure we don't read past the known file length
46 return False
47 if self.remaining_in_box >= 0:
48 # Inside box contents: ensure read does not go past box boundaries
49 return num_bytes <= self.remaining_in_box
50 else:
51 return True # No length known, just read
52
53 def _read_bytes(self, num_bytes: int) -> bytes:
54 if not self._can_read(num_bytes):
55 msg = "Not enough data in header"
56 raise SyntaxError(msg)
57
58 data = self.fp.read(num_bytes)
59 if len(data) < num_bytes:
60 msg = f"Expected to read {num_bytes} bytes but only got {len(data)}."
61 raise OSError(msg)
62
63 if self.remaining_in_box > 0:
64 self.remaining_in_box -= num_bytes
65 return data
66
67 def read_fields(self, field_format: str) -> tuple[int | bytes, ...]:
68 size = struct.calcsize(field_format)
69 data = self._read_bytes(size)
70 return struct.unpack(field_format, data)
71
72 def read_boxes(self) -> BoxReader:
73 size = self.remaining_in_box
74 data = self._read_bytes(size)
75 return BoxReader(io.BytesIO(data), size)
76
77 def has_next_box(self) -> bool:
78 if self.has_length:
79 return self.fp.tell() + self.remaining_in_box < self.length
80 else:
81 return True
82
83 def next_box_type(self) -> bytes:
84 # Skip the rest of the box if it has not been read
85 if self.remaining_in_box > 0:
86 self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
87 self.remaining_in_box = -1
88
89 # Read the length and type of the next box
90 lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s"))
91 if lbox == 1:
92 lbox = cast(int, self.read_fields(">Q")[0])
93 hlen = 16
94 else:
95 hlen = 8
96
97 if lbox < hlen or not self._can_read(lbox - hlen):
98 msg = "Invalid header length"
99 raise SyntaxError(msg)
100
101 self.remaining_in_box = lbox - hlen
102 return tbox
103
104
105def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]:
106 """Parse the JPEG 2000 codestream to extract the size and component
107 count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
108
109 hdr = fp.read(2)
110 lsiz = _binary.i16be(hdr)
111 if lsiz < 38:
112 msg = "SIZ marker length must be at least 38"
113 raise ValueError(msg)
114 siz = hdr + fp.read(lsiz - 2)
115 lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from(
116 ">HHIIIIIIIIH", siz
117 )
118
119 size = (xsiz - xosiz, ysiz - yosiz)
120 if csiz == 1:
121 ssiz = struct.unpack_from(">B", siz, 38)
122 if (ssiz[0] & 0x7F) + 1 > 8:
123 mode = "I;16"
124 else:
125 mode = "L"
126 elif csiz == 2:
127 mode = "LA"
128 elif csiz == 3:
129 mode = "RGB"
130 elif csiz == 4:
131 mode = "RGBA"
132 else:
133 msg = "unable to determine J2K image mode"
134 raise SyntaxError(msg)
135
136 return size, mode
137
138
139def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
140 """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
141 calculated as (num / denom) * 10^exp and stored in dots per meter,
142 to floating-point dots per inch."""
143 if denom == 0:
144 return None
145 return (254 * num * (10**exp)) / (10000 * denom)
146
147
148def _parse_jp2_header(
149 fp: IO[bytes],
150) -> tuple[
151 tuple[int, int],
152 str,
153 str | None,
154 tuple[float, float] | None,
155 ImagePalette.ImagePalette | None,
156]:
157 """Parse the JP2 header box to extract size, component count,
158 color space information, and optionally DPI information,
159 returning a (size, mode, mimetype, dpi) tuple."""
160
161 # Find the JP2 header box
162 reader = BoxReader(fp)
163 header = None
164 mimetype = None
165 while reader.has_next_box():
166 tbox = reader.next_box_type()
167
168 if tbox == b"jp2h":
169 header = reader.read_boxes()
170 break
171 elif tbox == b"ftyp":
172 if reader.read_fields(">4s")[0] == b"jpx ":
173 mimetype = "image/jpx"
174 assert header is not None
175
176 size = None
177 mode = None
178 bpc = None
179 nc = None
180 dpi = None # 2-tuple of DPI info, or None
181 palette = None
182 colr = None
183
184 while header.has_next_box():
185 tbox = header.next_box_type()
186
187 if tbox == b"ihdr":
188 height, width, nc, bpc = header.read_fields(">IIHB")
189 assert isinstance(height, int)
190 assert isinstance(width, int)
191 assert isinstance(bpc, int)
192 size = (width, height)
193 if nc == 1 and (bpc & 0x7F) > 8:
194 mode = "I;16"
195 elif nc == 1:
196 mode = "L"
197 elif nc == 2:
198 mode = "LA"
199 elif nc == 3:
200 mode = "RGB"
201 elif nc == 4:
202 mode = "RGBA"
203 elif tbox == b"colr":
204 meth, _, _, enumcs = header.read_fields(">BBBI")
205 if meth == 1:
206 if enumcs in (0, 15):
207 colr = "1"
208 elif enumcs == 12:
209 colr = "CMYK"
210 if nc == 4:
211 mode = "CMYK"
212 elif enumcs == 17:
213 colr = "L"
214 elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"):
215 ne, npc = header.read_fields(">HB")
216 assert isinstance(ne, int)
217 assert isinstance(npc, int)
218 max_bitdepth = 0
219 for bitdepth in header.read_fields(">" + ("B" * npc)):
220 assert isinstance(bitdepth, int)
221 if bitdepth > max_bitdepth:
222 max_bitdepth = bitdepth
223 if max_bitdepth <= 8:
224 if npc == 4:
225 palette_mode = "CMYK" if colr == "CMYK" else "RGBA"
226 else:
227 palette_mode = "RGB"
228 palette = ImagePalette.ImagePalette(palette_mode)
229 for i in range(ne):
230 color: list[int] = []
231 for value in header.read_fields(">" + ("B" * npc)):
232 assert isinstance(value, int)
233 color.append(value)
234 palette.getcolor(tuple(color))
235 mode = "P" if mode == "L" else "PA"
236 elif tbox == b"res ":
237 res = header.read_boxes()
238 while res.has_next_box():
239 tres = res.next_box_type()
240 if tres == b"resc":
241 vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
242 assert isinstance(vrcn, int)
243 assert isinstance(vrcd, int)
244 assert isinstance(hrcn, int)
245 assert isinstance(hrcd, int)
246 assert isinstance(vrce, int)
247 assert isinstance(hrce, int)
248 hres = _res_to_dpi(hrcn, hrcd, hrce)
249 vres = _res_to_dpi(vrcn, vrcd, vrce)
250 if hres is not None and vres is not None:
251 dpi = (hres, vres)
252 break
253
254 if size is None or mode is None:
255 msg = "Malformed JP2 header"
256 raise SyntaxError(msg)
257
258 return size, mode, mimetype, dpi, palette
259
260
261##
262# Image plugin for JPEG2000 images.
263
264
265class Jpeg2KImageFile(ImageFile.ImageFile):
266 format = "JPEG2000"
267 format_description = "JPEG 2000 (ISO 15444)"
268
269 def _open(self) -> None:
270 assert self.fp is not None
271 sig = self.fp.read(4)
272 if sig == b"\xff\x4f\xff\x51":
273 self.codec = "j2k"
274 self._size, self._mode = _parse_codestream(self.fp)
275 self._parse_comment()
276 else:
277 sig = sig + self.fp.read(8)
278
279 if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
280 self.codec = "jp2"
281 header = _parse_jp2_header(self.fp)
282 self._size, self._mode, self.custom_mimetype, dpi, self.palette = header
283 if dpi is not None:
284 self.info["dpi"] = dpi
285 if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"):
286 hdr = self.fp.read(2)
287 length = _binary.i16be(hdr)
288 self.fp.seek(length - 2, os.SEEK_CUR)
289 self._parse_comment()
290 else:
291 msg = "not a JPEG 2000 file"
292 raise SyntaxError(msg)
293
294 self._reduce = 0
295 self.layers = 0
296
297 fd = -1
298 length = -1
299
300 try:
301 fd = self.fp.fileno()
302 length = os.fstat(fd).st_size
303 except Exception:
304 fd = -1
305 try:
306 pos = self.fp.tell()
307 self.fp.seek(0, io.SEEK_END)
308 length = self.fp.tell()
309 self.fp.seek(pos)
310 except Exception:
311 length = -1
312
313 self.tile = [
314 ImageFile._Tile(
315 "jpeg2k",
316 (0, 0) + self.size,
317 0,
318 (self.codec, self._reduce, self.layers, fd, length),
319 )
320 ]
321
322 def _parse_comment(self) -> None:
323 assert self.fp is not None
324 while True:
325 marker = self.fp.read(2)
326 if not marker:
327 break
328 typ = marker[1]
329 if typ in (0x90, 0xD9):
330 # Start of tile or end of codestream
331 break
332 hdr = self.fp.read(2)
333 length = _binary.i16be(hdr)
334 if length < 2:
335 msg = "Marker length too small"
336 raise ValueError(msg)
337 if typ == 0x64:
338 # Comment
339 self.info["comment"] = self.fp.read(length - 2)[2:]
340 break
341 else:
342 self.fp.seek(length - 2, os.SEEK_CUR)
343
344 @property # type: ignore[override]
345 def reduce(
346 self,
347 ) -> (
348 Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image]
349 | int
350 ):
351 # https://github.com/python-pillow/Pillow/issues/4343 found that the
352 # new Image 'reduce' method was shadowed by this plugin's 'reduce'
353 # property. This attempts to allow for both scenarios
354 return self._reduce or super().reduce
355
356 @reduce.setter
357 def reduce(self, value: int) -> None:
358 self._reduce = value
359
360 def load(self) -> Image.core.PixelAccess | None:
361 if self.tile and self._reduce:
362 power = 1 << self._reduce
363 adjust = power >> 1
364 self._size = (
365 int((self.size[0] + adjust) / power),
366 int((self.size[1] + adjust) / power),
367 )
368
369 # Update the reduce and layers settings
370 t = self.tile[0]
371 assert isinstance(t[3], tuple)
372 t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
373 self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)]
374
375 return ImageFile.ImageFile.load(self)
376
377
378def _accept(prefix: bytes) -> bool:
379 return prefix.startswith(
380 (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a")
381 )
382
383
384# ------------------------------------------------------------
385# Save support
386
387
388def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
389 # Get the keyword arguments
390 info = im.encoderinfo
391
392 if isinstance(filename, str):
393 filename = filename.encode()
394 if filename.endswith(b".j2k") or info.get("no_jp2", False):
395 kind = "j2k"
396 else:
397 kind = "jp2"
398
399 offset = info.get("offset", None)
400 tile_offset = info.get("tile_offset", None)
401 tile_size = info.get("tile_size", None)
402 quality_mode = info.get("quality_mode", "rates")
403 quality_layers = info.get("quality_layers", None)
404 if quality_layers is not None and not (
405 isinstance(quality_layers, (list, tuple))
406 and all(
407 isinstance(quality_layer, (int, float)) for quality_layer in quality_layers
408 )
409 ):
410 msg = "quality_layers must be a sequence of numbers"
411 raise ValueError(msg)
412
413 num_resolutions = info.get("num_resolutions", 0)
414 cblk_size = info.get("codeblock_size", None)
415 precinct_size = info.get("precinct_size", None)
416 irreversible = info.get("irreversible", False)
417 progression = info.get("progression", "LRCP")
418 cinema_mode = info.get("cinema_mode", "no")
419 mct = info.get("mct", 0)
420 signed = info.get("signed", False)
421 comment = info.get("comment")
422 if isinstance(comment, str):
423 comment = comment.encode()
424 plt = info.get("plt", False)
425
426 fd = -1
427 if hasattr(fp, "fileno"):
428 try:
429 fd = fp.fileno()
430 except Exception:
431 fd = -1
432
433 im.encoderconfig = (
434 offset,
435 tile_offset,
436 tile_size,
437 quality_mode,
438 quality_layers,
439 num_resolutions,
440 cblk_size,
441 precinct_size,
442 irreversible,
443 progression,
444 cinema_mode,
445 mct,
446 signed,
447 fd,
448 comment,
449 plt,
450 )
451
452 ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)])
453
454
455# ------------------------------------------------------------
456# Registry stuff
457
458
459Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept)
460Image.register_save(Jpeg2KImageFile.format, _save)
461
462Image.register_extensions(
463 Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"]
464)
465
466Image.register_mime(Jpeg2KImageFile.format, "image/jp2")