-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
210 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
@@ -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' | ||
|
@@ -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() | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
||
|
||
|
@@ -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() | ||
|
||
|
Oops, something went wrong.