Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pillow-10.4.0-py3.8-linux-x86_64.egg/PIL/Jpeg2KImagePlugin.py: 14%

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

247 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 IO, Tuple, cast 

22 

23from . import Image, ImageFile, ImagePalette, _binary 

24 

25 

26class BoxReader: 

27 """ 

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

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

30 """ 

31 

32 def __init__(self, fp, length=-1): 

33 self.fp = fp 

34 self.has_length = length >= 0 

35 self.length = length 

36 self.remaining_in_box = -1 

37 

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

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

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

41 return False 

42 if self.remaining_in_box >= 0: 

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

44 return num_bytes <= self.remaining_in_box 

45 else: 

46 return True # No length known, just read 

47 

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

49 if not self._can_read(num_bytes): 

50 msg = "Not enough data in header" 

51 raise SyntaxError(msg) 

52 

53 data = self.fp.read(num_bytes) 

54 if len(data) < num_bytes: 

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

56 raise OSError(msg) 

57 

58 if self.remaining_in_box > 0: 

59 self.remaining_in_box -= num_bytes 

60 return data 

61 

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

63 size = struct.calcsize(field_format) 

64 data = self._read_bytes(size) 

65 return struct.unpack(field_format, data) 

66 

67 def read_boxes(self) -> BoxReader: 

68 size = self.remaining_in_box 

69 data = self._read_bytes(size) 

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

71 

72 def has_next_box(self) -> bool: 

73 if self.has_length: 

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

75 else: 

76 return True 

77 

78 def next_box_type(self) -> bytes: 

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

80 if self.remaining_in_box > 0: 

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

82 self.remaining_in_box = -1 

83 

84 # Read the length and type of the next box 

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

86 if lbox == 1: 

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

88 hlen = 16 

89 else: 

90 hlen = 8 

91 

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

93 msg = "Invalid header length" 

94 raise SyntaxError(msg) 

95 

96 self.remaining_in_box = lbox - hlen 

97 return tbox 

98 

99 

100def _parse_codestream(fp) -> tuple[tuple[int, int], str]: 

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

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

103 

104 hdr = fp.read(2) 

105 lsiz = _binary.i16be(hdr) 

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

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

108 ">HHIIIIIIIIH", siz 

109 ) 

110 

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

112 if csiz == 1: 

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

114 if (ssiz[0] & 0x7F) + 1 > 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 msg = "unable to determine J2K image mode" 

126 raise SyntaxError(msg) 

127 

128 return size, mode 

129 

130 

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

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

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

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

135 if denom == 0: 

136 return None 

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

138 

139 

140def _parse_jp2_header(fp): 

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

142 color space information, and optionally DPI information, 

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

144 

145 # Find the JP2 header box 

146 reader = BoxReader(fp) 

147 header = None 

148 mimetype = None 

149 while reader.has_next_box(): 

150 tbox = reader.next_box_type() 

151 

152 if tbox == b"jp2h": 

153 header = reader.read_boxes() 

154 break 

155 elif tbox == b"ftyp": 

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

157 mimetype = "image/jpx" 

158 

159 size = None 

160 mode = None 

161 bpc = None 

162 nc = None 

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

164 palette = None 

165 

166 while header.has_next_box(): 

167 tbox = header.next_box_type() 

168 

169 if tbox == b"ihdr": 

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

171 size = (width, height) 

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

173 mode = "I;16" 

174 elif nc == 1: 

175 mode = "L" 

176 elif nc == 2: 

177 mode = "LA" 

178 elif nc == 3: 

179 mode = "RGB" 

180 elif nc == 4: 

181 mode = "RGBA" 

182 elif tbox == b"colr" and nc == 4: 

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

184 if meth == 1 and enumcs == 12: 

185 mode = "CMYK" 

186 elif tbox == b"pclr" and mode in ("L", "LA"): 

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

188 bitdepths = header.read_fields(">" + ("B" * npc)) 

189 if max(bitdepths) <= 8: 

190 palette = ImagePalette.ImagePalette() 

191 for i in range(ne): 

192 palette.getcolor(header.read_fields(">" + ("B" * npc))) 

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

194 elif tbox == b"res ": 

195 res = header.read_boxes() 

196 while res.has_next_box(): 

197 tres = res.next_box_type() 

198 if tres == b"resc": 

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

200 hres = _res_to_dpi(hrcn, hrcd, hrce) 

201 vres = _res_to_dpi(vrcn, vrcd, vrce) 

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

203 dpi = (hres, vres) 

204 break 

205 

206 if size is None or mode is None: 

207 msg = "Malformed JP2 header" 

208 raise SyntaxError(msg) 

209 

210 return size, mode, mimetype, dpi, palette 

211 

212 

213## 

214# Image plugin for JPEG2000 images. 

215 

216 

217class Jpeg2KImageFile(ImageFile.ImageFile): 

218 format = "JPEG2000" 

219 format_description = "JPEG 2000 (ISO 15444)" 

220 

221 def _open(self) -> None: 

222 sig = self.fp.read(4) 

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

224 self.codec = "j2k" 

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

226 else: 

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

228 

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

230 self.codec = "jp2" 

231 header = _parse_jp2_header(self.fp) 

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

233 if dpi is not None: 

234 self.info["dpi"] = dpi 

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

236 self._parse_comment() 

237 else: 

238 msg = "not a JPEG 2000 file" 

239 raise SyntaxError(msg) 

240 

241 self._reduce = 0 

242 self.layers = 0 

243 

244 fd = -1 

245 length = -1 

246 

247 try: 

248 fd = self.fp.fileno() 

249 length = os.fstat(fd).st_size 

250 except Exception: 

251 fd = -1 

252 try: 

253 pos = self.fp.tell() 

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

255 length = self.fp.tell() 

256 self.fp.seek(pos) 

257 except Exception: 

258 length = -1 

259 

260 self.tile = [ 

261 ( 

262 "jpeg2k", 

263 (0, 0) + self.size, 

264 0, 

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

266 ) 

267 ] 

268 

269 def _parse_comment(self) -> None: 

270 hdr = self.fp.read(2) 

271 length = _binary.i16be(hdr) 

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

273 

274 while True: 

275 marker = self.fp.read(2) 

276 if not marker: 

277 break 

278 typ = marker[1] 

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

280 # Start of tile or end of codestream 

281 break 

282 hdr = self.fp.read(2) 

283 length = _binary.i16be(hdr) 

284 if typ == 0x64: 

285 # Comment 

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

287 break 

288 else: 

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

290 

291 @property 

292 def reduce(self): 

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

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

295 # property. This attempts to allow for both scenarios 

296 return self._reduce or super().reduce 

297 

298 @reduce.setter 

299 def reduce(self, value): 

300 self._reduce = value 

301 

302 def load(self): 

303 if self.tile and self._reduce: 

304 power = 1 << self._reduce 

305 adjust = power >> 1 

306 self._size = ( 

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

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

309 ) 

310 

311 # Update the reduce and layers settings 

312 t = self.tile[0] 

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

314 self.tile = [(t[0], (0, 0) + self.size, t[2], t3)] 

315 

316 return ImageFile.ImageFile.load(self) 

317 

318 

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

320 return ( 

321 prefix[:4] == b"\xff\x4f\xff\x51" 

322 or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" 

323 ) 

324 

325 

326# ------------------------------------------------------------ 

327# Save support 

328 

329 

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

331 # Get the keyword arguments 

332 info = im.encoderinfo 

333 

334 if isinstance(filename, str): 

335 filename = filename.encode() 

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

337 kind = "j2k" 

338 else: 

339 kind = "jp2" 

340 

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

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

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

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

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

346 if quality_layers is not None and not ( 

347 isinstance(quality_layers, (list, tuple)) 

348 and all( 

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

350 ) 

351 ): 

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

353 raise ValueError(msg) 

354 

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

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

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

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

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

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

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

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

363 comment = info.get("comment") 

364 if isinstance(comment, str): 

365 comment = comment.encode() 

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

367 

368 fd = -1 

369 if hasattr(fp, "fileno"): 

370 try: 

371 fd = fp.fileno() 

372 except Exception: 

373 fd = -1 

374 

375 im.encoderconfig = ( 

376 offset, 

377 tile_offset, 

378 tile_size, 

379 quality_mode, 

380 quality_layers, 

381 num_resolutions, 

382 cblk_size, 

383 precinct_size, 

384 irreversible, 

385 progression, 

386 cinema_mode, 

387 mct, 

388 signed, 

389 fd, 

390 comment, 

391 plt, 

392 ) 

393 

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

395 

396 

397# ------------------------------------------------------------ 

398# Registry stuff 

399 

400 

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

402Image.register_save(Jpeg2KImageFile.format, _save) 

403 

404Image.register_extensions( 

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

406) 

407 

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