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

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

466 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 

36__lazy_modules__ = { 

37 "PIL.JpegPresets", 

38 "PIL._binary", 

39 "array", 

40 "io", 

41 "math", 

42 "struct", 

43 "subprocess", 

44 "warnings", 

45} 

46 

47import array 

48import io 

49import math 

50import os 

51import struct 

52import subprocess 

53import sys 

54import warnings 

55 

56from . import Image, ImageFile 

57from ._binary import i16be as i16 

58from ._binary import i32be as i32 

59from ._binary import o8 

60from ._binary import o16be as o16 

61from ._deprecate import deprecate 

62from .JpegPresets import presets 

63 

64TYPE_CHECKING = False 

65if TYPE_CHECKING: 

66 from typing import IO, Any 

67 

68 from .MpoImagePlugin import MpoImageFile 

69 

70# 

71# Parser 

72 

73 

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

75 assert self.fp is not None 

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

77 ImageFile._safe_read(self.fp, n) 

78 

79 

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

81 # 

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

83 # Also look for well-known application markers. 

84 

85 assert self.fp is not None 

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

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

88 

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

90 

91 self.app[app] = s # compatibility 

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

93 

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

95 # extract JFIF information 

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

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

98 # extract JFIF properties 

99 try: 

100 jfif_unit = s[7] 

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

102 except Exception: 

103 pass 

104 else: 

105 if jfif_unit == 1: 

106 self.info["dpi"] = jfif_density 

107 elif jfif_unit == 2: # cm 

108 # 1 dpcm = 2.54 dpi 

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

110 self.info["jfif_unit"] = jfif_unit 

111 self.info["jfif_density"] = jfif_density 

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

113 # extract EXIF information 

114 if "exif" in self.info: 

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

116 else: 

117 self.info["exif"] = s 

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

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

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

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

122 # extract FlashPix information (incomplete) 

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

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

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

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

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

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

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

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

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

132 # Profile data (remainder of APP2 data) 

133 # Decoders should use the marker sequence numbers to 

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

135 # markers appear in the correct sequence. 

136 self.icclist.append(s) 

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

138 # parse the image resource block 

139 offset = 14 

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

141 try: 

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

143 offset += 4 

144 # resource code 

145 code = i16(s, offset) 

146 offset += 2 

147 # resource name (usually empty) 

148 name_len = s[offset] 

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

150 offset += 1 + name_len 

151 offset += offset & 1 # align 

152 # resource data block 

153 size = i32(s, offset) 

154 offset += 4 

155 data = s[offset : offset + size] 

156 if code == 0x03ED: # ResolutionInfo 

157 photoshop[code] = { 

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

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

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

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

162 } 

163 else: 

164 photoshop[code] = data 

165 offset += size 

166 offset += offset & 1 # align 

167 except struct.error: 

168 pass # insufficient data 

169 

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

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

172 # extract Adobe custom properties 

173 try: 

174 adobe_transform = s[11] 

175 except IndexError: 

176 pass 

177 else: 

178 self.info["adobe_transform"] = adobe_transform 

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

180 # extract MPO information 

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

182 # offset is current location minus buffer size 

183 # plus constant header size 

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

185 

186 

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

188 # 

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

190 assert self.fp is not None 

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

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

193 

194 self.info["comment"] = s 

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

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

197 

198 

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

200 # 

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

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

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

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

205 # looking for JFIF and Adobe APP markers. 

206 

207 assert self.fp is not None 

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

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

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

211 

212 self.bits = s[0] 

213 if self.bits != 8: 

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

215 raise SyntaxError(msg) 

216 

217 self.layers = s[5] 

218 if self.layers == 1: 

219 self._mode = "L" 

220 elif self.layers == 3: 

221 self._mode = "RGB" 

222 elif self.layers == 4: 

223 self._mode = "CMYK" 

224 else: 

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

226 raise SyntaxError(msg) 

227 

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

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

230 

231 if self.icclist: 

232 # fixup icc profile 

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

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

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

236 icc_profile = b"".join(profile) 

237 else: 

238 icc_profile = None # wrong number of fragments 

239 self.info["icc_profile"] = icc_profile 

240 self.icclist = [] 

241 

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

243 t = s[i : i + 3] 

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

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

246 

247 

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

249 # 

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

251 # than one table in each marker. 

252 

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

254 # compression quality. 

255 

256 assert self.fp is not None 

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

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

259 while len(s): 

260 v = s[0] 

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

262 qt_length = 1 + precision * 64 

263 if len(s) < qt_length: 

264 msg = "bad quantization table marker" 

265 raise SyntaxError(msg) 

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

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

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

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

270 s = s[qt_length:] 

271 

272 

273# 

274# JPEG marker table 

275 

276MARKER = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

340} 

341 

342 

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

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

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

346 

347 

348## 

349# Image plugin for JPEG and JFIF images. 

350 

351 

352class JpegImageFile(ImageFile.ImageFile): 

353 format = "JPEG" 

354 format_description = "JPEG (ISO 10918)" 

355 

356 def _open(self) -> None: 

357 assert self.fp is not None 

358 s = self.fp.read(3) 

359 

360 if not _accept(s): 

361 msg = "not a JPEG file" 

362 raise SyntaxError(msg) 

363 s = b"\xff" 

364 

365 # Create attributes 

366 self.bits = self.layers = 0 

367 self._exif_offset = 0 

368 

369 # JPEG specifics (internal) 

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

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

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

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

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

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

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

377 

378 while s: 

379 i = s[0] 

380 if i == 0xFF: 

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

382 if len(s) < 2: 

383 break 

384 i = i16(s) 

385 else: 

386 # Skip non-0xFF junk 

387 s = self.fp.read(1) 

388 continue 

389 

390 if i in MARKER: 

391 name, description, handler = MARKER[i] 

392 if handler is not None: 

393 handler(self, i) 

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

395 rawmode = self.mode 

396 if self.mode == "CMYK": 

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

398 self.tile = [ImageFile._Tile("jpeg", args=(rawmode, ""))] 

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

400 break 

401 s = self.fp.read(1) 

402 elif i in {0, 0xFFFF}: 

403 # padded marker or junk; move on 

404 s = b"\xff" 

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

406 s = self.fp.read(1) 

407 else: 

408 msg = "no marker found" 

409 raise SyntaxError(msg) 

410 

411 self._read_dpi_from_exif() 

412 

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

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

415 

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

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

418 super().__setstate__(state) 

419 

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

421 """ 

422 internal: read more image data 

423 For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker 

424 so libjpeg can finish decoding 

425 """ 

426 assert self.fp is not None 

427 s = self.fp.read(read_bytes) 

428 

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

430 # Premature EOF. 

431 # Pretend file is finished adding EOI marker 

432 self._ended = True 

433 return b"\xff\xd9" 

434 

435 return s 

436 

437 def draft( 

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

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

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

441 return None 

442 

443 # Protect from second call 

444 if self.decoderconfig: 

445 return None 

446 

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

448 scale = 1 

449 original_size = self.size 

450 

451 assert isinstance(a, tuple) 

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

453 self._mode = mode 

454 a = mode, "" 

455 

456 if size: 

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

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

459 if scale >= s: 

460 break 

461 e = ( 

462 0, 

463 0, 

464 (self.size[0] + s - 1) // s, 

465 (self.size[1] + s - 1) // s, 

466 ) 

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

468 scale = s 

469 

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

471 self.decoderconfig = (scale, 0) 

472 

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

474 return self.mode, box 

475 

476 def load_djpeg(self) -> None: 

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

478 import tempfile 

479 

480 deprecate( 

481 "load_djpeg", 

482 14, 

483 action="Use the built-in JPEG decoder instead, or call djpeg yourself.", 

484 ) 

485 

486 f, path = tempfile.mkstemp() 

487 os.close(f) 

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

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

490 else: 

491 try: 

492 os.unlink(path) 

493 except OSError: 

494 pass 

495 

496 msg = "Invalid Filename" 

497 raise ValueError(msg) 

498 

499 try: 

500 with Image.open(path) as _im: 

501 _im.load() 

502 self.im = _im.im 

503 finally: 

504 try: 

505 os.unlink(path) 

506 except OSError: 

507 pass 

508 

509 self._mode = self.im.mode 

510 self._size = self.im.size 

511 

512 self.tile = [] 

513 

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

515 return _getexif(self) 

516 

517 def _read_dpi_from_exif(self) -> None: 

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

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

520 return 

521 try: 

522 exif = self.getexif() 

523 dpi = exif[0x011A] 

524 if math.isnan(dpi): 

525 msg = "DPI is not a number" 

526 raise ValueError(msg) 

527 if exif.get(0x0128) == 3: # ResolutionUnit is cm 

528 # 1 dpcm = 2.54 dpi 

529 dpi *= 2.54 

530 except ( 

531 struct.error, # truncated EXIF 

532 KeyError, # dpi not included 

533 SyntaxError, # invalid/unreadable EXIF 

534 TypeError, # dpi is an invalid float 

535 ValueError, # dpi is an invalid float 

536 ): 

537 dpi = 72 

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

539 

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

541 return _getmp(self) 

542 

543 

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

545 if "exif" not in self.info: 

546 return None 

547 return self.getexif()._get_merged_dict() 

548 

549 

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

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

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

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

554 

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

556 # application marker. 

557 try: 

558 data = self.info["mp"] 

559 except KeyError: 

560 return None 

561 file_contents = io.BytesIO(data) 

562 head = file_contents.read(8) 

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

564 # process dictionary 

565 from . import TiffImagePlugin 

566 

567 try: 

568 info = TiffImagePlugin.ImageFileDirectory_v2(head) 

569 file_contents.seek(info.next) 

570 info.load(file_contents) 

571 mp = dict(info) 

572 except Exception as e: 

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

574 raise SyntaxError(msg) from e 

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

576 try: 

577 quant = mp[0xB001] 

578 except KeyError as e: 

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

580 raise SyntaxError(msg) from e 

581 # get MP entries 

582 mpentries = [] 

583 try: 

584 rawmpentries = mp[0xB002] 

585 for entrynum in range(quant): 

586 unpackedentry = struct.unpack_from( 

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

588 ) 

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

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

591 mpentryattr = { 

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

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

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

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

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

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

598 } 

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

600 mpentryattr["ImageDataFormat"] = "JPEG" 

601 else: 

602 msg = "unsupported picture format in MPO" 

603 raise SyntaxError(msg) 

604 mptypemap = { 

605 0x000000: "Undefined", 

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

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

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

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

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

611 0x030000: "Baseline MP Primary Image", 

612 } 

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

614 mpentry["Attribute"] = mpentryattr 

615 mpentries.append(mpentry) 

616 mp[0xB002] = mpentries 

617 except (KeyError, struct.error) as e: 

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

619 raise SyntaxError(msg) from e 

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

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

622 # file and so can't test it. 

623 return mp 

624 

625 

626# -------------------------------------------------------------------- 

627# stuff to save JPEG files 

628 

629RAWMODE = { 

630 "1": "L", 

631 "L": "L", 

632 "RGB": "RGB", 

633 "RGBX": "RGB", 

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

635 "YCbCr": "YCbCr", 

636} 

637 

638zigzag_index = ( 

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

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

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

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

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

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

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

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

647) # fmt: skip 

648 

649samplings = { 

650 (1, 1, 1, 1, 1, 1): 0, 

651 (2, 1, 1, 1, 1, 1): 1, 

652 (2, 2, 1, 1, 1, 1): 2, 

653} 

654 

655 

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

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

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

659 # so set subsampling to the default value. 

660 # 

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

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

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

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

665 return -1 

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

667 return samplings.get(sampling, -1) 

668 

669 

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

671 try: 

672 rawmode = RAWMODE[im.mode] 

673 except KeyError as e: 

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

675 raise OSError(msg) from e 

676 

677 info = im.encoderinfo 

678 

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

680 

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

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

683 qtables = info.get("qtables") 

684 

685 if quality == "keep": 

686 quality = -1 

687 subsampling = "keep" 

688 qtables = "keep" 

689 elif quality in presets: 

690 preset = presets[quality] 

691 quality = -1 

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

693 qtables = preset.get("quantization") 

694 elif not isinstance(quality, int): 

695 msg = "Invalid quality setting" 

696 raise ValueError(msg) 

697 else: 

698 if subsampling in presets: 

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

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

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

702 

703 if subsampling == "4:4:4": 

704 subsampling = 0 

705 elif subsampling == "4:2:2": 

706 subsampling = 1 

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

708 subsampling = 2 

709 elif subsampling == "4:1:1": 

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

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

712 subsampling = 2 

713 elif subsampling == "keep": 

714 if im.format != "JPEG": 

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

716 raise ValueError(msg) 

717 subsampling = get_sampling(im) 

718 

719 def validate_qtables( 

720 qtables: ( 

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

722 ), 

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

724 if qtables is None: 

725 return qtables 

726 if isinstance(qtables, str): 

727 try: 

728 lines = [ 

729 int(num) 

730 for line in qtables.splitlines() 

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

732 ] 

733 except ValueError as e: 

734 msg = "Invalid quantization table" 

735 raise ValueError(msg) from e 

736 else: 

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

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

739 if isinstance(qtables, dict): 

740 qtables = [ 

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

742 ] 

743 else: 

744 # Copy the sequence, so that it cannot be changed while it is read 

745 qtables = list(qtables) 

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

747 msg = "None or too many quantization tables" 

748 raise ValueError(msg) 

749 try: 

750 for idx, table in enumerate(qtables): 

751 if len(table) != 64: 

752 msg = "Invalid quantization table" 

753 raise TypeError(msg) 

754 qtables[idx] = list(array.array("H", table)) 

755 except TypeError as e: 

756 msg = "Invalid quantization table" 

757 raise ValueError(msg) from e 

758 return qtables 

759 

760 if qtables == "keep": 

761 if im.format != "JPEG": 

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

763 raise ValueError(msg) 

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

765 qtables = validate_qtables(qtables) 

766 

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

768 

769 MAX_BYTES_IN_MARKER = 65533 

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

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

772 max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len 

773 if len(xmp) > max_data_bytes_in_marker: 

774 msg = "XMP data is too long" 

775 raise ValueError(msg) 

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

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

778 

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

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

781 max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len 

782 markers = [] 

783 while icc_profile: 

784 markers.append(icc_profile[:max_data_bytes_in_marker]) 

785 icc_profile = icc_profile[max_data_bytes_in_marker:] 

786 i = 1 

787 for marker in markers: 

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

789 extra += ( 

790 b"\xff\xe2" 

791 + size 

792 + b"ICC_PROFILE\0" 

793 + o8(i) 

794 + o8(len(markers)) 

795 + marker 

796 ) 

797 i += 1 

798 

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

800 if comment and len(comment) > MAX_BYTES_IN_MARKER: 

801 msg = "Comment is too long" 

802 raise ValueError(msg) 

803 

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

805 # says "progression" 

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

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

808 

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

810 

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

812 if isinstance(exif, Image.Exif): 

813 exif = exif.tobytes() 

814 if len(exif) > MAX_BYTES_IN_MARKER: 

815 msg = "EXIF data is too long" 

816 raise ValueError(msg) 

817 

818 # get keyword arguments 

819 im.encoderconfig = ( 

820 quality, 

821 progressive, 

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

823 optimize, 

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

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

826 dpi, 

827 subsampling, 

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

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

830 qtables, 

831 comment, 

832 extra, 

833 exif, 

834 ) 

835 

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

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

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

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

840 if optimize or progressive: 

841 # CMYK can be bigger 

842 if im.mode == "CMYK": 

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

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

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

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

847 else: 

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

849 if exif: 

850 bufsize += len(exif) + 5 

851 if extra: 

852 bufsize += len(extra) 

853 if comment: 

854 bufsize += len(comment) + 5 

855 else: 

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

857 # as libjpeg without suspension requires one byte left after writing a marker. 

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

859 # and the comment (COM marker). 

860 bufsize = max(len(exif) + 5, len(extra), len(comment or b"") + 5) 

861 

862 ImageFile._save(im, fp, [ImageFile._Tile("jpeg", args=rawmode)], bufsize) 

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