Skip to content

Instantly share code, notes, and snippets.

@devilholk
Created June 1, 2019 08:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devilholk/2d18c18007a8f6c1ebb4fe1265260859 to your computer and use it in GitHub Desktop.
Save devilholk/2d18c18007a8f6c1ebb4fe1265260859 to your computer and use it in GitHub Desktop.
Our modified PNG saver that allows custom size palette
import PIL.PngImagePlugin
#Import all the things unless they start with two underscores. "from stuff import *" will not import things beginning with one underscore
#And we need things like _MAGIC etc
locals().update({k: v for k, v in PIL.PngImagePlugin.__dict__.items() if not k.startswith('__')})
#This function is taken from PIL.PngImagePlugin with a small change that is noted below
#In the bottom we register this modified function to be the PNG save function -devilholk
def overidden_save(im, fp, filename, chunk=putchunk):
# save an image to disk (called by the save method)
mode = im.mode
if mode == "P":
#
# attempt to minimize storage requirements for palette images
if "bits" in im.encoderinfo:
# number of bits specified by user
colors = 1 << im.encoderinfo["bits"]
else:
# check palette contents
if im.palette:
colors = max(min(len(im.palette.getdata()[1])//3, 256), 2)
else:
colors = 256
if colors <= 2:
bits = 1
elif colors <= 4:
bits = 2
elif colors <= 16:
bits = 4
else:
bits = 8
if bits != 8:
mode = "%s;%d" % (mode, bits)
# encoder options
im.encoderconfig = (im.encoderinfo.get("optimize", False),
im.encoderinfo.get("compress_level", -1),
im.encoderinfo.get("compress_type", -1),
im.encoderinfo.get("dictionary", b""))
# get the corresponding PNG mode
try:
rawmode, mode = _OUTMODES[mode]
except KeyError:
raise IOError("cannot write mode %s as PNG" % mode)
#
# write minimal PNG file
fp.write(_MAGIC)
chunk(fp, b"IHDR",
o32(im.size[0]), o32(im.size[1]), # 0: size
mode, # 8: depth/type
b'\0', # 10: compression
b'\0', # 11: filter category
b'\0') # 12: interlace flag
chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
if icc:
# ICC profile
# according to PNG spec, the iCCP chunk contains:
# Profile name 1-79 bytes (character string)
# Null separator 1 byte (null character)
# Compression method 1 byte (0)
# Compressed profile n bytes (zlib with deflate compression)
name = b"ICC Profile"
data = name + b"\0\0" + zlib.compress(icc)
chunk(fp, b"iCCP", data)
# You must either have sRGB or iCCP.
# Disallow sRGB chunks when an iCCP-chunk has been emitted.
chunks.remove(b"sRGB")
info = im.encoderinfo.get("pnginfo")
if info:
chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
for cid, data in info.chunks:
if cid in chunks:
chunks.remove(cid)
chunk(fp, cid, data)
elif cid in chunks_multiple_allowed:
chunk(fp, cid, data)
if im.mode == "P":
#This is changed from original version, here we get the palette bytes using im.palette instead of im.im.getpalette which would not give the truncated version -devilholk
palette_bytes = im.palette.getdata()[1]
chunk(fp, b"PLTE", palette_bytes)
transparency = im.encoderinfo.get('transparency',
im.info.get('transparency', None))
if transparency or transparency == 0:
if im.mode == "P":
# limit to actual palette size
alpha_bytes = 2**bits
if isinstance(transparency, bytes):
chunk(fp, b"tRNS", transparency[:alpha_bytes])
else:
transparency = max(0, min(255, transparency))
alpha = b'\xFF' * transparency + b'\0'
chunk(fp, b"tRNS", alpha[:alpha_bytes])
elif im.mode in ("1", "L", "I"):
transparency = max(0, min(65535, transparency))
chunk(fp, b"tRNS", o16(transparency))
elif im.mode == "RGB":
red, green, blue = transparency
chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
else:
if "transparency" in im.encoderinfo:
# don't bother with transparency if it's an RGBA
# and it's in the info dict. It's probably just stale.
raise IOError("cannot use transparency for this mode")
else:
if im.mode == "P" and im.im.getpalettemode() == "RGBA":
alpha = im.im.getpalette("RGBA", "A")
alpha_bytes = 2**bits
chunk(fp, b"tRNS", alpha[:alpha_bytes])
dpi = im.encoderinfo.get("dpi")
if dpi:
chunk(fp, b"pHYs",
o32(int(dpi[0] / 0.0254 + 0.5)),
o32(int(dpi[1] / 0.0254 + 0.5)),
b'\x01')
info = im.encoderinfo.get("pnginfo")
if info:
chunks = [b"bKGD", b"hIST"]
for cid, data in info.chunks:
if cid in chunks:
chunks.remove(cid)
chunk(fp, cid, data)
exif = im.encoderinfo.get("exif", im.info.get("exif"))
if exif:
if isinstance(exif, Image.Exif):
exif = exif.tobytes(8)
if exif.startswith(b"Exif\x00\x00"):
exif = exif[6:]
chunk(fp, b"eXIf", exif)
ImageFile._save(im, _idat(fp, chunk),
[("zip", (0, 0)+im.size, 0, rawmode)])
chunk(fp, b"IEND", b"")
if hasattr(fp, "flush"):
fp.flush()
#Register this function instead of original function
Image.register_save(PngImageFile.format, overidden_save)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment