356 lines
10 KiB
Python
Executable File
356 lines
10 KiB
Python
Executable File
#!/home/mongar/Escritorio/pruebas_oc/venv/bin/python3
|
|
|
|
# pripamtopng
|
|
#
|
|
# Python Raster Image PAM to PNG
|
|
|
|
import array
|
|
import struct
|
|
import sys
|
|
|
|
import png
|
|
|
|
Description = """Convert NetPBM PAM/PNM format files to PNG."""
|
|
|
|
|
|
def read_pam_header(infile):
|
|
"""
|
|
Read (the rest of a) PAM header.
|
|
`infile` should be positioned immediately after the initial 'P7' line
|
|
(at the beginning of the second line).
|
|
Returns are as for `read_pnm_header`.
|
|
"""
|
|
|
|
# Unlike PBM, PGM, and PPM, we can read the header a line at a time.
|
|
header = dict()
|
|
while True:
|
|
line = infile.readline().strip()
|
|
if line == b"ENDHDR":
|
|
break
|
|
if not line:
|
|
raise EOFError("PAM ended prematurely")
|
|
if line[0] == b"#":
|
|
continue
|
|
line = line.split(None, 1)
|
|
key = line[0]
|
|
if key not in header:
|
|
header[key] = line[1]
|
|
else:
|
|
header[key] += b" " + line[1]
|
|
|
|
required = [b"WIDTH", b"HEIGHT", b"DEPTH", b"MAXVAL"]
|
|
required_str = b", ".join(required).decode("ascii")
|
|
result = []
|
|
for token in required:
|
|
if token not in header:
|
|
raise png.Error("PAM file must specify " + required_str)
|
|
try:
|
|
x = int(header[token])
|
|
except ValueError:
|
|
raise png.Error(required_str + " must all be valid integers")
|
|
if x <= 0:
|
|
raise png.Error(required_str + " must all be positive integers")
|
|
result.append(x)
|
|
|
|
return (b"P7",) + tuple(result)
|
|
|
|
|
|
def read_pnm_header(infile):
|
|
"""
|
|
Read a PNM header, returning (format,width,height,depth,maxval).
|
|
Also reads a PAM header (by using a helper function).
|
|
`width` and `height` are in pixels.
|
|
`depth` is the number of channels in the image;
|
|
for PBM and PGM it is synthesized as 1, for PPM as 3;
|
|
for PAM images it is read from the header.
|
|
`maxval` is synthesized (as 1) for PBM images.
|
|
"""
|
|
|
|
# Generally, see http://netpbm.sourceforge.net/doc/ppm.html
|
|
# and http://netpbm.sourceforge.net/doc/pam.html
|
|
|
|
# Technically 'P7' must be followed by a newline,
|
|
# so by using rstrip() we are being liberal in what we accept.
|
|
# I think this is acceptable.
|
|
magic = infile.read(3).rstrip()
|
|
if magic == b"P7":
|
|
# PAM header parsing is completely different.
|
|
return read_pam_header(infile)
|
|
|
|
# Expected number of tokens in header (3 for P4, 4 for P6)
|
|
expected = 4
|
|
pbm = (b"P1", b"P4")
|
|
if magic in pbm:
|
|
expected = 3
|
|
header = [magic]
|
|
|
|
# We must read the rest of the header byte by byte because
|
|
# the final whitespace character may not be a newline.
|
|
# Of course all PNM files in the wild use a newline at this point,
|
|
# but we are strong and so we avoid
|
|
# the temptation to use readline.
|
|
bs = bytearray()
|
|
backs = bytearray()
|
|
|
|
def next():
|
|
if backs:
|
|
c = bytes(backs[0:1])
|
|
del backs[0]
|
|
else:
|
|
c = infile.read(1)
|
|
if not c:
|
|
raise png.Error("premature EOF reading PNM header")
|
|
bs.extend(c)
|
|
return c
|
|
|
|
def backup():
|
|
"""Push last byte of token onto front of backs."""
|
|
backs.insert(0, bs[-1])
|
|
del bs[-1]
|
|
|
|
def ignore():
|
|
del bs[:]
|
|
|
|
def tokens():
|
|
ls = lexInit
|
|
while True:
|
|
token, ls = ls()
|
|
if token:
|
|
yield token
|
|
|
|
def lexInit():
|
|
c = next()
|
|
# Skip comments
|
|
if b"#" <= c <= b"#":
|
|
while c not in b"\n\r":
|
|
c = next()
|
|
ignore()
|
|
return None, lexInit
|
|
# Skip whitespace (that precedes a token)
|
|
if c.isspace():
|
|
ignore()
|
|
return None, lexInit
|
|
if not c.isdigit():
|
|
raise png.Error("unexpected byte %r found in header" % c)
|
|
return None, lexNumber
|
|
|
|
def lexNumber():
|
|
# According to the specification it is legal to have comments
|
|
# that appear in the middle of a token.
|
|
# I've never seen it; and,
|
|
# it's a bit awkward to code good lexers in Python (no goto).
|
|
# So we break on such cases.
|
|
c = next()
|
|
while c.isdigit():
|
|
c = next()
|
|
backup()
|
|
token = bs[:]
|
|
ignore()
|
|
return token, lexInit
|
|
|
|
for token in tokens():
|
|
# All "tokens" are decimal integers, so convert them here.
|
|
header.append(int(token))
|
|
if len(header) == expected:
|
|
break
|
|
|
|
final = next()
|
|
if not final.isspace():
|
|
raise png.Error("expected header to end with whitespace, not %r" % final)
|
|
|
|
if magic in pbm:
|
|
# synthesize a MAXVAL
|
|
header.append(1)
|
|
depth = (1, 3)[magic == b"P6"]
|
|
return header[0], header[1], header[2], depth, header[3]
|
|
|
|
|
|
def convert_pnm_plain(w, infile, outfile):
|
|
"""
|
|
Convert a plain PNM file containing raw pixel data into
|
|
a PNG file with the parameters set in the writer object.
|
|
Works for plain PGM formats.
|
|
"""
|
|
|
|
# See convert_pnm_binary for the corresponding function for
|
|
# binary PNM formats.
|
|
|
|
rows = scan_rows_from_file_plain(infile, w.width, w.height, w.planes)
|
|
w.write(outfile, rows)
|
|
|
|
|
|
def scan_rows_from_file_plain(infile, width, height, planes):
|
|
"""
|
|
Generate a sequence of rows from the input file `infile`.
|
|
The input file should be in a "Netpbm-like" plain format.
|
|
The input file should be positioned at the beginning of the
|
|
first value (that is, immediately after the header).
|
|
The number of pixels to read is taken from
|
|
the image dimensions (`width`, `height`, `planes`).
|
|
|
|
Each row is yielded as a single sequence of values.
|
|
"""
|
|
|
|
# Values per row
|
|
vpr = width * planes
|
|
|
|
values = []
|
|
rows_output = 0
|
|
|
|
# The core problem is that input lines (text lines) may not
|
|
# correspond with pixel rows. We use two nested loops.
|
|
# The outer loop reads the input one text line at a time;
|
|
# this will contain a whole number of values, which are
|
|
# added to the `values` list.
|
|
# The inner loop strips the first `vpr` values from the
|
|
# list, until there aren't enough.
|
|
# Note we can't tell how many iterations the inner loop will
|
|
# run for, it could be 0 (if not enough values were read to
|
|
# make a whole pixel row) or many (if the entire image were
|
|
# on one input line), or somewhere in between.
|
|
# In PNM there is in general no requirement to have
|
|
# correspondence between text lines and pixel rows.
|
|
|
|
for inp in infile:
|
|
values.extend(map(int, inp.split()))
|
|
while len(values) >= vpr:
|
|
yield values[:vpr]
|
|
del values[:vpr]
|
|
rows_output += 1
|
|
if rows_output >= height:
|
|
# Diagnostic here if there are spare values?
|
|
return
|
|
# Diagnostic here for early EOF?
|
|
|
|
|
|
def convert_pnm_binary(w, infile, outfile):
|
|
"""
|
|
Convert a PNM file containing raw pixel data into
|
|
a PNG file with the parameters set in the writer object.
|
|
Works for (binary) PGM, PPM, and PAM formats.
|
|
"""
|
|
|
|
rows = scan_rows_from_file(infile, w.width, w.height, w.planes, w.bitdepth)
|
|
w.write(outfile, rows)
|
|
|
|
|
|
def scan_rows_from_file(infile, width, height, planes, bitdepth):
|
|
"""
|
|
Generate a sequence of rows from the input file `infile`.
|
|
The input file should be in a "Netpbm-like" binary format.
|
|
The input file should be positioned at the beginning of the first pixel.
|
|
The number of pixels to read is taken from
|
|
the image dimensions (`width`, `height`, `planes`);
|
|
the number of bytes per value is implied by `bitdepth`.
|
|
Each row is yielded as a single sequence of values.
|
|
"""
|
|
|
|
# Values per row
|
|
vpr = width * planes
|
|
# Bytes per row
|
|
bpr = vpr
|
|
if bitdepth > 8:
|
|
assert bitdepth == 16
|
|
bpr *= 2
|
|
fmt = ">%dH" % vpr
|
|
|
|
def line():
|
|
return array.array("H", struct.unpack(fmt, infile.read(bpr)))
|
|
|
|
else:
|
|
|
|
def line():
|
|
return array.array("B", infile.read(bpr))
|
|
|
|
for y in range(height):
|
|
yield line()
|
|
|
|
|
|
def parse_args(args):
|
|
"""
|
|
Create a parser and parse the command line arguments.
|
|
"""
|
|
from argparse import ArgumentParser
|
|
|
|
parser = ArgumentParser(description=Description)
|
|
version = "%(prog)s " + png.__version__
|
|
parser.add_argument("--version", action="version", version=version)
|
|
parser.add_argument(
|
|
"-c",
|
|
"--compression",
|
|
type=int,
|
|
metavar="level",
|
|
help="zlib compression level (0-9)",
|
|
)
|
|
parser.add_argument(
|
|
"input",
|
|
nargs="?",
|
|
default="-",
|
|
type=png.cli_open,
|
|
metavar="PAM/PNM",
|
|
help="input PAM/PNM file to convert",
|
|
)
|
|
args = parser.parse_args(args)
|
|
return args
|
|
|
|
|
|
def main(argv=None):
|
|
if argv is None:
|
|
argv = sys.argv
|
|
|
|
args = parse_args(argv[1:])
|
|
|
|
# Prepare input and output files
|
|
infile = args.input
|
|
|
|
# Call after parsing, so that --version and --help work.
|
|
outfile = png.binary_stdout()
|
|
|
|
# Encode PNM to PNG
|
|
format, width, height, depth, maxval = read_pnm_header(infile)
|
|
|
|
ok_formats = (b"P2", b"P5", b"P6", b"P7")
|
|
if format not in ok_formats:
|
|
raise NotImplementedError("file format %s not supported" % format)
|
|
|
|
# The NetPBM depth (number of channels) completely
|
|
# determines the PNG format.
|
|
# Observe:
|
|
# - L, LA, RGB, RGBA are the 4 modes supported by PNG;
|
|
# - they correspond to 1, 2, 3, 4 channels respectively.
|
|
# We use the number of channels in the source image to
|
|
# determine which one we have.
|
|
# We ignore the NetPBM image type and the PAM TUPLTYPE.
|
|
greyscale = depth <= 2
|
|
pamalpha = depth in (2, 4)
|
|
supported = [2 ** x - 1 for x in range(1, 17)]
|
|
try:
|
|
mi = supported.index(maxval)
|
|
except ValueError:
|
|
raise NotImplementedError(
|
|
"input maxval (%s) not in supported list %s" % (maxval, str(supported))
|
|
)
|
|
bitdepth = mi + 1
|
|
writer = png.Writer(
|
|
width,
|
|
height,
|
|
greyscale=greyscale,
|
|
bitdepth=bitdepth,
|
|
alpha=pamalpha,
|
|
compression=args.compression,
|
|
)
|
|
|
|
plain = format in (b"P1", b"P2", b"P3")
|
|
if plain:
|
|
convert_pnm_plain(writer, infile, outfile)
|
|
else:
|
|
convert_pnm_binary(writer, infile, outfile)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except png.Error as e:
|
|
print(e, file=sys.stderr)
|
|
sys.exit(99)
|