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

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

473 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 

45from typing import IO, Any 

46 

47from . import Image, ImageFile 

48from ._binary import i16be as i16 

49from ._binary import i32be as i32 

50from ._binary import o8 

51from ._binary import o16be as o16 

52from ._deprecate import deprecate 

53from .JpegPresets import presets 

54 

55TYPE_CHECKING = False 

56if TYPE_CHECKING: 

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 

197 self.bits = s[0] 

198 if self.bits != 8: 

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

200 raise SyntaxError(msg) 

201 

202 self.layers = s[5] 

203 if self.layers == 1: 

204 self._mode = "L" 

205 elif self.layers == 3: 

206 self._mode = "RGB" 

207 elif self.layers == 4: 

208 self._mode = "CMYK" 

209 else: 

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

211 raise SyntaxError(msg) 

212 

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

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

215 

216 if self.icclist: 

217 # fixup icc profile 

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

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

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

221 icc_profile = b"".join(profile) 

222 else: 

223 icc_profile = None # wrong number of fragments 

224 self.info["icc_profile"] = icc_profile 

225 self.icclist = [] 

226 

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

228 t = s[i : i + 3] 

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

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

231 

232 

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

234 # 

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

236 # than one table in each marker. 

237 

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

239 # compression quality. 

240 

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

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

243 while len(s): 

244 v = s[0] 

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

246 qt_length = 1 + precision * 64 

247 if len(s) < qt_length: 

248 msg = "bad quantization table marker" 

249 raise SyntaxError(msg) 

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

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

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

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

254 s = s[qt_length:] 

255 

256 

257# 

258# JPEG marker table 

259 

260MARKER = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

324} 

325 

326 

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

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

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

330 

331 

332## 

333# Image plugin for JPEG and JFIF images. 

334 

335 

336class JpegImageFile(ImageFile.ImageFile): 

337 format = "JPEG" 

338 format_description = "JPEG (ISO 10918)" 

339 

340 def _open(self) -> None: 

341 s = self.fp.read(3) 

342 

343 if not _accept(s): 

344 msg = "not a JPEG file" 

345 raise SyntaxError(msg) 

346 s = b"\xff" 

347 

348 # Create attributes 

349 self.bits = self.layers = 0 

350 self._exif_offset = 0 

351 

352 # JPEG specifics (internal) 

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

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

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

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

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

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

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

360 

361 while True: 

362 i = s[0] 

363 if i == 0xFF: 

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

365 i = i16(s) 

366 else: 

367 # Skip non-0xFF junk 

368 s = self.fp.read(1) 

369 continue 

370 

371 if i in MARKER: 

372 name, description, handler = MARKER[i] 

373 if handler is not None: 

374 handler(self, i) 

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

376 rawmode = self.mode 

377 if self.mode == "CMYK": 

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

379 self.tile = [ 

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

381 ] 

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

383 break 

384 s = self.fp.read(1) 

385 elif i in {0, 0xFFFF}: 

386 # padded marker or junk; move on 

387 s = b"\xff" 

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

389 s = self.fp.read(1) 

390 else: 

391 msg = "no marker found" 

392 raise SyntaxError(msg) 

393 

394 self._read_dpi_from_exif() 

395 

396 def __getattr__(self, name: str) -> Any: 

397 if name in ("huffman_ac", "huffman_dc"): 

398 deprecate(name, 12) 

399 return getattr(self, "_" + name) 

400 raise AttributeError(name) 

401 

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

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

404 

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

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

407 super().__setstate__(state) 

408 

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

410 """ 

411 internal: read more image data 

412 For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker 

413 so libjpeg can finish decoding 

414 """ 

415 s = self.fp.read(read_bytes) 

416 

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

418 # Premature EOF. 

419 # Pretend file is finished adding EOI marker 

420 self._ended = True 

421 return b"\xff\xd9" 

422 

423 return s 

424 

425 def draft( 

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

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

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

429 return None 

430 

431 # Protect from second call 

432 if self.decoderconfig: 

433 return None 

434 

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

436 scale = 1 

437 original_size = self.size 

438 

439 assert isinstance(a, tuple) 

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

441 self._mode = mode 

442 a = mode, "" 

443 

444 if size: 

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

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

447 if scale >= s: 

448 break 

449 assert e is not None 

450 e = ( 

451 e[0], 

452 e[1], 

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

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

455 ) 

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

457 scale = s 

458 

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

460 self.decoderconfig = (scale, 0) 

461 

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

463 return self.mode, box 

464 

465 def load_djpeg(self) -> None: 

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

467 

468 f, path = tempfile.mkstemp() 

469 os.close(f) 

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

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

472 else: 

473 try: 

474 os.unlink(path) 

475 except OSError: 

476 pass 

477 

478 msg = "Invalid Filename" 

479 raise ValueError(msg) 

480 

481 try: 

482 with Image.open(path) as _im: 

483 _im.load() 

484 self.im = _im.im 

485 finally: 

486 try: 

487 os.unlink(path) 

488 except OSError: 

489 pass 

490 

491 self._mode = self.im.mode 

492 self._size = self.im.size 

493 

494 self.tile = [] 

495 

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

497 return _getexif(self) 

498 

499 def _read_dpi_from_exif(self) -> None: 

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

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

502 return 

503 try: 

504 exif = self.getexif() 

505 resolution_unit = exif[0x0128] 

506 x_resolution = exif[0x011A] 

507 try: 

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

509 except TypeError: 

510 dpi = x_resolution 

511 if math.isnan(dpi): 

512 msg = "DPI is not a number" 

513 raise ValueError(msg) 

514 if resolution_unit == 3: # cm 

515 # 1 dpcm = 2.54 dpi 

516 dpi *= 2.54 

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

518 except ( 

519 struct.error, # truncated EXIF 

520 KeyError, # dpi not included 

521 SyntaxError, # invalid/unreadable EXIF 

522 TypeError, # dpi is an invalid float 

523 ValueError, # dpi is an invalid float 

524 ZeroDivisionError, # invalid dpi rational value 

525 ): 

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

527 

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

529 return _getmp(self) 

530 

531 

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

533 if "exif" not in self.info: 

534 return None 

535 return self.getexif()._get_merged_dict() 

536 

537 

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

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

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

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

542 

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

544 # application marker. 

545 try: 

546 data = self.info["mp"] 

547 except KeyError: 

548 return None 

549 file_contents = io.BytesIO(data) 

550 head = file_contents.read(8) 

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

552 # process dictionary 

553 from . import TiffImagePlugin 

554 

555 try: 

556 info = TiffImagePlugin.ImageFileDirectory_v2(head) 

557 file_contents.seek(info.next) 

558 info.load(file_contents) 

559 mp = dict(info) 

560 except Exception as e: 

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

562 raise SyntaxError(msg) from e 

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

564 try: 

565 quant = mp[0xB001] 

566 except KeyError as e: 

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

568 raise SyntaxError(msg) from e 

569 # get MP entries 

570 mpentries = [] 

571 try: 

572 rawmpentries = mp[0xB002] 

573 for entrynum in range(quant): 

574 unpackedentry = struct.unpack_from( 

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

576 ) 

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

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

579 mpentryattr = { 

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

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

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

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

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

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

586 } 

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

588 mpentryattr["ImageDataFormat"] = "JPEG" 

589 else: 

590 msg = "unsupported picture format in MPO" 

591 raise SyntaxError(msg) 

592 mptypemap = { 

593 0x000000: "Undefined", 

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

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

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

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

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

599 0x030000: "Baseline MP Primary Image", 

600 } 

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

602 mpentry["Attribute"] = mpentryattr 

603 mpentries.append(mpentry) 

604 mp[0xB002] = mpentries 

605 except KeyError as e: 

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

607 raise SyntaxError(msg) from e 

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

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

610 # file and so can't test it. 

611 return mp 

612 

613 

614# -------------------------------------------------------------------- 

615# stuff to save JPEG files 

616 

617RAWMODE = { 

618 "1": "L", 

619 "L": "L", 

620 "RGB": "RGB", 

621 "RGBX": "RGB", 

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

623 "YCbCr": "YCbCr", 

624} 

625 

626# fmt: off 

627zigzag_index = ( 

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

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

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

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

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

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

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

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

636) 

637 

638samplings = { 

639 (1, 1, 1, 1, 1, 1): 0, 

640 (2, 1, 1, 1, 1, 1): 1, 

641 (2, 2, 1, 1, 1, 1): 2, 

642} 

643# fmt: on 

644 

645 

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

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

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

649 # so set subsampling to the default value. 

650 # 

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

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

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

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

655 return -1 

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

657 return samplings.get(sampling, -1) 

658 

659 

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

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

662 msg = "cannot write empty image as JPEG" 

663 raise ValueError(msg) 

664 

665 try: 

666 rawmode = RAWMODE[im.mode] 

667 except KeyError as e: 

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

669 raise OSError(msg) from e 

670 

671 info = im.encoderinfo 

672 

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

674 

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

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

677 qtables = info.get("qtables") 

678 

679 if quality == "keep": 

680 quality = -1 

681 subsampling = "keep" 

682 qtables = "keep" 

683 elif quality in presets: 

684 preset = presets[quality] 

685 quality = -1 

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

687 qtables = preset.get("quantization") 

688 elif not isinstance(quality, int): 

689 msg = "Invalid quality setting" 

690 raise ValueError(msg) 

691 else: 

692 if subsampling in presets: 

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

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

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

696 

697 if subsampling == "4:4:4": 

698 subsampling = 0 

699 elif subsampling == "4:2:2": 

700 subsampling = 1 

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

702 subsampling = 2 

703 elif subsampling == "4:1:1": 

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

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

706 subsampling = 2 

707 elif subsampling == "keep": 

708 if im.format != "JPEG": 

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

710 raise ValueError(msg) 

711 subsampling = get_sampling(im) 

712 

713 def validate_qtables( 

714 qtables: ( 

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

716 ), 

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

718 if qtables is None: 

719 return qtables 

720 if isinstance(qtables, str): 

721 try: 

722 lines = [ 

723 int(num) 

724 for line in qtables.splitlines() 

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

726 ] 

727 except ValueError as e: 

728 msg = "Invalid quantization table" 

729 raise ValueError(msg) from e 

730 else: 

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

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

733 if isinstance(qtables, dict): 

734 qtables = [ 

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

736 ] 

737 elif isinstance(qtables, tuple): 

738 qtables = list(qtables) 

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

740 msg = "None or too many quantization tables" 

741 raise ValueError(msg) 

742 for idx, table in enumerate(qtables): 

743 try: 

744 if len(table) != 64: 

745 msg = "Invalid quantization table" 

746 raise TypeError(msg) 

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

748 except TypeError as e: 

749 msg = "Invalid quantization table" 

750 raise ValueError(msg) from e 

751 else: 

752 qtables[idx] = list(table_array) 

753 return qtables 

754 

755 if qtables == "keep": 

756 if im.format != "JPEG": 

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

758 raise ValueError(msg) 

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

760 qtables = validate_qtables(qtables) 

761 

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

763 

764 MAX_BYTES_IN_MARKER = 65533 

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

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

767 max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len 

768 if len(xmp) > max_data_bytes_in_marker: 

769 msg = "XMP data is too long" 

770 raise ValueError(msg) 

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

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

773 

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

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

776 max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len 

777 markers = [] 

778 while icc_profile: 

779 markers.append(icc_profile[:max_data_bytes_in_marker]) 

780 icc_profile = icc_profile[max_data_bytes_in_marker:] 

781 i = 1 

782 for marker in markers: 

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

784 extra += ( 

785 b"\xff\xe2" 

786 + size 

787 + b"ICC_PROFILE\0" 

788 + o8(i) 

789 + o8(len(markers)) 

790 + marker 

791 ) 

792 i += 1 

793 

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

795 

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

797 # says "progression" 

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

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

800 

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

802 

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

804 if isinstance(exif, Image.Exif): 

805 exif = exif.tobytes() 

806 if len(exif) > MAX_BYTES_IN_MARKER: 

807 msg = "EXIF data is too long" 

808 raise ValueError(msg) 

809 

810 # get keyword arguments 

811 im.encoderconfig = ( 

812 quality, 

813 progressive, 

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

815 optimize, 

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

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

818 dpi, 

819 subsampling, 

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

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

822 qtables, 

823 comment, 

824 extra, 

825 exif, 

826 ) 

827 

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

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

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

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

832 if optimize or progressive: 

833 # CMYK can be bigger 

834 if im.mode == "CMYK": 

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

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

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

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

839 else: 

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

841 if exif: 

842 bufsize += len(exif) + 5 

843 if extra: 

844 bufsize += len(extra) + 1 

845 else: 

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

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

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

849 

850 ImageFile._save( 

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

852 ) 

853 

854 

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

856 # ALTERNATIVE: handle JPEGs via the IJG command line utilities. 

857 tempfile = im._dump() 

858 subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) 

859 try: 

860 os.unlink(tempfile) 

861 except OSError: 

862 pass 

863 

864 

865## 

866# Factory for making JPEG and MPO instances 

867def jpeg_factory( 

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

869) -> JpegImageFile | MpoImageFile: 

870 im = JpegImageFile(fp, filename) 

871 try: 

872 mpheader = im._getmp() 

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

874 for segment, content in im.applist: 

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

876 # Ultra HDR images are not yet supported 

877 return im 

878 # It's actually an MPO 

879 from .MpoImagePlugin import MpoImageFile 

880 

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

882 im = MpoImageFile.adopt(im, mpheader) 

883 except (TypeError, IndexError): 

884 # It is really a JPEG 

885 pass 

886 except SyntaxError: 

887 warnings.warn( 

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

889 "interpreted as a base JPEG file" 

890 ) 

891 return im 

892 

893 

894# --------------------------------------------------------------------- 

895# Registry stuff 

896 

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

898Image.register_save(JpegImageFile.format, _save) 

899 

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

901 

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