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

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

623 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 

32import sys 

33from enum import IntEnum 

34from functools import cached_property 

35from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union 

36 

37from . import ( 

38 Image, 

39 ImageChops, 

40 ImageFile, 

41 ImageMath, 

42 ImageOps, 

43 ImagePalette, 

44 ImageSequence, 

45) 

46from ._binary import i16le as i16 

47from ._binary import o8 

48from ._binary import o16le as o16 

49 

50if TYPE_CHECKING: 

51 from . import _imaging 

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 not palette and self.global_palette: 

324 from copy import copy 

325 

326 palette = copy(self.global_palette) 

327 self.palette = palette 

328 else: 

329 if self.mode == "P": 

330 if ( 

331 LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY 

332 or palette 

333 ): 

334 self.pyaccess = None 

335 if "transparency" in self.info: 

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

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

338 self._mode = "RGBA" 

339 del self.info["transparency"] 

340 else: 

341 self._mode = "RGB" 

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

343 

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

345 if self._frame_palette: 

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

347 color = 0 

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

349 else: 

350 return (color, color, color) 

351 

352 self.dispose = None 

353 self.dispose_extent = frame_dispose_extent 

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

355 try: 

356 if self.disposal_method == 2: 

357 # replace with background colour 

358 

359 # only dispose the extent in this frame 

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

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

362 

363 Image._decompression_bomb_check(dispose_size) 

364 

365 # by convention, attempt to use transparency first 

366 dispose_mode = "P" 

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

368 if color is not None: 

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

370 dispose_mode = "RGBA" 

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

372 else: 

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

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

375 dispose_mode = "RGB" 

376 color = _rgb(color) 

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

378 else: 

379 # replace with previous contents 

380 if self.im is not None: 

381 # only dispose the extent in this frame 

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

383 elif frame_transparency is not None: 

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

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

386 

387 Image._decompression_bomb_check(dispose_size) 

388 dispose_mode = "P" 

389 color = frame_transparency 

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

391 dispose_mode = "RGBA" 

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

393 self.dispose = Image.core.fill( 

394 dispose_mode, dispose_size, color 

395 ) 

396 except AttributeError: 

397 pass 

398 

399 if interlace is not None: 

400 transparency = -1 

401 if frame_transparency is not None: 

402 if frame == 0: 

403 if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: 

404 self.info["transparency"] = frame_transparency 

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

406 transparency = frame_transparency 

407 self.tile = [ 

408 ( 

409 "gif", 

410 (x0, y0, x1, y1), 

411 self.__offset, 

412 (bits, interlace, transparency), 

413 ) 

414 ] 

415 

416 if info.get("comment"): 

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

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

419 if k in info: 

420 self.info[k] = info[k] 

421 elif k in self.info: 

422 del self.info[k] 

423 

424 def load_prepare(self) -> None: 

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

426 self._prev_im = None 

427 if self.__frame == 0: 

428 if self._frame_transparency is not None: 

429 self.im = Image.core.fill( 

430 temp_mode, self.size, self._frame_transparency 

431 ) 

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

433 self._prev_im = self.im 

434 if self._frame_palette: 

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

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

437 else: 

438 self.im = None 

439 self._mode = temp_mode 

440 self._frame_palette = None 

441 

442 super().load_prepare() 

443 

444 def load_end(self) -> None: 

445 if self.__frame == 0: 

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

447 if self._frame_transparency is not None: 

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

449 self._mode = "RGBA" 

450 else: 

451 self._mode = "RGB" 

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

453 return 

454 if not self._prev_im: 

455 return 

456 if self._frame_transparency is not None: 

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

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

459 else: 

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

461 

462 assert self.dispose_extent is not None 

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

464 

465 self.im = self._prev_im 

466 self._mode = self.im.mode 

467 if frame_im.mode == "RGBA": 

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

469 else: 

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

471 

472 def tell(self) -> int: 

473 return self.__frame 

474 

475 

476# -------------------------------------------------------------------- 

477# Write GIF files 

478 

479 

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

481 

482 

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

484 """ 

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

486 for saving in a Gif. 

487 

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

489 palette or 'L' mode. 

490 

491 :param im: Image object 

492 :returns: Image object 

493 """ 

494 if im.mode in RAWMODE: 

495 im.load() 

496 return im 

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

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

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

500 for rgba in im.palette.colors: 

501 if rgba[3] == 0: 

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

503 break 

504 return im 

505 return im.convert("L") 

506 

507 

508_Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette] 

509 

510 

511def _normalize_palette( 

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

513) -> Image.Image: 

514 """ 

515 Normalizes the palette for image. 

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

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

518 - Optimizes the palette if necessary/desired. 

519 

520 :param im: Image object 

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

522 :param info: encoderinfo 

523 :returns: Image object 

524 """ 

525 source_palette = None 

526 if palette: 

527 # a bytes palette 

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

529 source_palette = bytearray(palette[:768]) 

530 if isinstance(palette, ImagePalette.ImagePalette): 

531 source_palette = bytearray(palette.palette) 

532 

533 if im.mode == "P": 

534 if not source_palette: 

535 source_palette = im.im.getpalette("RGB")[:768] 

536 else: # L-mode 

537 if not source_palette: 

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

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

540 

541 used_palette_colors: list[int] | None 

542 if palette: 

543 used_palette_colors = [] 

544 assert source_palette is not None 

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

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

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

548 if index in used_palette_colors: 

549 index = None 

550 used_palette_colors.append(index) 

551 for i, index in enumerate(used_palette_colors): 

552 if index is None: 

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

554 if j not in used_palette_colors: 

555 used_palette_colors[i] = j 

556 break 

557 im = im.remap_palette(used_palette_colors) 

558 else: 

559 used_palette_colors = _get_optimize(im, info) 

560 if used_palette_colors is not None: 

561 im = im.remap_palette(used_palette_colors, source_palette) 

562 if "transparency" in info: 

563 try: 

564 info["transparency"] = used_palette_colors.index( 

565 info["transparency"] 

566 ) 

567 except ValueError: 

568 del info["transparency"] 

569 return im 

570 

571 im.palette.palette = source_palette 

572 return im 

573 

574 

575def _write_single_frame( 

576 im: Image.Image, 

577 fp: IO[bytes], 

578 palette: _Palette | None, 

579) -> None: 

580 im_out = _normalize_mode(im) 

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

582 im.encoderinfo.setdefault(k, v) 

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

584 

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

586 fp.write(s) 

587 

588 # local image header 

589 flags = 0 

590 if get_interlace(im): 

591 flags = flags | 64 

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

593 

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

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

596 

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

598 

599 

600def _getbbox( 

601 base_im: Image.Image, im_frame: Image.Image 

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

603 if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): 

604 im_frame = im_frame.convert("RGBA") 

605 base_im = base_im.convert("RGBA") 

606 delta = ImageChops.subtract_modulo(im_frame, base_im) 

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

608 

609 

610class _Frame(NamedTuple): 

611 im: Image.Image 

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

613 encoderinfo: dict[str, Any] 

614 

615 

616def _write_multiple_frames( 

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

618) -> bool: 

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

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

621 

622 im_frames: list[_Frame] = [] 

623 previous_im: Image.Image | None = None 

624 frame_count = 0 

625 background_im = None 

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

627 for im_frame in ImageSequence.Iterator(imSequence): 

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

629 im_frame = _normalize_mode(im_frame.copy()) 

630 if frame_count == 0: 

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

632 if k == "transparency": 

633 continue 

634 im.encoderinfo.setdefault(k, v) 

635 

636 encoderinfo = im.encoderinfo.copy() 

637 if "transparency" in im_frame.info: 

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

639 im_frame = _normalize_palette(im_frame, palette, encoderinfo) 

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

641 encoderinfo["duration"] = duration[frame_count] 

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

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

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

645 encoderinfo["disposal"] = disposal[frame_count] 

646 frame_count += 1 

647 

648 diff_frame = None 

649 if im_frames and previous_im: 

650 # delta frame 

651 delta, bbox = _getbbox(previous_im, im_frame) 

652 if not bbox: 

653 # This frame is identical to the previous frame 

654 if encoderinfo.get("duration"): 

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

656 continue 

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

658 if background_im is None: 

659 color = im.encoderinfo.get( 

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

661 ) 

662 background = _get_background(im_frame, color) 

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

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

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

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

667 if "transparency" not in encoderinfo: 

668 try: 

669 encoderinfo["transparency"] = ( 

670 im_frame.palette._new_color_index(im_frame) 

671 ) 

672 except ValueError: 

673 pass 

674 if "transparency" in encoderinfo: 

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

676 diff_frame = im_frame.copy() 

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

678 if delta.mode == "RGBA": 

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

680 mask = ImageMath.lambda_eval( 

681 lambda args: args["convert"]( 

682 args["max"]( 

683 args["max"]( 

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

685 ), 

686 args["a"], 

687 ) 

688 * 255, 

689 "1", 

690 ), 

691 r=r, 

692 g=g, 

693 b=b, 

694 a=a, 

695 ) 

696 else: 

697 if delta.mode == "P": 

698 # Convert to L without considering palette 

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

700 delta_l.putdata(delta.getdata()) 

701 delta = delta_l 

702 mask = ImageMath.lambda_eval( 

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

704 im=delta, 

705 ) 

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

707 else: 

708 bbox = None 

709 previous_im = im_frame 

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

711 

712 if len(im_frames) == 1: 

713 if "duration" in im.encoderinfo: 

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

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

716 return False 

717 

718 for frame_data in im_frames: 

719 im_frame = frame_data.im 

720 if not frame_data.bbox: 

721 # global header 

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

723 fp.write(s) 

724 offset = (0, 0) 

725 else: 

726 # compress difference 

727 if not palette: 

728 frame_data.encoderinfo["include_color_table"] = True 

729 

730 im_frame = im_frame.crop(frame_data.bbox) 

731 offset = frame_data.bbox[:2] 

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

733 return True 

734 

735 

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

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

738 

739 

740def _save( 

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

742) -> None: 

743 # header 

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

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

746 else: 

747 palette = None 

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

749 

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

751 _write_single_frame(im, fp, palette) 

752 

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

754 

755 if hasattr(fp, "flush"): 

756 fp.flush() 

757 

758 

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

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

761 

762 # workaround for @PIL153 

763 if min(im.size) < 16: 

764 interlace = 0 

765 

766 return interlace 

767 

768 

769def _write_local_header( 

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

771) -> None: 

772 try: 

773 transparency = im.encoderinfo["transparency"] 

774 except KeyError: 

775 transparency = None 

776 

777 if "duration" in im.encoderinfo: 

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

779 else: 

780 duration = 0 

781 

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

783 

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

785 packed_flag = 1 if transparency is not None else 0 

786 packed_flag |= disposal << 2 

787 

788 fp.write( 

789 b"!" 

790 + o8(249) # extension intro 

791 + o8(4) # length 

792 + o8(packed_flag) # packed fields 

793 + o16(duration) # duration 

794 + o8(transparency or 0) # transparency index 

795 + o8(0) 

796 ) 

797 

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

799 if include_color_table: 

800 palette_bytes = _get_palette_bytes(im) 

801 color_table_size = _get_color_table_size(palette_bytes) 

802 if color_table_size: 

803 flags = flags | 128 # local color table flag 

804 flags = flags | color_table_size 

805 

806 fp.write( 

807 b"," 

808 + o16(offset[0]) # offset 

809 + o16(offset[1]) 

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

811 + o16(im.size[1]) 

812 + o8(flags) # flags 

813 ) 

814 if include_color_table and color_table_size: 

815 fp.write(_get_header_palette(palette_bytes)) 

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

817 

818 

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

820 # Unused by default. 

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

822 # 

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

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

825 # below for information on how to enable this. 

826 tempfile = im._dump() 

827 

828 try: 

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

830 if im.mode != "RGB": 

831 subprocess.check_call( 

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

833 ) 

834 else: 

835 # Pipe ppmquant output into ppmtogif 

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

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

838 togif_cmd = ["ppmtogif"] 

839 quant_proc = subprocess.Popen( 

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

841 ) 

842 togif_proc = subprocess.Popen( 

843 togif_cmd, 

844 stdin=quant_proc.stdout, 

845 stdout=f, 

846 stderr=subprocess.DEVNULL, 

847 ) 

848 

849 # Allow ppmquant to receive SIGPIPE if ppmtogif exits 

850 assert quant_proc.stdout is not None 

851 quant_proc.stdout.close() 

852 

853 retcode = quant_proc.wait() 

854 if retcode: 

855 raise subprocess.CalledProcessError(retcode, quant_cmd) 

856 

857 retcode = togif_proc.wait() 

858 if retcode: 

859 raise subprocess.CalledProcessError(retcode, togif_cmd) 

860 finally: 

861 try: 

862 os.unlink(tempfile) 

863 except OSError: 

864 pass 

865 

866 

867# Force optimization so that we can test performance against 

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

869_FORCE_OPTIMIZE = False 

870 

871 

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

873 """ 

874 Palette optimization is a potentially expensive operation. 

875 

876 This function determines if the palette should be optimized using 

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

878 

879 :param im: Image object 

880 :param info: encoderinfo 

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

882 """ 

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

884 # Potentially expensive operation. 

885 

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

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

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

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

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

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

892 

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

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

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

896 # check which colors are used 

897 used_palette_colors = [] 

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

899 if count: 

900 used_palette_colors.append(i) 

901 

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

903 return used_palette_colors 

904 

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

906 im.palette.mode 

907 ) 

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

909 if ( 

910 # check that the palette would become smaller when saved 

911 len(used_palette_colors) <= current_palette_size // 2 

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

913 and current_palette_size > 2 

914 ): 

915 return used_palette_colors 

916 return None 

917 

918 

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

920 # calculate the palette size for the header 

921 if not palette_bytes: 

922 return 0 

923 elif len(palette_bytes) < 9: 

924 return 1 

925 else: 

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

927 

928 

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

930 """ 

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

932 suitable for direct inclusion in the GIF header 

933 

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

935 :returns: Null padded palette 

936 """ 

937 color_table_size = _get_color_table_size(palette_bytes) 

938 

939 # add the missing amount of bytes 

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

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

942 if actual_target_size_diff > 0: 

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

944 return palette_bytes 

945 

946 

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

948 """ 

949 Gets the palette for inclusion in the gif header 

950 

951 :param im: Image object 

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

953 """ 

954 return im.palette.palette if im.palette else b"" 

955 

956 

957def _get_background( 

958 im: Image.Image, 

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

960) -> int: 

961 background = 0 

962 if info_background: 

963 if isinstance(info_background, tuple): 

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

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

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

967 try: 

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

969 except ValueError as e: 

970 if str(e) not in ( 

971 # If all 256 colors are in use, 

972 # then there is no need for the background color 

973 "cannot allocate more than 256 colors", 

974 # Ignore non-opaque WebP background 

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

976 ): 

977 raise 

978 else: 

979 background = info_background 

980 return background 

981 

982 

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

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

985 

986 # Header Block 

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

988 

989 version = b"87a" 

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

991 info 

992 and ( 

993 "transparency" in info 

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

995 or info.get("duration") 

996 or info.get("comment") 

997 ) 

998 ): 

999 version = b"89a" 

1000 

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

1002 

1003 palette_bytes = _get_palette_bytes(im) 

1004 color_table_size = _get_color_table_size(palette_bytes) 

1005 

1006 header = [ 

1007 b"GIF" # signature 

1008 + version # version 

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

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

1011 # Logical Screen Descriptor 

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

1013 o8(color_table_size + 128), # packed fields 

1014 # background + reserved/aspect 

1015 o8(background) + o8(0), 

1016 # Global Color Table 

1017 _get_header_palette(palette_bytes), 

1018 ] 

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

1020 header.append( 

1021 b"!" 

1022 + o8(255) # extension intro 

1023 + o8(11) 

1024 + b"NETSCAPE2.0" 

1025 + o8(3) 

1026 + o8(1) 

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

1028 + o8(0) 

1029 ) 

1030 if info.get("comment"): 

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

1032 

1033 comment = info["comment"] 

1034 if isinstance(comment, str): 

1035 comment = comment.encode() 

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

1037 subblock = comment[i : i + 255] 

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

1039 

1040 comment_block += o8(0) 

1041 header.append(comment_block) 

1042 return header 

1043 

1044 

1045def _write_frame_data( 

1046 fp: IO[bytes], 

1047 im_frame: Image.Image, 

1048 offset: tuple[int, int], 

1049 params: dict[str, Any], 

1050) -> None: 

1051 try: 

1052 im_frame.encoderinfo = params 

1053 

1054 # local image header 

1055 _write_local_header(fp, im_frame, offset, 0) 

1056 

1057 ImageFile._save( 

1058 im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] 

1059 ) 

1060 

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

1062 finally: 

1063 del im_frame.encoderinfo 

1064 

1065 

1066# -------------------------------------------------------------------- 

1067# Legacy GIF utilities 

1068 

1069 

1070def getheader( 

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

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

1073 """ 

1074 Legacy Method to get Gif data from image. 

1075 

1076 Warning:: May modify image data. 

1077 

1078 :param im: Image object 

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

1080 :param info: encoderinfo 

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

1082 

1083 """ 

1084 if info is None: 

1085 info = {} 

1086 

1087 used_palette_colors = _get_optimize(im, info) 

1088 

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

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

1091 

1092 im_mod = _normalize_palette(im, palette, info) 

1093 im.palette = im_mod.palette 

1094 im.im = im_mod.im 

1095 header = _get_global_header(im, info) 

1096 

1097 return header, used_palette_colors 

1098 

1099 

1100def getdata( 

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

1102) -> list[bytes]: 

1103 """ 

1104 Legacy Method 

1105 

1106 Return a list of strings representing this image. 

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

1108 encoded image data. 

1109 

1110 To specify duration, add the time in milliseconds, 

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

1112 

1113 :param im: Image object 

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

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

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

1117 

1118 """ 

1119 from io import BytesIO 

1120 

1121 class Collector(BytesIO): 

1122 data = [] 

1123 

1124 if sys.version_info >= (3, 12): 

1125 from collections.abc import Buffer 

1126 

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

1128 self.data.append(data) 

1129 return len(data) 

1130 

1131 else: 

1132 

1133 def write(self, data: Any) -> int: 

1134 self.data.append(data) 

1135 return len(data) 

1136 

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

1138 

1139 fp = Collector() 

1140 

1141 _write_frame_data(fp, im, offset, params) 

1142 

1143 return fp.data 

1144 

1145 

1146# -------------------------------------------------------------------- 

1147# Registry 

1148 

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

1150Image.register_save(GifImageFile.format, _save) 

1151Image.register_save_all(GifImageFile.format, _save_all) 

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

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

1154 

1155# 

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

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

1158 

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