.. $URL: https://pypng.googlecode.com/svn/trunk/man/ex.rst $
.. $Rev: 22 $

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(255, 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.

From now on ``import png`` will not be mentioned.


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). ::

  s = ['110010010011',
       '101011010100',
       '110010110101',
       '100010010011']
  s = map(lambda x: map(int, x), s)

  f = open('png.png', 'wb')
  w = png.Writer(len(s[0]), len(s), greyscale=True, bitdepth=1)
  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``::

  # Assume f and s have been set up as per previous example
  palette=[(0x55,0x55,0x55), (0xff,0x99,0x99)]
  w = png.Writer(len(s[0]), len(s), palette=palette, bitdepth=1)
  f.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. ::

  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)
  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 imag 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:

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

>>> map(lambda x: (bool(x&2), bool(x&4), bool(x&1)), _)
[(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:

>>> 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.


PngSuite
^^^^^^^^

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.  Note that the pixels are
returned as an iterator (not always, and the interface doesn't guarantee it;
the returned value might be an iterator or 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.
