blob: 6aa558e8631d883530a0fd6e80883398c010349b [file]
.. $URL$
.. $Rev$
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
<https://docs.python.org/3/library/array.html#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 `~png.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 `~png.Writer` object and then call its `~png.Writer.write` method
with an open binary (writable) file, and the row data.
The `~png.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.
`~png.from_array` can't currently create a PNG file with a
palette.
But `~png.Writer` can,
by passing a *palette* argument to the :meth:`~png.Writer.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 produce all possible 3‑bit numbers:
>>> list(range(8))
[0, 1, 2, 3, 4, 5, 6, 7]
We can convert each number into an RGB triple by assigning bit 0 to
blue, bit 1 to red, bit 2 to green (the convention used by a certain
8‑bit micro):
>>> [(bool(x&2), bool(x&4), bool(x&1)) for x in _]
[(False, False, False), (False, False, True), (True, False, False),
(True, False, True), (False, True, False), (False, True, True), (True,
True, False), (True, True, True)]
(later on we will convert False into 0, and True into 255, so don't
worry about that just yet). 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,...]. It turns
out that ``itertools.chain(*...)`` is just what we need:
>>> list(itertools.chain(*_))
[False, False, False, False, False, True, True, False, False, True,
False, True, False, True, False, False, True, True, True, True, False,
True, True, True]
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 to convert False to 0 and True to 255 we can multiply by 255
(Python use's Iverson's convention, so ``False==0``, ``True==1``).
We could do that with ``map(lambda x:255*x, _)``. Or, we could use a
"magic" bound method:
>>> list(map((255).__mul__, _))
[0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255,
255, 255, 0, 255, 255, 255]
Now we write the PNG file out:
>>> p=_
>>> f=open('speccy.png', 'wb')
>>> w.write(f, [p]) ; f.close()
Random Colour
^^^^^^^^^^^^^
(an example 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 `~png.Reader` object (a
`png.Reader` instance), then call its `~png.Reader.read` method
to extract the size, and pixel data.
Reader
^^^^^^
The `~png.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 `~png.Reader.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 `~png.Reader.read`. The values in the row are all
integers less than 4, because the image has a bit depth of 2.
NumPy
-----
`NumPy <https://numpy.org/>`_ 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 <https://docs.python.org/3/library/array.html#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.