Here is a way of doing what you ask, without using anything external. It inserts a gAMA chunk as follows:
- it gets PIL to encode the image into an "in-memory" PNG
- it opens the output file on disk in binary mode
- it then takes the 8-byte PNG signature created by PIL and writes it to disk
- likewise the 25-byte IHDR created by PIL
- then it creates a gAMA chunk with your value, prepends the length, appends a CRC and writes to disk
- then it copies the remainder of the PIL generated image to disk
Note: The PNG Specification stipulates that the signature must come first, then the IHDR chunk. It also stipulates that gAMA must come before IDAT, so there is no reason my code should create an invalid PNG unless:
PIL suddenly starts generating gAMA chunks, which would lead to multiple such chunks and would be illegal - but we are only doing this precisely because PIL does not write gAMA chunks, or
an unreasonable gamma value is provided, or
some aspect of writing to on-disk PNG fails due to disk full or other I/O error.
#!/usr/bin/env python3
import zlib
import struct
from io import BytesIO
from PIL import Image
def savePNGwithGamma(im, filename, gamma):
"""Save the image as PNG with specified gamma"""
# Encode as PNG into memory buffer
buffer = BytesIO()
im.save(buffer, 'PNG')
# Seek back to start of in-memory PNG
buffer.seek(0)
# Open output file in binary mode
with open(filename, 'wb') as fd:
# Read 8-byte PNG signature from memory and write to on-disk PNG
signature = buffer.read(8)
fd.write(signature)
# Read 25-byte PNG IHDR chunk from memory and write to on-disk PNG
IHDR = buffer.read(25)
fd.write(IHDR)
# Create our lovely new gAMA chunk - https://www.w3.org/TR/2003/REC-PNG-20031110/#11gAMA
# Network byte ordering, scale factor of 100000
gAMA = b'gAMA' + struct.pack('!I',int(gamma*100000))
# 4-byte length, gAMA chunk, 4-byte CRC
gAMA = struct.pack('!I', 4) + gAMA + struct.pack('!I', zlib.crc32(gAMA))
# Insert into on-disk PNG after IHDR and before anything else such as PLTE, IDAT, IEND
fd.write(gAMA)
# Write remainder of PNG from memory to disk
fd.write(buffer.read())
################################################################################
# main
################################################################################
# Load sample image
im = Image.open('sample.png')
# Save with desired gamma
savePNGwithGamma(im, 'result.png', 2.2)