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

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

665 statements  

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# GIF file handling 

6# 

7# History: 

8# 1995-09-01 fl Created 

9# 1996-12-14 fl Added interlace support 

10# 1996-12-30 fl Added animation support 

11# 1997-01-05 fl Added write support, fixed local colour map bug 

12# 1997-02-23 fl Make sure to load raster data in getdata() 

13# 1997-07-05 fl Support external decoder (0.4) 

14# 1998-07-09 fl Handle all modes when saving (0.5) 

15# 1998-07-15 fl Renamed offset attribute to avoid name clash 

16# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6) 

17# 2001-04-17 fl Added palette optimization (0.7) 

18# 2002-06-06 fl Added transparency support for save (0.8) 

19# 2004-02-24 fl Disable interlacing for small images 

20# 

21# Copyright (c) 1997-2004 by Secret Labs AB 

22# Copyright (c) 1995-2004 by Fredrik Lundh 

23# 

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

25# 

26from __future__ import annotations 

27 

28import itertools 

29import math 

30import os 

31import subprocess 

32from enum import IntEnum 

33from functools import cached_property 

34from typing import IO, Any, Literal, NamedTuple, Union, cast 

35 

36from . import ( 

37 Image, 

38 ImageChops, 

39 ImageFile, 

40 ImageMath, 

41 ImageOps, 

42 ImagePalette, 

43 ImageSequence, 

44) 

45from ._binary import i16le as i16 

46from ._binary import o8 

47from ._binary import o16le as o16 

48from ._util import DeferredError 

49 

50TYPE_CHECKING = False 

51if TYPE_CHECKING: 

52 from . import _imaging 

53 from ._typing import Buffer 

54 

55 

56class LoadingStrategy(IntEnum): 

57 """.. versionadded:: 9.1.0""" 

58 

59 RGB_AFTER_FIRST = 0 

60 RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 

61 RGB_ALWAYS = 2 

62 

63 

64#: .. versionadded:: 9.1.0 

65LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST 

66 

67# -------------------------------------------------------------------- 

68# Identify/read GIF files 

69 

70 

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

72 return prefix.startswith((b"GIF87a", b"GIF89a")) 

73 

74 

75## 

76# Image plugin for GIF images. This plugin supports both GIF87 and 

77# GIF89 images. 

78 

79 

80class GifImageFile(ImageFile.ImageFile): 

81 format = "GIF" 

82 format_description = "Compuserve GIF" 

83 _close_exclusive_fp_after_loading = False 

84 

85 global_palette = None 

86 

87 def data(self) -> bytes | None: 

88 s = self.fp.read(1) 

89 if s and s[0]: 

90 return self.fp.read(s[0]) 

91 return None 

92 

93 def _is_palette_needed(self, p: bytes) -> bool: 

94 for i in range(0, len(p), 3): 

95 if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): 

96 return True 

97 return False 

98 

99 def _open(self) -> None: 

100 # Screen 

101 s = self.fp.read(13) 

102 if not _accept(s): 

103 msg = "not a GIF file" 

104 raise SyntaxError(msg) 

105 

106 self.info["version"] = s[:6] 

107 self._size = i16(s, 6), i16(s, 8) 

108 flags = s[10] 

109 bits = (flags & 7) + 1 

110 

111 if flags & 128: 

112 # get global palette 

113 self.info["background"] = s[11] 

114 # check if palette contains colour indices 

115 p = self.fp.read(3 << bits) 

116 if self._is_palette_needed(p): 

117 p = ImagePalette.raw("RGB", p) 

118 self.global_palette = self.palette = p 

119 

120 self._fp = self.fp # FIXME: hack 

121 self.__rewind = self.fp.tell() 

122 self._n_frames: int | None = None 

123 self._seek(0) # get ready to read first frame 

124 

125 @property 

126 def n_frames(self) -> int: 

127 if self._n_frames is None: 

128 current = self.tell() 

129 try: 

130 while True: 

131 self._seek(self.tell() + 1, False) 

132 except EOFError: 

133 self._n_frames = self.tell() + 1 

134 self.seek(current) 

135 return self._n_frames 

136 

137 @cached_property 

138 def is_animated(self) -> bool: 

139 if self._n_frames is not None: 

140 return self._n_frames != 1 

141 

142 current = self.tell() 

143 if current: 

144 return True 

145 

146 try: 

147 self._seek(1, False) 

148 is_animated = True 

149 except EOFError: 

150 is_animated = False 

151 

152 self.seek(current) 

153 return is_animated 

154 

155 def seek(self, frame: int) -> None: 

156 if not self._seek_check(frame): 

157 return 

158 if frame < self.__frame: 

159 self._im = None 

160 self._seek(0) 

161 

162 last_frame = self.__frame 

163 for f in range(self.__frame + 1, frame + 1): 

164 try: 

165 self._seek(f) 

166 except EOFError as e: 

167 self.seek(last_frame) 

168 msg = "no more images in GIF file" 

169 raise EOFError(msg) from e 

170 

171 def _seek(self, frame: int, update_image: bool = True) -> None: 

172 if isinstance(self._fp, DeferredError): 

173 raise self._fp.ex 

174 if frame == 0: 

175 # rewind 

176 self.__offset = 0 

177 self.dispose: _imaging.ImagingCore | None = None 

178 self.__frame = -1 

179 self._fp.seek(self.__rewind) 

180 self.disposal_method = 0 

181 if "comment" in self.info: 

182 del self.info["comment"] 

183 else: 

184 # ensure that the previous frame was loaded 

185 if self.tile and update_image: 

186 self.load() 

187 

188 if frame != self.__frame + 1: 

189 msg = f"cannot seek to frame {frame}" 

190 raise ValueError(msg) 

191 

192 self.fp = self._fp 

193 if self.__offset: 

194 # backup to last frame 

195 self.fp.seek(self.__offset) 

196 while self.data(): 

197 pass 

198 self.__offset = 0 

199 

200 s = self.fp.read(1) 

201 if not s or s == b";": 

202 msg = "no more images in GIF file" 

203 raise EOFError(msg) 

204 

205 palette: ImagePalette.ImagePalette | Literal[False] | None = None 

206 

207 info: dict[str, Any] = {} 

208 frame_transparency = None 

209 interlace = None 

210 frame_dispose_extent = None 

211 while True: 

212 if not s: 

213 s = self.fp.read(1) 

214 if not s or s == b";": 

215 break 

216 

217 elif s == b"!": 

218 # 

219 # extensions 

220 # 

221 s = self.fp.read(1) 

222 block = self.data() 

223 if s[0] == 249 and block is not None: 

224 # 

225 # graphic control extension 

226 # 

227 flags = block[0] 

228 if flags & 1: 

229 frame_transparency = block[3] 

230 info["duration"] = i16(block, 1) * 10 

231 

232 # disposal method - find the value of bits 4 - 6 

233 dispose_bits = 0b00011100 & flags 

234 dispose_bits = dispose_bits >> 2 

235 if dispose_bits: 

236 # only set the dispose if it is not 

237 # unspecified. I'm not sure if this is 

238 # correct, but it seems to prevent the last 

239 # frame from looking odd for some animations 

240 self.disposal_method = dispose_bits 

241 elif s[0] == 254: 

242 # 

243 # comment extension 

244 # 

245 comment = b"" 

246 

247 # Read this comment block 

248 while block: 

249 comment += block 

250 block = self.data() 

251 

252 if "comment" in info: 

253 # If multiple comment blocks in frame, separate with \n 

254 info["comment"] += b"\n" + comment 

255 else: 

256 info["comment"] = comment 

257 s = None 

258 continue 

259 elif s[0] == 255 and frame == 0 and block is not None: 

260 # 

261 # application extension 

262 # 

263 info["extension"] = block, self.fp.tell() 

264 if block.startswith(b"NETSCAPE2.0"): 

265 block = self.data() 

266 if block and len(block) >= 3 and block[0] == 1: 

267 self.info["loop"] = i16(block, 1) 

268 while self.data(): 

269 pass 

270 

271 elif s == b",": 

272 # 

273 # local image 

274 # 

275 s = self.fp.read(9) 

276 

277 # extent 

278 x0, y0 = i16(s, 0), i16(s, 2) 

279 x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) 

280 if (x1 > self.size[0] or y1 > self.size[1]) and update_image: 

281 self._size = max(x1, self.size[0]), max(y1, self.size[1]) 

282 Image._decompression_bomb_check(self._size) 

283 frame_dispose_extent = x0, y0, x1, y1 

284 flags = s[8] 

285 

286 interlace = (flags & 64) != 0 

287 

288 if flags & 128: 

289 bits = (flags & 7) + 1 

290 p = self.fp.read(3 << bits) 

291 if self._is_palette_needed(p): 

292 palette = ImagePalette.raw("RGB", p) 

293 else: 

294 palette = False 

295 

296 # image data 

297 bits = self.fp.read(1)[0] 

298 self.__offset = self.fp.tell() 

299 break 

300 s = None 

301 

302 if interlace is None: 

303 msg = "image not found in GIF frame" 

304 raise EOFError(msg) 

305 

306 self.__frame = frame 

307 if not update_image: 

308 return 

309 

310 self.tile = [] 

311 

312 if self.dispose: 

313 self.im.paste(self.dispose, self.dispose_extent) 

314 

315 self._frame_palette = palette if palette is not None else self.global_palette 

316 self._frame_transparency = frame_transparency 

317 if frame == 0: 

318 if self._frame_palette: 

319 if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: 

320 self._mode = "RGBA" if frame_transparency is not None else "RGB" 

321 else: 

322 self._mode = "P" 

323 else: 

324 self._mode = "L" 

325 

326 if palette: 

327 self.palette = palette 

328 elif self.global_palette: 

329 from copy import copy 

330 

331 self.palette = copy(self.global_palette) 

332 else: 

333 self.palette = None 

334 else: 

335 if self.mode == "P": 

336 if ( 

337 LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY 

338 or palette 

339 ): 

340 if "transparency" in self.info: 

341 self.im.putpalettealpha(self.info["transparency"], 0) 

342 self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) 

343 self._mode = "RGBA" 

344 del self.info["transparency"] 

345 else: 

346 self._mode = "RGB" 

347 self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) 

348 

349 def _rgb(color: int) -> tuple[int, int, int]: 

350 if self._frame_palette: 

351 if color * 3 + 3 > len(self._frame_palette.palette): 

352 color = 0 

353 return cast( 

354 tuple[int, int, int], 

355 tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), 

356 ) 

357 else: 

358 return (color, color, color) 

359 

360 self.dispose = None 

361 self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent 

362 if self.dispose_extent and self.disposal_method >= 2: 

363 try: 

364 if self.disposal_method == 2: 

365 # replace with background colour 

366 

367 # only dispose the extent in this frame 

368 x0, y0, x1, y1 = self.dispose_extent 

369 dispose_size = (x1 - x0, y1 - y0) 

370 

371 Image._decompression_bomb_check(dispose_size) 

372 

373 # by convention, attempt to use transparency first 

374 dispose_mode = "P" 

375 color = self.info.get("transparency", frame_transparency) 

376 if color is not None: 

377 if self.mode in ("RGB", "RGBA"): 

378 dispose_mode = "RGBA" 

379 color = _rgb(color) + (0,) 

380 else: 

381 color = self.info.get("background", 0) 

382 if self.mode in ("RGB", "RGBA"): 

383 dispose_mode = "RGB" 

384 color = _rgb(color) 

385 self.dispose = Image.core.fill(dispose_mode, dispose_size, color) 

386 else: 

387 # replace with previous contents 

388 if self._im is not None: 

389 # only dispose the extent in this frame 

390 self.dispose = self._crop(self.im, self.dispose_extent) 

391 elif frame_transparency is not None: 

392 x0, y0, x1, y1 = self.dispose_extent 

393 dispose_size = (x1 - x0, y1 - y0) 

394 

395 Image._decompression_bomb_check(dispose_size) 

396 dispose_mode = "P" 

397 color = frame_transparency 

398 if self.mode in ("RGB", "RGBA"): 

399 dispose_mode = "RGBA" 

400 color = _rgb(frame_transparency) + (0,) 

401 self.dispose = Image.core.fill( 

402 dispose_mode, dispose_size, color 

403 ) 

404 except AttributeError: 

405 pass 

406 

407 if interlace is not None: 

408 transparency = -1 

409 if frame_transparency is not None: 

410 if frame == 0: 

411 if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: 

412 self.info["transparency"] = frame_transparency 

413 elif self.mode not in ("RGB", "RGBA"): 

414 transparency = frame_transparency 

415 self.tile = [ 

416 ImageFile._Tile( 

417 "gif", 

418 (x0, y0, x1, y1), 

419 self.__offset, 

420 (bits, interlace, transparency), 

421 ) 

422 ] 

423 

424 if info.get("comment"): 

425 self.info["comment"] = info["comment"] 

426 for k in ["duration", "extension"]: 

427 if k in info: 

428 self.info[k] = info[k] 

429 elif k in self.info: 

430 del self.info[k] 

431 

432 def load_prepare(self) -> None: 

433 temp_mode = "P" if self._frame_palette else "L" 

434 self._prev_im = None 

435 if self.__frame == 0: 

436 if self._frame_transparency is not None: 

437 self.im = Image.core.fill( 

438 temp_mode, self.size, self._frame_transparency 

439 ) 

440 elif self.mode in ("RGB", "RGBA"): 

441 self._prev_im = self.im 

442 if self._frame_palette: 

443 self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) 

444 self.im.putpalette("RGB", *self._frame_palette.getdata()) 

445 else: 

446 self._im = None 

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

448 expanded_im = Image.core.fill(self.im.mode, self.size) 

449 if self._frame_palette: 

450 expanded_im.putpalette("RGB", *self._frame_palette.getdata()) 

451 expanded_im.paste(self.im, (0, 0) + self.im.size) 

452 

453 self.im = expanded_im 

454 self._mode = temp_mode 

455 self._frame_palette = None 

456 

457 super().load_prepare() 

458 

459 def load_end(self) -> None: 

460 if self.__frame == 0: 

461 if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: 

462 if self._frame_transparency is not None: 

463 self.im.putpalettealpha(self._frame_transparency, 0) 

464 self._mode = "RGBA" 

465 else: 

466 self._mode = "RGB" 

467 self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) 

468 return 

469 if not self._prev_im: 

470 return 

471 if self.size != self._prev_im.size: 

472 if self._frame_transparency is not None: 

473 expanded_im = Image.core.fill("RGBA", self.size) 

474 else: 

475 expanded_im = Image.core.fill("P", self.size) 

476 expanded_im.putpalette("RGB", "RGB", self.im.getpalette()) 

477 expanded_im = expanded_im.convert("RGB") 

478 expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size) 

479 

480 self._prev_im = expanded_im 

481 assert self._prev_im is not None 

482 if self._frame_transparency is not None: 

483 self.im.putpalettealpha(self._frame_transparency, 0) 

484 frame_im = self.im.convert("RGBA") 

485 else: 

486 frame_im = self.im.convert("RGB") 

487 

488 assert self.dispose_extent is not None 

489 frame_im = self._crop(frame_im, self.dispose_extent) 

490 

491 self.im = self._prev_im 

492 self._mode = self.im.mode 

493 if frame_im.mode == "RGBA": 

494 self.im.paste(frame_im, self.dispose_extent, frame_im) 

495 else: 

496 self.im.paste(frame_im, self.dispose_extent) 

497 

498 def tell(self) -> int: 

499 return self.__frame 

500 

501 

502# -------------------------------------------------------------------- 

503# Write GIF files 

504 

505 

506RAWMODE = {"1": "L", "L": "L", "P": "P"} 

507 

508 

509def _normalize_mode(im: Image.Image) -> Image.Image: 

510 """ 

511 Takes an image (or frame), returns an image in a mode that is appropriate 

512 for saving in a Gif. 

513 

514 It may return the original image, or it may return an image converted to 

515 palette or 'L' mode. 

516 

517 :param im: Image object 

518 :returns: Image object 

519 """ 

520 if im.mode in RAWMODE: 

521 im.load() 

522 return im 

523 if Image.getmodebase(im.mode) == "RGB": 

524 im = im.convert("P", palette=Image.Palette.ADAPTIVE) 

525 assert im.palette is not None 

526 if im.palette.mode == "RGBA": 

527 for rgba in im.palette.colors: 

528 if rgba[3] == 0: 

529 im.info["transparency"] = im.palette.colors[rgba] 

530 break 

531 return im 

532 return im.convert("L") 

533 

534 

535_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette] 

536 

537 

538def _normalize_palette( 

539 im: Image.Image, palette: _Palette | None, info: dict[str, Any] 

540) -> Image.Image: 

541 """ 

542 Normalizes the palette for image. 

543 - Sets the palette to the incoming palette, if provided. 

544 - Ensures that there's a palette for L mode images 

545 - Optimizes the palette if necessary/desired. 

546 

547 :param im: Image object 

548 :param palette: bytes object containing the source palette, or .... 

549 :param info: encoderinfo 

550 :returns: Image object 

551 """ 

552 source_palette = None 

553 if palette: 

554 # a bytes palette 

555 if isinstance(palette, (bytes, bytearray, list)): 

556 source_palette = bytearray(palette[:768]) 

557 if isinstance(palette, ImagePalette.ImagePalette): 

558 source_palette = bytearray(palette.palette) 

559 

560 if im.mode == "P": 

561 if not source_palette: 

562 im_palette = im.getpalette(None) 

563 assert im_palette is not None 

564 source_palette = bytearray(im_palette) 

565 else: # L-mode 

566 if not source_palette: 

567 source_palette = bytearray(i // 3 for i in range(768)) 

568 im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) 

569 assert source_palette is not None 

570 

571 if palette: 

572 used_palette_colors: list[int | None] = [] 

573 assert im.palette is not None 

574 for i in range(0, len(source_palette), 3): 

575 source_color = tuple(source_palette[i : i + 3]) 

576 index = im.palette.colors.get(source_color) 

577 if index in used_palette_colors: 

578 index = None 

579 used_palette_colors.append(index) 

580 for i, index in enumerate(used_palette_colors): 

581 if index is None: 

582 for j in range(len(used_palette_colors)): 

583 if j not in used_palette_colors: 

584 used_palette_colors[i] = j 

585 break 

586 dest_map: list[int] = [] 

587 for index in used_palette_colors: 

588 assert index is not None 

589 dest_map.append(index) 

590 im = im.remap_palette(dest_map) 

591 else: 

592 optimized_palette_colors = _get_optimize(im, info) 

593 if optimized_palette_colors is not None: 

594 im = im.remap_palette(optimized_palette_colors, source_palette) 

595 if "transparency" in info: 

596 try: 

597 info["transparency"] = optimized_palette_colors.index( 

598 info["transparency"] 

599 ) 

600 except ValueError: 

601 del info["transparency"] 

602 return im 

603 

604 assert im.palette is not None 

605 im.palette.palette = source_palette 

606 return im 

607 

608 

609def _write_single_frame( 

610 im: Image.Image, 

611 fp: IO[bytes], 

612 palette: _Palette | None, 

613) -> None: 

614 im_out = _normalize_mode(im) 

615 for k, v in im_out.info.items(): 

616 if isinstance(k, str): 

617 im.encoderinfo.setdefault(k, v) 

618 im_out = _normalize_palette(im_out, palette, im.encoderinfo) 

619 

620 for s in _get_global_header(im_out, im.encoderinfo): 

621 fp.write(s) 

622 

623 # local image header 

624 flags = 0 

625 if get_interlace(im): 

626 flags = flags | 64 

627 _write_local_header(fp, im, (0, 0), flags) 

628 

629 im_out.encoderconfig = (8, get_interlace(im)) 

630 ImageFile._save( 

631 im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])] 

632 ) 

633 

634 fp.write(b"\0") # end of image data 

635 

636 

637def _getbbox( 

638 base_im: Image.Image, im_frame: Image.Image 

639) -> tuple[Image.Image, tuple[int, int, int, int] | None]: 

640 palette_bytes = [ 

641 bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame) 

642 ] 

643 if palette_bytes[0] != palette_bytes[1]: 

644 im_frame = im_frame.convert("RGBA") 

645 base_im = base_im.convert("RGBA") 

646 delta = ImageChops.subtract_modulo(im_frame, base_im) 

647 return delta, delta.getbbox(alpha_only=False) 

648 

649 

650class _Frame(NamedTuple): 

651 im: Image.Image 

652 bbox: tuple[int, int, int, int] | None 

653 encoderinfo: dict[str, Any] 

654 

655 

656def _write_multiple_frames( 

657 im: Image.Image, fp: IO[bytes], palette: _Palette | None 

658) -> bool: 

659 duration = im.encoderinfo.get("duration") 

660 disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) 

661 

662 im_frames: list[_Frame] = [] 

663 previous_im: Image.Image | None = None 

664 frame_count = 0 

665 background_im = None 

666 for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): 

667 for im_frame in ImageSequence.Iterator(imSequence): 

668 # a copy is required here since seek can still mutate the image 

669 im_frame = _normalize_mode(im_frame.copy()) 

670 if frame_count == 0: 

671 for k, v in im_frame.info.items(): 

672 if k == "transparency": 

673 continue 

674 if isinstance(k, str): 

675 im.encoderinfo.setdefault(k, v) 

676 

677 encoderinfo = im.encoderinfo.copy() 

678 if "transparency" in im_frame.info: 

679 encoderinfo.setdefault("transparency", im_frame.info["transparency"]) 

680 im_frame = _normalize_palette(im_frame, palette, encoderinfo) 

681 if isinstance(duration, (list, tuple)): 

682 encoderinfo["duration"] = duration[frame_count] 

683 elif duration is None and "duration" in im_frame.info: 

684 encoderinfo["duration"] = im_frame.info["duration"] 

685 if isinstance(disposal, (list, tuple)): 

686 encoderinfo["disposal"] = disposal[frame_count] 

687 frame_count += 1 

688 

689 diff_frame = None 

690 if im_frames and previous_im: 

691 # delta frame 

692 delta, bbox = _getbbox(previous_im, im_frame) 

693 if not bbox: 

694 # This frame is identical to the previous frame 

695 if encoderinfo.get("duration"): 

696 im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] 

697 continue 

698 if im_frames[-1].encoderinfo.get("disposal") == 2: 

699 # To appear correctly in viewers using a convention, 

700 # only consider transparency, and not background color 

701 color = im.encoderinfo.get( 

702 "transparency", im.info.get("transparency") 

703 ) 

704 if color is not None: 

705 if background_im is None: 

706 background = _get_background(im_frame, color) 

707 background_im = Image.new("P", im_frame.size, background) 

708 first_palette = im_frames[0].im.palette 

709 assert first_palette is not None 

710 background_im.putpalette(first_palette, first_palette.mode) 

711 bbox = _getbbox(background_im, im_frame)[1] 

712 else: 

713 bbox = (0, 0) + im_frame.size 

714 elif encoderinfo.get("optimize") and im_frame.mode != "1": 

715 if "transparency" not in encoderinfo: 

716 assert im_frame.palette is not None 

717 try: 

718 encoderinfo["transparency"] = ( 

719 im_frame.palette._new_color_index(im_frame) 

720 ) 

721 except ValueError: 

722 pass 

723 if "transparency" in encoderinfo: 

724 # When the delta is zero, fill the image with transparency 

725 diff_frame = im_frame.copy() 

726 fill = Image.new("P", delta.size, encoderinfo["transparency"]) 

727 if delta.mode == "RGBA": 

728 r, g, b, a = delta.split() 

729 mask = ImageMath.lambda_eval( 

730 lambda args: args["convert"]( 

731 args["max"]( 

732 args["max"]( 

733 args["max"](args["r"], args["g"]), args["b"] 

734 ), 

735 args["a"], 

736 ) 

737 * 255, 

738 "1", 

739 ), 

740 r=r, 

741 g=g, 

742 b=b, 

743 a=a, 

744 ) 

745 else: 

746 if delta.mode == "P": 

747 # Convert to L without considering palette 

748 delta_l = Image.new("L", delta.size) 

749 delta_l.putdata(delta.getdata()) 

750 delta = delta_l 

751 mask = ImageMath.lambda_eval( 

752 lambda args: args["convert"](args["im"] * 255, "1"), 

753 im=delta, 

754 ) 

755 diff_frame.paste(fill, mask=ImageOps.invert(mask)) 

756 else: 

757 bbox = None 

758 previous_im = im_frame 

759 im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo)) 

760 

761 if len(im_frames) == 1: 

762 if "duration" in im.encoderinfo: 

763 # Since multiple frames will not be written, use the combined duration 

764 im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"] 

765 return False 

766 

767 for frame_data in im_frames: 

768 im_frame = frame_data.im 

769 if not frame_data.bbox: 

770 # global header 

771 for s in _get_global_header(im_frame, frame_data.encoderinfo): 

772 fp.write(s) 

773 offset = (0, 0) 

774 else: 

775 # compress difference 

776 if not palette: 

777 frame_data.encoderinfo["include_color_table"] = True 

778 

779 if frame_data.bbox != (0, 0) + im_frame.size: 

780 im_frame = im_frame.crop(frame_data.bbox) 

781 offset = frame_data.bbox[:2] 

782 _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) 

783 return True 

784 

785 

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

787 _save(im, fp, filename, save_all=True) 

788 

789 

790def _save( 

791 im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False 

792) -> None: 

793 # header 

794 if "palette" in im.encoderinfo or "palette" in im.info: 

795 palette = im.encoderinfo.get("palette", im.info.get("palette")) 

796 else: 

797 palette = None 

798 im.encoderinfo.setdefault("optimize", True) 

799 

800 if not save_all or not _write_multiple_frames(im, fp, palette): 

801 _write_single_frame(im, fp, palette) 

802 

803 fp.write(b";") # end of file 

804 

805 if hasattr(fp, "flush"): 

806 fp.flush() 

807 

808 

809def get_interlace(im: Image.Image) -> int: 

810 interlace = im.encoderinfo.get("interlace", 1) 

811 

812 # workaround for @PIL153 

813 if min(im.size) < 16: 

814 interlace = 0 

815 

816 return interlace 

817 

818 

819def _write_local_header( 

820 fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int 

821) -> None: 

822 try: 

823 transparency = im.encoderinfo["transparency"] 

824 except KeyError: 

825 transparency = None 

826 

827 if "duration" in im.encoderinfo: 

828 duration = int(im.encoderinfo["duration"] / 10) 

829 else: 

830 duration = 0 

831 

832 disposal = int(im.encoderinfo.get("disposal", 0)) 

833 

834 if transparency is not None or duration != 0 or disposal: 

835 packed_flag = 1 if transparency is not None else 0 

836 packed_flag |= disposal << 2 

837 

838 fp.write( 

839 b"!" 

840 + o8(249) # extension intro 

841 + o8(4) # length 

842 + o8(packed_flag) # packed fields 

843 + o16(duration) # duration 

844 + o8(transparency or 0) # transparency index 

845 + o8(0) 

846 ) 

847 

848 include_color_table = im.encoderinfo.get("include_color_table") 

849 if include_color_table: 

850 palette_bytes = _get_palette_bytes(im) 

851 color_table_size = _get_color_table_size(palette_bytes) 

852 if color_table_size: 

853 flags = flags | 128 # local color table flag 

854 flags = flags | color_table_size 

855 

856 fp.write( 

857 b"," 

858 + o16(offset[0]) # offset 

859 + o16(offset[1]) 

860 + o16(im.size[0]) # size 

861 + o16(im.size[1]) 

862 + o8(flags) # flags 

863 ) 

864 if include_color_table and color_table_size: 

865 fp.write(_get_header_palette(palette_bytes)) 

866 fp.write(o8(8)) # bits 

867 

868 

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

870 # Unused by default. 

871 # To use, uncomment the register_save call at the end of the file. 

872 # 

873 # If you need real GIF compression and/or RGB quantization, you 

874 # can use the external NETPBM/PBMPLUS utilities. See comments 

875 # below for information on how to enable this. 

876 tempfile = im._dump() 

877 

878 try: 

879 with open(filename, "wb") as f: 

880 if im.mode != "RGB": 

881 subprocess.check_call( 

882 ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL 

883 ) 

884 else: 

885 # Pipe ppmquant output into ppmtogif 

886 # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) 

887 quant_cmd = ["ppmquant", "256", tempfile] 

888 togif_cmd = ["ppmtogif"] 

889 quant_proc = subprocess.Popen( 

890 quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL 

891 ) 

892 togif_proc = subprocess.Popen( 

893 togif_cmd, 

894 stdin=quant_proc.stdout, 

895 stdout=f, 

896 stderr=subprocess.DEVNULL, 

897 ) 

898 

899 # Allow ppmquant to receive SIGPIPE if ppmtogif exits 

900 assert quant_proc.stdout is not None 

901 quant_proc.stdout.close() 

902 

903 retcode = quant_proc.wait() 

904 if retcode: 

905 raise subprocess.CalledProcessError(retcode, quant_cmd) 

906 

907 retcode = togif_proc.wait() 

908 if retcode: 

909 raise subprocess.CalledProcessError(retcode, togif_cmd) 

910 finally: 

911 try: 

912 os.unlink(tempfile) 

913 except OSError: 

914 pass 

915 

916 

917# Force optimization so that we can test performance against 

918# cases where it took lots of memory and time previously. 

919_FORCE_OPTIMIZE = False 

920 

921 

922def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: 

923 """ 

924 Palette optimization is a potentially expensive operation. 

925 

926 This function determines if the palette should be optimized using 

927 some heuristics, then returns the list of palette entries in use. 

928 

929 :param im: Image object 

930 :param info: encoderinfo 

931 :returns: list of indexes of palette entries in use, or None 

932 """ 

933 if im.mode in ("P", "L") and info and info.get("optimize"): 

934 # Potentially expensive operation. 

935 

936 # The palette saves 3 bytes per color not used, but palette 

937 # lengths are restricted to 3*(2**N) bytes. Max saving would 

938 # be 768 -> 6 bytes if we went all the way down to 2 colors. 

939 # * If we're over 128 colors, we can't save any space. 

940 # * If there aren't any holes, it's not worth collapsing. 

941 # * If we have a 'large' image, the palette is in the noise. 

942 

943 # create the new palette if not every color is used 

944 optimise = _FORCE_OPTIMIZE or im.mode == "L" 

945 if optimise or im.width * im.height < 512 * 512: 

946 # check which colors are used 

947 used_palette_colors = [] 

948 for i, count in enumerate(im.histogram()): 

949 if count: 

950 used_palette_colors.append(i) 

951 

952 if optimise or max(used_palette_colors) >= len(used_palette_colors): 

953 return used_palette_colors 

954 

955 assert im.palette is not None 

956 num_palette_colors = len(im.palette.palette) // Image.getmodebands( 

957 im.palette.mode 

958 ) 

959 current_palette_size = 1 << (num_palette_colors - 1).bit_length() 

960 if ( 

961 # check that the palette would become smaller when saved 

962 len(used_palette_colors) <= current_palette_size // 2 

963 # check that the palette is not already the smallest possible size 

964 and current_palette_size > 2 

965 ): 

966 return used_palette_colors 

967 return None 

968 

969 

970def _get_color_table_size(palette_bytes: bytes) -> int: 

971 # calculate the palette size for the header 

972 if not palette_bytes: 

973 return 0 

974 elif len(palette_bytes) < 9: 

975 return 1 

976 else: 

977 return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 

978 

979 

980def _get_header_palette(palette_bytes: bytes) -> bytes: 

981 """ 

982 Returns the palette, null padded to the next power of 2 (*3) bytes 

983 suitable for direct inclusion in the GIF header 

984 

985 :param palette_bytes: Unpadded palette bytes, in RGBRGB form 

986 :returns: Null padded palette 

987 """ 

988 color_table_size = _get_color_table_size(palette_bytes) 

989 

990 # add the missing amount of bytes 

991 # the palette has to be 2<<n in size 

992 actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3 

993 if actual_target_size_diff > 0: 

994 palette_bytes += o8(0) * 3 * actual_target_size_diff 

995 return palette_bytes 

996 

997 

998def _get_palette_bytes(im: Image.Image) -> bytes: 

999 """ 

1000 Gets the palette for inclusion in the gif header 

1001 

1002 :param im: Image object 

1003 :returns: Bytes, len<=768 suitable for inclusion in gif header 

1004 """ 

1005 if not im.palette: 

1006 return b"" 

1007 

1008 palette = bytes(im.palette.palette) 

1009 if im.palette.mode == "RGBA": 

1010 palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3)) 

1011 return palette 

1012 

1013 

1014def _get_background( 

1015 im: Image.Image, 

1016 info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None, 

1017) -> int: 

1018 background = 0 

1019 if info_background: 

1020 if isinstance(info_background, tuple): 

1021 # WebPImagePlugin stores an RGBA value in info["background"] 

1022 # So it must be converted to the same format as GifImagePlugin's 

1023 # info["background"] - a global color table index 

1024 assert im.palette is not None 

1025 try: 

1026 background = im.palette.getcolor(info_background, im) 

1027 except ValueError as e: 

1028 if str(e) not in ( 

1029 # If all 256 colors are in use, 

1030 # then there is no need for the background color 

1031 "cannot allocate more than 256 colors", 

1032 # Ignore non-opaque WebP background 

1033 "cannot add non-opaque RGBA color to RGB palette", 

1034 ): 

1035 raise 

1036 else: 

1037 background = info_background 

1038 return background 

1039 

1040 

1041def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]: 

1042 """Return a list of strings representing a GIF header""" 

1043 

1044 # Header Block 

1045 # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp 

1046 

1047 version = b"87a" 

1048 if im.info.get("version") == b"89a" or ( 

1049 info 

1050 and ( 

1051 "transparency" in info 

1052 or info.get("loop") is not None 

1053 or info.get("duration") 

1054 or info.get("comment") 

1055 ) 

1056 ): 

1057 version = b"89a" 

1058 

1059 background = _get_background(im, info.get("background")) 

1060 

1061 palette_bytes = _get_palette_bytes(im) 

1062 color_table_size = _get_color_table_size(palette_bytes) 

1063 

1064 header = [ 

1065 b"GIF" # signature 

1066 + version # version 

1067 + o16(im.size[0]) # canvas width 

1068 + o16(im.size[1]), # canvas height 

1069 # Logical Screen Descriptor 

1070 # size of global color table + global color table flag 

1071 o8(color_table_size + 128), # packed fields 

1072 # background + reserved/aspect 

1073 o8(background) + o8(0), 

1074 # Global Color Table 

1075 _get_header_palette(palette_bytes), 

1076 ] 

1077 if info.get("loop") is not None: 

1078 header.append( 

1079 b"!" 

1080 + o8(255) # extension intro 

1081 + o8(11) 

1082 + b"NETSCAPE2.0" 

1083 + o8(3) 

1084 + o8(1) 

1085 + o16(info["loop"]) # number of loops 

1086 + o8(0) 

1087 ) 

1088 if info.get("comment"): 

1089 comment_block = b"!" + o8(254) # extension intro 

1090 

1091 comment = info["comment"] 

1092 if isinstance(comment, str): 

1093 comment = comment.encode() 

1094 for i in range(0, len(comment), 255): 

1095 subblock = comment[i : i + 255] 

1096 comment_block += o8(len(subblock)) + subblock 

1097 

1098 comment_block += o8(0) 

1099 header.append(comment_block) 

1100 return header 

1101 

1102 

1103def _write_frame_data( 

1104 fp: IO[bytes], 

1105 im_frame: Image.Image, 

1106 offset: tuple[int, int], 

1107 params: dict[str, Any], 

1108) -> None: 

1109 try: 

1110 im_frame.encoderinfo = params 

1111 

1112 # local image header 

1113 _write_local_header(fp, im_frame, offset, 0) 

1114 

1115 ImageFile._save( 

1116 im_frame, 

1117 fp, 

1118 [ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])], 

1119 ) 

1120 

1121 fp.write(b"\0") # end of image data 

1122 finally: 

1123 del im_frame.encoderinfo 

1124 

1125 

1126# -------------------------------------------------------------------- 

1127# Legacy GIF utilities 

1128 

1129 

1130def getheader( 

1131 im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None 

1132) -> tuple[list[bytes], list[int] | None]: 

1133 """ 

1134 Legacy Method to get Gif data from image. 

1135 

1136 Warning:: May modify image data. 

1137 

1138 :param im: Image object 

1139 :param palette: bytes object containing the source palette, or .... 

1140 :param info: encoderinfo 

1141 :returns: tuple of(list of header items, optimized palette) 

1142 

1143 """ 

1144 if info is None: 

1145 info = {} 

1146 

1147 used_palette_colors = _get_optimize(im, info) 

1148 

1149 if "background" not in info and "background" in im.info: 

1150 info["background"] = im.info["background"] 

1151 

1152 im_mod = _normalize_palette(im, palette, info) 

1153 im.palette = im_mod.palette 

1154 im.im = im_mod.im 

1155 header = _get_global_header(im, info) 

1156 

1157 return header, used_palette_colors 

1158 

1159 

1160def getdata( 

1161 im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any 

1162) -> list[bytes]: 

1163 """ 

1164 Legacy Method 

1165 

1166 Return a list of strings representing this image. 

1167 The first string is a local image header, the rest contains 

1168 encoded image data. 

1169 

1170 To specify duration, add the time in milliseconds, 

1171 e.g. ``getdata(im_frame, duration=1000)`` 

1172 

1173 :param im: Image object 

1174 :param offset: Tuple of (x, y) pixels. Defaults to (0, 0) 

1175 :param \\**params: e.g. duration or other encoder info parameters 

1176 :returns: List of bytes containing GIF encoded frame data 

1177 

1178 """ 

1179 from io import BytesIO 

1180 

1181 class Collector(BytesIO): 

1182 data = [] 

1183 

1184 def write(self, data: Buffer) -> int: 

1185 self.data.append(data) 

1186 return len(data) 

1187 

1188 im.load() # make sure raster data is available 

1189 

1190 fp = Collector() 

1191 

1192 _write_frame_data(fp, im, offset, params) 

1193 

1194 return fp.data 

1195 

1196 

1197# -------------------------------------------------------------------- 

1198# Registry 

1199 

1200Image.register_open(GifImageFile.format, GifImageFile, _accept) 

1201Image.register_save(GifImageFile.format, _save) 

1202Image.register_save_all(GifImageFile.format, _save_all) 

1203Image.register_extension(GifImageFile.format, ".gif") 

1204Image.register_mime(GifImageFile.format, "image/gif") 

1205 

1206# 

1207# Uncomment the following line if you wish to use NETPBM/PBMPLUS 

1208# instead of the built-in "uncompressed" GIF encoder 

1209 

1210# Image.register_save(GifImageFile.format, _save_netpbm)