Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/GifImagePlugin.py: 7%

578 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

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# 

26 

27import itertools 

28import math 

29import os 

30import subprocess 

31from enum import IntEnum 

32 

33from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence 

34from ._binary import i16le as i16 

35from ._binary import o8 

36from ._binary import o16le as o16 

37 

38 

39class LoadingStrategy(IntEnum): 

40 """.. versionadded:: 9.1.0""" 

41 

42 RGB_AFTER_FIRST = 0 

43 RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 

44 RGB_ALWAYS = 2 

45 

46 

47#: .. versionadded:: 9.1.0 

48LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST 

49 

50# -------------------------------------------------------------------- 

51# Identify/read GIF files 

52 

53 

54def _accept(prefix): 

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

56 

57 

58## 

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

60# GIF89 images. 

61 

62 

63class GifImageFile(ImageFile.ImageFile): 

64 

65 format = "GIF" 

66 format_description = "Compuserve GIF" 

67 _close_exclusive_fp_after_loading = False 

68 

69 global_palette = None 

70 

71 def data(self): 

72 s = self.fp.read(1) 

73 if s and s[0]: 

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

75 return None 

76 

77 def _is_palette_needed(self, p): 

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

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

80 return True 

81 return False 

82 

83 def _open(self): 

84 

85 # Screen 

86 s = self.fp.read(13) 

87 if not _accept(s): 

88 raise SyntaxError("not a GIF file") 

89 

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

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

92 self.tile = [] 

93 flags = s[10] 

94 bits = (flags & 7) + 1 

95 

96 if flags & 128: 

97 # get global palette 

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

99 # check if palette contains colour indices 

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

101 if self._is_palette_needed(p): 

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

103 self.global_palette = self.palette = p 

104 

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

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

107 self._n_frames = None 

108 self._is_animated = None 

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

110 

111 @property 

112 def n_frames(self): 

113 if self._n_frames is None: 

114 current = self.tell() 

115 try: 

116 while True: 

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

118 except EOFError: 

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

120 self.seek(current) 

121 return self._n_frames 

122 

123 @property 

124 def is_animated(self): 

125 if self._is_animated is None: 

126 if self._n_frames is not None: 

127 self._is_animated = self._n_frames != 1 

128 else: 

129 current = self.tell() 

130 if current: 

131 self._is_animated = True 

132 else: 

133 try: 

134 self._seek(1, False) 

135 self._is_animated = True 

136 except EOFError: 

137 self._is_animated = False 

138 

139 self.seek(current) 

140 return self._is_animated 

141 

142 def seek(self, frame): 

143 if not self._seek_check(frame): 

144 return 

145 if frame < self.__frame: 

146 self.im = None 

147 self._seek(0) 

148 

149 last_frame = self.__frame 

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

151 try: 

152 self._seek(f) 

153 except EOFError as e: 

154 self.seek(last_frame) 

155 raise EOFError("no more images in GIF file") from e 

156 

157 def _seek(self, frame, update_image=True): 

158 

159 if frame == 0: 

160 # rewind 

161 self.__offset = 0 

162 self.dispose = None 

163 self.__frame = -1 

164 self._fp.seek(self.__rewind) 

165 self.disposal_method = 0 

166 if "comment" in self.info: 

167 del self.info["comment"] 

168 else: 

169 # ensure that the previous frame was loaded 

170 if self.tile and update_image: 

171 self.load() 

172 

173 if frame != self.__frame + 1: 

174 raise ValueError(f"cannot seek to frame {frame}") 

175 

176 self.fp = self._fp 

177 if self.__offset: 

178 # backup to last frame 

179 self.fp.seek(self.__offset) 

180 while self.data(): 

181 pass 

182 self.__offset = 0 

183 

184 s = self.fp.read(1) 

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

186 raise EOFError 

187 

188 self.tile = [] 

189 

190 palette = None 

191 

192 info = {} 

193 frame_transparency = None 

194 interlace = None 

195 frame_dispose_extent = None 

196 while True: 

197 

198 if not s: 

199 s = self.fp.read(1) 

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

201 break 

202 

203 elif s == b"!": 

204 # 

205 # extensions 

206 # 

207 s = self.fp.read(1) 

208 block = self.data() 

209 if s[0] == 249: 

210 # 

211 # graphic control extension 

212 # 

213 flags = block[0] 

214 if flags & 1: 

215 frame_transparency = block[3] 

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

217 

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

219 dispose_bits = 0b00011100 & flags 

220 dispose_bits = dispose_bits >> 2 

221 if dispose_bits: 

222 # only set the dispose if it is not 

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

224 # correct, but it seems to prevent the last 

225 # frame from looking odd for some animations 

226 self.disposal_method = dispose_bits 

227 elif s[0] == 254: 

228 # 

229 # comment extension 

230 # 

231 comment = b"" 

232 

233 # Read this comment block 

234 while block: 

235 comment += block 

236 block = self.data() 

237 

238 if "comment" in info: 

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

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

241 else: 

242 info["comment"] = comment 

243 s = None 

244 continue 

245 elif s[0] == 255 and frame == 0: 

246 # 

247 # application extension 

248 # 

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

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

251 block = self.data() 

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

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

254 while self.data(): 

255 pass 

256 

257 elif s == b",": 

258 # 

259 # local image 

260 # 

261 s = self.fp.read(9) 

262 

263 # extent 

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

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

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

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

268 Image._decompression_bomb_check(self._size) 

269 frame_dispose_extent = x0, y0, x1, y1 

270 flags = s[8] 

271 

272 interlace = (flags & 64) != 0 

273 

274 if flags & 128: 

275 bits = (flags & 7) + 1 

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

277 if self._is_palette_needed(p): 

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

279 

280 # image data 

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

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

283 break 

284 

285 else: 

286 pass 

287 # raise OSError, "illegal GIF tag `%x`" % s[0] 

288 s = None 

289 

290 if interlace is None: 

291 # self._fp = None 

292 raise EOFError 

293 

294 self.__frame = frame 

295 if not update_image: 

296 return 

297 

298 if self.dispose: 

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

300 

301 self._frame_palette = palette or self.global_palette 

302 if frame == 0: 

303 if self._frame_palette: 

304 self.mode = ( 

305 "RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P" 

306 ) 

307 else: 

308 self.mode = "L" 

309 

310 if not palette and self.global_palette: 

311 from copy import copy 

312 

313 palette = copy(self.global_palette) 

314 self.palette = palette 

315 else: 

316 self._frame_transparency = frame_transparency 

317 if self.mode == "P": 

318 if ( 

319 LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY 

320 or palette 

321 ): 

322 self.pyaccess = None 

323 if "transparency" in self.info: 

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

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

326 self.mode = "RGBA" 

327 del self.info["transparency"] 

328 else: 

329 self.mode = "RGB" 

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

331 

332 def _rgb(color): 

333 if self._frame_palette: 

334 color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) 

335 else: 

336 color = (color, color, color) 

337 return color 

338 

339 self.dispose_extent = frame_dispose_extent 

340 try: 

341 if self.disposal_method < 2: 

342 # do not dispose or none specified 

343 self.dispose = None 

344 elif self.disposal_method == 2: 

345 # replace with background colour 

346 

347 # only dispose the extent in this frame 

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

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

350 

351 Image._decompression_bomb_check(dispose_size) 

352 

353 # by convention, attempt to use transparency first 

354 dispose_mode = "P" 

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

356 if color is not None: 

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

358 dispose_mode = "RGBA" 

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

360 else: 

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

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

363 dispose_mode = "RGB" 

364 color = _rgb(color) 

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

366 else: 

367 # replace with previous contents 

368 if self.im is not None: 

369 # only dispose the extent in this frame 

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

371 elif frame_transparency is not None: 

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

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

374 

375 Image._decompression_bomb_check(dispose_size) 

376 dispose_mode = "P" 

377 color = frame_transparency 

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

379 dispose_mode = "RGBA" 

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

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

382 except AttributeError: 

383 pass 

384 

385 if interlace is not None: 

386 transparency = -1 

387 if frame_transparency is not None: 

388 if frame == 0: 

389 self.info["transparency"] = frame_transparency 

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

391 transparency = frame_transparency 

392 self.tile = [ 

393 ( 

394 "gif", 

395 (x0, y0, x1, y1), 

396 self.__offset, 

397 (bits, interlace, transparency), 

398 ) 

399 ] 

400 

401 if info.get("comment"): 

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

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

404 if k in info: 

405 self.info[k] = info[k] 

406 elif k in self.info: 

407 del self.info[k] 

408 

409 def load_prepare(self): 

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

411 self._prev_im = None 

412 if self.__frame == 0: 

413 if "transparency" in self.info: 

414 self.im = Image.core.fill( 

415 temp_mode, self.size, self.info["transparency"] 

416 ) 

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

418 self._prev_im = self.im 

419 if self._frame_palette: 

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

421 self.im.putpalette(*self._frame_palette.getdata()) 

422 else: 

423 self.im = None 

424 self.mode = temp_mode 

425 self._frame_palette = None 

426 

427 super().load_prepare() 

428 

429 def load_end(self): 

430 if self.__frame == 0: 

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

432 self.mode = "RGB" 

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

434 return 

435 if self.mode == "P" and self._prev_im: 

436 if self._frame_transparency is not None: 

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

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

439 else: 

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

441 else: 

442 if not self._prev_im: 

443 return 

444 frame_im = self.im 

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

446 

447 self.im = self._prev_im 

448 self.mode = self.im.mode 

449 if frame_im.mode == "RGBA": 

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

451 else: 

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

453 

454 def tell(self): 

455 return self.__frame 

456 

457 

458# -------------------------------------------------------------------- 

459# Write GIF files 

460 

461 

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

463 

464 

465def _normalize_mode(im): 

466 """ 

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

468 for saving in a Gif. 

469 

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

471 palette or 'L' mode. 

472 

473 :param im: Image object 

474 :returns: Image object 

475 """ 

476 if im.mode in RAWMODE: 

477 im.load() 

478 return im 

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

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

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

482 for rgba in im.palette.colors.keys(): 

483 if rgba[3] == 0: 

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

485 break 

486 return im 

487 return im.convert("L") 

488 

489 

490def _normalize_palette(im, palette, info): 

491 """ 

492 Normalizes the palette for image. 

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

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

495 - Optimizes the palette if necessary/desired. 

496 

497 :param im: Image object 

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

499 :param info: encoderinfo 

500 :returns: Image object 

501 """ 

502 source_palette = None 

503 if palette: 

504 # a bytes palette 

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

506 source_palette = bytearray(palette[:768]) 

507 if isinstance(palette, ImagePalette.ImagePalette): 

508 source_palette = bytearray(palette.palette) 

509 

510 if im.mode == "P": 

511 if not source_palette: 

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

513 else: # L-mode 

514 if not source_palette: 

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

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

517 

518 if palette: 

519 used_palette_colors = [] 

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

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

522 try: 

523 index = im.palette.colors[source_color] 

524 except KeyError: 

525 index = None 

526 used_palette_colors.append(index) 

527 for i, index in enumerate(used_palette_colors): 

528 if index is None: 

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

530 if j not in used_palette_colors: 

531 used_palette_colors[i] = j 

532 break 

533 im = im.remap_palette(used_palette_colors) 

534 else: 

535 used_palette_colors = _get_optimize(im, info) 

536 if used_palette_colors is not None: 

537 return im.remap_palette(used_palette_colors, source_palette) 

538 

539 im.palette.palette = source_palette 

540 return im 

541 

542 

543def _write_single_frame(im, fp, palette): 

544 im_out = _normalize_mode(im) 

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

546 im.encoderinfo.setdefault(k, v) 

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

548 

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

550 fp.write(s) 

551 

552 # local image header 

553 flags = 0 

554 if get_interlace(im): 

555 flags = flags | 64 

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

557 

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

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

560 

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

562 

563 

564def _write_multiple_frames(im, fp, palette): 

565 

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

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

568 

569 im_frames = [] 

570 frame_count = 0 

571 background_im = None 

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

573 for im_frame in ImageSequence.Iterator(imSequence): 

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

575 im_frame = _normalize_mode(im_frame.copy()) 

576 if frame_count == 0: 

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

578 if k == "transparency": 

579 continue 

580 im.encoderinfo.setdefault(k, v) 

581 

582 encoderinfo = im.encoderinfo.copy() 

583 im_frame = _normalize_palette(im_frame, palette, encoderinfo) 

584 if "transparency" in im_frame.info: 

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

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

587 encoderinfo["duration"] = duration[frame_count] 

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

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

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

591 encoderinfo["disposal"] = disposal[frame_count] 

592 frame_count += 1 

593 

594 if im_frames: 

595 # delta frame 

596 previous = im_frames[-1] 

597 if encoderinfo.get("disposal") == 2: 

598 if background_im is None: 

599 color = im.encoderinfo.get( 

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

601 ) 

602 background = _get_background(im_frame, color) 

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

604 background_im.putpalette(im_frames[0]["im"].palette) 

605 base_im = background_im 

606 else: 

607 base_im = previous["im"] 

608 if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): 

609 delta = ImageChops.subtract_modulo(im_frame, base_im) 

610 else: 

611 delta = ImageChops.subtract_modulo( 

612 im_frame.convert("RGB"), base_im.convert("RGB") 

613 ) 

614 bbox = delta.getbbox() 

615 if not bbox: 

616 # This frame is identical to the previous frame 

617 if duration: 

618 previous["encoderinfo"]["duration"] += encoderinfo["duration"] 

619 continue 

620 else: 

621 bbox = None 

622 im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) 

623 

624 if len(im_frames) > 1: 

625 for frame_data in im_frames: 

626 im_frame = frame_data["im"] 

627 if not frame_data["bbox"]: 

628 # global header 

629 for s in _get_global_header(im_frame, frame_data["encoderinfo"]): 

630 fp.write(s) 

631 offset = (0, 0) 

632 else: 

633 # compress difference 

634 if not palette: 

635 frame_data["encoderinfo"]["include_color_table"] = True 

636 

637 im_frame = im_frame.crop(frame_data["bbox"]) 

638 offset = frame_data["bbox"][:2] 

639 _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"]) 

640 return True 

641 elif "duration" in im.encoderinfo and isinstance( 

642 im.encoderinfo["duration"], (list, tuple) 

643 ): 

644 # Since multiple frames will not be written, add together the frame durations 

645 im.encoderinfo["duration"] = sum(im.encoderinfo["duration"]) 

646 

647 

648def _save_all(im, fp, filename): 

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

650 

651 

652def _save(im, fp, filename, save_all=False): 

653 # header 

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

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

656 else: 

657 palette = None 

658 im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) 

659 

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

661 _write_single_frame(im, fp, palette) 

662 

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

664 

665 if hasattr(fp, "flush"): 

666 fp.flush() 

667 

668 

669def get_interlace(im): 

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

671 

672 # workaround for @PIL153 

673 if min(im.size) < 16: 

674 interlace = 0 

675 

676 return interlace 

677 

678 

679def _write_local_header(fp, im, offset, flags): 

680 transparent_color_exists = False 

681 try: 

682 if "transparency" in im.encoderinfo: 

683 transparency = im.encoderinfo["transparency"] 

684 else: 

685 transparency = im.info["transparency"] 

686 transparency = int(transparency) 

687 except (KeyError, ValueError): 

688 pass 

689 else: 

690 # optimize the block away if transparent color is not used 

691 transparent_color_exists = True 

692 

693 used_palette_colors = _get_optimize(im, im.encoderinfo) 

694 if used_palette_colors is not None: 

695 # adjust the transparency index after optimize 

696 try: 

697 transparency = used_palette_colors.index(transparency) 

698 except ValueError: 

699 transparent_color_exists = False 

700 

701 if "duration" in im.encoderinfo: 

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

703 else: 

704 duration = 0 

705 

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

707 

708 if transparent_color_exists or duration != 0 or disposal: 

709 packed_flag = 1 if transparent_color_exists else 0 

710 packed_flag |= disposal << 2 

711 if not transparent_color_exists: 

712 transparency = 0 

713 

714 fp.write( 

715 b"!" 

716 + o8(249) # extension intro 

717 + o8(4) # length 

718 + o8(packed_flag) # packed fields 

719 + o16(duration) # duration 

720 + o8(transparency) # transparency index 

721 + o8(0) 

722 ) 

723 

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

725 if include_color_table: 

726 palette_bytes = _get_palette_bytes(im) 

727 color_table_size = _get_color_table_size(palette_bytes) 

728 if color_table_size: 

729 flags = flags | 128 # local color table flag 

730 flags = flags | color_table_size 

731 

732 fp.write( 

733 b"," 

734 + o16(offset[0]) # offset 

735 + o16(offset[1]) 

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

737 + o16(im.size[1]) 

738 + o8(flags) # flags 

739 ) 

740 if include_color_table and color_table_size: 

741 fp.write(_get_header_palette(palette_bytes)) 

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

743 

744 

745def _save_netpbm(im, fp, filename): 

746 

747 # Unused by default. 

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

749 # 

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

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

752 # below for information on how to enable this. 

753 tempfile = im._dump() 

754 

755 try: 

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

757 if im.mode != "RGB": 

758 subprocess.check_call( 

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

760 ) 

761 else: 

762 # Pipe ppmquant output into ppmtogif 

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

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

765 togif_cmd = ["ppmtogif"] 

766 quant_proc = subprocess.Popen( 

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

768 ) 

769 togif_proc = subprocess.Popen( 

770 togif_cmd, 

771 stdin=quant_proc.stdout, 

772 stdout=f, 

773 stderr=subprocess.DEVNULL, 

774 ) 

775 

776 # Allow ppmquant to receive SIGPIPE if ppmtogif exits 

777 quant_proc.stdout.close() 

778 

779 retcode = quant_proc.wait() 

780 if retcode: 

781 raise subprocess.CalledProcessError(retcode, quant_cmd) 

782 

783 retcode = togif_proc.wait() 

784 if retcode: 

785 raise subprocess.CalledProcessError(retcode, togif_cmd) 

786 finally: 

787 try: 

788 os.unlink(tempfile) 

789 except OSError: 

790 pass 

791 

792 

793# Force optimization so that we can test performance against 

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

795_FORCE_OPTIMIZE = False 

796 

797 

798def _get_optimize(im, info): 

799 """ 

800 Palette optimization is a potentially expensive operation. 

801 

802 This function determines if the palette should be optimized using 

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

804 

805 :param im: Image object 

806 :param info: encoderinfo 

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

808 """ 

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

810 # Potentially expensive operation. 

811 

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

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

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

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

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

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

818 

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

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

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

822 # check which colors are used 

823 used_palette_colors = [] 

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

825 if count: 

826 used_palette_colors.append(i) 

827 

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

829 return used_palette_colors 

830 

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

832 im.palette.mode 

833 ) 

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

835 if ( 

836 # check that the palette would become smaller when saved 

837 len(used_palette_colors) <= current_palette_size // 2 

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

839 and current_palette_size > 2 

840 ): 

841 return used_palette_colors 

842 

843 

844def _get_color_table_size(palette_bytes): 

845 # calculate the palette size for the header 

846 if not palette_bytes: 

847 return 0 

848 elif len(palette_bytes) < 9: 

849 return 1 

850 else: 

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

852 

853 

854def _get_header_palette(palette_bytes): 

855 """ 

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

857 suitable for direct inclusion in the GIF header 

858 

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

860 :returns: Null padded palette 

861 """ 

862 color_table_size = _get_color_table_size(palette_bytes) 

863 

864 # add the missing amount of bytes 

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

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

867 if actual_target_size_diff > 0: 

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

869 return palette_bytes 

870 

871 

872def _get_palette_bytes(im): 

873 """ 

874 Gets the palette for inclusion in the gif header 

875 

876 :param im: Image object 

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

878 """ 

879 return im.palette.palette 

880 

881 

882def _get_background(im, info_background): 

883 background = 0 

884 if info_background: 

885 background = info_background 

886 if isinstance(background, tuple): 

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

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

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

890 try: 

891 background = im.palette.getcolor(background, im) 

892 except ValueError as e: 

893 if str(e) == "cannot allocate more than 256 colors": 

894 # If all 256 colors are in use, 

895 # then there is no need for the background color 

896 return 0 

897 else: 

898 raise 

899 return background 

900 

901 

902def _get_global_header(im, info): 

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

904 

905 # Header Block 

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

907 

908 version = b"87a" 

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

910 info 

911 and ( 

912 "transparency" in info 

913 or "loop" in info 

914 or info.get("duration") 

915 or info.get("comment") 

916 ) 

917 ): 

918 version = b"89a" 

919 

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

921 

922 palette_bytes = _get_palette_bytes(im) 

923 color_table_size = _get_color_table_size(palette_bytes) 

924 

925 header = [ 

926 b"GIF" # signature 

927 + version # version 

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

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

930 # Logical Screen Descriptor 

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

932 o8(color_table_size + 128), # packed fields 

933 # background + reserved/aspect 

934 o8(background) + o8(0), 

935 # Global Color Table 

936 _get_header_palette(palette_bytes), 

937 ] 

938 if "loop" in info: 

939 header.append( 

940 b"!" 

941 + o8(255) # extension intro 

942 + o8(11) 

943 + b"NETSCAPE2.0" 

944 + o8(3) 

945 + o8(1) 

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

947 + o8(0) 

948 ) 

949 if info.get("comment"): 

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

951 

952 comment = info["comment"] 

953 if isinstance(comment, str): 

954 comment = comment.encode() 

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

956 subblock = comment[i : i + 255] 

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

958 

959 comment_block += o8(0) 

960 header.append(comment_block) 

961 return header 

962 

963 

964def _write_frame_data(fp, im_frame, offset, params): 

965 try: 

966 im_frame.encoderinfo = params 

967 

968 # local image header 

969 _write_local_header(fp, im_frame, offset, 0) 

970 

971 ImageFile._save( 

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

973 ) 

974 

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

976 finally: 

977 del im_frame.encoderinfo 

978 

979 

980# -------------------------------------------------------------------- 

981# Legacy GIF utilities 

982 

983 

984def getheader(im, palette=None, info=None): 

985 """ 

986 Legacy Method to get Gif data from image. 

987 

988 Warning:: May modify image data. 

989 

990 :param im: Image object 

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

992 :param info: encoderinfo 

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

994 

995 """ 

996 used_palette_colors = _get_optimize(im, info) 

997 

998 if info is None: 

999 info = {} 

1000 

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

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

1003 

1004 im_mod = _normalize_palette(im, palette, info) 

1005 im.palette = im_mod.palette 

1006 im.im = im_mod.im 

1007 header = _get_global_header(im, info) 

1008 

1009 return header, used_palette_colors 

1010 

1011 

1012def getdata(im, offset=(0, 0), **params): 

1013 """ 

1014 Legacy Method 

1015 

1016 Return a list of strings representing this image. 

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

1018 encoded image data. 

1019 

1020 To specify duration, add the time in milliseconds, 

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

1022 

1023 :param im: Image object 

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

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

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

1027 

1028 """ 

1029 

1030 class Collector: 

1031 data = [] 

1032 

1033 def write(self, data): 

1034 self.data.append(data) 

1035 

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

1037 

1038 fp = Collector() 

1039 

1040 _write_frame_data(fp, im, offset, params) 

1041 

1042 return fp.data 

1043 

1044 

1045# -------------------------------------------------------------------- 

1046# Registry 

1047 

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

1049Image.register_save(GifImageFile.format, _save) 

1050Image.register_save_all(GifImageFile.format, _save_all) 

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

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

1053 

1054# 

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

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

1057 

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