Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/Jpeg2KImagePlugin.py: 70%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

300 statements  

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 if self.fp.tell() + num_bytes >= 2**63: 

58 msg = "Box length too large" 

59 raise SyntaxError(msg) 

60 

61 data = self.fp.read(num_bytes) 

62 if len(data) < num_bytes: 

63 msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." 

64 raise OSError(msg) 

65 

66 if self.remaining_in_box > 0: 

67 self.remaining_in_box -= num_bytes 

68 return data 

69 

70 def read_fields(self, field_format: str) -> tuple[int | bytes, ...]: 

71 size = struct.calcsize(field_format) 

72 data = self._read_bytes(size) 

73 return struct.unpack(field_format, data) 

74 

75 def read_boxes(self) -> BoxReader: 

76 size = self.remaining_in_box 

77 data = self._read_bytes(size) 

78 return BoxReader(io.BytesIO(data), size) 

79 

80 def has_next_box(self) -> bool: 

81 if self.has_length: 

82 return self.fp.tell() + self.remaining_in_box < self.length 

83 else: 

84 return True 

85 

86 def next_box_type(self) -> bytes: 

87 # Skip the rest of the box if it has not been read 

88 if self.remaining_in_box > 0: 

89 if self.fp.tell() + self.remaining_in_box >= 2**63: 

90 msg = "Box length too large" 

91 raise SyntaxError(msg) 

92 self.fp.seek(self.remaining_in_box, os.SEEK_CUR) 

93 self.remaining_in_box = -1 

94 

95 # Read the length and type of the next box 

96 lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s")) 

97 if lbox == 1: 

98 lbox = cast(int, self.read_fields(">Q")[0]) 

99 hlen = 16 

100 else: 

101 hlen = 8 

102 

103 if lbox < hlen or not self._can_read(lbox - hlen): 

104 msg = "Invalid header length" 

105 raise SyntaxError(msg) 

106 

107 self.remaining_in_box = lbox - hlen 

108 return tbox 

109 

110 

111def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]: 

112 """Parse the JPEG 2000 codestream to extract the size and component 

113 count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" 

114 

115 hdr = fp.read(2) 

116 lsiz = _binary.i16be(hdr) 

117 if lsiz < 38: 

118 msg = "SIZ marker length must be at least 38" 

119 raise ValueError(msg) 

120 siz = hdr + fp.read(lsiz - 2) 

121 lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( 

122 ">HHIIIIIIIIH", siz 

123 ) 

124 

125 size = (xsiz - xosiz, ysiz - yosiz) 

126 if csiz == 1: 

127 ssiz = struct.unpack_from(">B", siz, 38) 

128 if (ssiz[0] & 0x7F) + 1 > 8: 

129 mode = "I;16" 

130 else: 

131 mode = "L" 

132 elif csiz == 2: 

133 mode = "LA" 

134 elif csiz == 3: 

135 mode = "RGB" 

136 elif csiz == 4: 

137 mode = "RGBA" 

138 else: 

139 msg = "unable to determine J2K image mode" 

140 raise SyntaxError(msg) 

141 

142 return size, mode 

143 

144 

145def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: 

146 """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, 

147 calculated as (num / denom) * 10^exp and stored in dots per meter, 

148 to floating-point dots per inch.""" 

149 if denom == 0: 

150 return None 

151 return (254 * num * (10**exp)) / (10000 * denom) 

152 

153 

154def _parse_jp2_header( 

155 fp: IO[bytes], 

156) -> tuple[ 

157 tuple[int, int], 

158 str, 

159 str | None, 

160 tuple[float, float] | None, 

161 ImagePalette.ImagePalette | None, 

162]: 

163 """Parse the JP2 header box to extract size, component count, 

164 color space information, and optionally DPI information, 

165 returning a (size, mode, mimetype, dpi) tuple.""" 

166 

167 # Find the JP2 header box 

168 reader = BoxReader(fp) 

169 header = None 

170 mimetype = None 

171 while reader.has_next_box(): 

172 tbox = reader.next_box_type() 

173 

174 if tbox == b"jp2h": 

175 header = reader.read_boxes() 

176 break 

177 elif tbox == b"ftyp": 

178 if reader.read_fields(">4s")[0] == b"jpx ": 

179 mimetype = "image/jpx" 

180 assert header is not None 

181 

182 size = None 

183 mode = None 

184 bpc = None 

185 nc = None 

186 dpi = None # 2-tuple of DPI info, or None 

187 palette = None 

188 colr = None 

189 

190 while header.has_next_box(): 

191 tbox = header.next_box_type() 

192 

193 if tbox == b"ihdr": 

194 height, width, nc, bpc = header.read_fields(">IIHB") 

195 assert isinstance(height, int) 

196 assert isinstance(width, int) 

197 assert isinstance(bpc, int) 

198 size = (width, height) 

199 if nc == 1 and (bpc & 0x7F) > 8: 

200 mode = "I;16" 

201 elif nc == 1: 

202 mode = "L" 

203 elif nc == 2: 

204 mode = "LA" 

205 elif nc == 3: 

206 mode = "RGB" 

207 elif nc == 4: 

208 mode = "RGBA" 

209 elif tbox == b"colr": 

210 meth, _, _, enumcs = header.read_fields(">BBBI") 

211 if meth == 1: 

212 if enumcs in (0, 15): 

213 colr = "1" 

214 elif enumcs == 12: 

215 colr = "CMYK" 

216 if nc == 4: 

217 mode = "CMYK" 

218 elif enumcs == 17: 

219 colr = "L" 

220 elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"): 

221 ne, npc = header.read_fields(">HB") 

222 assert isinstance(ne, int) 

223 assert isinstance(npc, int) 

224 max_bitdepth = 0 

225 for bitdepth in header.read_fields(">" + ("B" * npc)): 

226 assert isinstance(bitdepth, int) 

227 if bitdepth > max_bitdepth: 

228 max_bitdepth = bitdepth 

229 if max_bitdepth <= 8: 

230 if npc == 4: 

231 palette_mode = "CMYK" if colr == "CMYK" else "RGBA" 

232 else: 

233 palette_mode = "RGB" 

234 palette = ImagePalette.ImagePalette(palette_mode) 

235 for i in range(ne): 

236 color: list[int] = [] 

237 for value in header.read_fields(">" + ("B" * npc)): 

238 assert isinstance(value, int) 

239 color.append(value) 

240 palette.getcolor(tuple(color)) 

241 mode = "P" if mode == "L" else "PA" 

242 elif tbox == b"res ": 

243 res = header.read_boxes() 

244 while res.has_next_box(): 

245 tres = res.next_box_type() 

246 if tres == b"resc": 

247 vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") 

248 assert isinstance(vrcn, int) 

249 assert isinstance(vrcd, int) 

250 assert isinstance(hrcn, int) 

251 assert isinstance(hrcd, int) 

252 assert isinstance(vrce, int) 

253 assert isinstance(hrce, int) 

254 hres = _res_to_dpi(hrcn, hrcd, hrce) 

255 vres = _res_to_dpi(vrcn, vrcd, vrce) 

256 if hres is not None and vres is not None: 

257 dpi = (hres, vres) 

258 break 

259 

260 if size is None or mode is None: 

261 msg = "Malformed JP2 header" 

262 raise SyntaxError(msg) 

263 

264 return size, mode, mimetype, dpi, palette 

265 

266 

267## 

268# Image plugin for JPEG2000 images. 

269 

270 

271class Jpeg2KImageFile(ImageFile.ImageFile): 

272 format = "JPEG2000" 

273 format_description = "JPEG 2000 (ISO 15444)" 

274 

275 def _open(self) -> None: 

276 assert self.fp is not None 

277 sig = self.fp.read(4) 

278 if sig == b"\xff\x4f\xff\x51": 

279 self.codec = "j2k" 

280 self._size, self._mode = _parse_codestream(self.fp) 

281 self._parse_comment() 

282 else: 

283 sig = sig + self.fp.read(8) 

284 

285 if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": 

286 self.codec = "jp2" 

287 header = _parse_jp2_header(self.fp) 

288 self._size, self._mode, self.custom_mimetype, dpi, self.palette = header 

289 if dpi is not None: 

290 self.info["dpi"] = dpi 

291 if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): 

292 hdr = self.fp.read(2) 

293 length = _binary.i16be(hdr) 

294 self.fp.seek(length - 2, os.SEEK_CUR) 

295 self._parse_comment() 

296 else: 

297 msg = "not a JPEG 2000 file" 

298 raise SyntaxError(msg) 

299 

300 self._reduce = 0 

301 self.layers = 0 

302 

303 fd = -1 

304 length = -1 

305 

306 try: 

307 fd = self.fp.fileno() 

308 length = os.fstat(fd).st_size 

309 except Exception: 

310 fd = -1 

311 try: 

312 pos = self.fp.tell() 

313 self.fp.seek(0, io.SEEK_END) 

314 length = self.fp.tell() 

315 self.fp.seek(pos) 

316 except Exception: 

317 length = -1 

318 

319 self.tile = [ 

320 ImageFile._Tile( 

321 "jpeg2k", 

322 (0, 0) + self.size, 

323 0, 

324 (self.codec, self._reduce, self.layers, fd, length), 

325 ) 

326 ] 

327 

328 def _parse_comment(self) -> None: 

329 assert self.fp is not None 

330 while True: 

331 marker = self.fp.read(2) 

332 if not marker: 

333 break 

334 typ = marker[1] 

335 if typ in (0x90, 0xD9): 

336 # Start of tile or end of codestream 

337 break 

338 hdr = self.fp.read(2) 

339 length = _binary.i16be(hdr) 

340 if length < 2: 

341 msg = "Marker length too small" 

342 raise ValueError(msg) 

343 if typ == 0x64: 

344 # Comment 

345 self.info["comment"] = self.fp.read(length - 2)[2:] 

346 break 

347 else: 

348 self.fp.seek(length - 2, os.SEEK_CUR) 

349 

350 @property # type: ignore[override] 

351 def reduce( 

352 self, 

353 ) -> ( 

354 Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image] 

355 | int 

356 ): 

357 # https://github.com/python-pillow/Pillow/issues/4343 found that the 

358 # new Image 'reduce' method was shadowed by this plugin's 'reduce' 

359 # property. This attempts to allow for both scenarios 

360 return self._reduce or super().reduce 

361 

362 @reduce.setter 

363 def reduce(self, value: int) -> None: 

364 self._reduce = value 

365 

366 def load(self) -> Image.core.PixelAccess | None: 

367 if self.tile and self._reduce: 

368 power = 1 << self._reduce 

369 adjust = power >> 1 

370 self._size = ( 

371 int((self.size[0] + adjust) / power), 

372 int((self.size[1] + adjust) / power), 

373 ) 

374 

375 # Update the reduce and layers settings 

376 t = self.tile[0] 

377 assert isinstance(t[3], tuple) 

378 t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) 

379 self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)] 

380 

381 return ImageFile.ImageFile.load(self) 

382 

383 

384def _accept(prefix: bytes) -> bool: 

385 return prefix.startswith( 

386 (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a") 

387 ) 

388 

389 

390# ------------------------------------------------------------ 

391# Save support 

392 

393 

394def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

395 # Get the keyword arguments 

396 info = im.encoderinfo 

397 

398 if isinstance(filename, str): 

399 filename = filename.encode() 

400 if filename.endswith(b".j2k") or info.get("no_jp2", False): 

401 kind = "j2k" 

402 else: 

403 kind = "jp2" 

404 

405 offset = info.get("offset", None) 

406 tile_offset = info.get("tile_offset", None) 

407 tile_size = info.get("tile_size", None) 

408 quality_mode = info.get("quality_mode", "rates") 

409 quality_layers = info.get("quality_layers", None) 

410 if quality_layers is not None and not ( 

411 isinstance(quality_layers, (list, tuple)) 

412 and all( 

413 isinstance(quality_layer, (int, float)) for quality_layer in quality_layers 

414 ) 

415 ): 

416 msg = "quality_layers must be a sequence of numbers" 

417 raise ValueError(msg) 

418 

419 num_resolutions = info.get("num_resolutions", 0) 

420 cblk_size = info.get("codeblock_size", None) 

421 precinct_size = info.get("precinct_size", None) 

422 irreversible = info.get("irreversible", False) 

423 progression = info.get("progression", "LRCP") 

424 cinema_mode = info.get("cinema_mode", "no") 

425 mct = info.get("mct", 0) 

426 signed = info.get("signed", False) 

427 comment = info.get("comment") 

428 if isinstance(comment, str): 

429 comment = comment.encode() 

430 plt = info.get("plt", False) 

431 

432 fd = -1 

433 if hasattr(fp, "fileno"): 

434 try: 

435 fd = fp.fileno() 

436 except Exception: 

437 fd = -1 

438 

439 im.encoderconfig = ( 

440 offset, 

441 tile_offset, 

442 tile_size, 

443 quality_mode, 

444 quality_layers, 

445 num_resolutions, 

446 cblk_size, 

447 precinct_size, 

448 irreversible, 

449 progression, 

450 cinema_mode, 

451 mct, 

452 signed, 

453 fd, 

454 comment, 

455 plt, 

456 ) 

457 

458 ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) 

459 

460 

461# ------------------------------------------------------------ 

462# Registry stuff 

463 

464 

465Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) 

466Image.register_save(Jpeg2KImageFile.format, _save) 

467 

468Image.register_extensions( 

469 Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"] 

470) 

471 

472Image.register_mime(Jpeg2KImageFile.format, "image/jp2")