gooderp18绿色标准版
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

356 lines
10KB

  1. #!C:\odoobuild\WinPy64\python-3.12.3.amd64\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. Description = """Convert NetPBM PAM/PNM format files to PNG."""
  10. def read_pam_header(infile):
  11. """
  12. Read (the rest of a) PAM header.
  13. `infile` should be positioned immediately after the initial 'P7' line
  14. (at the beginning of the second line).
  15. Returns are as for `read_pnm_header`.
  16. """
  17. # Unlike PBM, PGM, and PPM, we can read the header a line at a time.
  18. header = dict()
  19. while True:
  20. line = infile.readline().strip()
  21. if line == b"ENDHDR":
  22. break
  23. if not line:
  24. raise EOFError("PAM ended prematurely")
  25. if line[0] == b"#":
  26. continue
  27. line = line.split(None, 1)
  28. key = line[0]
  29. if key not in header:
  30. header[key] = line[1]
  31. else:
  32. header[key] += b" " + line[1]
  33. required = [b"WIDTH", b"HEIGHT", b"DEPTH", b"MAXVAL"]
  34. required_str = b", ".join(required).decode("ascii")
  35. result = []
  36. for token in required:
  37. if token not in header:
  38. raise png.Error("PAM file must specify " + required_str)
  39. try:
  40. x = int(header[token])
  41. except ValueError:
  42. raise png.Error(required_str + " must all be valid integers")
  43. if x <= 0:
  44. raise png.Error(required_str + " must all be positive integers")
  45. result.append(x)
  46. return (b"P7",) + tuple(result)
  47. def read_pnm_header(infile):
  48. """
  49. Read a PNM header, returning (format,width,height,depth,maxval).
  50. Also reads a PAM header (by using a helper function).
  51. `width` and `height` are in pixels.
  52. `depth` is the number of channels in the image;
  53. for PBM and PGM it is synthesized as 1, for PPM as 3;
  54. for PAM images it is read from the header.
  55. `maxval` is synthesized (as 1) for PBM images.
  56. """
  57. # Generally, see http://netpbm.sourceforge.net/doc/ppm.html
  58. # and http://netpbm.sourceforge.net/doc/pam.html
  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. magic = infile.read(3).rstrip()
  63. if magic == b"P7":
  64. # PAM header parsing is completely different.
  65. return read_pam_header(infile)
  66. # Expected number of tokens in header (3 for P4, 4 for P6)
  67. expected = 4
  68. pbm = (b"P1", b"P4")
  69. if magic in pbm:
  70. expected = 3
  71. header = [magic]
  72. # We must read the rest of the header byte by byte because
  73. # the final whitespace character may not be a newline.
  74. # Of course all PNM files in the wild use a newline at this point,
  75. # but we are strong and so we avoid
  76. # the temptation to use readline.
  77. bs = bytearray()
  78. backs = bytearray()
  79. def next():
  80. if backs:
  81. c = bytes(backs[0:1])
  82. del backs[0]
  83. else:
  84. c = infile.read(1)
  85. if not c:
  86. raise png.Error("premature EOF reading PNM header")
  87. bs.extend(c)
  88. return c
  89. def backup():
  90. """Push last byte of token onto front of backs."""
  91. backs.insert(0, bs[-1])
  92. del bs[-1]
  93. def ignore():
  94. del bs[:]
  95. def tokens():
  96. ls = lexInit
  97. while True:
  98. token, ls = ls()
  99. if token:
  100. yield token
  101. def lexInit():
  102. c = next()
  103. # Skip comments
  104. if b"#" <= c <= b"#":
  105. while c not in b"\n\r":
  106. c = next()
  107. ignore()
  108. return None, lexInit
  109. # Skip whitespace (that precedes a token)
  110. if c.isspace():
  111. ignore()
  112. return None, lexInit
  113. if not c.isdigit():
  114. raise png.Error("unexpected byte %r found in header" % c)
  115. return None, lexNumber
  116. def lexNumber():
  117. # According to the specification it is legal to have comments
  118. # that appear in the middle of a token.
  119. # I've never seen it; and,
  120. # it's a bit awkward to code good lexers in Python (no goto).
  121. # So we break on such cases.
  122. c = next()
  123. while c.isdigit():
  124. c = next()
  125. backup()
  126. token = bs[:]
  127. ignore()
  128. return token, lexInit
  129. for token in tokens():
  130. # All "tokens" are decimal integers, so convert them here.
  131. header.append(int(token))
  132. if len(header) == expected:
  133. break
  134. final = next()
  135. if not final.isspace():
  136. raise png.Error("expected header to end with whitespace, not %r" % final)
  137. if magic in pbm:
  138. # synthesize a MAXVAL
  139. header.append(1)
  140. depth = (1, 3)[magic == b"P6"]
  141. return header[0], header[1], header[2], depth, header[3]
  142. def convert_pnm_plain(w, infile, outfile):
  143. """
  144. Convert a plain PNM file containing raw pixel data into
  145. a PNG file with the parameters set in the writer object.
  146. Works for plain PGM formats.
  147. """
  148. # See convert_pnm_binary for the corresponding function for
  149. # binary PNM formats.
  150. rows = scan_rows_from_file_plain(infile, w.width, w.height, w.planes)
  151. w.write(outfile, rows)
  152. def scan_rows_from_file_plain(infile, width, height, planes):
  153. """
  154. Generate a sequence of rows from the input file `infile`.
  155. The input file should be in a "Netpbm-like" plain format.
  156. The input file should be positioned at the beginning of the
  157. first value (that is, immediately after the header).
  158. The number of pixels to read is taken from
  159. the image dimensions (`width`, `height`, `planes`).
  160. Each row is yielded as a single sequence of values.
  161. """
  162. # Values per row
  163. vpr = width * planes
  164. values = []
  165. rows_output = 0
  166. # The core problem is that input lines (text lines) may not
  167. # correspond with pixel rows. We use two nested loops.
  168. # The outer loop reads the input one text line at a time;
  169. # this will contain a whole number of values, which are
  170. # added to the `values` list.
  171. # The inner loop strips the first `vpr` values from the
  172. # list, until there aren't enough.
  173. # Note we can't tell how many iterations the inner loop will
  174. # run for, it could be 0 (if not enough values were read to
  175. # make a whole pixel row) or many (if the entire image were
  176. # on one input line), or somewhere in between.
  177. # In PNM there is in general no requirement to have
  178. # correspondence between text lines and pixel rows.
  179. for inp in infile:
  180. values.extend(map(int, inp.split()))
  181. while len(values) >= vpr:
  182. yield values[:vpr]
  183. del values[:vpr]
  184. rows_output += 1
  185. if rows_output >= height:
  186. # Diagnostic here if there are spare values?
  187. return
  188. # Diagnostic here for early EOF?
  189. def convert_pnm_binary(w, infile, outfile):
  190. """
  191. Convert a PNM file containing raw pixel data into
  192. a PNG file with the parameters set in the writer object.
  193. Works for (binary) PGM, PPM, and PAM formats.
  194. """
  195. rows = scan_rows_from_file(infile, w.width, w.height, w.planes, w.bitdepth)
  196. w.write(outfile, rows)
  197. def scan_rows_from_file(infile, width, height, planes, bitdepth):
  198. """
  199. Generate a sequence of rows from the input file `infile`.
  200. The input file should be in a "Netpbm-like" binary format.
  201. The input file should be positioned at the beginning of the first pixel.
  202. The number of pixels to read is taken from
  203. the image dimensions (`width`, `height`, `planes`);
  204. the number of bytes per value is implied by `bitdepth`.
  205. Each row is yielded as a single sequence of values.
  206. """
  207. # Values per row
  208. vpr = width * planes
  209. # Bytes per row
  210. bpr = vpr
  211. if bitdepth > 8:
  212. assert bitdepth == 16
  213. bpr *= 2
  214. fmt = ">%dH" % vpr
  215. def line():
  216. return array.array("H", struct.unpack(fmt, infile.read(bpr)))
  217. else:
  218. def line():
  219. return array.array("B", infile.read(bpr))
  220. for y in range(height):
  221. yield line()
  222. def parse_args(args):
  223. """
  224. Create a parser and parse the command line arguments.
  225. """
  226. from argparse import ArgumentParser
  227. parser = ArgumentParser(description=Description)
  228. version = "%(prog)s " + png.__version__
  229. parser.add_argument("--version", action="version", version=version)
  230. parser.add_argument(
  231. "-c",
  232. "--compression",
  233. type=int,
  234. metavar="level",
  235. help="zlib compression level (0-9)",
  236. )
  237. parser.add_argument(
  238. "input",
  239. nargs="?",
  240. default="-",
  241. type=png.cli_open,
  242. metavar="PAM/PNM",
  243. help="input PAM/PNM file to convert",
  244. )
  245. args = parser.parse_args(args)
  246. return args
  247. def main(argv=None):
  248. if argv is None:
  249. argv = sys.argv
  250. args = parse_args(argv[1:])
  251. # Prepare input and output files
  252. infile = args.input
  253. # Call after parsing, so that --version and --help work.
  254. outfile = png.binary_stdout()
  255. # Encode PNM to PNG
  256. format, width, height, depth, maxval = read_pnm_header(infile)
  257. ok_formats = (b"P2", b"P5", b"P6", b"P7")
  258. if format not in ok_formats:
  259. raise NotImplementedError("file format %s not supported" % format)
  260. # The NetPBM depth (number of channels) completely
  261. # determines the PNG format.
  262. # Observe:
  263. # - L, LA, RGB, RGBA are the 4 modes supported by PNG;
  264. # - they correspond to 1, 2, 3, 4 channels respectively.
  265. # We use the number of channels in the source image to
  266. # determine which one we have.
  267. # We ignore the NetPBM image type and the PAM TUPLTYPE.
  268. greyscale = depth <= 2
  269. pamalpha = depth in (2, 4)
  270. supported = [2 ** x - 1 for x in range(1, 17)]
  271. try:
  272. mi = supported.index(maxval)
  273. except ValueError:
  274. raise NotImplementedError(
  275. "input maxval (%s) not in supported list %s" % (maxval, str(supported))
  276. )
  277. bitdepth = mi + 1
  278. writer = png.Writer(
  279. width,
  280. height,
  281. greyscale=greyscale,
  282. bitdepth=bitdepth,
  283. alpha=pamalpha,
  284. compression=args.compression,
  285. )
  286. plain = format in (b"P1", b"P2", b"P3")
  287. if plain:
  288. convert_pnm_plain(writer, infile, outfile)
  289. else:
  290. convert_pnm_binary(writer, infile, outfile)
  291. if __name__ == "__main__":
  292. try:
  293. sys.exit(main())
  294. except png.Error as e:
  295. print(e, file=sys.stderr)
  296. sys.exit(99)
上海开阖软件有限公司 沪ICP备12045867号-1