3030import subprocess
3131from enum import IntEnum
3232
33- from . import Image , ImageChops , ImageFile , ImagePalette , ImageSequence
33+ from . import (
34+ Image ,
35+ ImageChops ,
36+ ImageFile ,
37+ ImageMath ,
38+ ImageOps ,
39+ ImagePalette ,
40+ ImageSequence ,
41+ )
3442from ._binary import i16le as i16
3543from ._binary import o8
3644from ._binary import o16le as o16
@@ -534,7 +542,15 @@ def _normalize_palette(im, palette, info):
534542 else :
535543 used_palette_colors = _get_optimize (im , info )
536544 if used_palette_colors is not None :
537- return im .remap_palette (used_palette_colors , source_palette )
545+ im = im .remap_palette (used_palette_colors , source_palette )
546+ if "transparency" in info :
547+ try :
548+ info ["transparency" ] = used_palette_colors .index (
549+ info ["transparency" ]
550+ )
551+ except ValueError :
552+ del info ["transparency" ]
553+ return im
538554
539555 im .palette .palette = source_palette
540556 return im
@@ -562,20 +578,19 @@ def _write_single_frame(im, fp, palette):
562578
563579
564580def _getbbox (base_im , im_frame ):
565- if _get_palette_bytes (im_frame ) == _get_palette_bytes (base_im ):
566- delta = ImageChops .subtract_modulo (im_frame , base_im )
567- else :
568- delta = ImageChops .subtract_modulo (
569- im_frame .convert ("RGBA" ), base_im .convert ("RGBA" )
570- )
571- return delta .getbbox (alpha_only = False )
581+ if _get_palette_bytes (im_frame ) != _get_palette_bytes (base_im ):
582+ im_frame = im_frame .convert ("RGBA" )
583+ base_im = base_im .convert ("RGBA" )
584+ delta = ImageChops .subtract_modulo (im_frame , base_im )
585+ return delta , delta .getbbox (alpha_only = False )
572586
573587
574588def _write_multiple_frames (im , fp , palette ):
575589 duration = im .encoderinfo .get ("duration" )
576590 disposal = im .encoderinfo .get ("disposal" , im .info .get ("disposal" ))
577591
578592 im_frames = []
593+ previous_im = None
579594 frame_count = 0
580595 background_im = None
581596 for imSequence in itertools .chain ([im ], im .encoderinfo .get ("append_images" , [])):
@@ -589,9 +604,9 @@ def _write_multiple_frames(im, fp, palette):
589604 im .encoderinfo .setdefault (k , v )
590605
591606 encoderinfo = im .encoderinfo .copy ()
592- im_frame = _normalize_palette (im_frame , palette , encoderinfo )
593607 if "transparency" in im_frame .info :
594608 encoderinfo .setdefault ("transparency" , im_frame .info ["transparency" ])
609+ im_frame = _normalize_palette (im_frame , palette , encoderinfo )
595610 if isinstance (duration , (list , tuple )):
596611 encoderinfo ["duration" ] = duration [frame_count ]
597612 elif duration is None and "duration" in im_frame .info :
@@ -600,14 +615,16 @@ def _write_multiple_frames(im, fp, palette):
600615 encoderinfo ["disposal" ] = disposal [frame_count ]
601616 frame_count += 1
602617
618+ diff_frame = None
603619 if im_frames :
604620 # delta frame
605- previous = im_frames [- 1 ]
606- bbox = _getbbox (previous ["im" ], im_frame )
621+ delta , bbox = _getbbox (previous_im , im_frame )
607622 if not bbox :
608623 # This frame is identical to the previous frame
609624 if encoderinfo .get ("duration" ):
610- previous ["encoderinfo" ]["duration" ] += encoderinfo ["duration" ]
625+ im_frames [- 1 ]["encoderinfo" ]["duration" ] += encoderinfo [
626+ "duration"
627+ ]
611628 continue
612629 if encoderinfo .get ("disposal" ) == 2 :
613630 if background_im is None :
@@ -617,10 +634,44 @@ def _write_multiple_frames(im, fp, palette):
617634 background = _get_background (im_frame , color )
618635 background_im = Image .new ("P" , im_frame .size , background )
619636 background_im .putpalette (im_frames [0 ]["im" ].palette )
620- bbox = _getbbox (background_im , im_frame )
637+ delta , bbox = _getbbox (background_im , im_frame )
638+ if encoderinfo .get ("optimize" ) and im_frame .mode != "1" :
639+ if "transparency" not in encoderinfo :
640+ try :
641+ encoderinfo [
642+ "transparency"
643+ ] = im_frame .palette ._new_color_index (im_frame )
644+ except ValueError :
645+ pass
646+ if "transparency" in encoderinfo :
647+ # When the delta is zero, fill the image with transparency
648+ diff_frame = im_frame .copy ()
649+ fill = Image .new (
650+ "P" , diff_frame .size , encoderinfo ["transparency" ]
651+ )
652+ if delta .mode == "RGBA" :
653+ r , g , b , a = delta .split ()
654+ mask = ImageMath .eval (
655+ "convert(max(max(max(r, g), b), a) * 255, '1')" ,
656+ r = r ,
657+ g = g ,
658+ b = b ,
659+ a = a ,
660+ )
661+ else :
662+ if delta .mode == "P" :
663+ # Convert to L without considering palette
664+ delta_l = Image .new ("L" , delta .size )
665+ delta_l .putdata (delta .getdata ())
666+ delta = delta_l
667+ mask = ImageMath .eval ("convert(im * 255, '1')" , im = delta )
668+ diff_frame .paste (fill , mask = ImageOps .invert (mask ))
621669 else :
622670 bbox = None
623- im_frames .append ({"im" : im_frame , "bbox" : bbox , "encoderinfo" : encoderinfo })
671+ previous_im = im_frame
672+ im_frames .append (
673+ {"im" : diff_frame or im_frame , "bbox" : bbox , "encoderinfo" : encoderinfo }
674+ )
624675
625676 if len (im_frames ) > 1 :
626677 for frame_data in im_frames :
@@ -678,22 +729,10 @@ def get_interlace(im):
678729
679730
680731def _write_local_header (fp , im , offset , flags ):
681- transparent_color_exists = False
682732 try :
683- transparency = int (im .encoderinfo ["transparency" ])
684- except (KeyError , ValueError ):
685- pass
686- else :
687- # optimize the block away if transparent color is not used
688- transparent_color_exists = True
689-
690- used_palette_colors = _get_optimize (im , im .encoderinfo )
691- if used_palette_colors is not None :
692- # adjust the transparency index after optimize
693- try :
694- transparency = used_palette_colors .index (transparency )
695- except ValueError :
696- transparent_color_exists = False
733+ transparency = im .encoderinfo ["transparency" ]
734+ except KeyError :
735+ transparency = None
697736
698737 if "duration" in im .encoderinfo :
699738 duration = int (im .encoderinfo ["duration" ] / 10 )
@@ -702,19 +741,17 @@ def _write_local_header(fp, im, offset, flags):
702741
703742 disposal = int (im .encoderinfo .get ("disposal" , 0 ))
704743
705- if transparent_color_exists or duration != 0 or disposal :
706- packed_flag = 1 if transparent_color_exists else 0
744+ if transparency is not None or duration != 0 or disposal :
745+ packed_flag = 1 if transparency is not None else 0
707746 packed_flag |= disposal << 2
708- if not transparent_color_exists :
709- transparency = 0
710747
711748 fp .write (
712749 b"!"
713750 + o8 (249 ) # extension intro
714751 + o8 (4 ) # length
715752 + o8 (packed_flag ) # packed fields
716753 + o16 (duration ) # duration
717- + o8 (transparency ) # transparency index
754+ + o8 (transparency or 0 ) # transparency index
718755 + o8 (0 )
719756 )
720757
@@ -802,7 +839,7 @@ def _get_optimize(im, info):
802839 :param info: encoderinfo
803840 :returns: list of indexes of palette entries in use, or None
804841 """
805- if im .mode in ("P" , "L" ) and info and info .get ("optimize" , 0 ):
842+ if im .mode in ("P" , "L" ) and info and info .get ("optimize" ):
806843 # Potentially expensive operation.
807844
808845 # The palette saves 3 bytes per color not used, but palette
0 commit comments