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

469 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 assert self.fp is not None 

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

66 ImageFile._safe_read(self.fp, n) 

67 

68 

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

70 # 

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

72 # Also look for well-known application markers. 

73 

74 assert self.fp is not None 

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

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

77 

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

79 

80 self.app[app] = s # compatibility 

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

82 

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

84 # extract JFIF information 

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

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

87 # extract JFIF properties 

88 try: 

89 jfif_unit = s[7] 

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

91 except Exception: 

92 pass 

93 else: 

94 if jfif_unit == 1: 

95 self.info["dpi"] = jfif_density 

96 elif jfif_unit == 2: # cm 

97 # 1 dpcm = 2.54 dpi 

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

99 self.info["jfif_unit"] = jfif_unit 

100 self.info["jfif_density"] = jfif_density 

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

102 # extract EXIF information 

103 if "exif" in self.info: 

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

105 else: 

106 self.info["exif"] = s 

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

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

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

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

111 # extract FlashPix information (incomplete) 

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

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

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

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

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

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

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

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

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

121 # Profile data (remainder of APP2 data) 

122 # Decoders should use the marker sequence numbers to 

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

124 # markers appear in the correct sequence. 

125 self.icclist.append(s) 

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

127 # parse the image resource block 

128 offset = 14 

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

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

131 try: 

132 offset += 4 

133 # resource code 

134 code = i16(s, offset) 

135 offset += 2 

136 # resource name (usually empty) 

137 name_len = s[offset] 

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

139 offset += 1 + name_len 

140 offset += offset & 1 # align 

141 # resource data block 

142 size = i32(s, offset) 

143 offset += 4 

144 data = s[offset : offset + size] 

145 if code == 0x03ED: # ResolutionInfo 

146 photoshop[code] = { 

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

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

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

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

151 } 

152 else: 

153 photoshop[code] = data 

154 offset += size 

155 offset += offset & 1 # align 

156 except struct.error: 

157 break # insufficient data 

158 

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

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

161 # extract Adobe custom properties 

162 try: 

163 adobe_transform = s[11] 

164 except IndexError: 

165 pass 

166 else: 

167 self.info["adobe_transform"] = adobe_transform 

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

169 # extract MPO information 

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

171 # offset is current location minus buffer size 

172 # plus constant header size 

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

174 

175 

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

177 # 

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

179 assert self.fp is not None 

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

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

182 

183 self.info["comment"] = s 

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

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

186 

187 

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

189 # 

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

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

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

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

194 # looking for JFIF and Adobe APP markers. 

195 

196 assert self.fp is not None 

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

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

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

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

201 self._im = None 

202 

203 self.bits = s[0] 

204 if self.bits != 8: 

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

206 raise SyntaxError(msg) 

207 

208 self.layers = s[5] 

209 if self.layers == 1: 

210 self._mode = "L" 

211 elif self.layers == 3: 

212 self._mode = "RGB" 

213 elif self.layers == 4: 

214 self._mode = "CMYK" 

215 else: 

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

217 raise SyntaxError(msg) 

218 

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

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

221 

222 if self.icclist: 

223 # fixup icc profile 

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

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

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

227 icc_profile = b"".join(profile) 

228 else: 

229 icc_profile = None # wrong number of fragments 

230 self.info["icc_profile"] = icc_profile 

231 self.icclist = [] 

232 

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

234 t = s[i : i + 3] 

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

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

237 

238 

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

240 # 

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

242 # than one table in each marker. 

243 

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

245 # compression quality. 

246 

247 assert self.fp is not None 

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

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

250 while len(s): 

251 v = s[0] 

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

253 qt_length = 1 + precision * 64 

254 if len(s) < qt_length: 

255 msg = "bad quantization table marker" 

256 raise SyntaxError(msg) 

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

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

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

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

261 s = s[qt_length:] 

262 

263 

264# 

265# JPEG marker table 

266 

267MARKER = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

331} 

332 

333 

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

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

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

337 

338 

339## 

340# Image plugin for JPEG and JFIF images. 

341 

342 

343class JpegImageFile(ImageFile.ImageFile): 

344 format = "JPEG" 

345 format_description = "JPEG (ISO 10918)" 

346 

347 def _open(self) -> None: 

348 assert self.fp is not None 

349 s = self.fp.read(3) 

350 

351 if not _accept(s): 

352 msg = "not a JPEG file" 

353 raise SyntaxError(msg) 

354 s = b"\xff" 

355 

356 # Create attributes 

357 self.bits = self.layers = 0 

358 self._exif_offset = 0 

359 

360 # JPEG specifics (internal) 

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

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

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

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

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

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

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

368 

369 while True: 

370 i = s[0] 

371 if i == 0xFF: 

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

373 i = i16(s) 

374 else: 

375 # Skip non-0xFF junk 

376 s = self.fp.read(1) 

377 continue 

378 

379 if i in MARKER: 

380 name, description, handler = MARKER[i] 

381 if handler is not None: 

382 handler(self, i) 

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

384 rawmode = self.mode 

385 if self.mode == "CMYK": 

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

387 self.tile = [ 

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

389 ] 

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

391 break 

392 s = self.fp.read(1) 

393 elif i in {0, 0xFFFF}: 

394 # padded marker or junk; move on 

395 s = b"\xff" 

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

397 s = self.fp.read(1) 

398 else: 

399 msg = "no marker found" 

400 raise SyntaxError(msg) 

401 

402 self._read_dpi_from_exif() 

403 

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

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

406 

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

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

409 super().__setstate__(state) 

410 

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

412 """ 

413 internal: read more image data 

414 For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker 

415 so libjpeg can finish decoding 

416 """ 

417 assert self.fp is not None 

418 s = self.fp.read(read_bytes) 

419 

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

421 # Premature EOF. 

422 # Pretend file is finished adding EOI marker 

423 self._ended = True 

424 return b"\xff\xd9" 

425 

426 return s 

427 

428 def draft( 

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

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

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

432 return None 

433 

434 # Protect from second call 

435 if self.decoderconfig: 

436 return None 

437 

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

439 scale = 1 

440 original_size = self.size 

441 

442 assert isinstance(a, tuple) 

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

444 self._mode = mode 

445 a = mode, "" 

446 

447 if size: 

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

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

450 if scale >= s: 

451 break 

452 assert e is not None 

453 e = ( 

454 e[0], 

455 e[1], 

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

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

458 ) 

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

460 scale = s 

461 

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

463 self.decoderconfig = (scale, 0) 

464 

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

466 return self.mode, box 

467 

468 def load_djpeg(self) -> None: 

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

470 

471 f, path = tempfile.mkstemp() 

472 os.close(f) 

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

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

475 else: 

476 try: 

477 os.unlink(path) 

478 except OSError: 

479 pass 

480 

481 msg = "Invalid Filename" 

482 raise ValueError(msg) 

483 

484 try: 

485 with Image.open(path) as _im: 

486 _im.load() 

487 self.im = _im.im 

488 finally: 

489 try: 

490 os.unlink(path) 

491 except OSError: 

492 pass 

493 

494 self._mode = self.im.mode 

495 self._size = self.im.size 

496 

497 self.tile = [] 

498 

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

500 return _getexif(self) 

501 

502 def _read_dpi_from_exif(self) -> None: 

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

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

505 return 

506 try: 

507 exif = self.getexif() 

508 resolution_unit = exif[0x0128] 

509 x_resolution = exif[0x011A] 

510 try: 

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

512 except TypeError: 

513 dpi = x_resolution 

514 if math.isnan(dpi): 

515 msg = "DPI is not a number" 

516 raise ValueError(msg) 

517 if resolution_unit == 3: # cm 

518 # 1 dpcm = 2.54 dpi 

519 dpi *= 2.54 

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

521 except ( 

522 struct.error, # truncated EXIF 

523 KeyError, # dpi not included 

524 SyntaxError, # invalid/unreadable EXIF 

525 TypeError, # dpi is an invalid float 

526 ValueError, # dpi is an invalid float 

527 ZeroDivisionError, # invalid dpi rational value 

528 ): 

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

530 

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

532 return _getmp(self) 

533 

534 

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

536 if "exif" not in self.info: 

537 return None 

538 return self.getexif()._get_merged_dict() 

539 

540 

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

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

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

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

545 

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

547 # application marker. 

548 try: 

549 data = self.info["mp"] 

550 except KeyError: 

551 return None 

552 file_contents = io.BytesIO(data) 

553 head = file_contents.read(8) 

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

555 # process dictionary 

556 from . import TiffImagePlugin 

557 

558 try: 

559 info = TiffImagePlugin.ImageFileDirectory_v2(head) 

560 file_contents.seek(info.next) 

561 info.load(file_contents) 

562 mp = dict(info) 

563 except Exception as e: 

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

565 raise SyntaxError(msg) from e 

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

567 try: 

568 quant = mp[0xB001] 

569 except KeyError as e: 

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

571 raise SyntaxError(msg) from e 

572 # get MP entries 

573 mpentries = [] 

574 try: 

575 rawmpentries = mp[0xB002] 

576 for entrynum in range(quant): 

577 unpackedentry = struct.unpack_from( 

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

579 ) 

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

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

582 mpentryattr = { 

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

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

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

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

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

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

589 } 

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

591 mpentryattr["ImageDataFormat"] = "JPEG" 

592 else: 

593 msg = "unsupported picture format in MPO" 

594 raise SyntaxError(msg) 

595 mptypemap = { 

596 0x000000: "Undefined", 

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

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

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

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

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

602 0x030000: "Baseline MP Primary Image", 

603 } 

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

605 mpentry["Attribute"] = mpentryattr 

606 mpentries.append(mpentry) 

607 mp[0xB002] = mpentries 

608 except KeyError as e: 

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

610 raise SyntaxError(msg) from e 

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

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

613 # file and so can't test it. 

614 return mp 

615 

616 

617# -------------------------------------------------------------------- 

618# stuff to save JPEG files 

619 

620RAWMODE = { 

621 "1": "L", 

622 "L": "L", 

623 "RGB": "RGB", 

624 "RGBX": "RGB", 

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

626 "YCbCr": "YCbCr", 

627} 

628 

629# fmt: off 

630zigzag_index = ( 

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

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

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

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

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

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

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

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

639) 

640 

641samplings = { 

642 (1, 1, 1, 1, 1, 1): 0, 

643 (2, 1, 1, 1, 1, 1): 1, 

644 (2, 2, 1, 1, 1, 1): 2, 

645} 

646# fmt: on 

647 

648 

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

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

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

652 # so set subsampling to the default value. 

653 # 

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

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

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

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

658 return -1 

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

660 return samplings.get(sampling, -1) 

661 

662 

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

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

665 msg = "cannot write empty image as JPEG" 

666 raise ValueError(msg) 

667 

668 try: 

669 rawmode = RAWMODE[im.mode] 

670 except KeyError as e: 

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

672 raise OSError(msg) from e 

673 

674 info = im.encoderinfo 

675 

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

677 

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

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

680 qtables = info.get("qtables") 

681 

682 if quality == "keep": 

683 quality = -1 

684 subsampling = "keep" 

685 qtables = "keep" 

686 elif quality in presets: 

687 preset = presets[quality] 

688 quality = -1 

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

690 qtables = preset.get("quantization") 

691 elif not isinstance(quality, int): 

692 msg = "Invalid quality setting" 

693 raise ValueError(msg) 

694 else: 

695 if subsampling in presets: 

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

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

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

699 

700 if subsampling == "4:4:4": 

701 subsampling = 0 

702 elif subsampling == "4:2:2": 

703 subsampling = 1 

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

705 subsampling = 2 

706 elif subsampling == "4:1:1": 

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

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

709 subsampling = 2 

710 elif subsampling == "keep": 

711 if im.format != "JPEG": 

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

713 raise ValueError(msg) 

714 subsampling = get_sampling(im) 

715 

716 def validate_qtables( 

717 qtables: ( 

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

719 ), 

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

721 if qtables is None: 

722 return qtables 

723 if isinstance(qtables, str): 

724 try: 

725 lines = [ 

726 int(num) 

727 for line in qtables.splitlines() 

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

729 ] 

730 except ValueError as e: 

731 msg = "Invalid quantization table" 

732 raise ValueError(msg) from e 

733 else: 

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

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

736 if isinstance(qtables, dict): 

737 qtables = [ 

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

739 ] 

740 elif isinstance(qtables, tuple): 

741 qtables = list(qtables) 

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

743 msg = "None or too many quantization tables" 

744 raise ValueError(msg) 

745 for idx, table in enumerate(qtables): 

746 try: 

747 if len(table) != 64: 

748 msg = "Invalid quantization table" 

749 raise TypeError(msg) 

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

751 except TypeError as e: 

752 msg = "Invalid quantization table" 

753 raise ValueError(msg) from e 

754 else: 

755 qtables[idx] = list(table_array) 

756 return qtables 

757 

758 if qtables == "keep": 

759 if im.format != "JPEG": 

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

761 raise ValueError(msg) 

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

763 qtables = validate_qtables(qtables) 

764 

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

766 

767 MAX_BYTES_IN_MARKER = 65533 

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

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

770 max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len 

771 if len(xmp) > max_data_bytes_in_marker: 

772 msg = "XMP data is too long" 

773 raise ValueError(msg) 

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

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

776 

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

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

779 max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len 

780 markers = [] 

781 while icc_profile: 

782 markers.append(icc_profile[:max_data_bytes_in_marker]) 

783 icc_profile = icc_profile[max_data_bytes_in_marker:] 

784 i = 1 

785 for marker in markers: 

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

787 extra += ( 

788 b"\xff\xe2" 

789 + size 

790 + b"ICC_PROFILE\0" 

791 + o8(i) 

792 + o8(len(markers)) 

793 + marker 

794 ) 

795 i += 1 

796 

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

798 

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

800 # says "progression" 

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

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

803 

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

805 

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

807 if isinstance(exif, Image.Exif): 

808 exif = exif.tobytes() 

809 if len(exif) > MAX_BYTES_IN_MARKER: 

810 msg = "EXIF data is too long" 

811 raise ValueError(msg) 

812 

813 # get keyword arguments 

814 im.encoderconfig = ( 

815 quality, 

816 progressive, 

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

818 optimize, 

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

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

821 dpi, 

822 subsampling, 

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

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

825 qtables, 

826 comment, 

827 extra, 

828 exif, 

829 ) 

830 

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

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

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

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

835 if optimize or progressive: 

836 # CMYK can be bigger 

837 if im.mode == "CMYK": 

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

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

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

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

842 else: 

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

844 if exif: 

845 bufsize += len(exif) + 5 

846 if extra: 

847 bufsize += len(extra) + 1 

848 else: 

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

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

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

852 

853 ImageFile._save( 

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

855 ) 

856 

857 

858## 

859# Factory for making JPEG and MPO instances 

860def jpeg_factory( 

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

862) -> JpegImageFile | MpoImageFile: 

863 im = JpegImageFile(fp, filename) 

864 try: 

865 mpheader = im._getmp() 

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

867 for segment, content in im.applist: 

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

869 # Ultra HDR images are not yet supported 

870 return im 

871 # It's actually an MPO 

872 from .MpoImagePlugin import MpoImageFile 

873 

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

875 im = MpoImageFile.adopt(im, mpheader) 

876 except (TypeError, IndexError): 

877 # It is really a JPEG 

878 pass 

879 except SyntaxError: 

880 warnings.warn( 

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

882 "interpreted as a base JPEG file" 

883 ) 

884 return im 

885 

886 

887# --------------------------------------------------------------------- 

888# Registry stuff 

889 

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

891Image.register_save(JpegImageFile.format, _save) 

892 

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

894 

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