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

668 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 s = self.fp.read(1) 

91 if s and s[0]: 

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

93 return None 

94 

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

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

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

98 return True 

99 return False 

100 

101 def _open(self) -> None: 

102 # Screen 

103 s = self.fp.read(13) 

104 if not _accept(s): 

105 msg = "not a GIF file" 

106 raise SyntaxError(msg) 

107 

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

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

110 flags = s[10] 

111 bits = (flags & 7) + 1 

112 

113 if flags & 128: 

114 # get global palette 

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

116 # check if palette contains colour indices 

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

118 if self._is_palette_needed(p): 

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

120 self.global_palette = self.palette = p 

121 

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

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

124 self._n_frames: int | None = None 

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

126 

127 @property 

128 def n_frames(self) -> int: 

129 if self._n_frames is None: 

130 current = self.tell() 

131 try: 

132 while True: 

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

134 except EOFError: 

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

136 self.seek(current) 

137 return self._n_frames 

138 

139 @cached_property 

140 def is_animated(self) -> bool: 

141 if self._n_frames is not None: 

142 return self._n_frames != 1 

143 

144 current = self.tell() 

145 if current: 

146 return True 

147 

148 try: 

149 self._seek(1, False) 

150 is_animated = True 

151 except EOFError: 

152 is_animated = False 

153 

154 self.seek(current) 

155 return is_animated 

156 

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

158 if not self._seek_check(frame): 

159 return 

160 if frame < self.__frame: 

161 self._im = None 

162 self._seek(0) 

163 

164 last_frame = self.__frame 

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

166 try: 

167 self._seek(f) 

168 except EOFError as e: 

169 self.seek(last_frame) 

170 msg = "no more images in GIF file" 

171 raise EOFError(msg) from e 

172 

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

174 if isinstance(self._fp, DeferredError): 

175 raise self._fp.ex 

176 if frame == 0: 

177 # rewind 

178 self.__offset = 0 

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

180 self.__frame = -1 

181 self._fp.seek(self.__rewind) 

182 self.disposal_method = 0 

183 if "comment" in self.info: 

184 del self.info["comment"] 

185 else: 

186 # ensure that the previous frame was loaded 

187 if self.tile and update_image: 

188 self.load() 

189 

190 if frame != self.__frame + 1: 

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

192 raise ValueError(msg) 

193 

194 self.fp = self._fp 

195 if self.__offset: 

196 # backup to last frame 

197 self.fp.seek(self.__offset) 

198 while self.data(): 

199 pass 

200 self.__offset = 0 

201 

202 s = self.fp.read(1) 

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

204 msg = "no more images in GIF file" 

205 raise EOFError(msg) 

206 

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

208 

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

210 frame_transparency = None 

211 interlace = None 

212 frame_dispose_extent = None 

213 while True: 

214 if not s: 

215 s = self.fp.read(1) 

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

217 break 

218 

219 elif s == b"!": 

220 # 

221 # extensions 

222 # 

223 s = self.fp.read(1) 

224 block = self.data() 

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

226 # 

227 # graphic control extension 

228 # 

229 flags = block[0] 

230 if flags & 1: 

231 frame_transparency = block[3] 

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

233 

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

235 dispose_bits = 0b00011100 & flags 

236 dispose_bits = dispose_bits >> 2 

237 if dispose_bits: 

238 # only set the dispose if it is not 

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

240 # correct, but it seems to prevent the last 

241 # frame from looking odd for some animations 

242 self.disposal_method = dispose_bits 

243 elif s[0] == 254: 

244 # 

245 # comment extension 

246 # 

247 comment = b"" 

248 

249 # Read this comment block 

250 while block: 

251 comment += block 

252 block = self.data() 

253 

254 if "comment" in info: 

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

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

257 else: 

258 info["comment"] = comment 

259 s = None 

260 continue 

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

262 # 

263 # application extension 

264 # 

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

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

267 block = self.data() 

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

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

270 while self.data(): 

271 pass 

272 

273 elif s == b",": 

274 # 

275 # local image 

276 # 

277 s = self.fp.read(9) 

278 

279 # extent 

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

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

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

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

284 Image._decompression_bomb_check(self._size) 

285 frame_dispose_extent = x0, y0, x1, y1 

286 flags = s[8] 

287 

288 interlace = (flags & 64) != 0 

289 

290 if flags & 128: 

291 bits = (flags & 7) + 1 

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

293 if self._is_palette_needed(p): 

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

295 else: 

296 palette = False 

297 

298 # image data 

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

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

301 break 

302 s = None 

303 

304 if interlace is None: 

305 msg = "image not found in GIF frame" 

306 raise EOFError(msg) 

307 

308 self.__frame = frame 

309 if not update_image: 

310 return 

311 

312 self.tile = [] 

313 

314 if self.dispose: 

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

316 

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

318 self._frame_transparency = frame_transparency 

319 if frame == 0: 

320 if self._frame_palette: 

321 if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: 

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

323 else: 

324 self._mode = "P" 

325 else: 

326 self._mode = "L" 

327 

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

336 else: 

337 if self.mode == "P": 

338 if ( 

339 LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY 

340 or palette 

341 ): 

342 if "transparency" in self.info: 

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

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

345 self._mode = "RGBA" 

346 del self.info["transparency"] 

347 else: 

348 self._mode = "RGB" 

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

350 

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

352 if self._frame_palette: 

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

354 color = 0 

355 return cast( 

356 tuple[int, int, int], 

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

358 ) 

359 else: 

360 return (color, color, color) 

361 

362 self.dispose = None 

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

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

365 try: 

366 if self.disposal_method == 2: 

367 # replace with background colour 

368 

369 # only dispose the extent in this frame 

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

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

372 

373 Image._decompression_bomb_check(dispose_size) 

374 

375 # by convention, attempt to use transparency first 

376 dispose_mode = "P" 

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

378 if color is not None: 

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

380 dispose_mode = "RGBA" 

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

382 else: 

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

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

385 dispose_mode = "RGB" 

386 color = _rgb(color) 

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

388 else: 

389 # replace with previous contents 

390 if self._im is not None: 

391 # only dispose the extent in this frame 

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

393 elif frame_transparency is not None: 

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

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

396 

397 Image._decompression_bomb_check(dispose_size) 

398 dispose_mode = "P" 

399 color = frame_transparency 

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

401 dispose_mode = "RGBA" 

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

403 self.dispose = Image.core.fill( 

404 dispose_mode, dispose_size, color 

405 ) 

406 except AttributeError: 

407 pass 

408 

409 if interlace is not None: 

410 transparency = -1 

411 if frame_transparency is not None: 

412 if frame == 0: 

413 if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: 

414 self.info["transparency"] = frame_transparency 

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

416 transparency = frame_transparency 

417 self.tile = [ 

418 ImageFile._Tile( 

419 "gif", 

420 (x0, y0, x1, y1), 

421 self.__offset, 

422 (bits, interlace, transparency), 

423 ) 

424 ] 

425 

426 if info.get("comment"): 

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

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

429 if k in info: 

430 self.info[k] = info[k] 

431 elif k in self.info: 

432 del self.info[k] 

433 

434 def load_prepare(self) -> None: 

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

436 self._prev_im = None 

437 if self.__frame == 0: 

438 if self._frame_transparency is not None: 

439 self.im = Image.core.fill( 

440 temp_mode, self.size, self._frame_transparency 

441 ) 

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

443 self._prev_im = self.im 

444 if self._frame_palette: 

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

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

447 else: 

448 self._im = None 

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

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

451 if self._frame_palette: 

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

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

454 

455 self.im = expanded_im 

456 self._mode = temp_mode 

457 self._frame_palette = None 

458 

459 super().load_prepare() 

460 

461 def load_end(self) -> None: 

462 if self.__frame == 0: 

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

464 if self._frame_transparency is not None: 

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

466 self._mode = "RGBA" 

467 else: 

468 self._mode = "RGB" 

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

470 return 

471 if not self._prev_im: 

472 return 

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

474 if self._frame_transparency is not None: 

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

476 else: 

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

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

479 expanded_im = expanded_im.convert("RGB") 

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

481 

482 self._prev_im = expanded_im 

483 assert self._prev_im is not None 

484 if self._frame_transparency is not None: 

485 if self.mode == "L": 

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

487 else: 

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

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

490 else: 

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

492 

493 assert self.dispose_extent is not None 

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

495 

496 self.im = self._prev_im 

497 self._mode = self.im.mode 

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

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

500 else: 

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

502 

503 def tell(self) -> int: 

504 return self.__frame 

505 

506 

507# -------------------------------------------------------------------- 

508# Write GIF files 

509 

510 

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

512 

513 

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

515 """ 

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

517 for saving in a Gif. 

518 

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

520 palette or 'L' mode. 

521 

522 :param im: Image object 

523 :returns: Image object 

524 """ 

525 if im.mode in RAWMODE: 

526 im.load() 

527 return im 

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

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

530 assert im.palette is not None 

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

532 for rgba in im.palette.colors: 

533 if rgba[3] == 0: 

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

535 break 

536 return im 

537 return im.convert("L") 

538 

539 

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

541 

542 

543def _normalize_palette( 

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

545) -> Image.Image: 

546 """ 

547 Normalizes the palette for image. 

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

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

550 - Optimizes the palette if necessary/desired. 

551 

552 :param im: Image object 

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

554 :param info: encoderinfo 

555 :returns: Image object 

556 """ 

557 source_palette = None 

558 if palette: 

559 # a bytes palette 

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

561 source_palette = bytearray(palette[:768]) 

562 if isinstance(palette, ImagePalette.ImagePalette): 

563 source_palette = bytearray(palette.palette) 

564 

565 if im.mode == "P": 

566 if not source_palette: 

567 im_palette = im.getpalette(None) 

568 assert im_palette is not None 

569 source_palette = bytearray(im_palette) 

570 else: # L-mode 

571 if not source_palette: 

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

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

574 assert source_palette is not None 

575 

576 if palette: 

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

578 assert im.palette is not None 

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

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

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

582 if index in used_palette_colors: 

583 index = None 

584 used_palette_colors.append(index) 

585 for i, index in enumerate(used_palette_colors): 

586 if index is None: 

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

588 if j not in used_palette_colors: 

589 used_palette_colors[i] = j 

590 break 

591 dest_map: list[int] = [] 

592 for index in used_palette_colors: 

593 assert index is not None 

594 dest_map.append(index) 

595 im = im.remap_palette(dest_map) 

596 else: 

597 optimized_palette_colors = _get_optimize(im, info) 

598 if optimized_palette_colors is not None: 

599 im = im.remap_palette(optimized_palette_colors, source_palette) 

600 if "transparency" in info: 

601 try: 

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

603 info["transparency"] 

604 ) 

605 except ValueError: 

606 del info["transparency"] 

607 return im 

608 

609 assert im.palette is not None 

610 im.palette.palette = source_palette 

611 return im 

612 

613 

614def _write_single_frame( 

615 im: Image.Image, 

616 fp: IO[bytes], 

617 palette: _Palette | None, 

618) -> None: 

619 im_out = _normalize_mode(im) 

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

621 if isinstance(k, str): 

622 im.encoderinfo.setdefault(k, v) 

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

624 

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

626 fp.write(s) 

627 

628 # local image header 

629 flags = 0 

630 if get_interlace(im): 

631 flags = flags | 64 

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

633 

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

635 ImageFile._save( 

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

637 ) 

638 

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

640 

641 

642def _getbbox( 

643 base_im: Image.Image, im_frame: Image.Image 

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

645 palette_bytes = [ 

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

647 ] 

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

649 im_frame = im_frame.convert("RGBA") 

650 base_im = base_im.convert("RGBA") 

651 delta = ImageChops.subtract_modulo(im_frame, base_im) 

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

653 

654 

655class _Frame(NamedTuple): 

656 im: Image.Image 

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

658 encoderinfo: dict[str, Any] 

659 

660 

661def _write_multiple_frames( 

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

663) -> bool: 

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

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

666 

667 im_frames: list[_Frame] = [] 

668 previous_im: Image.Image | None = None 

669 frame_count = 0 

670 background_im = None 

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

672 for im_frame in ImageSequence.Iterator(imSequence): 

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

674 im_frame = _normalize_mode(im_frame.copy()) 

675 if frame_count == 0: 

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

677 if k == "transparency": 

678 continue 

679 if isinstance(k, str): 

680 im.encoderinfo.setdefault(k, v) 

681 

682 encoderinfo = im.encoderinfo.copy() 

683 if "transparency" in im_frame.info: 

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

685 im_frame = _normalize_palette(im_frame, palette, encoderinfo) 

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

687 encoderinfo["duration"] = duration[frame_count] 

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

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

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

691 encoderinfo["disposal"] = disposal[frame_count] 

692 frame_count += 1 

693 

694 diff_frame = None 

695 if im_frames and previous_im: 

696 # delta frame 

697 delta, bbox = _getbbox(previous_im, im_frame) 

698 if not bbox: 

699 # This frame is identical to the previous frame 

700 if encoderinfo.get("duration"): 

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

702 continue 

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

704 # To appear correctly in viewers using a convention, 

705 # only consider transparency, and not background color 

706 color = im.encoderinfo.get( 

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

708 ) 

709 if color is not None: 

710 if background_im is None: 

711 background = _get_background(im_frame, color) 

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

713 first_palette = im_frames[0].im.palette 

714 assert first_palette is not None 

715 background_im.putpalette(first_palette, first_palette.mode) 

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

717 else: 

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

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

720 if "transparency" not in encoderinfo: 

721 assert im_frame.palette is not None 

722 try: 

723 encoderinfo["transparency"] = ( 

724 im_frame.palette._new_color_index(im_frame) 

725 ) 

726 except ValueError: 

727 pass 

728 if "transparency" in encoderinfo: 

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

730 diff_frame = im_frame.copy() 

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

732 if delta.mode == "RGBA": 

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

734 mask = ImageMath.lambda_eval( 

735 lambda args: args["convert"]( 

736 args["max"]( 

737 args["max"]( 

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

739 ), 

740 args["a"], 

741 ) 

742 * 255, 

743 "1", 

744 ), 

745 r=r, 

746 g=g, 

747 b=b, 

748 a=a, 

749 ) 

750 else: 

751 if delta.mode == "P": 

752 # Convert to L without considering palette 

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

754 delta_l.putdata(delta.getdata()) 

755 delta = delta_l 

756 mask = ImageMath.lambda_eval( 

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

758 im=delta, 

759 ) 

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

761 else: 

762 bbox = None 

763 previous_im = im_frame 

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

765 

766 if len(im_frames) == 1: 

767 if "duration" in im.encoderinfo: 

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

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

770 return False 

771 

772 for frame_data in im_frames: 

773 im_frame = frame_data.im 

774 if not frame_data.bbox: 

775 # global header 

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

777 fp.write(s) 

778 offset = (0, 0) 

779 else: 

780 # compress difference 

781 if not palette: 

782 frame_data.encoderinfo["include_color_table"] = True 

783 

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

785 im_frame = im_frame.crop(frame_data.bbox) 

786 offset = frame_data.bbox[:2] 

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

788 return True 

789 

790 

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

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

793 

794 

795def _save( 

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

797) -> None: 

798 # header 

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

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

801 else: 

802 palette = None 

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

804 

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

806 _write_single_frame(im, fp, palette) 

807 

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

809 

810 if hasattr(fp, "flush"): 

811 fp.flush() 

812 

813 

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

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

816 

817 # workaround for @PIL153 

818 if min(im.size) < 16: 

819 interlace = 0 

820 

821 return interlace 

822 

823 

824def _write_local_header( 

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

826) -> None: 

827 try: 

828 transparency = im.encoderinfo["transparency"] 

829 except KeyError: 

830 transparency = None 

831 

832 if "duration" in im.encoderinfo: 

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

834 else: 

835 duration = 0 

836 

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

838 

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

840 packed_flag = 1 if transparency is not None else 0 

841 packed_flag |= disposal << 2 

842 

843 fp.write( 

844 b"!" 

845 + o8(249) # extension intro 

846 + o8(4) # length 

847 + o8(packed_flag) # packed fields 

848 + o16(duration) # duration 

849 + o8(transparency or 0) # transparency index 

850 + o8(0) 

851 ) 

852 

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

854 if include_color_table: 

855 palette_bytes = _get_palette_bytes(im) 

856 color_table_size = _get_color_table_size(palette_bytes) 

857 if color_table_size: 

858 flags = flags | 128 # local color table flag 

859 flags = flags | color_table_size 

860 

861 fp.write( 

862 b"," 

863 + o16(offset[0]) # offset 

864 + o16(offset[1]) 

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

866 + o16(im.size[1]) 

867 + o8(flags) # flags 

868 ) 

869 if include_color_table and color_table_size: 

870 fp.write(_get_header_palette(palette_bytes)) 

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

872 

873 

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

875 # Unused by default. 

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

877 # 

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

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

880 # below for information on how to enable this. 

881 tempfile = im._dump() 

882 

883 try: 

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

885 if im.mode != "RGB": 

886 subprocess.check_call( 

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

888 ) 

889 else: 

890 # Pipe ppmquant output into ppmtogif 

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

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

893 togif_cmd = ["ppmtogif"] 

894 quant_proc = subprocess.Popen( 

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

896 ) 

897 togif_proc = subprocess.Popen( 

898 togif_cmd, 

899 stdin=quant_proc.stdout, 

900 stdout=f, 

901 stderr=subprocess.DEVNULL, 

902 ) 

903 

904 # Allow ppmquant to receive SIGPIPE if ppmtogif exits 

905 assert quant_proc.stdout is not None 

906 quant_proc.stdout.close() 

907 

908 retcode = quant_proc.wait() 

909 if retcode: 

910 raise subprocess.CalledProcessError(retcode, quant_cmd) 

911 

912 retcode = togif_proc.wait() 

913 if retcode: 

914 raise subprocess.CalledProcessError(retcode, togif_cmd) 

915 finally: 

916 try: 

917 os.unlink(tempfile) 

918 except OSError: 

919 pass 

920 

921 

922# Force optimization so that we can test performance against 

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

924_FORCE_OPTIMIZE = False 

925 

926 

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

928 """ 

929 Palette optimization is a potentially expensive operation. 

930 

931 This function determines if the palette should be optimized using 

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

933 

934 :param im: Image object 

935 :param info: encoderinfo 

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

937 """ 

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

939 # Potentially expensive operation. 

940 

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

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

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

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

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

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

947 

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

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

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

951 # check which colors are used 

952 used_palette_colors = [] 

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

954 if count: 

955 used_palette_colors.append(i) 

956 

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

958 return used_palette_colors 

959 

960 assert im.palette is not None 

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

962 im.palette.mode 

963 ) 

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

965 if ( 

966 # check that the palette would become smaller when saved 

967 len(used_palette_colors) <= current_palette_size // 2 

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

969 and current_palette_size > 2 

970 ): 

971 return used_palette_colors 

972 return None 

973 

974 

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

976 # calculate the palette size for the header 

977 if not palette_bytes: 

978 return 0 

979 elif len(palette_bytes) < 9: 

980 return 1 

981 else: 

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

983 

984 

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

986 """ 

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

988 suitable for direct inclusion in the GIF header 

989 

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

991 :returns: Null padded palette 

992 """ 

993 color_table_size = _get_color_table_size(palette_bytes) 

994 

995 # add the missing amount of bytes 

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

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

998 if actual_target_size_diff > 0: 

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

1000 return palette_bytes 

1001 

1002 

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

1004 """ 

1005 Gets the palette for inclusion in the gif header 

1006 

1007 :param im: Image object 

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

1009 """ 

1010 if not im.palette: 

1011 return b"" 

1012 

1013 palette = bytes(im.palette.palette) 

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

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

1016 return palette 

1017 

1018 

1019def _get_background( 

1020 im: Image.Image, 

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

1022) -> int: 

1023 background = 0 

1024 if info_background: 

1025 if isinstance(info_background, tuple): 

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

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

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

1029 assert im.palette is not None 

1030 try: 

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

1032 except ValueError as e: 

1033 if str(e) not in ( 

1034 # If all 256 colors are in use, 

1035 # then there is no need for the background color 

1036 "cannot allocate more than 256 colors", 

1037 # Ignore non-opaque WebP background 

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

1039 ): 

1040 raise 

1041 else: 

1042 background = info_background 

1043 return background 

1044 

1045 

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

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

1048 

1049 # Header Block 

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

1051 

1052 version = b"87a" 

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

1054 info 

1055 and ( 

1056 "transparency" in info 

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

1058 or info.get("duration") 

1059 or info.get("comment") 

1060 ) 

1061 ): 

1062 version = b"89a" 

1063 

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

1065 

1066 palette_bytes = _get_palette_bytes(im) 

1067 color_table_size = _get_color_table_size(palette_bytes) 

1068 

1069 header = [ 

1070 b"GIF" # signature 

1071 + version # version 

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

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

1074 # Logical Screen Descriptor 

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

1076 o8(color_table_size + 128), # packed fields 

1077 # background + reserved/aspect 

1078 o8(background) + o8(0), 

1079 # Global Color Table 

1080 _get_header_palette(palette_bytes), 

1081 ] 

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

1083 header.append( 

1084 b"!" 

1085 + o8(255) # extension intro 

1086 + o8(11) 

1087 + b"NETSCAPE2.0" 

1088 + o8(3) 

1089 + o8(1) 

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

1091 + o8(0) 

1092 ) 

1093 if info.get("comment"): 

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

1095 

1096 comment = info["comment"] 

1097 if isinstance(comment, str): 

1098 comment = comment.encode() 

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

1100 subblock = comment[i : i + 255] 

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

1102 

1103 comment_block += o8(0) 

1104 header.append(comment_block) 

1105 return header 

1106 

1107 

1108def _write_frame_data( 

1109 fp: IO[bytes], 

1110 im_frame: Image.Image, 

1111 offset: tuple[int, int], 

1112 params: dict[str, Any], 

1113) -> None: 

1114 try: 

1115 im_frame.encoderinfo = params 

1116 

1117 # local image header 

1118 _write_local_header(fp, im_frame, offset, 0) 

1119 

1120 ImageFile._save( 

1121 im_frame, 

1122 fp, 

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

1124 ) 

1125 

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

1127 finally: 

1128 del im_frame.encoderinfo 

1129 

1130 

1131# -------------------------------------------------------------------- 

1132# Legacy GIF utilities 

1133 

1134 

1135def getheader( 

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

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

1138 """ 

1139 Legacy Method to get Gif data from image. 

1140 

1141 Warning:: May modify image data. 

1142 

1143 :param im: Image object 

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

1145 :param info: encoderinfo 

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

1147 

1148 """ 

1149 if info is None: 

1150 info = {} 

1151 

1152 used_palette_colors = _get_optimize(im, info) 

1153 

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

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

1156 

1157 im_mod = _normalize_palette(im, palette, info) 

1158 im.palette = im_mod.palette 

1159 im.im = im_mod.im 

1160 header = _get_global_header(im, info) 

1161 

1162 return header, used_palette_colors 

1163 

1164 

1165def getdata( 

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

1167) -> list[bytes]: 

1168 """ 

1169 Legacy Method 

1170 

1171 Return a list of strings representing this image. 

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

1173 encoded image data. 

1174 

1175 To specify duration, add the time in milliseconds, 

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

1177 

1178 :param im: Image object 

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

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

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

1182 

1183 """ 

1184 from io import BytesIO 

1185 

1186 class Collector(BytesIO): 

1187 data = [] 

1188 

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

1190 self.data.append(data) 

1191 return len(data) 

1192 

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

1194 

1195 fp = Collector() 

1196 

1197 _write_frame_data(fp, im, offset, params) 

1198 

1199 return fp.data 

1200 

1201 

1202# -------------------------------------------------------------------- 

1203# Registry 

1204 

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

1206Image.register_save(GifImageFile.format, _save) 

1207Image.register_save_all(GifImageFile.format, _save_all) 

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

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

1210 

1211# 

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

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

1214 

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