mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-04 13:40:54 +03:00
Merge pull request #2313 from wiredfool/pr_2196
Unified different GIF optimize conditions #2196, Test for #2196
This commit is contained in:
commit
f36a04570f
|
@ -434,18 +434,16 @@ def _get_local_header(fp, im, offset, flags):
|
||||||
# optimize the block away if transparent color is not used
|
# optimize the block away if transparent color is not used
|
||||||
transparent_color_exists = True
|
transparent_color_exists = True
|
||||||
|
|
||||||
if _get_optimize(im, im.encoderinfo):
|
used_palette_colors = _get_optimize(im, im.encoderinfo)
|
||||||
used_palette_colors = _get_used_palette_colors(im)
|
if used_palette_colors is not None:
|
||||||
|
|
||||||
# adjust the transparency index after optimize
|
# adjust the transparency index after optimize
|
||||||
if len(used_palette_colors) < 256:
|
for i, palette_color in enumerate(used_palette_colors):
|
||||||
for i in range(len(used_palette_colors)):
|
if palette_color == transparency:
|
||||||
if used_palette_colors[i] == transparency:
|
transparency = i
|
||||||
transparency = i
|
transparent_color_exists = True
|
||||||
transparent_color_exists = True
|
break
|
||||||
break
|
else:
|
||||||
else:
|
transparent_color_exists = False
|
||||||
transparent_color_exists = False
|
|
||||||
|
|
||||||
if "duration" in im.encoderinfo:
|
if "duration" in im.encoderinfo:
|
||||||
duration = int(im.encoderinfo["duration"] / 10)
|
duration = int(im.encoderinfo["duration"] / 10)
|
||||||
|
@ -552,9 +550,28 @@ def _save_netpbm(im, fp, filename):
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# GIF utilities
|
# GIF utilities
|
||||||
|
|
||||||
def _get_optimize(im, info):
|
# Force optimization so that we can test performance against
|
||||||
return im.mode in ("P", "L") and info and info.get("optimize", 0)
|
# cases where it took lots of memory and time previously.
|
||||||
|
_FORCE_OPTIMIZE = False
|
||||||
|
|
||||||
|
def _get_optimize(im, info):
|
||||||
|
if im.mode in ("P", "L") and info and info.get("optimize", 0):
|
||||||
|
# Potentially expensive operation.
|
||||||
|
|
||||||
|
# The palette saves 3 bytes per color not used, but palette
|
||||||
|
# lengths are restricted to 3*(2**N) bytes. Max saving would
|
||||||
|
# be 768 -> 6 bytes if we went all the way down to 2 colors.
|
||||||
|
# * If we're over 128 colors, we can't save any space.
|
||||||
|
# * If there aren't any holes, it's not worth collapsing.
|
||||||
|
# * If we have a 'large' image, the palette is in the noise.
|
||||||
|
|
||||||
|
# create the new palette if not every color is used
|
||||||
|
used_palette_colors = _get_used_palette_colors(im)
|
||||||
|
if _FORCE_OPTIMIZE or im.mode == 'L' or \
|
||||||
|
(len(used_palette_colors) <= 128 and
|
||||||
|
max(used_palette_colors) > len(used_palette_colors) and
|
||||||
|
im.width * im.height < 512 * 512):
|
||||||
|
return used_palette_colors
|
||||||
|
|
||||||
def _get_used_palette_colors(im):
|
def _get_used_palette_colors(im):
|
||||||
used_palette_colors = []
|
used_palette_colors = []
|
||||||
|
@ -586,10 +603,6 @@ def _get_header_palette(palette_bytes):
|
||||||
palette_bytes += o8(0) * 3 * actual_target_size_diff
|
palette_bytes += o8(0) * 3 * actual_target_size_diff
|
||||||
return palette_bytes
|
return palette_bytes
|
||||||
|
|
||||||
# Force optimization so that we can test performance against
|
|
||||||
# cases where it took lots of memory and time previously.
|
|
||||||
_FORCE_OPTIMIZE = False
|
|
||||||
|
|
||||||
def _get_palette_bytes(im, palette, info):
|
def _get_palette_bytes(im, palette, info):
|
||||||
if im.mode == "P":
|
if im.mode == "P":
|
||||||
if palette and isinstance(palette, bytes):
|
if palette and isinstance(palette, bytes):
|
||||||
|
@ -602,79 +615,64 @@ def _get_palette_bytes(im, palette, info):
|
||||||
else:
|
else:
|
||||||
source_palette = bytearray(i//3 for i in range(768))
|
source_palette = bytearray(i//3 for i in range(768))
|
||||||
|
|
||||||
used_palette_colors = palette_bytes = None
|
palette_bytes = None
|
||||||
|
|
||||||
if _get_optimize(im, info):
|
used_palette_colors = _get_optimize(im, info)
|
||||||
used_palette_colors = _get_used_palette_colors(im)
|
if used_palette_colors is not None:
|
||||||
|
palette_bytes = b""
|
||||||
|
new_positions = [0]*256
|
||||||
|
|
||||||
# Potentially expensive operation.
|
# pick only the used colors from the palette
|
||||||
|
for i, oldPosition in enumerate(used_palette_colors):
|
||||||
|
palette_bytes += source_palette[oldPosition*3:oldPosition*3+3]
|
||||||
|
new_positions[oldPosition] = i
|
||||||
|
|
||||||
# The palette saves 3 bytes per color not used, but palette
|
# replace the palette color id of all pixel with the new id
|
||||||
# lengths are restricted to 3*(2**N) bytes. Max saving would
|
|
||||||
# be 768 -> 6 bytes if we went all the way down to 2 colors.
|
|
||||||
# * If we're over 128 colors, we can't save any space.
|
|
||||||
# * If there aren't any holes, it's not worth collapsing.
|
|
||||||
# * If we have a 'large' image, the palette is in the noise.
|
|
||||||
|
|
||||||
# create the new palette if not every color is used
|
# Palette images are [0..255], mapped through a 1 or 3
|
||||||
if _FORCE_OPTIMIZE or im.mode == 'L' or \
|
# byte/color map. We need to remap the whole image
|
||||||
(len(used_palette_colors) <= 128 and
|
# from palette 1 to palette 2. New_positions is
|
||||||
max(used_palette_colors) > len(used_palette_colors) and
|
# an array of indexes into palette 1. Palette 2 is
|
||||||
im.width * im.height < 512 * 512):
|
# palette 1 with any holes removed.
|
||||||
palette_bytes = b""
|
|
||||||
new_positions = [0]*256
|
|
||||||
|
|
||||||
# pick only the used colors from the palette
|
# We're going to leverage the convert mechanism to use the
|
||||||
for i, oldPosition in enumerate(used_palette_colors):
|
# C code to remap the image from palette 1 to palette 2,
|
||||||
palette_bytes += source_palette[oldPosition*3:oldPosition*3+3]
|
# by forcing the source image into 'L' mode and adding a
|
||||||
new_positions[oldPosition] = i
|
# mapping 'L' mode palette, then converting back to 'L'
|
||||||
|
# sans palette thus converting the image bytes, then
|
||||||
|
# assigning the optimized RGB palette.
|
||||||
|
|
||||||
# replace the palette color id of all pixel with the new id
|
# perf reference, 9500x4000 gif, w/~135 colors
|
||||||
|
# 14 sec prepatch, 1 sec postpatch with optimization forced.
|
||||||
|
|
||||||
# Palette images are [0..255], mapped through a 1 or 3
|
mapping_palette = bytearray(new_positions)
|
||||||
# byte/color map. We need to remap the whole image
|
|
||||||
# from palette 1 to palette 2. New_positions is
|
|
||||||
# an array of indexes into palette 1. Palette 2 is
|
|
||||||
# palette 1 with any holes removed.
|
|
||||||
|
|
||||||
# We're going to leverage the convert mechanism to use the
|
m_im = im.copy()
|
||||||
# C code to remap the image from palette 1 to palette 2,
|
m_im.mode = 'P'
|
||||||
# by forcing the source image into 'L' mode and adding a
|
|
||||||
# mapping 'L' mode palette, then converting back to 'L'
|
|
||||||
# sans palette thus converting the image bytes, then
|
|
||||||
# assigning the optimized RGB palette.
|
|
||||||
|
|
||||||
# perf reference, 9500x4000 gif, w/~135 colors
|
m_im.palette = ImagePalette.ImagePalette("RGB",
|
||||||
# 14 sec prepatch, 1 sec postpatch with optimization forced.
|
palette=mapping_palette*3,
|
||||||
|
size=768)
|
||||||
|
#possibly set palette dirty, then
|
||||||
|
#m_im.putpalette(mapping_palette, 'L') # converts to 'P'
|
||||||
|
# or just force it.
|
||||||
|
# UNDONE -- this is part of the general issue with palettes
|
||||||
|
m_im.im.putpalette(*m_im.palette.getdata())
|
||||||
|
|
||||||
mapping_palette = bytearray(new_positions)
|
m_im = m_im.convert('L')
|
||||||
|
|
||||||
m_im = im.copy()
|
# Internally, we require 768 bytes for a palette.
|
||||||
m_im.mode = 'P'
|
new_palette_bytes = (palette_bytes +
|
||||||
|
(768 - len(palette_bytes)) * b'\x00')
|
||||||
|
m_im.putpalette(new_palette_bytes)
|
||||||
|
m_im.palette = ImagePalette.ImagePalette("RGB",
|
||||||
|
palette=palette_bytes,
|
||||||
|
size=len(palette_bytes))
|
||||||
|
|
||||||
m_im.palette = ImagePalette.ImagePalette("RGB",
|
# oh gawd, this is modifying the image in place so I can pass by ref.
|
||||||
palette=mapping_palette*3,
|
# REFACTOR SOONEST
|
||||||
size=768)
|
im.frombytes(m_im.tobytes())
|
||||||
#possibly set palette dirty, then
|
|
||||||
#m_im.putpalette(mapping_palette, 'L') # converts to 'P'
|
|
||||||
# or just force it.
|
|
||||||
# UNDONE -- this is part of the general issue with palettes
|
|
||||||
m_im.im.putpalette(*m_im.palette.getdata())
|
|
||||||
|
|
||||||
m_im = m_im.convert('L')
|
|
||||||
|
|
||||||
# Internally, we require 768 bytes for a palette.
|
|
||||||
new_palette_bytes = (palette_bytes +
|
|
||||||
(768 - len(palette_bytes)) * b'\x00')
|
|
||||||
m_im.putpalette(new_palette_bytes)
|
|
||||||
m_im.palette = ImagePalette.ImagePalette("RGB",
|
|
||||||
palette=palette_bytes,
|
|
||||||
size=len(palette_bytes))
|
|
||||||
|
|
||||||
# oh gawd, this is modifying the image in place so I can pass by ref.
|
|
||||||
# REFACTOR SOONEST
|
|
||||||
im.frombytes(m_im.tobytes())
|
|
||||||
|
|
||||||
if not palette_bytes:
|
if not palette_bytes:
|
||||||
palette_bytes = source_palette
|
palette_bytes = source_palette
|
||||||
|
|
||||||
|
|
|
@ -362,5 +362,28 @@ class TestFileGif(PillowTestCase):
|
||||||
reread = Image.open(out)
|
reread = Image.open(out)
|
||||||
self.assertEqual(reread.n_frames, 10)
|
self.assertEqual(reread.n_frames, 10)
|
||||||
|
|
||||||
|
def test_transparent_optimize(self):
|
||||||
|
# from issue #2195, if the transparent color is incorrectly
|
||||||
|
# optimized out, gif loses transparency Need a palette that
|
||||||
|
# isn't using the 0 color, and one that's > 128 items where
|
||||||
|
# the transparent color is actually the top palette entry to
|
||||||
|
# trigger the bug.
|
||||||
|
|
||||||
|
from PIL import ImagePalette
|
||||||
|
|
||||||
|
data = bytes(bytearray(range(1,254)))
|
||||||
|
palette = ImagePalette.ImagePalette("RGB", list(range(256))*3)
|
||||||
|
|
||||||
|
im = Image.new('L', (253,1))
|
||||||
|
im.frombytes(data)
|
||||||
|
im.putpalette(palette)
|
||||||
|
|
||||||
|
out = self.tempfile('temp.gif')
|
||||||
|
im.save(out, transparency=253)
|
||||||
|
reloaded = Image.open(out)
|
||||||
|
|
||||||
|
self.assertEqual(reloaded.info['transparency'], 253)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user