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

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

292 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 

18__lazy_modules__ = {"io", "struct"} 

19 

20import io 

21import os 

22import struct 

23from typing import cast 

24 

25from . import Image, ImageFile, ImagePalette, _binary 

26 

27TYPE_CHECKING = False 

28if TYPE_CHECKING: 

29 from collections.abc import Callable 

30 from typing import IO 

31 

32 

33class BoxReader: 

34 """ 

35 A small helper class to read fields stored in JPEG2000 header boxes 

36 and to easily step into and read sub-boxes. 

37 """ 

38 

39 def __init__(self, fp: IO[bytes], length: int = -1) -> None: 

40 self.fp = fp 

41 self.has_length = length >= 0 

42 self.length = length 

43 self.remaining_in_box = -1 

44 

45 def _can_read(self, num_bytes: int) -> bool: 

46 if self.has_length and self.fp.tell() + num_bytes > self.length: 

47 # Outside box: ensure we don't read past the known file length 

48 return False 

49 if self.remaining_in_box >= 0: 

50 # Inside box contents: ensure read does not go past box boundaries 

51 return num_bytes <= self.remaining_in_box 

52 else: 

53 return True # No length known, just read 

54 

55 def _read_bytes(self, num_bytes: int) -> bytes: 

56 if not self._can_read(num_bytes): 

57 msg = "Not enough data in header" 

58 raise SyntaxError(msg) 

59 if self.fp.tell() + num_bytes >= 2**63: 

60 msg = "Box length too large" 

61 raise SyntaxError(msg) 

62 

63 data = self.fp.read(num_bytes) 

64 if len(data) < num_bytes: 

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

66 raise OSError(msg) 

67 

68 if self.remaining_in_box > 0: 

69 self.remaining_in_box -= num_bytes 

70 return data 

71 

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

73 size = struct.calcsize(field_format) 

74 data = self._read_bytes(size) 

75 return struct.unpack(field_format, data) 

76 

77 def read_boxes(self) -> BoxReader: 

78 size = self.remaining_in_box 

79 data = self._read_bytes(size) 

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

81 

82 def has_next_box(self) -> bool: 

83 if self.has_length: 

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

85 else: 

86 return True 

87 

88 def next_box_type(self) -> bytes: 

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

90 if self.remaining_in_box > 0: 

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

92 msg = "Box length too large" 

93 raise SyntaxError(msg) 

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

95 self.remaining_in_box = -1 

96 

97 # Read the length and type of the next box 

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

99 if lbox == 1: 

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

101 hlen = 16 

102 else: 

103 hlen = 8 

104 

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

106 msg = "Invalid header length" 

107 raise SyntaxError(msg) 

108 

109 self.remaining_in_box = lbox - hlen 

110 return tbox 

111 

112 

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

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

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

116 

117 hdr = fp.read(2) 

118 lsiz = _binary.i16be(hdr) 

119 if lsiz < 38: 

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

121 raise ValueError(msg) 

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

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

124 ">HHIIIIIIIIH", siz 

125 ) 

126 

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

128 if csiz == 1: 

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

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

131 mode = "I;16" 

132 else: 

133 mode = "L" 

134 elif csiz == 2: 

135 mode = "LA" 

136 elif csiz == 3: 

137 mode = "RGB" 

138 elif csiz == 4: 

139 mode = "RGBA" 

140 else: 

141 msg = "unable to determine J2K image mode" 

142 raise SyntaxError(msg) 

143 

144 return size, mode 

145 

146 

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

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

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

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

151 if denom == 0: 

152 return None 

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

154 

155 

156def _parse_jp2_header( 

157 fp: IO[bytes], 

158) -> tuple[ 

159 tuple[int, int], 

160 str, 

161 str | None, 

162 tuple[float, float] | None, 

163 ImagePalette.ImagePalette | None, 

164]: 

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

166 color space information, and optionally DPI information, 

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

168 

169 # Find the JP2 header box 

170 reader = BoxReader(fp) 

171 header = None 

172 mimetype = None 

173 while reader.has_next_box(): 

174 tbox = reader.next_box_type() 

175 

176 if tbox == b"jp2h": 

177 header = reader.read_boxes() 

178 break 

179 elif tbox == b"ftyp": 

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

181 mimetype = "image/jpx" 

182 assert header is not None 

183 

184 size = None 

185 mode = None 

186 bpc = None 

187 nc = None 

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

189 palette = None 

190 colr = None 

191 

192 while header.has_next_box(): 

193 tbox = header.next_box_type() 

194 

195 if tbox == b"ihdr": 

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

197 assert isinstance(height, int) 

198 assert isinstance(width, int) 

199 assert isinstance(bpc, int) 

200 size = (width, height) 

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

202 mode = "I;16" 

203 elif nc == 1: 

204 mode = "L" 

205 elif nc == 2: 

206 mode = "LA" 

207 elif nc == 3: 

208 mode = "RGB" 

209 elif nc == 4: 

210 mode = "RGBA" 

211 elif tbox == b"colr": 

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

213 if meth == 1: 

214 if enumcs in (0, 15): 

215 colr = "1" 

216 elif enumcs == 12: 

217 colr = "CMYK" 

218 if nc == 4: 

219 mode = "CMYK" 

220 elif enumcs == 17: 

221 colr = "L" 

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

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

224 assert isinstance(ne, int) 

225 assert isinstance(npc, int) 

226 max_bitdepth = 0 

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

228 assert isinstance(bitdepth, int) 

229 if bitdepth > max_bitdepth: 

230 max_bitdepth = bitdepth 

231 if max_bitdepth <= 8: 

232 if npc == 4: 

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

234 else: 

235 palette_mode = "RGB" 

236 palette = ImagePalette.ImagePalette(palette_mode) 

237 for i in range(ne): 

238 color: list[int] = [] 

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

240 assert isinstance(value, int) 

241 color.append(value) 

242 palette.getcolor(tuple(color)) 

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

244 elif tbox == b"res ": 

245 res = header.read_boxes() 

246 while res.has_next_box(): 

247 tres = res.next_box_type() 

248 if tres == b"resc": 

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

250 assert isinstance(vrcn, int) 

251 assert isinstance(vrcd, int) 

252 assert isinstance(hrcn, int) 

253 assert isinstance(hrcd, int) 

254 assert isinstance(vrce, int) 

255 assert isinstance(hrce, int) 

256 hres = _res_to_dpi(hrcn, hrcd, hrce) 

257 vres = _res_to_dpi(vrcn, vrcd, vrce) 

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

259 dpi = (hres, vres) 

260 break 

261 

262 if size is None or mode is None: 

263 msg = "Malformed JP2 header" 

264 raise SyntaxError(msg) 

265 

266 return size, mode, mimetype, dpi, palette 

267 

268 

269## 

270# Image plugin for JPEG2000 images. 

271 

272 

273class Jpeg2KImageFile(ImageFile.ImageFile): 

274 format = "JPEG2000" 

275 format_description = "JPEG 2000 (ISO 15444)" 

276 

277 def _open(self) -> None: 

278 assert self.fp is not None 

279 sig = self.fp.read(4) 

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

281 self.codec = "j2k" 

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

283 self._parse_comment() 

284 else: 

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

286 

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

288 self.codec = "jp2" 

289 header = _parse_jp2_header(self.fp) 

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

291 if dpi is not None: 

292 self.info["dpi"] = dpi 

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

294 _parse_codestream(self.fp) 

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 try: 

304 fd = self.fp.fileno() 

305 except Exception: 

306 fd = -1 

307 

308 self.tile = [ 

309 ImageFile._Tile( 

310 "jpeg2k", 

311 args=(self.codec, self._reduce, self.layers, fd, self._get_fp_length()), 

312 ) 

313 ] 

314 

315 def _parse_comment(self) -> None: 

316 assert self.fp is not None 

317 while True: 

318 marker = self.fp.read(2) 

319 if not marker: 

320 break 

321 typ = marker[1] 

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

323 # Start of tile or end of codestream 

324 break 

325 hdr = self.fp.read(2) 

326 length = _binary.i16be(hdr) 

327 if length < 2: 

328 msg = "Marker length too small" 

329 raise ValueError(msg) 

330 if typ == 0x64: 

331 # Comment 

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

333 break 

334 else: 

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

336 

337 @property # type: ignore[override] 

338 def reduce( 

339 self, 

340 ) -> ( 

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

342 | int 

343 ): 

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

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

346 # property. This attempts to allow for both scenarios 

347 return self._reduce or super().reduce 

348 

349 @reduce.setter 

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

351 self._reduce = value 

352 

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

354 if self.tile and self._reduce: 

355 power = 1 << self._reduce 

356 adjust = power >> 1 

357 self._size = ( 

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

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

360 ) 

361 

362 # Update the reduce and layers settings 

363 t = self.tile[0] 

364 assert isinstance(t.args, tuple) 

365 args = (t.args[0], self._reduce, self.layers, t.args[3], t.args[4]) 

366 self.tile = [t._replace(args=args)] 

367 

368 return ImageFile.ImageFile.load(self) 

369 

370 

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

372 return prefix.startswith( 

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

374 ) 

375 

376 

377# ------------------------------------------------------------ 

378# Save support 

379 

380 

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

382 if im.mode not in {"L", "I;16", "I;16B", "LA", "RGB", "YCbCr", "RGBA", "CMYK"}: 

383 msg = f"cannot write mode {im.mode} as JPEG2000" 

384 raise OSError(msg) 

385 

386 # Get the keyword arguments 

387 info = im.encoderinfo 

388 

389 if isinstance(filename, str): 

390 filename = filename.encode() 

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

392 kind = "j2k" 

393 else: 

394 kind = "jp2" 

395 

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

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

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

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

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

401 if quality_layers is not None and not ( 

402 isinstance(quality_layers, (list, tuple)) 

403 and all( 

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

405 ) 

406 ): 

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

408 raise ValueError(msg) 

409 

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

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

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

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

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

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

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

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

418 comment = info.get("comment") 

419 if isinstance(comment, str): 

420 comment = comment.encode() 

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

422 

423 fd = -1 

424 if hasattr(fp, "fileno"): 

425 try: 

426 fd = fp.fileno() 

427 except Exception: 

428 fd = -1 

429 

430 im.encoderconfig = ( 

431 offset, 

432 tile_offset, 

433 tile_size, 

434 quality_mode, 

435 quality_layers, 

436 num_resolutions, 

437 cblk_size, 

438 precinct_size, 

439 irreversible, 

440 progression, 

441 cinema_mode, 

442 mct, 

443 signed, 

444 fd, 

445 comment, 

446 plt, 

447 ) 

448 

449 ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", args=kind)]) 

450 

451 

452# ------------------------------------------------------------ 

453# Registry stuff 

454 

455 

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

457Image.register_save(Jpeg2KImageFile.format, _save) 

458 

459Image.register_extensions( 

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

461) 

462 

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