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

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

670 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 Any, NamedTuple, cast 

35 

36from . import ( 

37 Image, 

38 ImageChops, 

39 ImageFile, 

40 ImageMath, 

41 ImageOps, 

42 ImagePalette, 

43 ImageSequence, 

44) 

45from ._binary import i16le as i16 

46from ._binary import o8 

47from ._binary import o16le as o16 

48from ._util import DeferredError 

49 

50TYPE_CHECKING = False 

51if TYPE_CHECKING: 

52 from typing import IO, Literal 

53 

54 from . import _imaging 

55 from ._typing import Buffer 

56 

57 

58class LoadingStrategy(IntEnum): 

59 """.. versionadded:: 9.1.0""" 

60 

61 RGB_AFTER_FIRST = 0 

62 RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 

63 RGB_ALWAYS = 2 

64 

65 

66#: .. versionadded:: 9.1.0 

67LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST 

68 

69# -------------------------------------------------------------------- 

70# Identify/read GIF files 

71 

72 

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

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

75 

76 

77## 

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

79# GIF89 images. 

80 

81 

82class GifImageFile(ImageFile.ImageFile): 

83 format = "GIF" 

84 format_description = "Compuserve GIF" 

85 _close_exclusive_fp_after_loading = False 

86 

87 global_palette = None 

88 

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

90 assert self.fp is not None 

91 s = self.fp.read(1) 

92 if s and s[0]: 

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

94 return None 

95 

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

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

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

99 return True 

100 return False 

101 

102 def _open(self) -> None: 

103 # Screen 

104 assert self.fp is not None 

105 s = self.fp.read(13) 

106 if not _accept(s): 

107 msg = "not a GIF file" 

108 raise SyntaxError(msg) 

109 

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

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

112 flags = s[10] 

113 bits = (flags & 7) + 1 

114 

115 if flags & 128: 

116 # get global palette 

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

118 # check if palette contains colour indices 

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

120 if self._is_palette_needed(p): 

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

122 self.global_palette = self.palette = palette 

123 

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

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

126 self._n_frames: int | None = None 

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

128 

129 @property 

130 def n_frames(self) -> int: 

131 if self._n_frames is None: 

132 current = self.tell() 

133 try: 

134 while True: 

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

136 except EOFError: 

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

138 self.seek(current) 

139 return self._n_frames 

140 

141 @cached_property 

142 def is_animated(self) -> bool: 

143 if self._n_frames is not None: 

144 return self._n_frames != 1 

145 

146 current = self.tell() 

147 if current: 

148 return True 

149 

150 try: 

151 self._seek(1, False) 

152 is_animated = True 

153 except EOFError: 

154 is_animated = False 

155 

156 self.seek(current) 

157 return is_animated 

158 

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

160 if not self._seek_check(frame): 

161 return 

162 if frame < self.__frame: 

163 self._im = None 

164 self._seek(0) 

165 

166 last_frame = self.__frame 

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

168 try: 

169 self._seek(f) 

170 except EOFError as e: 

171 self.seek(last_frame) 

172 msg = "no more images in GIF file" 

173 raise EOFError(msg) from e 

174 

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

176 if isinstance(self._fp, DeferredError): 

177 raise self._fp.ex 

178 if frame == 0: 

179 # rewind 

180 self.__offset = 0 

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

182 self.__frame = -1 

183 self._fp.seek(self.__rewind) 

184 self.disposal_method = 0 

185 if "comment" in self.info: 

186 del self.info["comment"] 

187 else: 

188 # ensure that the previous frame was loaded 

189 if self.tile and update_image: 

190 self.load() 

191 

192 if frame != self.__frame + 1: 

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

194 raise ValueError(msg) 

195 

196 self.fp = self._fp 

197 if self.__offset: 

198 # backup to last frame 

199 self.fp.seek(self.__offset) 

200 while self.data(): 

201 pass 

202 self.__offset = 0 

203 

204 s = self.fp.read(1) 

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

206 msg = "no more images in GIF file" 

207 raise EOFError(msg) 

208 

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

210 

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

212 frame_transparency = None 

213 interlace = None 

214 frame_dispose_extent = None 

215 while True: 

216 if not s: 

217 s = self.fp.read(1) 

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

219 break 

220 

221 elif s == b"!": 

222 # 

223 # extensions 

224 # 

225 s = self.fp.read(1) 

226 block = self.data() 

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

228 # 

229 # graphic control extension 

230 # 

231 flags = block[0] 

232 if flags & 1: 

233 frame_transparency = block[3] 

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

235 

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

237 dispose_bits = 0b00011100 & flags 

238 dispose_bits = dispose_bits >> 2 

239 if dispose_bits: 

240 # only set the dispose if it is not 

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

242 # correct, but it seems to prevent the last 

243 # frame from looking odd for some animations 

244 self.disposal_method = dispose_bits 

245 elif s[0] == 254: 

246 # 

247 # comment extension 

248 # 

249 comment = b"" 

250 

251 # Read this comment block 

252 while block: 

253 comment += block 

254 block = self.data() 

255 

256 if "comment" in info: 

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

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

259 else: 

260 info["comment"] = comment 

261 s = b"" 

262 continue 

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

264 # 

265 # application extension 

266 # 

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

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

269 block = self.data() 

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

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

272 while self.data(): 

273 pass 

274 

275 elif s == b",": 

276 # 

277 # local image 

278 # 

279 s = self.fp.read(9) 

280 

281 # extent 

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

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

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

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

286 Image._decompression_bomb_check(self._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 if self._frame_palette: 

323 if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: 

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

325 else: 

326 self._mode = "P" 

327 else: 

328 self._mode = "L" 

329 

330 if palette: 

331 self.palette = palette 

332 elif self.global_palette: 

333 from copy import copy 

334 

335 self.palette = copy(self.global_palette) 

336 else: 

337 self.palette = None 

338 else: 

339 if self.mode == "P": 

340 if ( 

341 LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY 

342 or palette 

343 ): 

344 if "transparency" in self.info: 

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

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

347 self._mode = "RGBA" 

348 del self.info["transparency"] 

349 else: 

350 self._mode = "RGB" 

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

352 

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

354 if self._frame_palette: 

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

356 color = 0 

357 return cast( 

358 tuple[int, int, int], 

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

360 ) 

361 else: 

362 return (color, color, color) 

363 

364 self.dispose = None 

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

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

367 try: 

368 if self.disposal_method == 2: 

369 # replace with background colour 

370 

371 # only dispose the extent in this frame 

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

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

374 

375 Image._decompression_bomb_check(dispose_size) 

376 

377 # by convention, attempt to use transparency first 

378 dispose_mode = "P" 

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

380 if color is not None: 

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

382 dispose_mode = "RGBA" 

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

384 else: 

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

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

387 dispose_mode = "RGB" 

388 color = _rgb(color) 

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

390 else: 

391 # replace with previous contents 

392 if self._im is not None: 

393 # only dispose the extent in this frame 

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

395 elif frame_transparency is not None: 

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

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

398 

399 Image._decompression_bomb_check(dispose_size) 

400 dispose_mode = "P" 

401 color = frame_transparency 

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

403 dispose_mode = "RGBA" 

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

405 self.dispose = Image.core.fill( 

406 dispose_mode, dispose_size, color 

407 ) 

408 except AttributeError: 

409 pass 

410 

411 if interlace is not None: 

412 transparency = -1 

413 if frame_transparency is not None: 

414 if frame == 0: 

415 if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: 

416 self.info["transparency"] = frame_transparency 

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

418 transparency = frame_transparency 

419 self.tile = [ 

420 ImageFile._Tile( 

421 "gif", 

422 (x0, y0, x1, y1), 

423 self.__offset, 

424 (bits, interlace, transparency), 

425 ) 

426 ] 

427 

428 if info.get("comment"): 

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

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

431 if k in info: 

432 self.info[k] = info[k] 

433 elif k in self.info: 

434 del self.info[k] 

435 

436 def load_prepare(self) -> None: 

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

438 self._prev_im = None 

439 if self.__frame == 0: 

440 if self._frame_transparency is not None: 

441 self.im = Image.core.fill( 

442 temp_mode, self.size, self._frame_transparency 

443 ) 

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

445 self._prev_im = self.im 

446 if self._frame_palette: 

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

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

449 else: 

450 self._im = None 

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

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

453 if self._frame_palette: 

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

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

456 

457 self.im = expanded_im 

458 self._mode = temp_mode 

459 self._frame_palette = None 

460 

461 super().load_prepare() 

462 

463 def load_end(self) -> None: 

464 if self.__frame == 0: 

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

466 if self._frame_transparency is not None: 

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

468 self._mode = "RGBA" 

469 else: 

470 self._mode = "RGB" 

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

472 return 

473 if not self._prev_im: 

474 return 

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

476 if self._frame_transparency is not None: 

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

478 else: 

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

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

481 expanded_im = expanded_im.convert("RGB") 

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

483 

484 self._prev_im = expanded_im 

485 assert self._prev_im is not None 

486 if self._frame_transparency is not None: 

487 if self.mode == "L": 

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

489 else: 

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

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

492 else: 

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

494 

495 assert self.dispose_extent is not None 

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

497 

498 self.im = self._prev_im 

499 self._mode = self.im.mode 

500 if frame_im.mode in ("LA", "RGBA"): 

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

502 else: 

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

504 

505 def tell(self) -> int: 

506 return self.__frame 

507 

508 

509# -------------------------------------------------------------------- 

510# Write GIF files 

511 

512 

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

514 

515 

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

517 """ 

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

519 for saving in a Gif. 

520 

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

522 palette or 'L' mode. 

523 

524 :param im: Image object 

525 :returns: Image object 

526 """ 

527 if im.mode in RAWMODE: 

528 im.load() 

529 return im 

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

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

532 assert im.palette is not None 

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

534 for rgba in im.palette.colors: 

535 if rgba[3] == 0: 

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

537 break 

538 return im 

539 return im.convert("L") 

540 

541 

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

543 

544 

545def _normalize_palette( 

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

547) -> Image.Image: 

548 """ 

549 Normalizes the palette for image. 

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

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

552 - Optimizes the palette if necessary/desired. 

553 

554 :param im: Image object 

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

556 :param info: encoderinfo 

557 :returns: Image object 

558 """ 

559 source_palette = None 

560 if palette: 

561 # a bytes palette 

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

563 source_palette = bytearray(palette[:768]) 

564 if isinstance(palette, ImagePalette.ImagePalette): 

565 source_palette = bytearray(palette.palette) 

566 

567 if im.mode == "P": 

568 if not source_palette: 

569 im_palette = im.getpalette(None) 

570 assert im_palette is not None 

571 source_palette = bytearray(im_palette) 

572 else: # L-mode 

573 if not source_palette: 

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

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

576 assert source_palette is not None 

577 

578 if palette: 

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

580 assert im.palette is not None 

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

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

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

584 if index in used_palette_colors: 

585 index = None 

586 used_palette_colors.append(index) 

587 for i, index in enumerate(used_palette_colors): 

588 if index is None: 

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

590 if j not in used_palette_colors: 

591 used_palette_colors[i] = j 

592 break 

593 dest_map: list[int] = [] 

594 for index in used_palette_colors: 

595 assert index is not None 

596 dest_map.append(index) 

597 im = im.remap_palette(dest_map) 

598 else: 

599 optimized_palette_colors = _get_optimize(im, info) 

600 if optimized_palette_colors is not None: 

601 im = im.remap_palette(optimized_palette_colors, source_palette) 

602 if "transparency" in info: 

603 try: 

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

605 info["transparency"] 

606 ) 

607 except ValueError: 

608 del info["transparency"] 

609 return im 

610 

611 assert im.palette is not None 

612 im.palette.palette = source_palette 

613 return im 

614 

615 

616def _write_single_frame( 

617 im: Image.Image, 

618 fp: IO[bytes], 

619 palette: _Palette | None, 

620) -> None: 

621 im_out = _normalize_mode(im) 

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

623 if isinstance(k, str): 

624 im.encoderinfo.setdefault(k, v) 

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

626 

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

628 fp.write(s) 

629 

630 # local image header 

631 flags = 0 

632 if get_interlace(im): 

633 flags = flags | 64 

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

635 

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

637 ImageFile._save( 

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

639 ) 

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", im.info.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"] += encoderinfo["duration"] 

704 continue 

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

706 # To appear correctly in viewers using a convention, 

707 # only consider transparency, and not background color 

708 color = im.encoderinfo.get( 

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

710 ) 

711 if color is not None: 

712 if background_im is None: 

713 background = _get_background(im_frame, color) 

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

715 first_palette = im_frames[0].im.palette 

716 assert first_palette is not None 

717 background_im.putpalette(first_palette, first_palette.mode) 

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

719 else: 

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

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

722 if "transparency" not in encoderinfo: 

723 assert im_frame.palette is not None 

724 try: 

725 encoderinfo["transparency"] = ( 

726 im_frame.palette._new_color_index(im_frame) 

727 ) 

728 except ValueError: 

729 pass 

730 if "transparency" in encoderinfo: 

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

732 diff_frame = im_frame.copy() 

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

734 if delta.mode == "RGBA": 

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

736 mask = ImageMath.lambda_eval( 

737 lambda args: args["convert"]( 

738 args["max"]( 

739 args["max"]( 

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

741 ), 

742 args["a"], 

743 ) 

744 * 255, 

745 "1", 

746 ), 

747 r=r, 

748 g=g, 

749 b=b, 

750 a=a, 

751 ) 

752 else: 

753 if delta.mode == "P": 

754 # Convert to L without considering palette 

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

756 delta_l.putdata(delta.get_flattened_data()) 

757 delta = delta_l 

758 mask = ImageMath.lambda_eval( 

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

760 im=delta, 

761 ) 

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

763 else: 

764 bbox = None 

765 previous_im = im_frame 

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

767 

768 if len(im_frames) == 1: 

769 if "duration" in im.encoderinfo: 

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

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

772 return False 

773 

774 for frame_data in im_frames: 

775 im_frame = frame_data.im 

776 if not frame_data.bbox: 

777 # global header 

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

779 fp.write(s) 

780 offset = (0, 0) 

781 else: 

782 # compress difference 

783 if not palette: 

784 frame_data.encoderinfo["include_color_table"] = True 

785 

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

787 im_frame = im_frame.crop(frame_data.bbox) 

788 offset = frame_data.bbox[:2] 

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

790 return True 

791 

792 

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

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

795 

796 

797def _save( 

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

799) -> None: 

800 # header 

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

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

803 else: 

804 palette = None 

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

806 

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

808 _write_single_frame(im, fp, palette) 

809 

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

811 

812 if hasattr(fp, "flush"): 

813 fp.flush() 

814 

815 

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

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

818 

819 # workaround for @PIL153 

820 if min(im.size) < 16: 

821 interlace = 0 

822 

823 return interlace 

824 

825 

826def _write_local_header( 

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

828) -> None: 

829 try: 

830 transparency = im.encoderinfo["transparency"] 

831 except KeyError: 

832 transparency = None 

833 

834 if "duration" in im.encoderinfo: 

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

836 else: 

837 duration = 0 

838 

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

840 

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

842 packed_flag = 1 if transparency is not None else 0 

843 packed_flag |= disposal << 2 

844 

845 fp.write( 

846 b"!" 

847 + o8(249) # extension intro 

848 + o8(4) # length 

849 + o8(packed_flag) # packed fields 

850 + o16(duration) # duration 

851 + o8(transparency or 0) # transparency index 

852 + o8(0) 

853 ) 

854 

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

856 if include_color_table: 

857 palette_bytes = _get_palette_bytes(im) 

858 color_table_size = _get_color_table_size(palette_bytes) 

859 if color_table_size: 

860 flags = flags | 128 # local color table flag 

861 flags = flags | color_table_size 

862 

863 fp.write( 

864 b"," 

865 + o16(offset[0]) # offset 

866 + o16(offset[1]) 

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

868 + o16(im.size[1]) 

869 + o8(flags) # flags 

870 ) 

871 if include_color_table and color_table_size: 

872 fp.write(_get_header_palette(palette_bytes)) 

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

874 

875 

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

877 # Unused by default. 

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

879 # 

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

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

882 # below for information on how to enable this. 

883 tempfile = im._dump() 

884 

885 try: 

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

887 if im.mode != "RGB": 

888 subprocess.check_call( 

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

890 ) 

891 else: 

892 # Pipe ppmquant output into ppmtogif 

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

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

895 togif_cmd = ["ppmtogif"] 

896 quant_proc = subprocess.Popen( 

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

898 ) 

899 togif_proc = subprocess.Popen( 

900 togif_cmd, 

901 stdin=quant_proc.stdout, 

902 stdout=f, 

903 stderr=subprocess.DEVNULL, 

904 ) 

905 

906 # Allow ppmquant to receive SIGPIPE if ppmtogif exits 

907 assert quant_proc.stdout is not None 

908 quant_proc.stdout.close() 

909 

910 retcode = quant_proc.wait() 

911 if retcode: 

912 raise subprocess.CalledProcessError(retcode, quant_cmd) 

913 

914 retcode = togif_proc.wait() 

915 if retcode: 

916 raise subprocess.CalledProcessError(retcode, togif_cmd) 

917 finally: 

918 try: 

919 os.unlink(tempfile) 

920 except OSError: 

921 pass 

922 

923 

924# Force optimization so that we can test performance against 

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

926_FORCE_OPTIMIZE = False 

927 

928 

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

930 """ 

931 Palette optimization is a potentially expensive operation. 

932 

933 This function determines if the palette should be optimized using 

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

935 

936 :param im: Image object 

937 :param info: encoderinfo 

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

939 """ 

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

941 # Potentially expensive operation. 

942 

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

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

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

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

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

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

949 

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

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

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

953 # check which colors are used 

954 used_palette_colors = [] 

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

956 if count: 

957 used_palette_colors.append(i) 

958 

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

960 return used_palette_colors 

961 

962 assert im.palette is not None 

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

964 im.palette.mode 

965 ) 

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

967 if ( 

968 # check that the palette would become smaller when saved 

969 len(used_palette_colors) <= current_palette_size // 2 

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

971 and current_palette_size > 2 

972 ): 

973 return used_palette_colors 

974 return None 

975 

976 

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

978 # calculate the palette size for the header 

979 if not palette_bytes: 

980 return 0 

981 elif len(palette_bytes) < 9: 

982 return 1 

983 else: 

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

985 

986 

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

988 """ 

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

990 suitable for direct inclusion in the GIF header 

991 

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

993 :returns: Null padded palette 

994 """ 

995 color_table_size = _get_color_table_size(palette_bytes) 

996 

997 # add the missing amount of bytes 

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

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

1000 if actual_target_size_diff > 0: 

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

1002 return palette_bytes 

1003 

1004 

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

1006 """ 

1007 Gets the palette for inclusion in the gif header 

1008 

1009 :param im: Image object 

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

1011 """ 

1012 if not im.palette: 

1013 return b"" 

1014 

1015 palette = bytes(im.palette.palette) 

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

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

1018 return palette 

1019 

1020 

1021def _get_background( 

1022 im: Image.Image, 

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

1024) -> int: 

1025 background = 0 

1026 if info_background: 

1027 if isinstance(info_background, tuple): 

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

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

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

1031 assert im.palette is not None 

1032 try: 

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

1034 except ValueError as e: 

1035 if str(e) not in ( 

1036 # If all 256 colors are in use, 

1037 # then there is no need for the background color 

1038 "cannot allocate more than 256 colors", 

1039 # Ignore non-opaque WebP background 

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

1041 ): 

1042 raise 

1043 else: 

1044 background = info_background 

1045 return background 

1046 

1047 

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

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

1050 

1051 # Header Block 

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

1053 

1054 version = b"87a" 

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

1056 info 

1057 and ( 

1058 "transparency" in info 

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

1060 or info.get("duration") 

1061 or info.get("comment") 

1062 ) 

1063 ): 

1064 version = b"89a" 

1065 

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

1067 

1068 palette_bytes = _get_palette_bytes(im) 

1069 color_table_size = _get_color_table_size(palette_bytes) 

1070 

1071 header = [ 

1072 b"GIF" # signature 

1073 + version # version 

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

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

1076 # Logical Screen Descriptor 

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

1078 o8(color_table_size + 128), # packed fields 

1079 # background + reserved/aspect 

1080 o8(background) + o8(0), 

1081 # Global Color Table 

1082 _get_header_palette(palette_bytes), 

1083 ] 

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

1085 header.append( 

1086 b"!" 

1087 + o8(255) # extension intro 

1088 + o8(11) 

1089 + b"NETSCAPE2.0" 

1090 + o8(3) 

1091 + o8(1) 

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

1093 + o8(0) 

1094 ) 

1095 if info.get("comment"): 

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

1097 

1098 comment = info["comment"] 

1099 if isinstance(comment, str): 

1100 comment = comment.encode() 

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

1102 subblock = comment[i : i + 255] 

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

1104 

1105 comment_block += o8(0) 

1106 header.append(comment_block) 

1107 return header 

1108 

1109 

1110def _write_frame_data( 

1111 fp: IO[bytes], 

1112 im_frame: Image.Image, 

1113 offset: tuple[int, int], 

1114 params: dict[str, Any], 

1115) -> None: 

1116 try: 

1117 im_frame.encoderinfo = params 

1118 

1119 # local image header 

1120 _write_local_header(fp, im_frame, offset, 0) 

1121 

1122 ImageFile._save( 

1123 im_frame, 

1124 fp, 

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

1126 ) 

1127 

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

1129 finally: 

1130 del im_frame.encoderinfo 

1131 

1132 

1133# -------------------------------------------------------------------- 

1134# Legacy GIF utilities 

1135 

1136 

1137def getheader( 

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

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

1140 """ 

1141 Legacy Method to get Gif data from image. 

1142 

1143 Warning:: May modify image data. 

1144 

1145 :param im: Image object 

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

1147 :param info: encoderinfo 

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

1149 

1150 """ 

1151 if info is None: 

1152 info = {} 

1153 

1154 used_palette_colors = _get_optimize(im, info) 

1155 

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

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

1158 

1159 im_mod = _normalize_palette(im, palette, info) 

1160 im.palette = im_mod.palette 

1161 im.im = im_mod.im 

1162 header = _get_global_header(im, info) 

1163 

1164 return header, used_palette_colors 

1165 

1166 

1167def getdata( 

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

1169) -> list[bytes]: 

1170 """ 

1171 Legacy Method 

1172 

1173 Return a list of strings representing this image. 

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

1175 encoded image data. 

1176 

1177 To specify duration, add the time in milliseconds, 

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

1179 

1180 :param im: Image object 

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

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

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

1184 

1185 """ 

1186 from io import BytesIO 

1187 

1188 class Collector(BytesIO): 

1189 data = [] 

1190 

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

1192 self.data.append(data) 

1193 return len(data) 

1194 

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

1196 

1197 fp = Collector() 

1198 

1199 _write_frame_data(fp, im, offset, params) 

1200 

1201 return fp.data 

1202 

1203 

1204# -------------------------------------------------------------------- 

1205# Registry 

1206 

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

1208Image.register_save(GifImageFile.format, _save) 

1209Image.register_save_all(GifImageFile.format, _save_all) 

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

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

1212 

1213# 

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

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

1216 

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