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