.. $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 basic strategy is to create a :class:`~png.Writer` object (instance of
:class:`png.Writer`) and then call its :meth:`png.write` method
with an open (binary) file, and the pixel data.  The :class:`Writer` object
encapsulates all the information about the PNG file: image size, colour,
bit depth, and so on.

A Ramp
^^^^^^

Create a one row image, that has all grey values from 0 to 255.  This is
a bit like Netpbm's ``pgmramp``. ::

  import png
  f = open('ramp.png', 'wb')      # binary mode is important
  w = png.Writer(256, 1, greyscale=True)
  w.write(f, [range(256)])
  f.close()

Note that our single row, generated by ``range(256)``, must itself be
enclosed in a list.  That's because the :meth:`png.write` method expects
a list of rows.


A Little Message
^^^^^^^^^^^^^^^^

A list of strings holds a graphic in ASCII graphic form.  We convert it
to a list of integer lists (the required form for the :meth:`write` method),
and write it out as a black-and-white PNG (bilevel greyscale). ::

  import png
  s = ['110010010011',
       '101011010100',
       '110010110101',
       '100010010011']
  s = [[int(c) for c in row] for row in s]

  w = png.Writer(len(s[0]), len(s), greyscale=True, bitdepth=1)
  f = open('png.png', 'wb')
  w.write(f, s)
  f.close()

Note how we use ``len(s[0])`` (the length of the first row) for the *x*
argument and ``len(s)`` (the number of rows) for the *y* argument.


A Palette
^^^^^^^^^

The previous example, "a little message", can be converted to colour
simply by creating a PNG file with a palette.  The only difference is
that a *palette* argument is passed to the :meth:`write` method instead of
``greyscale=True``::

  import png
  s = ['110010010011',
       '101011010100',
       '110010110101',
       '100010010011']
  s = [[int(c) for c in row] for row in s]

  palette=[(0x55,0x55,0x55), (0xff,0x99,0x99)]
  w = png.Writer(len(s[0]), len(s), palette=palette, bitdepth=1)
  f = open('png.png', 'wb')
  w.write(f, s)

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 generally 3 times as long as
for greyscale, because there are 3 channels, RGB, instead of just
one, grey.  Below, the *p* 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
  p = [(255,0,0, 0,255,0, 0,0,255),
       (128,0,0, 0,128,0, 0,0,128)]
  f = open('swatch.png', 'wb')
  w = png.Writer(3, 2, greyscale=False)
  w.write(f, p)
  f.close()


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()


Reading
-------

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


Reader
^^^^^^

The :meth:`~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 :meth:`png.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 :meth:`read`.  The values in the row are all
integers less than 4, because the image has a bit depth of 2.

NumPy
-----

`NumPy <http://www.numpy.org/>`_ is a package for scientific computing with Python.
It is not part
of a standard Python installation, it is
`downloaded and installed separately <https://pypi.org/project/numpy/>`_
if needed.
Numpy's array manipulation facilities make it good for doing
certain type of image processing, and scientific users of NumPy may wish
to output PNG files for visualisation.

PyPNG does not have any direct integration with NumPy, but the basic
data format used by PyPNG, an iterator over rows, is fairly easy to get
into two- or three-dimensional NumPy arrays.

.. note::

  Using a NumPy array in PyPNG mostly just works.
  Sometimes though you might have a problem.
  An example is that apparently transposing a NumPy array
  means that it then
  `cannot be saved to a PNG using PyPNG <https://github.com/drj11/pypng/issues/91>`_.

  >>> import numpy
  >>> a = numpy.array([[1,2,3],[4,5,6]], dtype=numpy.uint8)
  >>> png.from_array(a, mode="L").save("/tmp/foo.png") # works
  >>> at = a.transpose()
  >>> png.from_array(at, mode="L").save("/tmp/foo.png") # does not work

  When trying to save the transposed array,
  this currently (2019-03) gives a traceback and the error:
  ``TypeError: can't set bytearray slice from numpy.ndarray``.

  That's because in this case the NumPy array cannot be used to
  extend a Python `bytearray` instance.
  Unfortunately it seems difficult to tell which sorts of
  NumPy arrays are going to cause difficulty.

  A workaround is to use ``.copy()`` to copy the NumPy array.

Examples
^^^^^^^^

The code in this section is extracted from ``exnumpy.py``, which is a
complete runnable example in the ``code/`` subdirectory of the source
distribution. Code was originally written by Mel Raab, but has been
hacked around since then.


PNG to NumPy array (reading)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The best thing to do (I think) is to convert each PyPNG row to a
1‑dimensional numpy array, then stack all of those arrays together to
make a 2‑dimensional array.  A number of features make this surprising
compact.  Say `pngdata` is the row iterator returned from
:meth:`png.Reader.asDirect`.  The following code will slurp it into a
2‑dimensional numpy array:

.. literalinclude:: ../code/exnumpy.py
   :start-after: extract 001 start
   :end-before: extract 001 end

Note that the use of ``numpy.uint16``, above, means that an array with
data type ``numpy.uint16`` is created which is suitable for
bit depth 16 images.  Replace ``numpy.uint16`` with ``numpy.uint8`` to
create an array with a byte data type (suitable for bit depths up to 8).

Reshaping
^^^^^^^^^

For some operations it's easier to have the image data in a
3‑dimensional array.  This plays to NumPy's strengths:

.. literalinclude:: ../code/exnumpy.py
   :start-after: extract 002 start
   :end-before: extract 002 end

NumPy array to PNG (writing)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Reshape your NumPy data into a 2‑dimensional array, then use the fact
that a NumPy array is an iterator over its rows:

.. literalinclude:: ../code/exnumpy.py
   :start-after: extract 003 start
   :end-before: extract 003 end

