123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- #!D:\TestPythonProject\TEST\venv\Scripts\python.exe
- # pripamtopng
- #
- # Python Raster Image PAM to PNG
- import array
- import struct
- import sys
- import 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 ("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
- supported = (b"P5", b"P6", b"P7")
- # 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.
- type = infile.read(3).rstrip()
- if type not in supported:
- raise NotImplementedError("file format %s not supported" % type)
- if type == 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 type in pbm:
- expected = 3
- header = [type]
- # 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 type in pbm:
- # synthesize a MAXVAL
- header.append(1)
- depth = (1, 3)[type == b"P6"]
- return header[0], header[1], header[2], depth, header[3]
- def convert_pnm(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()
- 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)
- # 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,
- )
- convert_pnm(writer, infile, outfile)
- if __name__ == "__main__":
- try:
- sys.exit(main())
- except png.Error as e:
- print(e, file=sys.stderr)
- sys.exit(99)
|