Skip to content

Commit

Permalink
1.12.14.1
Browse files Browse the repository at this point in the history
  • Loading branch information
Dnyarri committed Dec 31, 2024
1 parent a0a07c3 commit d9769ca
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 80 deletions.
49 changes: 37 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
# PyPNM - PPM and PGM image files reading and writing

## Overview
## Overview and justification

PPM and PGM (particular cases of PNM format group) are simplest file formats for RGB and L images, correspondingly. This simplicity lead to some adverse 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 to 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 a separator scheme nobody else uses. What as to PNM support in Python, say, Pillow... sorry, I promised not to mention 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. Yes, right, I mean it: both greyscale and RGB are supported with 16-bit per channel color depth (0...65535 range) directly, i.e. without any reconversion to weird data type.
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 module 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. Both greyscale and RGB with 16-bit per channel color depth (0...65535 range) are supported directly, without limitations and without dancing with tambourine and proclaiming it to be a novel method.

## Target Image representation
## Format compatibility

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. Therefore, it would be convenient to have module read/write image data to/from such a structure. Note that for L images memory structure is still list(list(list(int))), just innermost list have only one component.
Current PyPNM module read and write capabilities are briefly summarized below.

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

## Target image representation

Main goal of module under discussion is not just bytes reading and writing but representing image as some logically organized structure for further image editing.

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. Therefore, it would be convenient to have module read/write image data to/from such a 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.

Note that for the same reason 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).

## Installation

Expand All @@ -32,8 +55,8 @@ In case you downloaded file **pnmlpnm.py** from Github or somewhere else as plai

Module file **pnmlpnm.py** contains 100% pure Python implementation of everything one may need to read/write 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 ints and nested list and creating binary PPM (P6) or PGM (P5) data structure in memory. Suitable for generating data to display with Tkinter.
- **pnm2list** - reading binary or ascii RGB PPM or L PGM file and returning image data as nested list of int.
- **list2bin** - getting image data as nested list of int and creating binary PPM (P6) or PGM (P5) data structure in memory. Suitable for generating data to display with Tkinter.
- **list2pnm** - writing data created with list2bin 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.
Expand Down Expand Up @@ -89,14 +112,16 @@ Program **viewer.py** is a small illustrative utility: using *pnmlpnm* package,

[![Example of ascii ppm opened in viewer.py and converted to binary ppm on the fly to be rendered with Tkinter](https://dnyarri.github.io/pypnm/viewer.png)](https://dnyarri.github.io/pypnm.html)

As a result, 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 12 kbytes.
As a result, 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 16 kbytes.

## References

### References
1. [Netpbm file formats description](https://netpbm.sourceforge.net/doc/).

[Netpbm file formats description](https://netpbm.sourceforge.net/doc/)
2. [PyPNM at PyPI](https://pypi.org/project/PyPNM/) - installing PyPN with pip. Does not contain viewer example etc., only core converter.

[PyPNM at PyPI](https://pypi.org/project/PyPNM/) installing PyPN with pip. Does not provide example etc., only core converter.
3. [PyPNM at Github](https://github.com/Dnyarri/PyPNM/) containing example viewer application, illustrating using `list2bin` to produce data for Tkinter `PhotoImage(data=...)` to display, and of open/save various portable map formats.

[PyPNM at Github](https://github.com/Dnyarri/PyPNM) containing example application of using `list2bin` to produce data for Tkinter `PhotoImage(data=...)` to display, and examples of open/save.
4. [PixelArtScaling](https://github.com/Dnyarri/PixelArtScaling/) - usage example, pure Python image rescaling applications using Scale2x and Scale3x, PNG I/O is based on [PyPNG](https://gitlab.com/drj11/pypng), and PPM/PGM I/O - on [PyPNM](https://pypi.org/project/PyPNM/), thus making all applications cross-platform.

[PixelArtScaling](https://github.com/Dnyarri/PixelArtScaling) - image rescaling applications using Scale2x and Scale3x, PNG I/O is based on PyPNG, and PPM/PGM I/O - on current PyPNM. That is, everything is based on standard Python and therefore quite OS-independent.
5. [POVRay Thread: Linen and Stitch](https://dnyarri.github.io/povthread.html) - usage example, contains image filtering application «Averager», implementing non-standard adaptive image averaging. Filter before/after preview based on statically linked PyPNM list2bin code and Tkinter `PhotoImage(data=...)` class.
205 changes: 155 additions & 50 deletions pypnm/pnmlpnm.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env python3

"""Functions to read PPM and PGM files to nested 3D list and write back.
"""Functions to read PPM and PGM files to nested 3D list of int and/or write back.
Overview
----------
pnmlpnm (pnm-list-pnm) is a pack of functions for dealing with PPM and PGM image files. Functions included are:
- 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 ints and nested list and creating binary PPM (P6) or PGM (P5) data structure in memory. Suitable for generating data to display with Tkinter.
- pnm2list - reading binary or ascii RGB PPM or L PGM file and returning image data as nested list of int.
- list2bin - getting image data as nested list of int and creating binary PPM (P6) or PGM (P5) data structure in memory. Suitable for generating data to display with Tkinter `PhotoImage(data=...)` class.
- list2pnm - writing data created with list2bin 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.
Expand Down Expand Up @@ -57,30 +57,38 @@
Netpbm specs: https://netpbm.sourceforge.net/doc/
History:
---------
PyPNM at PyPI: https://pypi.org/project/PyPNM/
PyPNM at GitHub: https://github.com/Dnyarri/PyPNM/
Version history
----------------
0.11.26.0 Initial working version 26 Nov 2024.
0.11.27.3 Implemented fix for Adobe Photoshop CS6 using linebreaks in header.
0.11.27.3 Implemented fix for Adobe Photoshop CS6 using linebreaks instead of spaces in header.
0.11.28.0 Rewritten to use less arguments for output; X, Y, Z autodetected.
0.11.29.0 Added ASCII write support.
0.11.30.0 Switched to array; this allowed 16 bpc P5 and P6 files writing.
0.11.30.0 Switched to array, thus allowing 16 bpc P5 and P6 files writing.
0.11.30.2 Fixed 16 bpc P5 and P6 files reading. Solution looks ugly but works.
1.12.1.2 Initial public release.
0.11.30.2 Seems like finally fixed 16 bpc P5 and P6 files reading. Looks ugly but works.
1.12.12.1 PBM read support added. PBM write is not planned.
1.12.1.2 Seem to be ready for release.
1.12.14.1 Reoptimized to comprehensions.
"""

__author__ = 'Ilya Razmanov'
__copyright__ = '(c) 2024 Ilya Razmanov'
__credits__ = 'Ilya Razmanov'
__license__ = 'unlicense'
__version__ = '1.12.12.1'
__version__ = '1.12.14.1'
__maintainer__ = 'Ilya Razmanov'
__email__ = '[email protected]'
__status__ = 'Production'
Expand All @@ -107,7 +115,7 @@ def pnm2list(filename: str) -> tuple[int, int, int, int, list[list[list[int]]]]:
"""

magic_list = ['P6', 'P3', 'P5', 'P2']
magic_list = ['P6', 'P3', 'P5', 'P2', 'P4', 'P1']

with open(filename, 'rb') as file: # Open file in binary mode
magic = file.readline().strip().decode()
Expand All @@ -120,41 +128,48 @@ def pnm2list(filename: str) -> tuple[int, int, int, int, list[list[list[int]]]]:
while comment_line.startswith('#'):
comment_line = file.readline().decode()

# Reading dimensions. Photoshop CS6 uses EOLN as separator, GIMP, XnView etc. use space
size_temp = comment_line.split()
if len(size_temp) < 2: # Part for Photoshop
X = int(size_temp[0])
Y = int(file.readline().decode())
else: # Part for most other software
X, Y = map(int, comment_line.split())

# Color depth
maxcolors = int(file.readline().strip().decode())

''' ┌─────┐
│ RGB │
└────-┘ '''

if magic == 'P6': # RGB bin

# Reading dimensions. Photoshop CS6 uses EOLN as separator, GIMP, XnView etc. use space
size_temp = comment_line.split()
if len(size_temp) < 2: # Part for Photoshop
X = int(size_temp[0])
Y = int(file.readline().decode())
else: # Part for most other software
X, Y = map(int, comment_line.split())

# Color depth
maxcolors = int(file.readline().strip().decode())

# Channel number
Z = 3
list_3d = []
for _ in range(Y):
row = []
for _ in range(X):

if maxcolors < 256:
red = int.from_bytes(file.read(1))
green = int.from_bytes(file.read(1))
blue = int.from_bytes(file.read(1))
else:
red = int.from_bytes(file.read(2))
green = int.from_bytes(file.read(2))
blue = int.from_bytes(file.read(2))
# Building 3D list of ints, converted from bytes
list_3d = [
[
[int.from_bytes(file.read(1)), int.from_bytes(file.read(1)), int.from_bytes(file.read(1))] if (maxcolors < 256) else [int.from_bytes(file.read(2)), int.from_bytes(file.read(2)), int.from_bytes(file.read(2))] # Consecutive reading of R, G, B
for x in range(X)
] for y in range(Y)
]

if magic == 'P3': # RGB ASCII

row.append([red, green, blue])
list_3d.append(row)
# Reading dimensions. Photoshop CS6 uses EOLN as separator, GIMP, XnView etc. use space
size_temp = comment_line.split()
if len(size_temp) < 2: # Part for Photoshop
X = int(size_temp[0])
Y = int(file.readline().decode())
else: # Part for most other software
X, Y = map(int, comment_line.split())

if magic == 'P3': # RGB ascii
# Color depth
maxcolors = int(file.readline().strip().decode())

# Channel number
Z = 3

list_1d = [] # Toss everything to 1D list because linebreaks in PNM are unpredictable
Expand All @@ -175,19 +190,43 @@ def pnm2list(filename: str) -> tuple[int, int, int, int, list[list[list[int]]]]:
└───┘ '''

if magic == 'P5': # L bin

# Reading dimensions. Photoshop CS6 uses EOLN as separator, GIMP, XnView etc. use space
size_temp = comment_line.split()
if len(size_temp) < 2: # Part for Photoshop
X = int(size_temp[0])
Y = int(file.readline().decode())
else: # Part for most other software
X, Y = map(int, comment_line.split())

# Color depth
maxcolors = int(file.readline().strip().decode())

# Channel number
Z = 1
list_3d = []
for _ in range(Y):
row = []
for _ in range(X):
if maxcolors < 256:
channel = [int.from_bytes(file.read(1))]
else:
channel = [int.from_bytes(file.read(2))]
row.append(channel)
list_3d.append(row)

if magic == 'P2': # L ascii
# Building 3D list of ints, converted from bytes
list_3d = [
[
[
int.from_bytes(file.read(1)) if (maxcolors < 256) else int.from_bytes(file.read(2))
] for x in range(X)
] for y in range(Y)
]

if magic == 'P2': # L ASCII

# Reading dimensions. Photoshop CS6 uses EOLN as separator, GIMP, XnView etc. use space
size_temp = comment_line.split()
if len(size_temp) < 2: # Part for Photoshop
X = int(size_temp[0])
Y = int(file.readline().decode())
else: # Part for most other software
X, Y = map(int, comment_line.split())

# Color depth
maxcolors = int(file.readline().strip().decode())

# Channel number
Z = 1

list_1d = [] # Toss everything to 1D list because linebreaks in ASCII PGM are unpredictable
Expand All @@ -203,6 +242,72 @@ def pnm2list(filename: str) -> tuple[int, int, int, int, list[list[list[int]]]]:
] for y in range(Y)
]


''' ┌─────┐
│ Bit │
└────-┘ '''

if magic == 'P4': # Bit bin
# Reading dimensions. Photoshop CS6 uses EOLN as separator, GIMP, XnView etc. use space
size_temp = comment_line.split()
if len(size_temp) < 2: # Part for Photoshop
X = int(size_temp[0])
Y = int(file.readline().decode())
else: # Part for most other software
X, Y = map(int, comment_line.split())

# Color depth
maxcolors = 255 # Force conversion from bit to L

# Channel number
Z = 1

raw_data = file.read() # Reading the rest of file

row_width = (X + 7) // 8 # Rounded up version of width, to get whole bytes included junk at EOLNs

list_3d = []
for y in range(Y):
row = []
for x in range(row_width):
single_byte = raw_data[(y * row_width) + x]
single_byte_bits = [int(bit) for bit in bin(single_byte)[2:].zfill(8)]
single_byte_bits_normalized = [[255 * (1 - c)] for c in single_byte_bits] # renormalizing colors from ink on/off to L model, replacing int with [int]
row.extend(single_byte_bits_normalized) # assembling row, junk included

list_3d.append(row[0:X]) # apparently cutting junk off

if magic == 'P1': # Bit ASCII

# Reading dimensions. Photoshop CS6 uses EOLN as separator, GIMP, XnView etc. use space
size_temp = comment_line.split()
if len(size_temp) < 2: # Part for Photoshop
X = int(size_temp[0])
Y = int(file.readline().decode())
else: # Part for most other software
X, Y = map(int, comment_line.split())

# Color depth
maxcolors = 255 # Force conversion from bit to L

# Channel number
Z = 1

list_1d = [] # Toss everything to 1D list because linebreaks in ASCII PBM are unpredictable
for y in file:
row_data = y.strip()
bits = [(255 * (1 - int(row_data[i : i + 1]))) for i in range(0, len(row_data), 1)]
list_1d.extend(bits)

list_3d = [ # Now break 1D toss into component compounds, building 3D list
[
[
list_1d[z + x * Z + y * X * Z] for z in range(Z)
] for x in range(X)
] for y in range(Y)
]


return (X, Y, Z, maxcolors, list_3d) # Output mimic that of pnglpng


Expand Down Expand Up @@ -260,7 +365,7 @@ def list2bin(in_list_3d: list[list[list[int]]], maxcolors: int) -> bytes:
header = array.array('B', f'{magic}\n{X} {Y}\n{maxcolors}\n'.encode())
content = array.array(datatype, in_list_1d)

content.byteswap() # Critical!
content.byteswap() # Critical for 16 bits per channel

pnm = header.tobytes() + content.tobytes()

Expand Down
Loading

0 comments on commit d9769ca

Please sign in to comment.