0

I want to read, manipulate then save a png image while keeping its original info properties mainly gamma. I'm using Pillow version 9.0.1

Code from SO mentions it can be done like this:

from PIL import Image
img = Image.open("sample.png") # Sample image provided below code block
info = img.info
img.save("output.png", **info)

Sample image

But info does not carry over; the images no longer look alike due to the loss of the gamma info and further evident using the test:

# Test
output_info = Image.open("output.png").info
print(info)                  # {'gamma': 2147.48365}
print(output_info)           # {}
print(output_info == info)   # False, should be True

The question: Why does PIL's Image.save not write gamma?

It's not specific to gamma, no other chunk get written like chromaticity and text.


sample.png compared to output.png (as viewed using a gAMA-aware viewer, like chromium) original.png on the left, output.png on the right

freshpasta
  • 556
  • 2
  • 11
  • *"... without using PngInfo..."* do you mean the `PngInfo` within PIL? And what's the objection to that please? – Mark Setchell Mar 24 '22 at 07:40
  • @MarkSetchell I don't want to wrangle bytes and deal with standards. ```Image.save```'s ```pnginfo``` parameter should be enough, but it isn't. And I'm asking why that is the case. I think I might've implied that I need a solution to embed gamma info which I don't. – freshpasta Mar 24 '22 at 14:50
  • Maybe raise an issue on the PIL Github repository in that case... and add a link here for other folks to find. – Mark Setchell Mar 24 '22 at 15:32
  • @MarkSetchell Will do. Just thought this case had different documented semantics I just didn't know of. Thanks. – freshpasta Mar 24 '22 at 15:46

2 Answers2

0

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)
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • I really do appreciate the high effort answer but that's not what I'm looking for. I have read [your answer](https://stackoverflow.com/a/62387041/17595968) from 2 years ago where you mention "I don't know why Pillow is failing to save your gamma info" which is what I want to know. I apologize, my question might've not been clear. – freshpasta Mar 24 '22 at 15:05
0

Looks like my expectations were wrong. PIL's PNG file writer does not support a gamma keyword argument therefore it is being silently ignored. Exactly as documented:

The save() method supports the following options:

  • Image.save ignores unrecognized keyword arguments:

Keyword options can be used to provide additional instructions to the writer. If a writer doesn’t recognise an option, it is silently ignored.

freshpasta
  • 556
  • 2
  • 11