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

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

462 statements  

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# JPEG (JFIF) file handling 

6# 

7# See "Digital Compression and Coding of Continuous-Tone Still Images, 

8# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) 

9# 

10# History: 

11# 1995-09-09 fl Created 

12# 1995-09-13 fl Added full parser 

13# 1996-03-25 fl Added hack to use the IJG command line utilities 

14# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug 

15# 1996-05-28 fl Added draft support, JFIF version (0.1) 

16# 1996-12-30 fl Added encoder options, added progression property (0.2) 

17# 1997-08-27 fl Save mode 1 images as BW (0.3) 

18# 1998-07-12 fl Added YCbCr to draft and save methods (0.4) 

19# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1) 

20# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2) 

21# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3) 

22# 2003-04-25 fl Added experimental EXIF decoder (0.5) 

23# 2003-06-06 fl Added experimental EXIF GPSinfo decoder 

24# 2003-09-13 fl Extract COM markers 

25# 2009-09-06 fl Added icc_profile support (from Florian Hoech) 

26# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6) 

27# 2009-03-08 fl Added subsampling support (from Justin Huff). 

28# 

29# Copyright (c) 1997-2003 by Secret Labs AB. 

30# Copyright (c) 1995-1996 by Fredrik Lundh. 

31# 

32# See the README file for information on usage and redistribution. 

33# 

34from __future__ import annotations 

35 

36import array 

37import io 

38import math 

39import os 

40import struct 

41import subprocess 

42import sys 

43import tempfile 

44import warnings 

45 

46from . import Image, ImageFile 

47from ._binary import i16be as i16 

48from ._binary import i32be as i32 

49from ._binary import o8 

50from ._binary import o16be as o16 

51from .JpegPresets import presets 

52 

53TYPE_CHECKING = False 

54if TYPE_CHECKING: 

55 from typing import IO, Any 

56 

57 from .MpoImagePlugin import MpoImageFile 

58 

59# 

60# Parser 

61 

62 

63def Skip(self: JpegImageFile, marker: int) -> None: 

64 n = i16(self.fp.read(2)) - 2 

65 ImageFile._safe_read(self.fp, n) 

66 

67 

68def APP(self: JpegImageFile, marker: int) -> None: 

69 # 

70 # Application marker. Store these in the APP dictionary. 

71 # Also look for well-known application markers. 

72 

73 n = i16(self.fp.read(2)) - 2 

74 s = ImageFile._safe_read(self.fp, n) 

75 

76 app = f"APP{marker & 15}" 

77 

78 self.app[app] = s # compatibility 

79 self.applist.append((app, s)) 

80 

81 if marker == 0xFFE0 and s.startswith(b"JFIF"): 

82 # extract JFIF information 

83 self.info["jfif"] = version = i16(s, 5) # version 

84 self.info["jfif_version"] = divmod(version, 256) 

85 # extract JFIF properties 

86 try: 

87 jfif_unit = s[7] 

88 jfif_density = i16(s, 8), i16(s, 10) 

89 except Exception: 

90 pass 

91 else: 

92 if jfif_unit == 1: 

93 self.info["dpi"] = jfif_density 

94 elif jfif_unit == 2: # cm 

95 # 1 dpcm = 2.54 dpi 

96 self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) 

97 self.info["jfif_unit"] = jfif_unit 

98 self.info["jfif_density"] = jfif_density 

99 elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"): 

100 # extract EXIF information 

101 if "exif" in self.info: 

102 self.info["exif"] += s[6:] 

103 else: 

104 self.info["exif"] = s 

105 self._exif_offset = self.fp.tell() - n + 6 

106 elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"): 

107 self.info["xmp"] = s.split(b"\x00", 1)[1] 

108 elif marker == 0xFFE2 and s.startswith(b"FPXR\0"): 

109 # extract FlashPix information (incomplete) 

110 self.info["flashpix"] = s # FIXME: value will change 

111 elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"): 

112 # Since an ICC profile can be larger than the maximum size of 

113 # a JPEG marker (64K), we need provisions to split it into 

114 # multiple markers. The format defined by the ICC specifies 

115 # one or more APP2 markers containing the following data: 

116 # Identifying string ASCII "ICC_PROFILE\0" (12 bytes) 

117 # Marker sequence number 1, 2, etc (1 byte) 

118 # Number of markers Total of APP2's used (1 byte) 

119 # Profile data (remainder of APP2 data) 

120 # Decoders should use the marker sequence numbers to 

121 # reassemble the profile, rather than assuming that the APP2 

122 # markers appear in the correct sequence. 

123 self.icclist.append(s) 

124 elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"): 

125 # parse the image resource block 

126 offset = 14 

127 photoshop = self.info.setdefault("photoshop", {}) 

128 while s[offset : offset + 4] == b"8BIM": 

129 try: 

130 offset += 4 

131 # resource code 

132 code = i16(s, offset) 

133 offset += 2 

134 # resource name (usually empty) 

135 name_len = s[offset] 

136 # name = s[offset+1:offset+1+name_len] 

137 offset += 1 + name_len 

138 offset += offset & 1 # align 

139 # resource data block 

140 size = i32(s, offset) 

141 offset += 4 

142 data = s[offset : offset + size] 

143 if code == 0x03ED: # ResolutionInfo 

144 photoshop[code] = { 

145 "XResolution": i32(data, 0) / 65536, 

146 "DisplayedUnitsX": i16(data, 4), 

147 "YResolution": i32(data, 8) / 65536, 

148 "DisplayedUnitsY": i16(data, 12), 

149 } 

150 else: 

151 photoshop[code] = data 

152 offset += size 

153 offset += offset & 1 # align 

154 except struct.error: 

155 break # insufficient data 

156 

157 elif marker == 0xFFEE and s.startswith(b"Adobe"): 

158 self.info["adobe"] = i16(s, 5) 

159 # extract Adobe custom properties 

160 try: 

161 adobe_transform = s[11] 

162 except IndexError: 

163 pass 

164 else: 

165 self.info["adobe_transform"] = adobe_transform 

166 elif marker == 0xFFE2 and s.startswith(b"MPF\0"): 

167 # extract MPO information 

168 self.info["mp"] = s[4:] 

169 # offset is current location minus buffer size 

170 # plus constant header size 

171 self.info["mpoffset"] = self.fp.tell() - n + 4 

172 

173 

174def COM(self: JpegImageFile, marker: int) -> None: 

175 # 

176 # Comment marker. Store these in the APP dictionary. 

177 n = i16(self.fp.read(2)) - 2 

178 s = ImageFile._safe_read(self.fp, n) 

179 

180 self.info["comment"] = s 

181 self.app["COM"] = s # compatibility 

182 self.applist.append(("COM", s)) 

183 

184 

185def SOF(self: JpegImageFile, marker: int) -> None: 

186 # 

187 # Start of frame marker. Defines the size and mode of the 

188 # image. JPEG is colour blind, so we use some simple 

189 # heuristics to map the number of layers to an appropriate 

190 # mode. Note that this could be made a bit brighter, by 

191 # looking for JFIF and Adobe APP markers. 

192 

193 n = i16(self.fp.read(2)) - 2 

194 s = ImageFile._safe_read(self.fp, n) 

195 self._size = i16(s, 3), i16(s, 1) 

196 if self._im is not None and self.size != self.im.size: 

197 self._im = None 

198 

199 self.bits = s[0] 

200 if self.bits != 8: 

201 msg = f"cannot handle {self.bits}-bit layers" 

202 raise SyntaxError(msg) 

203 

204 self.layers = s[5] 

205 if self.layers == 1: 

206 self._mode = "L" 

207 elif self.layers == 3: 

208 self._mode = "RGB" 

209 elif self.layers == 4: 

210 self._mode = "CMYK" 

211 else: 

212 msg = f"cannot handle {self.layers}-layer images" 

213 raise SyntaxError(msg) 

214 

215 if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: 

216 self.info["progressive"] = self.info["progression"] = 1 

217 

218 if self.icclist: 

219 # fixup icc profile 

220 self.icclist.sort() # sort by sequence number 

221 if self.icclist[0][13] == len(self.icclist): 

222 profile = [p[14:] for p in self.icclist] 

223 icc_profile = b"".join(profile) 

224 else: 

225 icc_profile = None # wrong number of fragments 

226 self.info["icc_profile"] = icc_profile 

227 self.icclist = [] 

228 

229 for i in range(6, len(s), 3): 

230 t = s[i : i + 3] 

231 # 4-tuples: id, vsamp, hsamp, qtable 

232 self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) 

233 

234 

235def DQT(self: JpegImageFile, marker: int) -> None: 

236 # 

237 # Define quantization table. Note that there might be more 

238 # than one table in each marker. 

239 

240 # FIXME: The quantization tables can be used to estimate the 

241 # compression quality. 

242 

243 n = i16(self.fp.read(2)) - 2 

244 s = ImageFile._safe_read(self.fp, n) 

245 while len(s): 

246 v = s[0] 

247 precision = 1 if (v // 16 == 0) else 2 # in bytes 

248 qt_length = 1 + precision * 64 

249 if len(s) < qt_length: 

250 msg = "bad quantization table marker" 

251 raise SyntaxError(msg) 

252 data = array.array("B" if precision == 1 else "H", s[1:qt_length]) 

253 if sys.byteorder == "little" and precision > 1: 

254 data.byteswap() # the values are always big-endian 

255 self.quantization[v & 15] = [data[i] for i in zigzag_index] 

256 s = s[qt_length:] 

257 

258 

259# 

260# JPEG marker table 

261 

262MARKER = { 

263 0xFFC0: ("SOF0", "Baseline DCT", SOF), 

264 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF), 

265 0xFFC2: ("SOF2", "Progressive DCT", SOF), 

266 0xFFC3: ("SOF3", "Spatial lossless", SOF), 

267 0xFFC4: ("DHT", "Define Huffman table", Skip), 

268 0xFFC5: ("SOF5", "Differential sequential DCT", SOF), 

269 0xFFC6: ("SOF6", "Differential progressive DCT", SOF), 

270 0xFFC7: ("SOF7", "Differential spatial", SOF), 

271 0xFFC8: ("JPG", "Extension", None), 

272 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF), 

273 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF), 

274 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF), 

275 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip), 

276 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF), 

277 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF), 

278 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF), 

279 0xFFD0: ("RST0", "Restart 0", None), 

280 0xFFD1: ("RST1", "Restart 1", None), 

281 0xFFD2: ("RST2", "Restart 2", None), 

282 0xFFD3: ("RST3", "Restart 3", None), 

283 0xFFD4: ("RST4", "Restart 4", None), 

284 0xFFD5: ("RST5", "Restart 5", None), 

285 0xFFD6: ("RST6", "Restart 6", None), 

286 0xFFD7: ("RST7", "Restart 7", None), 

287 0xFFD8: ("SOI", "Start of image", None), 

288 0xFFD9: ("EOI", "End of image", None), 

289 0xFFDA: ("SOS", "Start of scan", Skip), 

290 0xFFDB: ("DQT", "Define quantization table", DQT), 

291 0xFFDC: ("DNL", "Define number of lines", Skip), 

292 0xFFDD: ("DRI", "Define restart interval", Skip), 

293 0xFFDE: ("DHP", "Define hierarchical progression", SOF), 

294 0xFFDF: ("EXP", "Expand reference component", Skip), 

295 0xFFE0: ("APP0", "Application segment 0", APP), 

296 0xFFE1: ("APP1", "Application segment 1", APP), 

297 0xFFE2: ("APP2", "Application segment 2", APP), 

298 0xFFE3: ("APP3", "Application segment 3", APP), 

299 0xFFE4: ("APP4", "Application segment 4", APP), 

300 0xFFE5: ("APP5", "Application segment 5", APP), 

301 0xFFE6: ("APP6", "Application segment 6", APP), 

302 0xFFE7: ("APP7", "Application segment 7", APP), 

303 0xFFE8: ("APP8", "Application segment 8", APP), 

304 0xFFE9: ("APP9", "Application segment 9", APP), 

305 0xFFEA: ("APP10", "Application segment 10", APP), 

306 0xFFEB: ("APP11", "Application segment 11", APP), 

307 0xFFEC: ("APP12", "Application segment 12", APP), 

308 0xFFED: ("APP13", "Application segment 13", APP), 

309 0xFFEE: ("APP14", "Application segment 14", APP), 

310 0xFFEF: ("APP15", "Application segment 15", APP), 

311 0xFFF0: ("JPG0", "Extension 0", None), 

312 0xFFF1: ("JPG1", "Extension 1", None), 

313 0xFFF2: ("JPG2", "Extension 2", None), 

314 0xFFF3: ("JPG3", "Extension 3", None), 

315 0xFFF4: ("JPG4", "Extension 4", None), 

316 0xFFF5: ("JPG5", "Extension 5", None), 

317 0xFFF6: ("JPG6", "Extension 6", None), 

318 0xFFF7: ("JPG7", "Extension 7", None), 

319 0xFFF8: ("JPG8", "Extension 8", None), 

320 0xFFF9: ("JPG9", "Extension 9", None), 

321 0xFFFA: ("JPG10", "Extension 10", None), 

322 0xFFFB: ("JPG11", "Extension 11", None), 

323 0xFFFC: ("JPG12", "Extension 12", None), 

324 0xFFFD: ("JPG13", "Extension 13", None), 

325 0xFFFE: ("COM", "Comment", COM), 

326} 

327 

328 

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

330 # Magic number was taken from https://en.wikipedia.org/wiki/JPEG 

331 return prefix.startswith(b"\xff\xd8\xff") 

332 

333 

334## 

335# Image plugin for JPEG and JFIF images. 

336 

337 

338class JpegImageFile(ImageFile.ImageFile): 

339 format = "JPEG" 

340 format_description = "JPEG (ISO 10918)" 

341 

342 def _open(self) -> None: 

343 s = self.fp.read(3) 

344 

345 if not _accept(s): 

346 msg = "not a JPEG file" 

347 raise SyntaxError(msg) 

348 s = b"\xff" 

349 

350 # Create attributes 

351 self.bits = self.layers = 0 

352 self._exif_offset = 0 

353 

354 # JPEG specifics (internal) 

355 self.layer: list[tuple[int, int, int, int]] = [] 

356 self._huffman_dc: dict[Any, Any] = {} 

357 self._huffman_ac: dict[Any, Any] = {} 

358 self.quantization: dict[int, list[int]] = {} 

359 self.app: dict[str, bytes] = {} # compatibility 

360 self.applist: list[tuple[str, bytes]] = [] 

361 self.icclist: list[bytes] = [] 

362 

363 while True: 

364 i = s[0] 

365 if i == 0xFF: 

366 s = s + self.fp.read(1) 

367 i = i16(s) 

368 else: 

369 # Skip non-0xFF junk 

370 s = self.fp.read(1) 

371 continue 

372 

373 if i in MARKER: 

374 name, description, handler = MARKER[i] 

375 if handler is not None: 

376 handler(self, i) 

377 if i == 0xFFDA: # start of scan 

378 rawmode = self.mode 

379 if self.mode == "CMYK": 

380 rawmode = "CMYK;I" # assume adobe conventions 

381 self.tile = [ 

382 ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, "")) 

383 ] 

384 # self.__offset = self.fp.tell() 

385 break 

386 s = self.fp.read(1) 

387 elif i in {0, 0xFFFF}: 

388 # padded marker or junk; move on 

389 s = b"\xff" 

390 elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) 

391 s = self.fp.read(1) 

392 else: 

393 msg = "no marker found" 

394 raise SyntaxError(msg) 

395 

396 self._read_dpi_from_exif() 

397 

398 def __getstate__(self) -> list[Any]: 

399 return super().__getstate__() + [self.layers, self.layer] 

400 

401 def __setstate__(self, state: list[Any]) -> None: 

402 self.layers, self.layer = state[6:] 

403 super().__setstate__(state) 

404 

405 def load_read(self, read_bytes: int) -> bytes: 

406 """ 

407 internal: read more image data 

408 For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker 

409 so libjpeg can finish decoding 

410 """ 

411 s = self.fp.read(read_bytes) 

412 

413 if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): 

414 # Premature EOF. 

415 # Pretend file is finished adding EOI marker 

416 self._ended = True 

417 return b"\xff\xd9" 

418 

419 return s 

420 

421 def draft( 

422 self, mode: str | None, size: tuple[int, int] | None 

423 ) -> tuple[str, tuple[int, int, float, float]] | None: 

424 if len(self.tile) != 1: 

425 return None 

426 

427 # Protect from second call 

428 if self.decoderconfig: 

429 return None 

430 

431 d, e, o, a = self.tile[0] 

432 scale = 1 

433 original_size = self.size 

434 

435 assert isinstance(a, tuple) 

436 if a[0] == "RGB" and mode in ["L", "YCbCr"]: 

437 self._mode = mode 

438 a = mode, "" 

439 

440 if size: 

441 scale = min(self.size[0] // size[0], self.size[1] // size[1]) 

442 for s in [8, 4, 2, 1]: 

443 if scale >= s: 

444 break 

445 assert e is not None 

446 e = ( 

447 e[0], 

448 e[1], 

449 (e[2] - e[0] + s - 1) // s + e[0], 

450 (e[3] - e[1] + s - 1) // s + e[1], 

451 ) 

452 self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) 

453 scale = s 

454 

455 self.tile = [ImageFile._Tile(d, e, o, a)] 

456 self.decoderconfig = (scale, 0) 

457 

458 box = (0, 0, original_size[0] / scale, original_size[1] / scale) 

459 return self.mode, box 

460 

461 def load_djpeg(self) -> None: 

462 # ALTERNATIVE: handle JPEGs via the IJG command line utilities 

463 

464 f, path = tempfile.mkstemp() 

465 os.close(f) 

466 if os.path.exists(self.filename): 

467 subprocess.check_call(["djpeg", "-outfile", path, self.filename]) 

468 else: 

469 try: 

470 os.unlink(path) 

471 except OSError: 

472 pass 

473 

474 msg = "Invalid Filename" 

475 raise ValueError(msg) 

476 

477 try: 

478 with Image.open(path) as _im: 

479 _im.load() 

480 self.im = _im.im 

481 finally: 

482 try: 

483 os.unlink(path) 

484 except OSError: 

485 pass 

486 

487 self._mode = self.im.mode 

488 self._size = self.im.size 

489 

490 self.tile = [] 

491 

492 def _getexif(self) -> dict[int, Any] | None: 

493 return _getexif(self) 

494 

495 def _read_dpi_from_exif(self) -> None: 

496 # If DPI isn't in JPEG header, fetch from EXIF 

497 if "dpi" in self.info or "exif" not in self.info: 

498 return 

499 try: 

500 exif = self.getexif() 

501 resolution_unit = exif[0x0128] 

502 x_resolution = exif[0x011A] 

503 try: 

504 dpi = float(x_resolution[0]) / x_resolution[1] 

505 except TypeError: 

506 dpi = x_resolution 

507 if math.isnan(dpi): 

508 msg = "DPI is not a number" 

509 raise ValueError(msg) 

510 if resolution_unit == 3: # cm 

511 # 1 dpcm = 2.54 dpi 

512 dpi *= 2.54 

513 self.info["dpi"] = dpi, dpi 

514 except ( 

515 struct.error, # truncated EXIF 

516 KeyError, # dpi not included 

517 SyntaxError, # invalid/unreadable EXIF 

518 TypeError, # dpi is an invalid float 

519 ValueError, # dpi is an invalid float 

520 ZeroDivisionError, # invalid dpi rational value 

521 ): 

522 self.info["dpi"] = 72, 72 

523 

524 def _getmp(self) -> dict[int, Any] | None: 

525 return _getmp(self) 

526 

527 

528def _getexif(self: JpegImageFile) -> dict[int, Any] | None: 

529 if "exif" not in self.info: 

530 return None 

531 return self.getexif()._get_merged_dict() 

532 

533 

534def _getmp(self: JpegImageFile) -> dict[int, Any] | None: 

535 # Extract MP information. This method was inspired by the "highly 

536 # experimental" _getexif version that's been in use for years now, 

537 # itself based on the ImageFileDirectory class in the TIFF plugin. 

538 

539 # The MP record essentially consists of a TIFF file embedded in a JPEG 

540 # application marker. 

541 try: 

542 data = self.info["mp"] 

543 except KeyError: 

544 return None 

545 file_contents = io.BytesIO(data) 

546 head = file_contents.read(8) 

547 endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<" 

548 # process dictionary 

549 from . import TiffImagePlugin 

550 

551 try: 

552 info = TiffImagePlugin.ImageFileDirectory_v2(head) 

553 file_contents.seek(info.next) 

554 info.load(file_contents) 

555 mp = dict(info) 

556 except Exception as e: 

557 msg = "malformed MP Index (unreadable directory)" 

558 raise SyntaxError(msg) from e 

559 # it's an error not to have a number of images 

560 try: 

561 quant = mp[0xB001] 

562 except KeyError as e: 

563 msg = "malformed MP Index (no number of images)" 

564 raise SyntaxError(msg) from e 

565 # get MP entries 

566 mpentries = [] 

567 try: 

568 rawmpentries = mp[0xB002] 

569 for entrynum in range(quant): 

570 unpackedentry = struct.unpack_from( 

571 f"{endianness}LLLHH", rawmpentries, entrynum * 16 

572 ) 

573 labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") 

574 mpentry = dict(zip(labels, unpackedentry)) 

575 mpentryattr = { 

576 "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)), 

577 "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)), 

578 "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)), 

579 "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27, 

580 "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24, 

581 "MPType": mpentry["Attribute"] & 0x00FFFFFF, 

582 } 

583 if mpentryattr["ImageDataFormat"] == 0: 

584 mpentryattr["ImageDataFormat"] = "JPEG" 

585 else: 

586 msg = "unsupported picture format in MPO" 

587 raise SyntaxError(msg) 

588 mptypemap = { 

589 0x000000: "Undefined", 

590 0x010001: "Large Thumbnail (VGA Equivalent)", 

591 0x010002: "Large Thumbnail (Full HD Equivalent)", 

592 0x020001: "Multi-Frame Image (Panorama)", 

593 0x020002: "Multi-Frame Image: (Disparity)", 

594 0x020003: "Multi-Frame Image: (Multi-Angle)", 

595 0x030000: "Baseline MP Primary Image", 

596 } 

597 mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown") 

598 mpentry["Attribute"] = mpentryattr 

599 mpentries.append(mpentry) 

600 mp[0xB002] = mpentries 

601 except KeyError as e: 

602 msg = "malformed MP Index (bad MP Entry)" 

603 raise SyntaxError(msg) from e 

604 # Next we should try and parse the individual image unique ID list; 

605 # we don't because I've never seen this actually used in a real MPO 

606 # file and so can't test it. 

607 return mp 

608 

609 

610# -------------------------------------------------------------------- 

611# stuff to save JPEG files 

612 

613RAWMODE = { 

614 "1": "L", 

615 "L": "L", 

616 "RGB": "RGB", 

617 "RGBX": "RGB", 

618 "CMYK": "CMYK;I", # assume adobe conventions 

619 "YCbCr": "YCbCr", 

620} 

621 

622# fmt: off 

623zigzag_index = ( 

624 0, 1, 5, 6, 14, 15, 27, 28, 

625 2, 4, 7, 13, 16, 26, 29, 42, 

626 3, 8, 12, 17, 25, 30, 41, 43, 

627 9, 11, 18, 24, 31, 40, 44, 53, 

628 10, 19, 23, 32, 39, 45, 52, 54, 

629 20, 22, 33, 38, 46, 51, 55, 60, 

630 21, 34, 37, 47, 50, 56, 59, 61, 

631 35, 36, 48, 49, 57, 58, 62, 63, 

632) 

633 

634samplings = { 

635 (1, 1, 1, 1, 1, 1): 0, 

636 (2, 1, 1, 1, 1, 1): 1, 

637 (2, 2, 1, 1, 1, 1): 2, 

638} 

639# fmt: on 

640 

641 

642def get_sampling(im: Image.Image) -> int: 

643 # There's no subsampling when images have only 1 layer 

644 # (grayscale images) or when they are CMYK (4 layers), 

645 # so set subsampling to the default value. 

646 # 

647 # NOTE: currently Pillow can't encode JPEG to YCCK format. 

648 # If YCCK support is added in the future, subsampling code will have 

649 # to be updated (here and in JpegEncode.c) to deal with 4 layers. 

650 if not isinstance(im, JpegImageFile) or im.layers in (1, 4): 

651 return -1 

652 sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] 

653 return samplings.get(sampling, -1) 

654 

655 

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

657 if im.width == 0 or im.height == 0: 

658 msg = "cannot write empty image as JPEG" 

659 raise ValueError(msg) 

660 

661 try: 

662 rawmode = RAWMODE[im.mode] 

663 except KeyError as e: 

664 msg = f"cannot write mode {im.mode} as JPEG" 

665 raise OSError(msg) from e 

666 

667 info = im.encoderinfo 

668 

669 dpi = [round(x) for x in info.get("dpi", (0, 0))] 

670 

671 quality = info.get("quality", -1) 

672 subsampling = info.get("subsampling", -1) 

673 qtables = info.get("qtables") 

674 

675 if quality == "keep": 

676 quality = -1 

677 subsampling = "keep" 

678 qtables = "keep" 

679 elif quality in presets: 

680 preset = presets[quality] 

681 quality = -1 

682 subsampling = preset.get("subsampling", -1) 

683 qtables = preset.get("quantization") 

684 elif not isinstance(quality, int): 

685 msg = "Invalid quality setting" 

686 raise ValueError(msg) 

687 else: 

688 if subsampling in presets: 

689 subsampling = presets[subsampling].get("subsampling", -1) 

690 if isinstance(qtables, str) and qtables in presets: 

691 qtables = presets[qtables].get("quantization") 

692 

693 if subsampling == "4:4:4": 

694 subsampling = 0 

695 elif subsampling == "4:2:2": 

696 subsampling = 1 

697 elif subsampling == "4:2:0": 

698 subsampling = 2 

699 elif subsampling == "4:1:1": 

700 # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0. 

701 # Set 4:2:0 if someone is still using that value. 

702 subsampling = 2 

703 elif subsampling == "keep": 

704 if im.format != "JPEG": 

705 msg = "Cannot use 'keep' when original image is not a JPEG" 

706 raise ValueError(msg) 

707 subsampling = get_sampling(im) 

708 

709 def validate_qtables( 

710 qtables: ( 

711 str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None 

712 ), 

713 ) -> list[list[int]] | None: 

714 if qtables is None: 

715 return qtables 

716 if isinstance(qtables, str): 

717 try: 

718 lines = [ 

719 int(num) 

720 for line in qtables.splitlines() 

721 for num in line.split("#", 1)[0].split() 

722 ] 

723 except ValueError as e: 

724 msg = "Invalid quantization table" 

725 raise ValueError(msg) from e 

726 else: 

727 qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] 

728 if isinstance(qtables, (tuple, list, dict)): 

729 if isinstance(qtables, dict): 

730 qtables = [ 

731 qtables[key] for key in range(len(qtables)) if key in qtables 

732 ] 

733 elif isinstance(qtables, tuple): 

734 qtables = list(qtables) 

735 if not (0 < len(qtables) < 5): 

736 msg = "None or too many quantization tables" 

737 raise ValueError(msg) 

738 for idx, table in enumerate(qtables): 

739 try: 

740 if len(table) != 64: 

741 msg = "Invalid quantization table" 

742 raise TypeError(msg) 

743 table_array = array.array("H", table) 

744 except TypeError as e: 

745 msg = "Invalid quantization table" 

746 raise ValueError(msg) from e 

747 else: 

748 qtables[idx] = list(table_array) 

749 return qtables 

750 

751 if qtables == "keep": 

752 if im.format != "JPEG": 

753 msg = "Cannot use 'keep' when original image is not a JPEG" 

754 raise ValueError(msg) 

755 qtables = getattr(im, "quantization", None) 

756 qtables = validate_qtables(qtables) 

757 

758 extra = info.get("extra", b"") 

759 

760 MAX_BYTES_IN_MARKER = 65533 

761 if xmp := info.get("xmp"): 

762 overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" 

763 max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len 

764 if len(xmp) > max_data_bytes_in_marker: 

765 msg = "XMP data is too long" 

766 raise ValueError(msg) 

767 size = o16(2 + overhead_len + len(xmp)) 

768 extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp 

769 

770 if icc_profile := info.get("icc_profile"): 

771 overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) 

772 max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len 

773 markers = [] 

774 while icc_profile: 

775 markers.append(icc_profile[:max_data_bytes_in_marker]) 

776 icc_profile = icc_profile[max_data_bytes_in_marker:] 

777 i = 1 

778 for marker in markers: 

779 size = o16(2 + overhead_len + len(marker)) 

780 extra += ( 

781 b"\xff\xe2" 

782 + size 

783 + b"ICC_PROFILE\0" 

784 + o8(i) 

785 + o8(len(markers)) 

786 + marker 

787 ) 

788 i += 1 

789 

790 comment = info.get("comment", im.info.get("comment")) 

791 

792 # "progressive" is the official name, but older documentation 

793 # says "progression" 

794 # FIXME: issue a warning if the wrong form is used (post-1.1.7) 

795 progressive = info.get("progressive", False) or info.get("progression", False) 

796 

797 optimize = info.get("optimize", False) 

798 

799 exif = info.get("exif", b"") 

800 if isinstance(exif, Image.Exif): 

801 exif = exif.tobytes() 

802 if len(exif) > MAX_BYTES_IN_MARKER: 

803 msg = "EXIF data is too long" 

804 raise ValueError(msg) 

805 

806 # get keyword arguments 

807 im.encoderconfig = ( 

808 quality, 

809 progressive, 

810 info.get("smooth", 0), 

811 optimize, 

812 info.get("keep_rgb", False), 

813 info.get("streamtype", 0), 

814 dpi, 

815 subsampling, 

816 info.get("restart_marker_blocks", 0), 

817 info.get("restart_marker_rows", 0), 

818 qtables, 

819 comment, 

820 extra, 

821 exif, 

822 ) 

823 

824 # if we optimize, libjpeg needs a buffer big enough to hold the whole image 

825 # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is 

826 # channels*size, this is a value that's been used in a django patch. 

827 # https://github.com/matthewwithanm/django-imagekit/issues/50 

828 if optimize or progressive: 

829 # CMYK can be bigger 

830 if im.mode == "CMYK": 

831 bufsize = 4 * im.size[0] * im.size[1] 

832 # keep sets quality to -1, but the actual value may be high. 

833 elif quality >= 95 or quality == -1: 

834 bufsize = 2 * im.size[0] * im.size[1] 

835 else: 

836 bufsize = im.size[0] * im.size[1] 

837 if exif: 

838 bufsize += len(exif) + 5 

839 if extra: 

840 bufsize += len(extra) + 1 

841 else: 

842 # The EXIF info needs to be written as one block, + APP1, + one spare byte. 

843 # Ensure that our buffer is big enough. Same with the icc_profile block. 

844 bufsize = max(len(exif) + 5, len(extra) + 1) 

845 

846 ImageFile._save( 

847 im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize 

848 ) 

849 

850 

851## 

852# Factory for making JPEG and MPO instances 

853def jpeg_factory( 

854 fp: IO[bytes], filename: str | bytes | None = None 

855) -> JpegImageFile | MpoImageFile: 

856 im = JpegImageFile(fp, filename) 

857 try: 

858 mpheader = im._getmp() 

859 if mpheader is not None and mpheader[45057] > 1: 

860 for segment, content in im.applist: 

861 if segment == "APP1" and b' hdrgm:Version="' in content: 

862 # Ultra HDR images are not yet supported 

863 return im 

864 # It's actually an MPO 

865 from .MpoImagePlugin import MpoImageFile 

866 

867 # Don't reload everything, just convert it. 

868 im = MpoImageFile.adopt(im, mpheader) 

869 except (TypeError, IndexError): 

870 # It is really a JPEG 

871 pass 

872 except SyntaxError: 

873 warnings.warn( 

874 "Image appears to be a malformed MPO file, it will be " 

875 "interpreted as a base JPEG file" 

876 ) 

877 return im 

878 

879 

880# --------------------------------------------------------------------- 

881# Registry stuff 

882 

883Image.register_open(JpegImageFile.format, jpeg_factory, _accept) 

884Image.register_save(JpegImageFile.format, _save) 

885 

886Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"]) 

887 

888Image.register_mime(JpegImageFile.format, "image/jpeg")