PyPNG Code Examples

This section discusses some example Python programs that use the png module for reading and writing PNG files.

Writing

The simplest way to write a PNG is to make a 2D array, pass it to png.from_array and save it:

import png

rows = [
    [0, 1, 1, 1, 1, 1, 1, 0],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 1, 1, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [0, 1, 1, 1, 1, 1, 1, 0],
]
image = png.from_array(rows, "L;1")
image.save("smile.png")

Python doesn’t really have 2D arrays, so here i use a list of lists. Those who are already objecting that this isn’t space efficient should be reassured that it is possible to use an array.array which saves space horizontally, and it is also possible to stream using an iterator which saves space vertically.

The "L;1" argument to from_array is the mode. "L" makes a greyscale image, one that has only lightness. The ";1" sets the bitdepth. The bitdepth can be anywhere from 1 (with a maximum channel value of 1) to 16 (with a maximum channel value of 65,535).

The default bitdepth is 8, which has a maximum channel value of 255.

You can write colour RGB images with a mode of “RGB”, and you can add an alpha channel to either mode to get “LA” or “RGBA”.

Writer objects

png.from_array is in some ways a simplification. It uses a png.Writer object and you can use that too. While the png.Writer interface is currently more flexible (you can write extra chunks for example), it is also more complicated.

The basic strategy is to create a Writer object and then call its write method with an open binary (writable) file, and the row data. The Writer object encapsulates all the information about the PNG file: image size, colour, bit depth, and so on.

A Palette

The previous example, a black-and-white smiley face, can be converted to colour by creating a PNG file with a palette.

from_array can’t currently create a PNG file with a palette. But Writer can, by passing a palette argument to the write() method:

import png

rows = [
    [0, 1, 1, 1, 1, 1, 1, 0],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 1, 1, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [0, 1, 1, 1, 1, 1, 1, 0],
]

palette = [(0x55, 0x55, 0x55), (0xFF, 0x99, 0x99)]
w = png.Writer(size=(8, 8), palette=palette, bitdepth=1)
f = open("pal.png", "wb")
w.write(f, rows)

Note that the palette consists of two entries (the bit depth is 1 so there are only 2 possible colours). Each entry is an RGB triple. If we wanted transparency then we can use RGBA 4‑tuples for each palette entry.

Colour

For colour images the input rows are like [ R, G, B, R, G, B, R, G, B, ... ]; a row for w pixels will have 3 * w values in it, because there are 3 channels, RGB. Below, the rows literal has 2 rows of 9 values (3 RGB pixels per row). The spaces are just for your benefit, to mark out the separate pixels; they have no meaning in the code.

import png
rows = [(255,0,0, 0,255,0, 0,0,255),
        (128,0,0, 0,128,0, 0,0,128)]
image = png.from_array(rows, "RGB")
image.save("swatch.png")

More Colour

A further colour example illustrates some of the manoeuvres you have to perform in Python to get the pixel data in the right format.

Say we want to produce a PNG image with 1 row of 8 pixels, with all the colours from a 3‑bit colour system (with 1‑bit for each channel; such systems were common on 8‑bit micros from the 1980s).

We want 8 (R, G, B) triples: 0 0 0, 0 0 1, 0 1 0, and so on. itertools to the rescue! That’s:

>>> list(itertools.product([0,1], repeat=3))
[(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)]

Here we have each pixel as a tuple. We want to flatten the pixels so that we have just one row. In other words instead of [(R,G,B), (R,G,B), ...] we want [R,G,B,R,G,B,...]. itertools again to the rescue! it turns out that itertools.chain(*...) is just what we need:

>>> pixels = itertools.product([0,1], repeat=3)
>>> list(itertools.chain(*pixels))
[0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1]

Note that the list is not necessary, we can usually use the iterator directly instead. I just used list here so we can see the result. Now we can pass a single row image to png.from_array:

>>> row = list(itertools.chain(*itertools.product([0,1], repeat=3)))
>>> png.from_array([row], "RGB;1").save("rgb.png")

Random Colour

The previous example show a powerful technique: programmatically computing each pixel. We continue the theme here. This example is suggested by https://github.com/SuperArcherG)

A 64×64 tile with randomly selected RGB colours is written to the file random.png.

# An example program that create a random 64×64 RGB PNG file.
# Inspired by https://github.com/drj11/pypng/issues/120
import random

import png

width = 64
height = 64

# values per row
vpr = 3 * width

# Create a 2D matrix, a sequence of rows. Each row has vpr values.
m = [[0] * vpr for y_ in range(height)]

for y in range(len(m)):
    for x in range(len(m[y])):
        m[y][x] = random.randint(0, 255)

png.from_array(m, "RGB").save("random.png")

This example is also available in the file code/exrandom.py.

Reading

The basic strategy is to create a Reader object (a png.Reader instance), then call its read method to extract the size, and pixel data.

Reader

The Reader constructor can take either a filename, a file-like object, or a sequence of bytes directly. Here we use urllib to download a PNG file from the internet.

>>> r=png.Reader(file=urllib.urlopen('http://www.schaik.com/pngsuite/basn0g02.png'))
>>> r.read()
(32, 32, <itertools.imap object at 0x10b7eb0>, {'greyscale': True,
'alpha': False, 'interlace': 0, 'bitdepth': 2, 'gamma': 1.0})

The read method returns a 4‑tuple consisting of:

  • width: Width of PNG image in pixels;

  • height: Height of PNG image in pixes;

  • rows: A sequence or iterator for the row data;

  • info: An info dictionary containing much of the image metadata.

Note that the pixels are returned as an iterator or a sequence. Generally if PyPNG can manage to efficiently return a row iterator then it will, but at other times it will return a sequence.

>>> l=list(_[2])
>>> l[0]
array('B', [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 0, 0, 0, 0,
1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3])

We have extracted the top row of the image. Note that the row itself is an array (see module array), but in general any suitable sequence type may be returned by read. The values in the row are all integers less than 4, because the image has a bit depth of 2.

NumPy

NumPy is a package for scientific computing with Python. It is not part of a standard Python installation, and therefore only gets minimal support from PyPNG.

For reading, consider vstack:

numpy.vstack([numpy.uint8(row) for row in rows])

That is usually sufficient to read all the rows into a NumPy array.

For writing a NumPy array when 2-Dimensional (rank 2) can often be used as an iterator of rows. However, there are some caveats, because NumPy arrays do not behave like Python sequences in the right way in all circumstances (for example after a resshape or a transpose). Usually a sufficient workaround is to .copy() the Numpy array or convert to a true Python array.array array.

There used to be more examples here, but frankly i’m fed up with NumPy changing details of their types in ways that break compatibility with Python types, so i’m done.