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

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

645 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 

28__lazy_modules__ = { 

29 "PIL._binary", 

30 "PIL._util", 

31 "itertools", 

32 "math", 

33 "subprocess", 

34} 

35 

36import itertools 

37import math 

38from enum import IntEnum 

39from functools import cached_property 

40from typing import NamedTuple, cast 

41 

42from . import ( 

43 Image, 

44 ImageChops, 

45 ImageFile, 

46 ImagePalette, 

47 ImageSequence, 

48) 

49from ._binary import i16le as i16 

50from ._binary import o8 

51from ._binary import o16le as o16 

52from ._util import DeferredError 

53 

54TYPE_CHECKING = False 

55if TYPE_CHECKING: 

56 from typing import IO, Any, Literal 

57 

58 from . import _imaging 

59 from ._typing import Buffer 

60 

61 

62class LoadingStrategy(IntEnum): 

63 """.. versionadded:: 9.1.0""" 

64 

65 RGB_AFTER_FIRST = 0 

66 RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 

67 RGB_ALWAYS = 2 

68 

69 

70#: .. versionadded:: 9.1.0 

71LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST 

72 

73# -------------------------------------------------------------------- 

74# Identify/read GIF files 

75 

76 

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

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

79 

80 

81## 

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

83# GIF89 images. 

84 

85 

86class GifImageFile(ImageFile.ImageFile): 

87 format = "GIF" 

88 format_description = "Compuserve GIF" 

89 _close_exclusive_fp_after_loading = False 

90 

91 global_palette = None 

92 

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

94 assert self.fp is not None 

95 s = self.fp.read(1) 

96 if s and s[0]: 

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

98 return None 

99 

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

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

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

103 return True 

104 return False 

105 

106 def _open(self) -> None: 

107 # Screen 

108 assert self.fp is not None 

109 s = self.fp.read(13) 

110 if not _accept(s): 

111 msg = "not a GIF file" 

112 raise SyntaxError(msg) 

113 

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

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

116 flags = s[10] 

117 bits = (flags & 7) + 1 

118 

119 if flags & 128: 

120 # get global palette 

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

122 # check if palette contains colour indices 

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

124 if self._is_palette_needed(p): 

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

126 self.global_palette = palette 

127 

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

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

130 self._n_frames: int | None = None 

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

132 

133 @property 

134 def n_frames(self) -> int: 

135 if self._n_frames is None: 

136 current = self.tell() 

137 try: 

138 while True: 

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

140 except EOFError: 

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

142 self.seek(current) 

143 return self._n_frames 

144 

145 @cached_property 

146 def is_animated(self) -> bool: 

147 if self._n_frames is not None: 

148 return self._n_frames != 1 

149 

150 current = self.tell() 

151 if current: 

152 return True 

153 

154 try: 

155 self._seek(1, False) 

156 is_animated = True 

157 except EOFError: 

158 is_animated = False 

159 

160 self.seek(current) 

161 return is_animated 

162 

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

164 if not self._seek_check(frame): 

165 return 

166 if frame < self.__frame: 

167 self._im = None 

168 self._seek(0) 

169 

170 last_frame = self.__frame 

171 try: 

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

173 self._seek(f) 

174 except EOFError as e: 

175 self.seek(last_frame) 

176 msg = "no more images in GIF file" 

177 raise EOFError(msg) from e 

178 

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

180 if isinstance(self._fp, DeferredError): 

181 raise self._fp.ex 

182 if frame == 0: 

183 # rewind 

184 self.__offset = 0 

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

186 self.__frame = -1 

187 self._fp.seek(self.__rewind) 

188 self.disposal_method = 0 

189 if "comment" in self.info: 

190 del self.info["comment"] 

191 else: 

192 # ensure that the previous frame was loaded 

193 if self.tile and update_image: 

194 self.load() 

195 

196 if frame != self.__frame + 1: 

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

198 raise ValueError(msg) 

199 

200 self.fp = self._fp 

201 if self.__offset: 

202 # backup to last frame 

203 self.fp.seek(self.__offset) 

204 while self.data(): 

205 pass 

206 self.__offset = 0 

207 

208 s = self.fp.read(1) 

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

210 msg = "no more images in GIF file" 

211 raise EOFError(msg) 

212 

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

214 

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

216 comment_blocks: list[bytes] = [] 

217 frame_transparency = None 

218 interlace = None 

219 frame_dispose_extent = None 

220 while True: 

221 if not s: 

222 s = self.fp.read(1) 

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

224 break 

225 

226 elif s == b"!": 

227 # 

228 # extensions 

229 # 

230 s = self.fp.read(1) 

231 block = self.data() 

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

233 # 

234 # graphic control extension 

235 # 

236 flags = block[0] 

237 if flags & 1: 

238 frame_transparency = block[3] 

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

240 

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

242 dispose_bits = 0b00011100 & flags 

243 dispose_bits = dispose_bits >> 2 

244 if dispose_bits: 

245 # only set the dispose if it is not 

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

247 # correct, but it seems to prevent the last 

248 # frame from looking odd for some animations 

249 self.disposal_method = dispose_bits 

250 elif s[0] == 254: 

251 # 

252 # comment extension 

253 # 

254 if block and comment_blocks: 

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

256 comment_blocks.append(b"\n") 

257 while block: 

258 comment_blocks.append(block) 

259 block = self.data() 

260 s = b"" 

261 continue 

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

263 # 

264 # application extension 

265 # 

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

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

268 block = self.data() 

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

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

271 while self.data(): 

272 pass 

273 

274 elif s == b",": 

275 # 

276 # local image 

277 # 

278 s = self.fp.read(9) 

279 

280 # extent 

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

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

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

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

285 Image._decompression_bomb_check(size) 

286 self._size = size 

287 frame_dispose_extent = x0, y0, x1, y1 

288 flags = s[8] 

289 

290 interlace = (flags & 64) != 0 

291 

292 if flags & 128: 

293 bits = (flags & 7) + 1 

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

295 if self._is_palette_needed(p): 

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

297 else: 

298 palette = False 

299 

300 # image data 

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

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

303 break 

304 s = b"" 

305 

306 if interlace is None: 

307 msg = "image not found in GIF frame" 

308 raise EOFError(msg) 

309 

310 self.__frame = frame 

311 if not update_image: 

312 return 

313 

314 self.tile = [] 

315 

316 if self.dispose: 

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

318 

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

320 self._frame_transparency = frame_transparency 

321 if frame == 0: 

322 self.palette = None 

323 if self._frame_palette: 

324 if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: 

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

326 else: 

327 self._mode = "P" 

328 if palette: 

329 self.palette = palette 

330 elif self.global_palette: 

331 from copy import copy 

332 

333 self.palette = copy(self.global_palette) 

334 else: 

335 self._mode = "L" 

336 elif self.mode == "P" and ( 

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 self.palette = None 

349 

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

351 if self._frame_palette: 

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

353 color = 0 

354 return cast( 

355 "tuple[int, int, int]", 

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

357 ) 

358 else: 

359 return (color, color, color) 

360 

361 self.dispose = None 

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

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

364 try: 

365 if self.disposal_method == 2: 

366 # replace with background colour 

367 

368 # only dispose the extent in this frame 

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

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

371 

372 Image._decompression_bomb_check(dispose_size) 

373 

374 # by convention, attempt to use transparency first 

375 dispose_mode = "P" 

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

377 if color is not None: 

378 if self.mode in {"RGB", "RGBA"}: 

379 dispose_mode = "RGBA" 

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

381 else: 

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

383 if self.mode in {"RGB", "RGBA"}: 

384 dispose_mode = "RGB" 

385 color = _rgb(color) 

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

387 else: 

388 # replace with previous contents 

389 if self._im is not None: 

390 # only dispose the extent in this frame 

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

392 elif frame_transparency is not None: 

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

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

395 

396 Image._decompression_bomb_check(dispose_size) 

397 dispose_mode = "P" 

398 color = frame_transparency 

399 if self.mode in {"RGB", "RGBA"}: 

400 dispose_mode = "RGBA" 

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

402 self.dispose = Image.core.fill( 

403 dispose_mode, dispose_size, color 

404 ) 

405 except AttributeError: 

406 pass 

407 

408 if interlace is not None: 

409 transparency = -1 

410 if frame_transparency is not None: 

411 if frame == 0: 

412 if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: 

413 self.info["transparency"] = frame_transparency 

414 elif self.mode not in {"RGB", "RGBA"}: 

415 transparency = frame_transparency 

416 self.tile = [ 

417 ImageFile._Tile( 

418 "gif", 

419 (x0, y0, x1, y1), 

420 self.__offset, 

421 (bits, interlace, transparency), 

422 ) 

423 ] 

424 

425 if comment_blocks: 

426 self.info["comment"] = b"".join(comment_blocks) 

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

428 if k in info: 

429 self.info[k] = info[k] 

430 elif k in self.info: 

431 del self.info[k] 

432 

433 def load_prepare(self) -> None: 

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

435 self._prev_im = None 

436 if self.__frame == 0: 

437 self.im = Image.core.fill( 

438 temp_mode, self.size, self._frame_transparency or 0 

439 ) 

440 if temp_mode == "P": 

441 assert self._frame_palette 

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

443 elif self.mode in {"RGB", "RGBA"}: 

444 self._prev_im = self.im 

445 if self._frame_palette: 

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

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

448 else: 

449 self._im = None 

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

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

452 if self._frame_palette: 

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

454 expanded_im.paste(self.im, (0, 0, *self.im.size)) 

455 

456 self.im = expanded_im 

457 self._mode = temp_mode 

458 self._frame_palette = None 

459 

460 super().load_prepare() 

461 

462 def load_end(self) -> None: 

463 if self.__frame == 0: 

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

465 if self._frame_transparency is not None: 

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

467 self._mode = "RGBA" 

468 else: 

469 self._mode = "RGB" 

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

471 return 

472 if not self._prev_im: 

473 return 

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

475 if self._frame_transparency is not None: 

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

477 else: 

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

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

480 expanded_im = expanded_im.convert("RGB") 

481 expanded_im.paste(self._prev_im, (0, 0, *self._prev_im.size)) 

482 

483 self._prev_im = expanded_im 

484 assert self._prev_im is not None 

485 if self._frame_transparency is not None: 

486 if self.mode == "L": 

487 frame_im = self.im.convert_transparent("LA", self._frame_transparency) 

488 else: 

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

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

491 else: 

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

493 

494 assert self.dispose_extent is not None 

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

496 

497 self.im = self._prev_im 

498 self._mode = self.im.mode 

499 if frame_im.mode in {"LA", "RGBA"}: 

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

501 else: 

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

503 

504 def tell(self) -> int: 

505 return self.__frame 

506 

507 

508# -------------------------------------------------------------------- 

509# Write GIF files 

510 

511 

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

513 

514 

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

516 """ 

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

518 for saving in a Gif. 

519 

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

521 palette or 'L' mode. 

522 

523 :param im: Image object 

524 :returns: Image object 

525 """ 

526 if im.mode in RAWMODE: 

527 im.load() 

528 return im 

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

530 if im.mode not in {"RGB", "RGBA"}: 

531 # Of the RGB-based modes, quantization only supports RGB and RGBA 

532 im = im.convert("RGBA" if im.has_transparency_data else "RGB") 

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

534 assert im.palette is not None 

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

536 for rgba in im.palette.colors: 

537 if rgba[3] == 0: 

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

539 break 

540 return im 

541 return im.convert("L") 

542 

543 

544_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette 

545 

546 

547def _normalize_palette( 

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

549) -> Image.Image: 

550 """ 

551 Normalizes the palette for image. 

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

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

554 - Optimizes the palette if necessary/desired. 

555 

556 :param im: Image object 

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

558 :param info: encoderinfo 

559 :returns: Image object 

560 """ 

561 source_palette = None 

562 if palette: 

563 # a bytes palette 

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

565 source_palette = bytearray(palette[:768]) 

566 if isinstance(palette, ImagePalette.ImagePalette): 

567 source_palette = bytearray(palette.palette) 

568 

569 if im.mode == "P": 

570 if not source_palette: 

571 im_palette = im.getpalette(None) 

572 assert im_palette is not None 

573 source_palette = bytearray(im_palette) 

574 else: # L-mode 

575 if not source_palette: 

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

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

578 assert source_palette is not None 

579 

580 if palette: 

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

582 assert im.palette is not None 

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

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

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

586 if index in used_palette_colors: 

587 index = None 

588 used_palette_colors.append(index) 

589 for i, index in enumerate(used_palette_colors): 

590 if index is None: 

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

592 if j not in used_palette_colors: 

593 used_palette_colors[i] = j 

594 break 

595 dest_map: list[int] = [] 

596 for index in used_palette_colors: 

597 assert index is not None 

598 dest_map.append(index) 

599 im = im.remap_palette(dest_map) 

600 else: 

601 optimized_palette_colors = _get_optimize(im, info) 

602 if optimized_palette_colors is not None: 

603 im = im.remap_palette(optimized_palette_colors, source_palette) 

604 if "transparency" in info: 

605 try: 

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

607 info["transparency"] 

608 ) 

609 except ValueError: 

610 del info["transparency"] 

611 return im 

612 

613 assert im.palette is not None 

614 im.palette.palette = source_palette 

615 return im 

616 

617 

618def _write_single_frame( 

619 im: Image.Image, 

620 fp: IO[bytes], 

621 palette: _Palette | None, 

622) -> None: 

623 im_out = _normalize_mode(im) 

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

625 if isinstance(k, str): 

626 im.encoderinfo.setdefault(k, v) 

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

628 

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

630 fp.write(s) 

631 

632 # local image header 

633 flags = 0 

634 if get_interlace(im): 

635 flags = flags | 64 

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

637 

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

639 ImageFile._save(im_out, fp, [ImageFile._Tile("gif", args=RAWMODE[im_out.mode])]) 

640 

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

642 

643 

644def _getbbox( 

645 base_im: Image.Image, im_frame: Image.Image 

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

647 palette_bytes = [ 

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

649 ] 

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

651 im_frame = im_frame.convert("RGBA") 

652 base_im = base_im.convert("RGBA") 

653 delta = ImageChops.subtract_modulo(im_frame, base_im) 

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

655 

656 

657class _Frame(NamedTuple): 

658 im: Image.Image 

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

660 encoderinfo: dict[str, Any] 

661 

662 

663def _write_multiple_frames( 

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

665) -> bool: 

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

667 disposal = im.encoderinfo.get("disposal") 

668 

669 im_frames: list[_Frame] = [] 

670 previous_im: Image.Image | None = None 

671 frame_count = 0 

672 background_im = None 

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

674 for im_frame in ImageSequence.Iterator(imSequence): 

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

676 im_frame = _normalize_mode(im_frame.copy()) 

677 if frame_count == 0: 

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

679 if k == "transparency": 

680 continue 

681 if isinstance(k, str): 

682 im.encoderinfo.setdefault(k, v) 

683 

684 encoderinfo = im.encoderinfo.copy() 

685 if "transparency" in im_frame.info: 

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

687 im_frame = _normalize_palette(im_frame, palette, encoderinfo) 

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

689 encoderinfo["duration"] = duration[frame_count] 

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

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

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

693 encoderinfo["disposal"] = disposal[frame_count] 

694 frame_count += 1 

695 

696 diff_frame = None 

697 if im_frames and previous_im: 

698 # delta frame 

699 delta, bbox = _getbbox(previous_im, im_frame) 

700 if not bbox: 

701 # This frame is identical to the previous frame 

702 if encoderinfo.get("duration"): 

703 im_frames[-1].encoderinfo["duration"] = ( 

704 im_frames[-1].encoderinfo.get("duration", 0) 

705 + encoderinfo["duration"] 

706 ) 

707 continue 

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

709 # To appear correctly in viewers using a convention, 

710 # only consider transparency, and not background color 

711 color = im.encoderinfo.get( 

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

713 ) 

714 if color is not None: 

715 if background_im is None: 

716 background = _get_background(im_frame, color) 

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

718 first_palette = im_frames[0].im.palette 

719 assert first_palette is not None 

720 background_im.putpalette(first_palette) 

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

722 else: 

723 bbox = (0, 0, *im_frame.size) 

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

725 if "transparency" not in encoderinfo: 

726 assert im_frame.palette is not None 

727 try: 

728 encoderinfo["transparency"] = ( 

729 im_frame.palette._new_color_index(im_frame) 

730 ) 

731 except ValueError: 

732 pass 

733 if "transparency" in encoderinfo: 

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

735 if delta.mode == "RGBA": 

736 # Each pixel is unchanged only if all four bands are zero 

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

738 delta = ImageChops.lighter( 

739 ImageChops.lighter(r, g), 

740 ImageChops.lighter(b, a), 

741 ) 

742 # Map zero pixels (palette indices, for "P") 

743 # to a "1" mask without consulting the palette. 

744 unchanged = delta.point([255, *(0,) * 255], "1") 

745 diff_frame = im_frame.copy() 

746 diff_frame.paste(encoderinfo["transparency"], mask=unchanged) 

747 else: 

748 bbox = None 

749 previous_im = im_frame 

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

751 

752 if len(im_frames) == 1: 

753 if "duration" in im_frames[0].encoderinfo: 

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

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

756 return False 

757 

758 for frame_data in im_frames: 

759 im_frame = frame_data.im 

760 if not frame_data.bbox: 

761 # global header 

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

763 fp.write(s) 

764 offset = (0, 0) 

765 else: 

766 # compress difference 

767 if not palette: 

768 frame_data.encoderinfo["include_color_table"] = True 

769 

770 if frame_data.bbox != (0, 0, *im_frame.size): 

771 im_frame = im_frame.crop(frame_data.bbox) 

772 offset = frame_data.bbox[:2] 

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

774 return True 

775 

776 

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

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

779 

780 

781def _save( 

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

783) -> None: 

784 # header 

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

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

787 else: 

788 palette = None 

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

790 

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

792 _write_single_frame(im, fp, palette) 

793 

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

795 

796 if hasattr(fp, "flush"): 

797 fp.flush() 

798 

799 

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

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

802 

803 # workaround for @PIL153 

804 if min(im.size) < 16: 

805 interlace = 0 

806 

807 return interlace 

808 

809 

810def _write_local_header( 

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

812) -> None: 

813 try: 

814 transparency = im.encoderinfo["transparency"] 

815 except KeyError: 

816 transparency = None 

817 

818 if "duration" in im.encoderinfo: 

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

820 else: 

821 duration = 0 

822 

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

824 

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

826 packed_flag = 1 if transparency is not None else 0 

827 packed_flag |= disposal << 2 

828 

829 fp.write( 

830 b"!" 

831 + o8(249) # extension intro 

832 + o8(4) # length 

833 + o8(packed_flag) # packed fields 

834 + o16(duration) # duration 

835 + o8(transparency or 0) # transparency index 

836 + o8(0) 

837 ) 

838 

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

840 if include_color_table: 

841 palette_bytes = _get_palette_bytes(im) 

842 color_table_size = _get_color_table_size(palette_bytes) 

843 if color_table_size: 

844 flags = flags | 128 # local color table flag 

845 flags = flags | color_table_size 

846 

847 fp.write( 

848 b"," 

849 + o16(offset[0]) # offset 

850 + o16(offset[1]) 

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

852 + o16(im.size[1]) 

853 + o8(flags) # flags 

854 ) 

855 if include_color_table and color_table_size: 

856 fp.write(_get_header_palette(palette_bytes)) 

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

858 

859 

860# Force optimization so that we can test performance against 

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

862_FORCE_OPTIMIZE = False 

863 

864 

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

866 """ 

867 Palette optimization is a potentially expensive operation. 

868 

869 This function determines if the palette should be optimized using 

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

871 

872 :param im: Image object 

873 :param info: encoderinfo 

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

875 """ 

876 if ( 

877 im.mode in {"L", "P"} 

878 and info 

879 and info.get("optimize") 

880 and im.width != 0 

881 and im.height != 0 

882 ): 

883 # Potentially expensive operation. 

884 

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

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

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

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

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

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

891 

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

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

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

895 # check which colors are used 

896 used_palette_colors = [] 

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

898 if count: 

899 used_palette_colors.append(i) 

900 

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

902 return used_palette_colors 

903 

904 assert im.palette is not None 

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 if not im.palette: 

955 return b"" 

956 

957 palette = bytes(im.palette.palette) 

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

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

960 return palette 

961 

962 

963def _get_background( 

964 im: Image.Image, 

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

966) -> int: 

967 background = 0 

968 if info_background: 

969 if isinstance(info_background, tuple): 

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

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

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

973 assert im.palette is not None 

974 try: 

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

976 except ValueError as e: 

977 if str(e) not in ( 

978 # If all 256 colors are in use, 

979 # then there is no need for the background color 

980 "cannot allocate more than 256 colors", 

981 # Ignore non-opaque WebP background 

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

983 ): 

984 raise 

985 else: 

986 background = info_background 

987 return background 

988 

989 

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

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

992 

993 # Header Block 

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

995 

996 version = b"87a" 

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

998 info 

999 and ( 

1000 "transparency" in info 

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

1002 or info.get("duration") 

1003 or info.get("comment") 

1004 ) 

1005 ): 

1006 version = b"89a" 

1007 

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

1009 

1010 palette_bytes = _get_palette_bytes(im) 

1011 color_table_size = _get_color_table_size(palette_bytes) 

1012 

1013 header = [ 

1014 b"GIF" # signature 

1015 + version # version 

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

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

1018 # Logical Screen Descriptor 

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

1020 o8(color_table_size + 128), # packed fields 

1021 # background + reserved/aspect 

1022 o8(background) + o8(0), 

1023 # Global Color Table 

1024 _get_header_palette(palette_bytes), 

1025 ] 

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

1027 header.append( 

1028 b"!" 

1029 + o8(255) # extension intro 

1030 + o8(11) 

1031 + b"NETSCAPE2.0" 

1032 + o8(3) 

1033 + o8(1) 

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

1035 + o8(0) 

1036 ) 

1037 if info.get("comment"): 

1038 header.append(b"!" + o8(254)) # extension intro 

1039 

1040 comment = info["comment"] 

1041 if isinstance(comment, str): 

1042 comment = comment.encode() 

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

1044 subblock = comment[i : i + 255] 

1045 header.append(o8(len(subblock)) + subblock) 

1046 

1047 header.append(o8(0)) 

1048 return header 

1049 

1050 

1051def _write_frame_data( 

1052 fp: IO[bytes], 

1053 im_frame: Image.Image, 

1054 offset: tuple[int, int], 

1055 params: dict[str, Any], 

1056) -> None: 

1057 try: 

1058 im_frame.encoderinfo = params 

1059 

1060 # local image header 

1061 _write_local_header(fp, im_frame, offset, 0) 

1062 

1063 ImageFile._save( 

1064 im_frame, 

1065 fp, 

1066 [ImageFile._Tile("gif", (0, 0, *im_frame.size), 0, RAWMODE[im_frame.mode])], 

1067 ) 

1068 

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

1070 finally: 

1071 del im_frame.encoderinfo 

1072 

1073 

1074# -------------------------------------------------------------------- 

1075# Legacy GIF utilities 

1076 

1077 

1078def getheader( 

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

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

1081 """ 

1082 Legacy Method to get Gif data from image. 

1083 

1084 Warning:: May modify image data. 

1085 

1086 :param im: Image object 

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

1088 :param info: encoderinfo 

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

1090 

1091 """ 

1092 if info is None: 

1093 info = {} 

1094 

1095 used_palette_colors = _get_optimize(im, info) 

1096 

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

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

1099 

1100 im_mod = _normalize_palette(im, palette, info) 

1101 im.palette = im_mod.palette 

1102 im.im = im_mod.im 

1103 header = _get_global_header(im, info) 

1104 

1105 return header, used_palette_colors 

1106 

1107 

1108def getdata( 

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

1110) -> list[bytes]: 

1111 """ 

1112 Legacy Method 

1113 

1114 Return a list of strings representing this image. 

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

1116 encoded image data. 

1117 

1118 To specify duration, add the time in milliseconds, 

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

1120 

1121 :param im: Image object 

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

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

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

1125 

1126 """ 

1127 from io import BytesIO 

1128 

1129 class Collector(BytesIO): 

1130 data = [] 

1131 

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

1133 self.data.append(data) 

1134 return len(data) 

1135 

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

1137 

1138 fp = Collector() 

1139 

1140 _write_frame_data(fp, im, offset, params) 

1141 

1142 return fp.data 

1143 

1144 

1145# -------------------------------------------------------------------- 

1146# Registry 

1147 

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

1149Image.register_save(GifImageFile.format, _save) 

1150Image.register_save_all(GifImageFile.format, _save_all) 

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

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