3

I want to write a Python program that makes PNG files. My big problem is with generating the CRC and the data in the IDAT chunk. Python 2.6.4 does have a zlib module, but there are extra settings needed. The PNG specification REQUIRES the IDAT data to be compressed with zlib's deflate method with a window size of 32768 bytes, but I can't find how to set those parameters in the Python zlib module.

As for the CRC for each chunk, the zlib module documentation indicates that it contains a CRC function. I believe that calling that CRC function as crc32(data,-1) will generate the CRC that I need, though if necessary I can translate the C code given in the PNG specification.

Note that I can generate the rest of the PNG file and the data that is to be compressed for the IDAT chunk, I just don't know how to properly compress the image data for the IDAT chunk after implementing the initial filtering step.

EDITED:

The problem with PyPNG is that it will not write tEXt chunks. A minor annoyance is that one has to manipulate the image as (R, G, B) data; I'd prefer to manipulate palette values of the pixels directly and then define the associations between palette values and color data. I'm also left unsure if PyPNG takes advantage of the "compression" allowed by using 1-, 2-, and 4- bit palette values in the image data to fit more than one pixel in a byte.

fagricipni
  • 927
  • 1
  • 7
  • 9
  • Why not use a PNG library instead of a zlib library? – Mark Byers Jan 06 '11 at 05:51
  • As noted in my edit PyPNG will not write tEXt chunks. – fagricipni Jan 06 '11 at 07:36
  • (I'm the maintainer of PyPNG) Palette based images aren't something I play with much, but PyPNG does have pretty much full support for them. Yes, you can do palette images just like you want to, see [this example from the documentation](http://packages.python.org/pypng/ex.html#a-palette) (I agree this might not be clear). Yes you can have 2-bits per pixel palette images and this will pack 4 pixels into a byte. Simply specify bitdepth=2 when creating the png.Writer instance (this is not clear). And of course, that work for other bitdepths too. – David Jones Mar 16 '11 at 09:56
  • You're right that you can't add a tEXt chunk with PyPNG. That's [Issue 4](http://code.google.com/p/pypng/issues/detail?id=4), and now I know someone wants it, I may well add the feature soon. Would you like to work together to create a good API? – David Jones Mar 16 '11 at 09:58

6 Answers6

1

Even if you can't use PyPNG for the tEXt chunk reason, you can use its code! (it's MIT licensed). Here's how a chunk is written:

def write_chunk(outfile, tag, data=''):
    """
    Write a PNG chunk to the output file, including length and
    checksum.
    """

    # http://www.w3.org/TR/PNG/#5Chunk-layout
    outfile.write(struct.pack("!I", len(data)))
    outfile.write(tag)
    outfile.write(data)
    checksum = zlib.crc32(tag)
    checksum = zlib.crc32(data, checksum)
    outfile.write(struct.pack("!i", checksum))

Note the use of zlib.crc32 to create the CRC checksum, and also note how the checksum runs over both the tag and the data.

For compressing the IDAT chunks you basically just use zlib. As others have noted the adler checksum and the default window size are all okay (by the way the PNG spec does not require a window size of 32768, it requires that the window is at most 32768 bytes; this is all a bit odd, because in any case 32768 is the maximum window size permitted by the current version of the zlib spec).

The code to do this in PyPNG is not particular great, see the write_passes() function. The bit that actually compresses the data and writes a chunk is this:

                compressor = zlib.compressobj()
                compressed = compressor.compress(tostring(data))
                if len(compressed):
                    # print >> sys.stderr, len(data), len(compressed)
                    write_chunk(outfile, 'IDAT', compressed)

PyPNG never uses scanline filtering. Partly this is because it would be very slow in Python, partly because I haven't written the code. If you have Python code to do filtering, it would be a most welcome contribution to PyPNG. :)

David Jones
  • 4,766
  • 3
  • 32
  • 45
0

The zlib.crc32 works fine, and the zlib compressor has correct defaults for png generation.

For the casual reader who looks for png generation from Python code, here is a complete example that you can use as a starter for your own png generator code - all you need is the standard zlib module and some bytes-encoding:

#! /usr/bin/python
""" Converts a list of list into gray-scale PNG image. """
__copyright__ = "Copyright (C) 2014 Guido Draheim"
__licence__ = "Public Domain"

import zlib
import struct

def makeGrayPNG(data, height = None, width = None):
    def I1(value):
        return struct.pack("!B", value & (2**8-1))
    def I4(value):
        return struct.pack("!I", value & (2**32-1))
    # compute width&height from data if not explicit
    if height is None:
        height = len(data) # rows
    if width is None:
        width = 0
        for row in data:
            if width < len(row):
                width = len(row)
    # generate these chunks depending on image type
    makeIHDR = True
    makeIDAT = True
    makeIEND = True
    png = b"\x89" + "PNG\r\n\x1A\n".encode('ascii')
    if makeIHDR:
        colortype = 0 # true gray image (no palette)
        bitdepth = 8 # with one byte per pixel (0..255)
        compression = 0 # zlib (no choice here)
        filtertype = 0 # adaptive (each scanline seperately)
        interlaced = 0 # no
        IHDR = I4(width) + I4(height) + I1(bitdepth)
        IHDR += I1(colortype) + I1(compression)
        IHDR += I1(filtertype) + I1(interlaced)
        block = "IHDR".encode('ascii') + IHDR
        png += I4(len(IHDR)) + block + I4(zlib.crc32(block))
    if makeIDAT:
        raw = b""
        for y in xrange(height):
            raw += b"\0" # no filter for this scanline
            for x in xrange(width):
                c = b"\0" # default black pixel
                if y < len(data) and x < len(data[y]):
                    c = I1(data[y][x])
                raw += c
        compressor = zlib.compressobj()
        compressed = compressor.compress(raw)
        compressed += compressor.flush() #!!
        block = "IDAT".encode('ascii') + compressed
        png += I4(len(compressed)) + block + I4(zlib.crc32(block))
    if makeIEND:
        block = "IEND".encode('ascii')
        png += I4(0) + block + I4(zlib.crc32(block))
    return png

def _example():
    with open("cross3x3.png","wb") as f:
        f.write(makeGrayPNG([[0,255,0],[255,255,255],[0,255,0]]))
Guido U. Draheim
  • 3,038
  • 1
  • 20
  • 19
0

Don't you want to use some existing software to generate your PNGs? How about PyPNG?

sjr
  • 9,769
  • 1
  • 25
  • 36
0

There are libraries that can write PNG files for you, such as PIL. That will be easier and faster, and as an added bonus you can read and write tons of formats.

Lennart Regebro
  • 167,292
  • 41
  • 224
  • 251
0

It looks like you will have to resort to call zlib "by hand" using ctypes -- It is not that hard:

>>> import ctypes                                                     
>>> z = ctypes.cdll.LoadLibrary("libzip.so.1")
>>> z.zlibVersion.restype=ctypes.c_char_p
>>> z.zlibVersion()
'1.2.3'

You can check the zlib library docmentation here: http://zlib.net/manual.html

jsbueno
  • 99,910
  • 10
  • 151
  • 209
0

Short answer: (1) "deflate" and "32Kb window" are the defaults (2) uses adler32 not crc32

Long answer:

""" The PNG specification REQUIRES the IDAT data to be compressed with zlib's deflate method with a window size of 32768 bytes, but I can't find how to set those parameters in the Python zlib module. """

You don't need to set them. Those are the defaults.

If you really want to specify non-default arguments to zlib, you can use zlib.compressobj() ... it has several args that are not documented in the Python docs. Reading material:

source: Python's gzip.py (see how it calls zlib.compressobj)

source: Python's zlibmodule.c (see its defaults)

SO: This question (see answers by MizardX and myself, and comments on each)

docs: The manual on the zlib site

"""As for the CRC for each chunk, the zlib module documentation indicates that it contains a CRC function. I believe that calling that CRC function as crc32(data,-1) will generate the CRC that I need, though if necessary I can translate the C code given in the PNG specification."""

Please check out the zlib specification aka RFC 1950 ... it says that the checksum used is adler32

The zlib compress or compressobj output will include the appropriate CRC; why do you think that you will need to do it yourself?

Edit So you do need a CRC-32. Good news: zlib.crc32() will do the job:

Code:

import zlib

crc_table = None

def make_crc_table():
  global crc_table
  crc_table = [0] * 256
  for n in xrange(256):
    c = n
    for k in xrange(8):
        if c & 1:
            c = 0xedb88320L ^ (c >> 1)
        else: 
            c = c >> 1
    crc_table[n] = c

make_crc_table()    

"""
/* Update a running CRC with the bytes buf[0..len-1]--the CRC
should be initialized to all 1's, and the transmitted value
is the 1's complement of the final running CRC (see the
crc() routine below)). */
"""
def update_crc(crc, buf):
  c = crc
  for byte in buf:
    c = crc_table[int((c ^ ord(byte)) & 0xff)] ^ (c >> 8)
  return c

# /* Return the CRC of the bytes buf[0..len-1]. */
def crc(buf):
  return update_crc(0xffffffffL, buf) ^ 0xffffffffL

if __name__ == "__main__":
    tests = [
        "",
        "\x00",
        "\x01",
        "Twas brillig and the slithy toves did gyre and gimble in the wabe",
        ]

    for test in tests:
        model = crc(test) & 0xFFFFFFFFL
        zlib_result = zlib.crc32(test) & 0xFFFFFFFFL
        print (model, zlib_result, model == zlib_result)

Output from Python 2.7 is below. Also tested with Python 2.1 to 2.6 inclusive and 1.5.2 JFTHOI.

(0L, 0L, True)
(3523407757L, 3523407757L, True)
(2768625435L, 2768625435L, True)
(4186783197L, 4186783197L, True)
Community
  • 1
  • 1
John Machin
  • 81,303
  • 11
  • 141
  • 189
  • The **zlib** output contains a adler32 checksum, but the **PNG** specification requires each PNG chunk to have a CRC checksum for the chunk itself. For uncompressed chunks; eg, IHDR, PLTE, and tEXt; this CRC is the only checksum; but for zlib compressed chunks; eg, IDAT, zTXt; it is a bit redundant because the zlib data in the chunk includes its own adler32 checksum, but the CRC is required on all chunks by the PNG specification. I'd _like_ to be able to use an already-written function to calculate this CRC, but, as I have said, I can write the code for the PNG CRC algorithm _if_ I have to. – fagricipni Jan 07 '11 at 01:30
  • I realized that I should note that a description of the PNG CRC algorithm is at http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#CRC-algorithm . In retrospect, it seems obvious to me that I needed to provide the details of the PNG CRC algorithm to say if the Python zlib provided crc32 will correctly generate the CRC needed for PNG chunks. This particular component is not so critical because I _can_ translate the C code given in the link on that page to Python. – fagricipni Jan 07 '11 at 02:14
  • @fagricipni: OK, I get your point about PNG requiring a CRC-32 checksum for its chunks. I'd be very surprised if zlib.crc32 didn't do the job: zlib and PNG come out of the same stable (more or less) and AFAIK there's only one CRC-32 without suffixes/subscripts/superscripts. Note that an implementation in Python of the sample code in the PNG spec is likely to be rather slow. – John Machin Jan 07 '11 at 08:35