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

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 

19 

20from . import Image, ImageFile 

21 

22 

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 """ 

28 

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 

34 

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 

44 

45 def _read_bytes(self, num_bytes): 

46 if not self._can_read(num_bytes): 

47 raise SyntaxError("Not enough data in header") 

48 

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 ) 

54 

55 if self.remaining_in_box > 0: 

56 self.remaining_in_box -= num_bytes 

57 return data 

58 

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) 

63 

64 def read_boxes(self): 

65 size = self.remaining_in_box 

66 data = self._read_bytes(size) 

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

68 

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 

74 

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 

80 

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 

88 

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

90 raise SyntaxError("Invalid header length") 

91 

92 self.remaining_in_box = lbox - hlen 

93 return tbox 

94 

95 

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.""" 

99 

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) 

111 

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 

126 

127 return size, mode 

128 

129 

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) 

136 

137 

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.""" 

142 

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() 

149 

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" 

156 

157 size = None 

158 mode = None 

159 bpc = None 

160 nc = None 

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

162 

163 while header.has_next_box(): 

164 tbox = header.next_box_type() 

165 

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 

190 

191 if size is None or mode is None: 

192 raise SyntaxError("Malformed JP2 header") 

193 

194 return size, mode, mimetype, dpi 

195 

196 

197## 

198# Image plugin for JPEG2000 images. 

199 

200 

201class Jpeg2KImageFile(ImageFile.ImageFile): 

202 format = "JPEG2000" 

203 format_description = "JPEG 2000 (ISO 15444)" 

204 

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) 

212 

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") 

221 

222 if self.size is None or self.mode is None: 

223 raise SyntaxError("unable to determine size/mode") 

224 

225 self._reduce = 0 

226 self.layers = 0 

227 

228 fd = -1 

229 length = -1 

230 

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 

243 

244 self.tile = [ 

245 ( 

246 "jpeg2k", 

247 (0, 0) + self.size, 

248 0, 

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

250 ) 

251 ] 

252 

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 

259 

260 @reduce.setter 

261 def reduce(self, value): 

262 self._reduce = value 

263 

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 ) 

272 

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)] 

277 

278 return ImageFile.ImageFile.load(self) 

279 

280 

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 ) 

286 

287 

288# ------------------------------------------------------------ 

289# Save support 

290 

291 

292def _save(im, fp, filename): 

293 # Get the keyword arguments 

294 info = im.encoderinfo 

295 

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

297 kind = "j2k" 

298 else: 

299 kind = "jp2" 

300 

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") 

316 

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 

325 

326 if hasattr(fp, "fileno"): 

327 try: 

328 fd = fp.fileno() 

329 except Exception: 

330 fd = -1 

331 

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 ) 

347 

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

349 

350 

351# ------------------------------------------------------------ 

352# Registry stuff 

353 

354 

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

356Image.register_save(Jpeg2KImageFile.format, _save) 

357 

358Image.register_extensions( 

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

360) 

361 

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