PyPNM - Pythonic Portable Any Map I/O
Introduction
Portable Pixel Map (PPM) and Portable Gray Map (PGM) (particular cases on PNM format group) are simplest file formats for RGB and L images, correspondingly. In theory, you may import ASCII PGM even in Excel, turning Excel into image editor. Unfortunately, this simplicity lead to some consequences:
- lack of strict official specification. Instead, you may find words like "usual" in format description. Surely, there is someone who implement this part of image format in unprohibited, yet a totally unusual way;
- unwillingness of many software developers to provide any good support for simple and open format. It took years for almighty Adobe Photoshop developers to include PNM module in distribution rather than count on third-party developers, and surely (see above) they used this chance to implement their own unusual separator scheme nobody else uses. What as to PNM support in Python, say, Pillow... sorry, I promised not to discuss Pillow anywhere ladies and children are allowed to read it.
As a result, novice Python user (like me) may find it difficult to get reliable input/output modules for PPM and PGM image formats; therefore current PyPNM package was developed, combining input/output functions for 8-bits and 16-bits per channel binary and ascii PGM and PPM files, i.e. P2, P5, P3 and P6 PNM file types. P1 and P4 (1 bit) PBM formats are supported only for reading.
PyPNM key functionality comprise:
- conversion of in-memory image representation as 3D nested list into PNM-like byte structure for easy preview with Tkinter PhotoImage(data=...) method;
- reading PPM, PGM and PBM files from disk into image representation as 3D nested list;
- writing image representation as 3D nested list to PPM or PGM file on disk.
(Interlude start) Actually what I needed was small and simple facility for displaying image-like data for image editing/filtering applications like «Averager» from POVThread. Yes, I want to be able to write image filter in Python and view result without huge extra packages. Using Tkinter with PPM/PGM looked like simplest solution, so I created some module for turning nested 3D lists to PPM-like bytes structures in memory. Since it required learning PNM specs anyway, I decided to add PPM/PGM files reading and writing on top of that. Etc., as Kurt Vonnegut kept saying. (Interlude end)
You may easily acquire PyPNM module either from Github or from PyPI and use it absolutely free.
PyPNM version |
Download site |
Download content |
---|---|---|
Current version |
PyPNM module and sample viewer application, |
|
PyPNM module only. |
||
Python 3.4 compatible |
PyPNM module and sample viewer application, |
|
PyPNM module only. |
Update: in ver. 1.15.1.1 image viewing function updated to show LA and RGBA images over chessboard background (like Photoshop and GIMP). This mode requires enabling newly added option, which, in turn, requires RTFM.
Previously on PyPNM: in ver. 1.14.8.19 memory usage for writing PPM and PGM files greatly reduced. Binary files (P6 and P5 correspondingly) are written per image row (which is considered as reasonable tradeoff between memory usage and disk thrashing), ASCII files (P3 and P2) are written per sample (which is minimal memory using scenario).
Format compatibility
Image format | File format | Read | Write |
---|---|---|---|
16 bits per channel RGB | P6 Binary PPM | ✅ | ✅ |
16 bits per channel RGB | P3 ASCII PPM | ✅ | ✅ |
8 bits per channel RGB | P6 Binary PPM | ✅ | ✅ |
8 bits per channel RGB | P3 ASCII PPM | ✅ | ✅ |
16 bits per channel L | P5 Binary PGM | ✅ | ✅ |
16 bits per channel L | P2 ASCII PGM | ✅ | ✅ |
8 bits per channel L | P5 Binary PGM | ✅ | ✅ |
8 bits per channel L | P2 ASCII PGM | ✅ | ✅ |
1 bit ink on/off | P4 Binary PBM | ✅ | ❌ |
1 bit ink on/off | P1 ASCII PBM | ✅ | ❌ |
Image representation
Is seems logical to represent an RGB image as nested 3D structure - X, Y-sized matrix of three-component RGB vectors. Since in Python list seem to be about the only variant for mutable structures like that, it is suitable to represent image as list(list(list(int))) structure (for example, this structure was used for ScaleNx, and worked well). Therefore, it would be convenient to have module read/write image data from/to such a common structure.
Note that for L images memory structure is still list(list(list(int))), with innermost list having only one component, thus enabling further image editing with the same nested Y, X, Z loop regardless of color mode. I understand that this may sound surprising for professional image I/O programmers, but for normal people writing a loop once and for all is expected behaviour.
Also, since this module is supposed to be used for image editing rather than just reading, when reading 1 bit PBM files into image this module promotes data to 8 bit L, inverting values and multiplying by 255, so that source 1 (ink on) is changed to 0 (black), and source 0 (ink off) is changed to 255 (white). For the same reason writing 1 bit PBM files is not planned - 1 bit images are next to useless for editing.
pnmlpnm.py
Module pnmlpnm.py contains 100% pure Python implementation of everything one may need to read/write from/to a variety of PGM and PPM files. I/O functions are written as functions/procedures, as simple as possible, and listed below:
- pnm2list - reading binary or ascii RGB PPM or L PGM file and returning image data as ints and nested list;
- list2bin - getting image data as nested list of ints and creating binary PPM (P6) or PGM (P5) data structure in memory. Suitable for generating data to display with Tkinter;
- list2pnm - getting image data as nested list of ints and writing binary PPM (P6) or PGM (P5) to file;
- list2pnmascii - alternative function to write ASCII PPM (P3) or PGM (P2) files;
- create_image - creating empty nested 3D list for image representation. Not used within this particular module but often needed by programs this module is supposed to be used with.
Detailed functions arguments description is provided in docstrings, but in general looks as simple as that - you feed the function with your image data list and a filename, and get PNM file written; or you feed it to another function and get your image displayed.
viewer.py

Program viewer.py is a small illustrative utility: using pnmlpnm package, it reads different flavours of PGM and PPM files, and allows saving them as different types of PGM/PNM, i.e. it can read ascii PPM and write it as binary PPM or vs. Also this program shows image data using pnmlpnm and Tkinter. No, there is no mistake: it does not feed PPM files to Tkinter directly. Instead, it uses nested 3D list data loaded using pnmlpnm to generate in-memory bytes object of PPM structure using preview_data = pnmlpnm.list2bin(image3D, maxcolors), and then feeds this in-memory bytes object to Tkinter as preview = PhotoImage(data=preview_data) (note using data=, not file=). This way it shows, for example, ascii PPM which Tkinter itself cannot handle.
Fun fact: Icon in viewer.py is not the icon, but bytes of PPM produced with pnmlpnm.py and then hardcoded into viewer as byte string. It's 2x2 pixels of basic colors, so when rescaling Tkinter turns it into a four-point gradient.
All this means that you may use pnmlpnm and Tkinter to visualize any data that can be represented as greyscale or RGB without huge external packages and writing files on disk; all you need is Tkinter, included into standard CPython distributions, and highly compatible pure Python pnmlpnm.py taking only 14 kbytes.
Compatibility issue: Tkinter included in Python 3.10 shows 16 bpc images incorrectly. This problem is not related to PyPNM, and was fixed in higher Python versions distributions.
Now when you have initial understanding of what pnmlpnm module is and how it may be used to work with images in general and PGM and PPM files in particular, it's time to pull it from Github and start working with images yourself.
...or move back to main page.