pripamtopng 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. #!D:\TestPythonProject\TEST\venv\Scripts\python.exe
  2. # pripamtopng
  3. #
  4. # Python Raster Image PAM to PNG
  5. import array
  6. import struct
  7. import sys
  8. import png
  9. def read_pam_header(infile):
  10. """
  11. Read (the rest of a) PAM header.
  12. `infile` should be positioned immediately after the initial 'P7' line
  13. (at the beginning of the second line).
  14. Returns are as for `read_pnm_header`.
  15. """
  16. # Unlike PBM, PGM, and PPM, we can read the header a line at a time.
  17. header = dict()
  18. while True:
  19. line = infile.readline().strip()
  20. if line == b"ENDHDR":
  21. break
  22. if not line:
  23. raise EOFError("PAM ended prematurely")
  24. if line[0] == b"#":
  25. continue
  26. line = line.split(None, 1)
  27. key = line[0]
  28. if key not in header:
  29. header[key] = line[1]
  30. else:
  31. header[key] += b" " + line[1]
  32. required = [b"WIDTH", b"HEIGHT", b"DEPTH", b"MAXVAL"]
  33. required_str = b", ".join(required).decode("ascii")
  34. result = []
  35. for token in required:
  36. if token not in header:
  37. raise png.Error("PAM file must specify " + required_str)
  38. try:
  39. x = int(header[token])
  40. except ValueError:
  41. raise png.Error(required_str + " must all be valid integers")
  42. if x <= 0:
  43. raise png.Error(required_str + " must all be positive integers")
  44. result.append(x)
  45. return ("P7",) + tuple(result)
  46. def read_pnm_header(infile):
  47. """
  48. Read a PNM header, returning (format,width,height,depth,maxval).
  49. Also reads a PAM header (by using a helper function).
  50. `width` and `height` are in pixels.
  51. `depth` is the number of channels in the image;
  52. for PBM and PGM it is synthesized as 1, for PPM as 3;
  53. for PAM images it is read from the header.
  54. `maxval` is synthesized (as 1) for PBM images.
  55. """
  56. # Generally, see http://netpbm.sourceforge.net/doc/ppm.html
  57. # and http://netpbm.sourceforge.net/doc/pam.html
  58. supported = (b"P5", b"P6", b"P7")
  59. # Technically 'P7' must be followed by a newline,
  60. # so by using rstrip() we are being liberal in what we accept.
  61. # I think this is acceptable.
  62. type = infile.read(3).rstrip()
  63. if type not in supported:
  64. raise NotImplementedError("file format %s not supported" % type)
  65. if type == b"P7":
  66. # PAM header parsing is completely different.
  67. return read_pam_header(infile)
  68. # Expected number of tokens in header (3 for P4, 4 for P6)
  69. expected = 4
  70. pbm = (b"P1", b"P4")
  71. if type in pbm:
  72. expected = 3
  73. header = [type]
  74. # We must read the rest of the header byte by byte because
  75. # the final whitespace character may not be a newline.
  76. # Of course all PNM files in the wild use a newline at this point,
  77. # but we are strong and so we avoid
  78. # the temptation to use readline.
  79. bs = bytearray()
  80. backs = bytearray()
  81. def next():
  82. if backs:
  83. c = bytes(backs[0:1])
  84. del backs[0]
  85. else:
  86. c = infile.read(1)
  87. if not c:
  88. raise png.Error("premature EOF reading PNM header")
  89. bs.extend(c)
  90. return c
  91. def backup():
  92. """Push last byte of token onto front of backs."""
  93. backs.insert(0, bs[-1])
  94. del bs[-1]
  95. def ignore():
  96. del bs[:]
  97. def tokens():
  98. ls = lexInit
  99. while True:
  100. token, ls = ls()
  101. if token:
  102. yield token
  103. def lexInit():
  104. c = next()
  105. # Skip comments
  106. if b"#" <= c <= b"#":
  107. while c not in b"\n\r":
  108. c = next()
  109. ignore()
  110. return None, lexInit
  111. # Skip whitespace (that precedes a token)
  112. if c.isspace():
  113. ignore()
  114. return None, lexInit
  115. if not c.isdigit():
  116. raise png.Error("unexpected byte %r found in header" % c)
  117. return None, lexNumber
  118. def lexNumber():
  119. # According to the specification it is legal to have comments
  120. # that appear in the middle of a token.
  121. # I've never seen it; and,
  122. # it's a bit awkward to code good lexers in Python (no goto).
  123. # So we break on such cases.
  124. c = next()
  125. while c.isdigit():
  126. c = next()
  127. backup()
  128. token = bs[:]
  129. ignore()
  130. return token, lexInit
  131. for token in tokens():
  132. # All "tokens" are decimal integers, so convert them here.
  133. header.append(int(token))
  134. if len(header) == expected:
  135. break
  136. final = next()
  137. if not final.isspace():
  138. raise png.Error("expected header to end with whitespace, not %r" % final)
  139. if type in pbm:
  140. # synthesize a MAXVAL
  141. header.append(1)
  142. depth = (1, 3)[type == b"P6"]
  143. return header[0], header[1], header[2], depth, header[3]
  144. def convert_pnm(w, infile, outfile):
  145. """
  146. Convert a PNM file containing raw pixel data into
  147. a PNG file with the parameters set in the writer object.
  148. Works for (binary) PGM, PPM, and PAM formats.
  149. """
  150. rows = scan_rows_from_file(infile, w.width, w.height, w.planes, w.bitdepth)
  151. w.write(outfile, rows)
  152. def scan_rows_from_file(infile, width, height, planes, bitdepth):
  153. """
  154. Generate a sequence of rows from the input file `infile`.
  155. The input file should be in a "Netpbm-like" binary format.
  156. The input file should be positioned at the beginning of the first pixel.
  157. The number of pixels to read is taken from
  158. the image dimensions (`width`, `height`, `planes`);
  159. the number of bytes per value is implied by `bitdepth`.
  160. Each row is yielded as a single sequence of values.
  161. """
  162. # Values per row
  163. vpr = width * planes
  164. # Bytes per row
  165. bpr = vpr
  166. if bitdepth > 8:
  167. assert bitdepth == 16
  168. bpr *= 2
  169. fmt = ">%dH" % vpr
  170. def line():
  171. return array.array("H", struct.unpack(fmt, infile.read(bpr)))
  172. else:
  173. def line():
  174. return array.array("B", infile.read(bpr))
  175. for y in range(height):
  176. yield line()
  177. def parse_args(args):
  178. """
  179. Create a parser and parse the command line arguments.
  180. """
  181. from argparse import ArgumentParser
  182. parser = ArgumentParser()
  183. version = "%(prog)s " + png.__version__
  184. parser.add_argument("--version", action="version", version=version)
  185. parser.add_argument(
  186. "-c",
  187. "--compression",
  188. type=int,
  189. metavar="level",
  190. help="zlib compression level (0-9)",
  191. )
  192. parser.add_argument(
  193. "input",
  194. nargs="?",
  195. default="-",
  196. type=png.cli_open,
  197. metavar="PAM/PNM",
  198. help="input PAM/PNM file to convert",
  199. )
  200. args = parser.parse_args(args)
  201. return args
  202. def main(argv=None):
  203. if argv is None:
  204. argv = sys.argv
  205. args = parse_args(argv[1:])
  206. # Prepare input and output files
  207. infile = args.input
  208. # Call after parsing, so that --version and --help work.
  209. outfile = png.binary_stdout()
  210. # Encode PNM to PNG
  211. format, width, height, depth, maxval = read_pnm_header(infile)
  212. # The NetPBM depth (number of channels) completely
  213. # determines the PNG format.
  214. # Observe:
  215. # - L, LA, RGB, RGBA are the 4 modes supported by PNG;
  216. # - they correspond to 1, 2, 3, 4 channels respectively.
  217. # We use the number of channels in the source image to
  218. # determine which one we have.
  219. # We ignore the NetPBM image type and the PAM TUPLTYPE.
  220. greyscale = depth <= 2
  221. pamalpha = depth in (2, 4)
  222. supported = [2 ** x - 1 for x in range(1, 17)]
  223. try:
  224. mi = supported.index(maxval)
  225. except ValueError:
  226. raise NotImplementedError(
  227. "input maxval (%s) not in supported list %s" % (maxval, str(supported))
  228. )
  229. bitdepth = mi + 1
  230. writer = png.Writer(
  231. width,
  232. height,
  233. greyscale=greyscale,
  234. bitdepth=bitdepth,
  235. alpha=pamalpha,
  236. compression=args.compression,
  237. )
  238. convert_pnm(writer, infile, outfile)
  239. if __name__ == "__main__":
  240. try:
  241. sys.exit(main())
  242. except png.Error as e:
  243. print(e, file=sys.stderr)
  244. sys.exit(99)