Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/Jpeg2KImagePlugin.py: 12%
205 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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#
16import io
17import os
18import struct
20from . import Image, ImageFile
23class BoxReader:
24 """
25 A small helper class to read fields stored in JPEG2000 header boxes
26 and to easily step into and read sub-boxes.
27 """
29 def __init__(self, fp, length=-1):
30 self.fp = fp
31 self.has_length = length >= 0
32 self.length = length
33 self.remaining_in_box = -1
35 def _can_read(self, num_bytes):
36 if self.has_length and self.fp.tell() + num_bytes > self.length:
37 # Outside box: ensure we don't read past the known file length
38 return False
39 if self.remaining_in_box >= 0:
40 # Inside box contents: ensure read does not go past box boundaries
41 return num_bytes <= self.remaining_in_box
42 else:
43 return True # No length known, just read
45 def _read_bytes(self, num_bytes):
46 if not self._can_read(num_bytes):
47 raise SyntaxError("Not enough data in header")
49 data = self.fp.read(num_bytes)
50 if len(data) < num_bytes:
51 raise OSError(
52 f"Expected to read {num_bytes} bytes but only got {len(data)}."
53 )
55 if self.remaining_in_box > 0:
56 self.remaining_in_box -= num_bytes
57 return data
59 def read_fields(self, field_format):
60 size = struct.calcsize(field_format)
61 data = self._read_bytes(size)
62 return struct.unpack(field_format, data)
64 def read_boxes(self):
65 size = self.remaining_in_box
66 data = self._read_bytes(size)
67 return BoxReader(io.BytesIO(data), size)
69 def has_next_box(self):
70 if self.has_length:
71 return self.fp.tell() + self.remaining_in_box < self.length
72 else:
73 return True
75 def next_box_type(self):
76 # Skip the rest of the box if it has not been read
77 if self.remaining_in_box > 0:
78 self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
79 self.remaining_in_box = -1
81 # Read the length and type of the next box
82 lbox, tbox = self.read_fields(">I4s")
83 if lbox == 1:
84 lbox = self.read_fields(">Q")[0]
85 hlen = 16
86 else:
87 hlen = 8
89 if lbox < hlen or not self._can_read(lbox - hlen):
90 raise SyntaxError("Invalid header length")
92 self.remaining_in_box = lbox - hlen
93 return tbox
96def _parse_codestream(fp):
97 """Parse the JPEG 2000 codestream to extract the size and component
98 count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
100 hdr = fp.read(2)
101 lsiz = struct.unpack(">H", hdr)[0]
102 siz = hdr + fp.read(lsiz - 2)
103 lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from(
104 ">HHIIIIIIIIH", siz
105 )
106 ssiz = [None] * csiz
107 xrsiz = [None] * csiz
108 yrsiz = [None] * csiz
109 for i in range(csiz):
110 ssiz[i], xrsiz[i], yrsiz[i] = struct.unpack_from(">BBB", siz, 36 + 3 * i)
112 size = (xsiz - xosiz, ysiz - yosiz)
113 if csiz == 1:
114 if (yrsiz[0] & 0x7F) > 8:
115 mode = "I;16"
116 else:
117 mode = "L"
118 elif csiz == 2:
119 mode = "LA"
120 elif csiz == 3:
121 mode = "RGB"
122 elif csiz == 4:
123 mode = "RGBA"
124 else:
125 mode = None
127 return size, mode
130def _res_to_dpi(num, denom, exp):
131 """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
132 calculated as (num / denom) * 10^exp and stored in dots per meter,
133 to floating-point dots per inch."""
134 if denom != 0:
135 return (254 * num * (10**exp)) / (10000 * denom)
138def _parse_jp2_header(fp):
139 """Parse the JP2 header box to extract size, component count,
140 color space information, and optionally DPI information,
141 returning a (size, mode, mimetype, dpi) tuple."""
143 # Find the JP2 header box
144 reader = BoxReader(fp)
145 header = None
146 mimetype = None
147 while reader.has_next_box():
148 tbox = reader.next_box_type()
150 if tbox == b"jp2h":
151 header = reader.read_boxes()
152 break
153 elif tbox == b"ftyp":
154 if reader.read_fields(">4s")[0] == b"jpx ":
155 mimetype = "image/jpx"
157 size = None
158 mode = None
159 bpc = None
160 nc = None
161 dpi = None # 2-tuple of DPI info, or None
163 while header.has_next_box():
164 tbox = header.next_box_type()
166 if tbox == b"ihdr":
167 height, width, nc, bpc = header.read_fields(">IIHB")
168 size = (width, height)
169 if nc == 1 and (bpc & 0x7F) > 8:
170 mode = "I;16"
171 elif nc == 1:
172 mode = "L"
173 elif nc == 2:
174 mode = "LA"
175 elif nc == 3:
176 mode = "RGB"
177 elif nc == 4:
178 mode = "RGBA"
179 elif tbox == b"res ":
180 res = header.read_boxes()
181 while res.has_next_box():
182 tres = res.next_box_type()
183 if tres == b"resc":
184 vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
185 hres = _res_to_dpi(hrcn, hrcd, hrce)
186 vres = _res_to_dpi(vrcn, vrcd, vrce)
187 if hres is not None and vres is not None:
188 dpi = (hres, vres)
189 break
191 if size is None or mode is None:
192 raise SyntaxError("Malformed JP2 header")
194 return size, mode, mimetype, dpi
197##
198# Image plugin for JPEG2000 images.
201class Jpeg2KImageFile(ImageFile.ImageFile):
202 format = "JPEG2000"
203 format_description = "JPEG 2000 (ISO 15444)"
205 def _open(self):
206 sig = self.fp.read(4)
207 if sig == b"\xff\x4f\xff\x51":
208 self.codec = "j2k"
209 self._size, self.mode = _parse_codestream(self.fp)
210 else:
211 sig = sig + self.fp.read(8)
213 if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
214 self.codec = "jp2"
215 header = _parse_jp2_header(self.fp)
216 self._size, self.mode, self.custom_mimetype, dpi = header
217 if dpi is not None:
218 self.info["dpi"] = dpi
219 else:
220 raise SyntaxError("not a JPEG 2000 file")
222 if self.size is None or self.mode is None:
223 raise SyntaxError("unable to determine size/mode")
225 self._reduce = 0
226 self.layers = 0
228 fd = -1
229 length = -1
231 try:
232 fd = self.fp.fileno()
233 length = os.fstat(fd).st_size
234 except Exception:
235 fd = -1
236 try:
237 pos = self.fp.tell()
238 self.fp.seek(0, io.SEEK_END)
239 length = self.fp.tell()
240 self.fp.seek(pos)
241 except Exception:
242 length = -1
244 self.tile = [
245 (
246 "jpeg2k",
247 (0, 0) + self.size,
248 0,
249 (self.codec, self._reduce, self.layers, fd, length),
250 )
251 ]
253 @property
254 def reduce(self):
255 # https://github.com/python-pillow/Pillow/issues/4343 found that the
256 # new Image 'reduce' method was shadowed by this plugin's 'reduce'
257 # property. This attempts to allow for both scenarios
258 return self._reduce or super().reduce
260 @reduce.setter
261 def reduce(self, value):
262 self._reduce = value
264 def load(self):
265 if self.tile and self._reduce:
266 power = 1 << self._reduce
267 adjust = power >> 1
268 self._size = (
269 int((self.size[0] + adjust) / power),
270 int((self.size[1] + adjust) / power),
271 )
273 # Update the reduce and layers settings
274 t = self.tile[0]
275 t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
276 self.tile = [(t[0], (0, 0) + self.size, t[2], t3)]
278 return ImageFile.ImageFile.load(self)
281def _accept(prefix):
282 return (
283 prefix[:4] == b"\xff\x4f\xff\x51"
284 or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
285 )
288# ------------------------------------------------------------
289# Save support
292def _save(im, fp, filename):
293 # Get the keyword arguments
294 info = im.encoderinfo
296 if filename.endswith(".j2k") or info.get("no_jp2", False):
297 kind = "j2k"
298 else:
299 kind = "jp2"
301 offset = info.get("offset", None)
302 tile_offset = info.get("tile_offset", None)
303 tile_size = info.get("tile_size", None)
304 quality_mode = info.get("quality_mode", "rates")
305 quality_layers = info.get("quality_layers", None)
306 if quality_layers is not None and not (
307 isinstance(quality_layers, (list, tuple))
308 and all(
309 [
310 isinstance(quality_layer, (int, float))
311 for quality_layer in quality_layers
312 ]
313 )
314 ):
315 raise ValueError("quality_layers must be a sequence of numbers")
317 num_resolutions = info.get("num_resolutions", 0)
318 cblk_size = info.get("codeblock_size", None)
319 precinct_size = info.get("precinct_size", None)
320 irreversible = info.get("irreversible", False)
321 progression = info.get("progression", "LRCP")
322 cinema_mode = info.get("cinema_mode", "no")
323 mct = info.get("mct", 0)
324 fd = -1
326 if hasattr(fp, "fileno"):
327 try:
328 fd = fp.fileno()
329 except Exception:
330 fd = -1
332 im.encoderconfig = (
333 offset,
334 tile_offset,
335 tile_size,
336 quality_mode,
337 quality_layers,
338 num_resolutions,
339 cblk_size,
340 precinct_size,
341 irreversible,
342 progression,
343 cinema_mode,
344 mct,
345 fd,
346 )
348 ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)])
351# ------------------------------------------------------------
352# Registry stuff
355Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept)
356Image.register_save(Jpeg2KImageFile.format, _save)
358Image.register_extensions(
359 Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"]
360)
362Image.register_mime(Jpeg2KImageFile.format, "image/jp2")