Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/pillow-11.0.0-py3.10-linux-x86_64.egg/PIL/GifImagePlugin.py: 10%

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

657 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, TYPE_CHECKING, Any, Literal, NamedTuple, Union 

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 

48 

49if TYPE_CHECKING: 

50 from . import _imaging 

51 from ._typing import Buffer 

52 

53 

54class LoadingStrategy(IntEnum): 

55 """.. versionadded:: 9.1.0""" 

56 

57 RGB_AFTER_FIRST = 0 

58 RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 

59 RGB_ALWAYS = 2 

60 

61 

62#: .. versionadded:: 9.1.0 

63LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST 

64 

65# -------------------------------------------------------------------- 

66# Identify/read GIF files 

67 

68 

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

70 return prefix[:6] in [b"GIF87a", b"GIF89a"] 

71 

72 

73## 

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

75# GIF89 images. 

76 

77 

78class GifImageFile(ImageFile.ImageFile): 

79 format = "GIF" 

80 format_description = "Compuserve GIF" 

81 _close_exclusive_fp_after_loading = False 

82 

83 global_palette = None 

84 

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

86 s = self.fp.read(1) 

87 if s and s[0]: 

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

89 return None 

90 

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

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

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

94 return True 

95 return False 

96 

97 def _open(self) -> None: 

98 # Screen 

99 s = self.fp.read(13) 

100 if not _accept(s): 

101 msg = "not a GIF file" 

102 raise SyntaxError(msg) 

103 

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

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

106 self.tile = [] 

107 flags = s[10] 

108 bits = (flags & 7) + 1 

109 

110 if flags & 128: 

111 # get global palette 

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

113 # check if palette contains colour indices 

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

115 if self._is_palette_needed(p): 

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

117 self.global_palette = self.palette = p 

118 

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

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

121 self._n_frames: int | None = None 

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

123 

124 @property 

125 def n_frames(self) -> int: 

126 if self._n_frames is None: 

127 current = self.tell() 

128 try: 

129 while True: 

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

131 except EOFError: 

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

133 self.seek(current) 

134 return self._n_frames 

135 

136 @cached_property 

137 def is_animated(self) -> bool: 

138 if self._n_frames is not None: 

139 return self._n_frames != 1 

140 

141 current = self.tell() 

142 if current: 

143 return True 

144 

145 try: 

146 self._seek(1, False) 

147 is_animated = True 

148 except EOFError: 

149 is_animated = False 

150 

151 self.seek(current) 

152 return is_animated 

153 

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

155 if not self._seek_check(frame): 

156 return 

157 if frame < self.__frame: 

158 self._im = None 

159 self._seek(0) 

160 

161 last_frame = self.__frame 

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

163 try: 

164 self._seek(f) 

165 except EOFError as e: 

166 self.seek(last_frame) 

167 msg = "no more images in GIF file" 

168 raise EOFError(msg) from e 

169 

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

171 if frame == 0: 

172 # rewind 

173 self.__offset = 0 

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

175 self.__frame = -1 

176 self._fp.seek(self.__rewind) 

177 self.disposal_method = 0 

178 if "comment" in self.info: 

179 del self.info["comment"] 

180 else: 

181 # ensure that the previous frame was loaded 

182 if self.tile and update_image: 

183 self.load() 

184 

185 if frame != self.__frame + 1: 

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

187 raise ValueError(msg) 

188 

189 self.fp = self._fp 

190 if self.__offset: 

191 # backup to last frame 

192 self.fp.seek(self.__offset) 

193 while self.data(): 

194 pass 

195 self.__offset = 0 

196 

197 s = self.fp.read(1) 

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

199 msg = "no more images in GIF file" 

200 raise EOFError(msg) 

201 

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

203 

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

205 frame_transparency = None 

206 interlace = None 

207 frame_dispose_extent = None 

208 while True: 

209 if not s: 

210 s = self.fp.read(1) 

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

212 break 

213 

214 elif s == b"!": 

215 # 

216 # extensions 

217 # 

218 s = self.fp.read(1) 

219 block = self.data() 

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

221 # 

222 # graphic control extension 

223 # 

224 flags = block[0] 

225 if flags & 1: 

226 frame_transparency = block[3] 

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

228 

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

230 dispose_bits = 0b00011100 & flags 

231 dispose_bits = dispose_bits >> 2 

232 if dispose_bits: 

233 # only set the dispose if it is not 

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

235 # correct, but it seems to prevent the last 

236 # frame from looking odd for some animations 

237 self.disposal_method = dispose_bits 

238 elif s[0] == 254: 

239 # 

240 # comment extension 

241 # 

242 comment = b"" 

243 

244 # Read this comment block 

245 while block: 

246 comment += block 

247 block = self.data() 

248 

249 if "comment" in info: 

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

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

252 else: 

253 info["comment"] = comment 

254 s = None 

255 continue 

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

257 # 

258 # application extension 

259 # 

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

261 if block[:11] == b"NETSCAPE2.0": 

262 block = self.data() 

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

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

265 while self.data(): 

266 pass 

267 

268 elif s == b",": 

269 # 

270 # local image 

271 # 

272 s = self.fp.read(9) 

273 

274 # extent 

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

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

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

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

279 Image._decompression_bomb_check(self._size) 

280 frame_dispose_extent = x0, y0, x1, y1 

281 flags = s[8] 

282 

283 interlace = (flags & 64) != 0 

284 

285 if flags & 128: 

286 bits = (flags & 7) + 1 

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

288 if self._is_palette_needed(p): 

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

290 else: 

291 palette = False 

292 

293 # image data 

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

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

296 break 

297 s = None 

298 

299 if interlace is None: 

300 msg = "image not found in GIF frame" 

301 raise EOFError(msg) 

302 

303 self.__frame = frame 

304 if not update_image: 

305 return 

306 

307 self.tile = [] 

308 

309 if self.dispose: 

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

311 

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

313 self._frame_transparency = frame_transparency 

314 if frame == 0: 

315 if self._frame_palette: 

316 if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: 

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

318 else: 

319 self._mode = "P" 

320 else: 

321 self._mode = "L" 

322 

323 if palette: 

324 self.palette = palette 

325 elif self.global_palette: 

326 from copy import copy 

327 

328 self.palette = copy(self.global_palette) 

329 else: 

330 self.palette = None 

331 else: 

332 if self.mode == "P": 

333 if ( 

334 LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY 

335 or palette 

336 ): 

337 if "transparency" in self.info: 

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

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

340 self._mode = "RGBA" 

341 del self.info["transparency"] 

342 else: 

343 self._mode = "RGB" 

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

345 

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

347 if self._frame_palette: 

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

349 color = 0 

350 return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) 

351 else: 

352 return (color, color, color) 

353 

354 self.dispose = None 

355 self.dispose_extent = frame_dispose_extent 

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

357 try: 

358 if self.disposal_method == 2: 

359 # replace with background colour 

360 

361 # only dispose the extent in this frame 

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

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

364 

365 Image._decompression_bomb_check(dispose_size) 

366 

367 # by convention, attempt to use transparency first 

368 dispose_mode = "P" 

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

370 if color is not None: 

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

372 dispose_mode = "RGBA" 

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

374 else: 

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

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

377 dispose_mode = "RGB" 

378 color = _rgb(color) 

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

380 else: 

381 # replace with previous contents 

382 if self._im is not None: 

383 # only dispose the extent in this frame 

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

385 elif frame_transparency is not None: 

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

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

388 

389 Image._decompression_bomb_check(dispose_size) 

390 dispose_mode = "P" 

391 color = frame_transparency 

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

393 dispose_mode = "RGBA" 

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

395 self.dispose = Image.core.fill( 

396 dispose_mode, dispose_size, color 

397 ) 

398 except AttributeError: 

399 pass 

400 

401 if interlace is not None: 

402 transparency = -1 

403 if frame_transparency is not None: 

404 if frame == 0: 

405 if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: 

406 self.info["transparency"] = frame_transparency 

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

408 transparency = frame_transparency 

409 self.tile = [ 

410 ImageFile._Tile( 

411 "gif", 

412 (x0, y0, x1, y1), 

413 self.__offset, 

414 (bits, interlace, transparency), 

415 ) 

416 ] 

417 

418 if info.get("comment"): 

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

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

421 if k in info: 

422 self.info[k] = info[k] 

423 elif k in self.info: 

424 del self.info[k] 

425 

426 def load_prepare(self) -> None: 

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

428 self._prev_im = None 

429 if self.__frame == 0: 

430 if self._frame_transparency is not None: 

431 self.im = Image.core.fill( 

432 temp_mode, self.size, self._frame_transparency 

433 ) 

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

435 self._prev_im = self.im 

436 if self._frame_palette: 

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

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

439 else: 

440 self._im = None 

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

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

443 if self._frame_palette: 

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

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

446 

447 self.im = expanded_im 

448 self._mode = temp_mode 

449 self._frame_palette = None 

450 

451 super().load_prepare() 

452 

453 def load_end(self) -> None: 

454 if self.__frame == 0: 

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

456 if self._frame_transparency is not None: 

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

458 self._mode = "RGBA" 

459 else: 

460 self._mode = "RGB" 

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

462 return 

463 if not self._prev_im: 

464 return 

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

466 if self._frame_transparency is not None: 

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

468 else: 

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

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

471 expanded_im = expanded_im.convert("RGB") 

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

473 

474 self._prev_im = expanded_im 

475 assert self._prev_im is not None 

476 if self._frame_transparency is not None: 

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

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

479 else: 

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

481 

482 assert self.dispose_extent is not None 

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

484 

485 self.im = self._prev_im 

486 self._mode = self.im.mode 

487 if frame_im.mode == "RGBA": 

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

489 else: 

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

491 

492 def tell(self) -> int: 

493 return self.__frame 

494 

495 

496# -------------------------------------------------------------------- 

497# Write GIF files 

498 

499 

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

501 

502 

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

504 """ 

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

506 for saving in a Gif. 

507 

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

509 palette or 'L' mode. 

510 

511 :param im: Image object 

512 :returns: Image object 

513 """ 

514 if im.mode in RAWMODE: 

515 im.load() 

516 return im 

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

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

519 assert im.palette is not None 

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

521 for rgba in im.palette.colors: 

522 if rgba[3] == 0: 

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

524 break 

525 return im 

526 return im.convert("L") 

527 

528 

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

530 

531 

532def _normalize_palette( 

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

534) -> Image.Image: 

535 """ 

536 Normalizes the palette for image. 

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

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

539 - Optimizes the palette if necessary/desired. 

540 

541 :param im: Image object 

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

543 :param info: encoderinfo 

544 :returns: Image object 

545 """ 

546 source_palette = None 

547 if palette: 

548 # a bytes palette 

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

550 source_palette = bytearray(palette[:768]) 

551 if isinstance(palette, ImagePalette.ImagePalette): 

552 source_palette = bytearray(palette.palette) 

553 

554 if im.mode == "P": 

555 if not source_palette: 

556 im_palette = im.getpalette(None) 

557 assert im_palette is not None 

558 source_palette = bytearray(im_palette) 

559 else: # L-mode 

560 if not source_palette: 

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

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

563 assert source_palette is not None 

564 

565 if palette: 

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

567 assert im.palette is not None 

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

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

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

571 if index in used_palette_colors: 

572 index = None 

573 used_palette_colors.append(index) 

574 for i, index in enumerate(used_palette_colors): 

575 if index is None: 

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

577 if j not in used_palette_colors: 

578 used_palette_colors[i] = j 

579 break 

580 dest_map: list[int] = [] 

581 for index in used_palette_colors: 

582 assert index is not None 

583 dest_map.append(index) 

584 im = im.remap_palette(dest_map) 

585 else: 

586 optimized_palette_colors = _get_optimize(im, info) 

587 if optimized_palette_colors is not None: 

588 im = im.remap_palette(optimized_palette_colors, source_palette) 

589 if "transparency" in info: 

590 try: 

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

592 info["transparency"] 

593 ) 

594 except ValueError: 

595 del info["transparency"] 

596 return im 

597 

598 assert im.palette is not None 

599 im.palette.palette = source_palette 

600 return im 

601 

602 

603def _write_single_frame( 

604 im: Image.Image, 

605 fp: IO[bytes], 

606 palette: _Palette | None, 

607) -> None: 

608 im_out = _normalize_mode(im) 

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

610 if isinstance(k, str): 

611 im.encoderinfo.setdefault(k, v) 

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

613 

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

615 fp.write(s) 

616 

617 # local image header 

618 flags = 0 

619 if get_interlace(im): 

620 flags = flags | 64 

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

622 

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

624 ImageFile._save( 

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

626 ) 

627 

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

629 

630 

631def _getbbox( 

632 base_im: Image.Image, im_frame: Image.Image 

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

634 palette_bytes = [ 

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

636 ] 

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

638 im_frame = im_frame.convert("RGBA") 

639 base_im = base_im.convert("RGBA") 

640 delta = ImageChops.subtract_modulo(im_frame, base_im) 

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

642 

643 

644class _Frame(NamedTuple): 

645 im: Image.Image 

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

647 encoderinfo: dict[str, Any] 

648 

649 

650def _write_multiple_frames( 

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

652) -> bool: 

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

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

655 

656 im_frames: list[_Frame] = [] 

657 previous_im: Image.Image | None = None 

658 frame_count = 0 

659 background_im = None 

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

661 for im_frame in ImageSequence.Iterator(imSequence): 

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

663 im_frame = _normalize_mode(im_frame.copy()) 

664 if frame_count == 0: 

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

666 if k == "transparency": 

667 continue 

668 if isinstance(k, str): 

669 im.encoderinfo.setdefault(k, v) 

670 

671 encoderinfo = im.encoderinfo.copy() 

672 if "transparency" in im_frame.info: 

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

674 im_frame = _normalize_palette(im_frame, palette, encoderinfo) 

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

676 encoderinfo["duration"] = duration[frame_count] 

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

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

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

680 encoderinfo["disposal"] = disposal[frame_count] 

681 frame_count += 1 

682 

683 diff_frame = None 

684 if im_frames and previous_im: 

685 # delta frame 

686 delta, bbox = _getbbox(previous_im, im_frame) 

687 if not bbox: 

688 # This frame is identical to the previous frame 

689 if encoderinfo.get("duration"): 

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

691 continue 

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

693 if background_im is None: 

694 color = im.encoderinfo.get( 

695 "transparency", im.info.get("transparency", (0, 0, 0)) 

696 ) 

697 background = _get_background(im_frame, color) 

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

699 assert im_frames[0].im.palette is not None 

700 background_im.putpalette(im_frames[0].im.palette) 

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

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

703 if "transparency" not in encoderinfo: 

704 assert im_frame.palette is not None 

705 try: 

706 encoderinfo["transparency"] = ( 

707 im_frame.palette._new_color_index(im_frame) 

708 ) 

709 except ValueError: 

710 pass 

711 if "transparency" in encoderinfo: 

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

713 diff_frame = im_frame.copy() 

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

715 if delta.mode == "RGBA": 

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

717 mask = ImageMath.lambda_eval( 

718 lambda args: args["convert"]( 

719 args["max"]( 

720 args["max"]( 

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

722 ), 

723 args["a"], 

724 ) 

725 * 255, 

726 "1", 

727 ), 

728 r=r, 

729 g=g, 

730 b=b, 

731 a=a, 

732 ) 

733 else: 

734 if delta.mode == "P": 

735 # Convert to L without considering palette 

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

737 delta_l.putdata(delta.getdata()) 

738 delta = delta_l 

739 mask = ImageMath.lambda_eval( 

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

741 im=delta, 

742 ) 

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

744 else: 

745 bbox = None 

746 previous_im = im_frame 

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

748 

749 if len(im_frames) == 1: 

750 if "duration" in im.encoderinfo: 

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

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

753 return False 

754 

755 for frame_data in im_frames: 

756 im_frame = frame_data.im 

757 if not frame_data.bbox: 

758 # global header 

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

760 fp.write(s) 

761 offset = (0, 0) 

762 else: 

763 # compress difference 

764 if not palette: 

765 frame_data.encoderinfo["include_color_table"] = True 

766 

767 im_frame = im_frame.crop(frame_data.bbox) 

768 offset = frame_data.bbox[:2] 

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

770 return True 

771 

772 

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

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

775 

776 

777def _save( 

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

779) -> None: 

780 # header 

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

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

783 else: 

784 palette = None 

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

786 

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

788 _write_single_frame(im, fp, palette) 

789 

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

791 

792 if hasattr(fp, "flush"): 

793 fp.flush() 

794 

795 

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

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

798 

799 # workaround for @PIL153 

800 if min(im.size) < 16: 

801 interlace = 0 

802 

803 return interlace 

804 

805 

806def _write_local_header( 

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

808) -> None: 

809 try: 

810 transparency = im.encoderinfo["transparency"] 

811 except KeyError: 

812 transparency = None 

813 

814 if "duration" in im.encoderinfo: 

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

816 else: 

817 duration = 0 

818 

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

820 

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

822 packed_flag = 1 if transparency is not None else 0 

823 packed_flag |= disposal << 2 

824 

825 fp.write( 

826 b"!" 

827 + o8(249) # extension intro 

828 + o8(4) # length 

829 + o8(packed_flag) # packed fields 

830 + o16(duration) # duration 

831 + o8(transparency or 0) # transparency index 

832 + o8(0) 

833 ) 

834 

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

836 if include_color_table: 

837 palette_bytes = _get_palette_bytes(im) 

838 color_table_size = _get_color_table_size(palette_bytes) 

839 if color_table_size: 

840 flags = flags | 128 # local color table flag 

841 flags = flags | color_table_size 

842 

843 fp.write( 

844 b"," 

845 + o16(offset[0]) # offset 

846 + o16(offset[1]) 

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

848 + o16(im.size[1]) 

849 + o8(flags) # flags 

850 ) 

851 if include_color_table and color_table_size: 

852 fp.write(_get_header_palette(palette_bytes)) 

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

854 

855 

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

857 # Unused by default. 

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

859 # 

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

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

862 # below for information on how to enable this. 

863 tempfile = im._dump() 

864 

865 try: 

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

867 if im.mode != "RGB": 

868 subprocess.check_call( 

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

870 ) 

871 else: 

872 # Pipe ppmquant output into ppmtogif 

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

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

875 togif_cmd = ["ppmtogif"] 

876 quant_proc = subprocess.Popen( 

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

878 ) 

879 togif_proc = subprocess.Popen( 

880 togif_cmd, 

881 stdin=quant_proc.stdout, 

882 stdout=f, 

883 stderr=subprocess.DEVNULL, 

884 ) 

885 

886 # Allow ppmquant to receive SIGPIPE if ppmtogif exits 

887 assert quant_proc.stdout is not None 

888 quant_proc.stdout.close() 

889 

890 retcode = quant_proc.wait() 

891 if retcode: 

892 raise subprocess.CalledProcessError(retcode, quant_cmd) 

893 

894 retcode = togif_proc.wait() 

895 if retcode: 

896 raise subprocess.CalledProcessError(retcode, togif_cmd) 

897 finally: 

898 try: 

899 os.unlink(tempfile) 

900 except OSError: 

901 pass 

902 

903 

904# Force optimization so that we can test performance against 

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

906_FORCE_OPTIMIZE = False 

907 

908 

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

910 """ 

911 Palette optimization is a potentially expensive operation. 

912 

913 This function determines if the palette should be optimized using 

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

915 

916 :param im: Image object 

917 :param info: encoderinfo 

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

919 """ 

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

921 # Potentially expensive operation. 

922 

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

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

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

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

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

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

929 

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

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

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

933 # check which colors are used 

934 used_palette_colors = [] 

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

936 if count: 

937 used_palette_colors.append(i) 

938 

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

940 return used_palette_colors 

941 

942 assert im.palette is not None 

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

944 im.palette.mode 

945 ) 

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

947 if ( 

948 # check that the palette would become smaller when saved 

949 len(used_palette_colors) <= current_palette_size // 2 

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

951 and current_palette_size > 2 

952 ): 

953 return used_palette_colors 

954 return None 

955 

956 

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

958 # calculate the palette size for the header 

959 if not palette_bytes: 

960 return 0 

961 elif len(palette_bytes) < 9: 

962 return 1 

963 else: 

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

965 

966 

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

968 """ 

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

970 suitable for direct inclusion in the GIF header 

971 

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

973 :returns: Null padded palette 

974 """ 

975 color_table_size = _get_color_table_size(palette_bytes) 

976 

977 # add the missing amount of bytes 

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

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

980 if actual_target_size_diff > 0: 

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

982 return palette_bytes 

983 

984 

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

986 """ 

987 Gets the palette for inclusion in the gif header 

988 

989 :param im: Image object 

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

991 """ 

992 if not im.palette: 

993 return b"" 

994 

995 palette = bytes(im.palette.palette) 

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

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

998 return palette 

999 

1000 

1001def _get_background( 

1002 im: Image.Image, 

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

1004) -> int: 

1005 background = 0 

1006 if info_background: 

1007 if isinstance(info_background, tuple): 

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

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

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

1011 assert im.palette is not None 

1012 try: 

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

1014 except ValueError as e: 

1015 if str(e) not in ( 

1016 # If all 256 colors are in use, 

1017 # then there is no need for the background color 

1018 "cannot allocate more than 256 colors", 

1019 # Ignore non-opaque WebP background 

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

1021 ): 

1022 raise 

1023 else: 

1024 background = info_background 

1025 return background 

1026 

1027 

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

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

1030 

1031 # Header Block 

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

1033 

1034 version = b"87a" 

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

1036 info 

1037 and ( 

1038 "transparency" in info 

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

1040 or info.get("duration") 

1041 or info.get("comment") 

1042 ) 

1043 ): 

1044 version = b"89a" 

1045 

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

1047 

1048 palette_bytes = _get_palette_bytes(im) 

1049 color_table_size = _get_color_table_size(palette_bytes) 

1050 

1051 header = [ 

1052 b"GIF" # signature 

1053 + version # version 

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

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

1056 # Logical Screen Descriptor 

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

1058 o8(color_table_size + 128), # packed fields 

1059 # background + reserved/aspect 

1060 o8(background) + o8(0), 

1061 # Global Color Table 

1062 _get_header_palette(palette_bytes), 

1063 ] 

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

1065 header.append( 

1066 b"!" 

1067 + o8(255) # extension intro 

1068 + o8(11) 

1069 + b"NETSCAPE2.0" 

1070 + o8(3) 

1071 + o8(1) 

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

1073 + o8(0) 

1074 ) 

1075 if info.get("comment"): 

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

1077 

1078 comment = info["comment"] 

1079 if isinstance(comment, str): 

1080 comment = comment.encode() 

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

1082 subblock = comment[i : i + 255] 

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

1084 

1085 comment_block += o8(0) 

1086 header.append(comment_block) 

1087 return header 

1088 

1089 

1090def _write_frame_data( 

1091 fp: IO[bytes], 

1092 im_frame: Image.Image, 

1093 offset: tuple[int, int], 

1094 params: dict[str, Any], 

1095) -> None: 

1096 try: 

1097 im_frame.encoderinfo = params 

1098 

1099 # local image header 

1100 _write_local_header(fp, im_frame, offset, 0) 

1101 

1102 ImageFile._save( 

1103 im_frame, 

1104 fp, 

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

1106 ) 

1107 

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

1109 finally: 

1110 del im_frame.encoderinfo 

1111 

1112 

1113# -------------------------------------------------------------------- 

1114# Legacy GIF utilities 

1115 

1116 

1117def getheader( 

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

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

1120 """ 

1121 Legacy Method to get Gif data from image. 

1122 

1123 Warning:: May modify image data. 

1124 

1125 :param im: Image object 

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

1127 :param info: encoderinfo 

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

1129 

1130 """ 

1131 if info is None: 

1132 info = {} 

1133 

1134 used_palette_colors = _get_optimize(im, info) 

1135 

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

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

1138 

1139 im_mod = _normalize_palette(im, palette, info) 

1140 im.palette = im_mod.palette 

1141 im.im = im_mod.im 

1142 header = _get_global_header(im, info) 

1143 

1144 return header, used_palette_colors 

1145 

1146 

1147def getdata( 

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

1149) -> list[bytes]: 

1150 """ 

1151 Legacy Method 

1152 

1153 Return a list of strings representing this image. 

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

1155 encoded image data. 

1156 

1157 To specify duration, add the time in milliseconds, 

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

1159 

1160 :param im: Image object 

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

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

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

1164 

1165 """ 

1166 from io import BytesIO 

1167 

1168 class Collector(BytesIO): 

1169 data = [] 

1170 

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

1172 self.data.append(data) 

1173 return len(data) 

1174 

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

1176 

1177 fp = Collector() 

1178 

1179 _write_frame_data(fp, im, offset, params) 

1180 

1181 return fp.data 

1182 

1183 

1184# -------------------------------------------------------------------- 

1185# Registry 

1186 

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

1188Image.register_save(GifImageFile.format, _save) 

1189Image.register_save_all(GifImageFile.format, _save_all) 

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

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

1192 

1193# 

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

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

1196 

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