plan9topng.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. #!/usr/bin/env python
  2. # $Rev: 184 $
  3. # $URL: http://pypng.googlecode.com/svn/trunk/code/plan9topng.py $
  4. # Imported from //depot/prj/plan9topam/master/code/plan9topam.py#4 on
  5. # 2009-06-15.
  6. """Command line tool to convert from Plan 9 image format to PNG format.
  7. Plan 9 image format description:
  8. http://plan9.bell-labs.com/magic/man2html/6/image
  9. """
  10. # http://www.python.org/doc/2.3.5/lib/module-itertools.html
  11. import itertools
  12. # http://www.python.org/doc/2.3.5/lib/module-re.html
  13. import re
  14. # http://www.python.org/doc/2.3.5/lib/module-sys.html
  15. import sys
  16. def block(s, n):
  17. # See http://www.python.org/doc/2.6.2/library/functions.html#zip
  18. return zip(*[iter(s)]*n)
  19. def convert(f, output=sys.stdout) :
  20. """Convert Plan 9 file to PNG format. Works with either uncompressed
  21. or compressed files.
  22. """
  23. r = f.read(11)
  24. if r == 'compressed\n' :
  25. png(output, *decompress(f))
  26. else :
  27. png(output, *glue(f, r))
  28. def glue(f, r) :
  29. """Return (metadata, stream) pair where `r` is the initial portion of
  30. the metadata that has already been read from the stream `f`.
  31. """
  32. r = r + f.read(60-len(r))
  33. return (r, f)
  34. def meta(r) :
  35. """Convert 60 character string `r`, the metadata from an image file.
  36. Returns a 5-tuple (*chan*,*minx*,*miny*,*limx*,*limy*). 5-tuples may
  37. settle into lists in transit.
  38. As per http://plan9.bell-labs.com/magic/man2html/6/image the metadata
  39. comprises 5 words separated by blanks. As it happens each word starts
  40. at an index that is a multiple of 12, but this routine does not care
  41. about that."""
  42. r = r.split()
  43. # :todo: raise FormatError
  44. assert len(r) == 5
  45. r = [r[0]] + map(int, r[1:])
  46. return r
  47. def bitdepthof(pixel) :
  48. """Return the bitdepth for a Plan9 pixel format string."""
  49. maxd = 0
  50. for c in re.findall(r'[a-z]\d*', pixel) :
  51. if c[0] != 'x':
  52. maxd = max(maxd, int(c[1:]))
  53. return maxd
  54. def maxvalof(pixel):
  55. """Return the netpbm MAXVAL for a Plan9 pixel format string."""
  56. bitdepth = bitdepthof(pixel)
  57. return (2**bitdepth)-1
  58. def pixmeta(metadata, f) :
  59. """Convert (uncompressed) Plan 9 image file to pair of (*metadata*,
  60. *pixels*). This is intended to be used by PyPNG format. *metadata*
  61. is the metadata returned in a dictionary, *pixels* is an iterator that
  62. yields each row in boxed row flat pixel format.
  63. `f`, the input file, should be cued up to the start of the image data.
  64. """
  65. chan,minx,miny,limx,limy = metadata
  66. rows = limy - miny
  67. width = limx - minx
  68. nchans = len(re.findall('[a-wyz]', chan))
  69. alpha = 'a' in chan
  70. # Iverson's convention for the win!
  71. ncolour = nchans - alpha
  72. greyscale = ncolour == 1
  73. bitdepth = bitdepthof(chan)
  74. maxval = 2**bitdepth - 1
  75. # PNG style metadata
  76. meta=dict(size=(width,rows), bitdepth=bitdepthof(chan),
  77. greyscale=greyscale, alpha=alpha, planes=nchans)
  78. return itertools.imap(lambda x: itertools.chain(*x),
  79. block(unpack(f, rows, width, chan, maxval), width)), meta
  80. def png(out, metadata, f):
  81. """Convert to PNG format. `metadata` should be a Plan9 5-tuple; `f`
  82. the input file (see :meth:`pixmeta`).
  83. """
  84. import png
  85. pixels,meta = pixmeta(metadata, f)
  86. p = png.Writer(**meta)
  87. p.write(out, pixels)
  88. def spam():
  89. """Not really spam, but old PAM code, which is in limbo."""
  90. if nchans == 3 or nchans == 1 :
  91. # PGM (P5) or PPM (P6) format.
  92. output.write('P%d\n%d %d %d\n' % (5+(nchans==3), width, rows, maxval))
  93. else :
  94. # PAM format.
  95. output.write("""P7
  96. WIDTH %d
  97. HEIGHT %d
  98. DEPTH %d
  99. MAXVAL %d
  100. """ % (width, rows, nchans, maxval))
  101. def unpack(f, rows, width, pixel, maxval) :
  102. """Unpack `f` into pixels. Assumes the pixel format is such that the depth
  103. is either a multiple or a divisor of 8.
  104. `f` is assumed to be an iterator that returns blocks of input such
  105. that each block contains a whole number of pixels. An iterator is
  106. returned that yields each pixel as an n-tuple. `pixel` describes the
  107. pixel format using the Plan9 syntax ("k8", "r8g8b8", and so on).
  108. """
  109. def mask(w) :
  110. """An integer, to be used as a mask, with bottom `w` bits set to 1."""
  111. return (1 << w)-1
  112. def deblock(f, depth, width) :
  113. """A "packer" used to convert multiple bytes into single pixels.
  114. `depth` is the pixel depth in bits (>= 8), `width` is the row width in
  115. pixels.
  116. """
  117. w = depth // 8
  118. i = 0
  119. for block in f :
  120. for i in range(len(block)//w) :
  121. p = block[w*i:w*(i+1)]
  122. i += w
  123. # Convert p to little-endian integer, x
  124. x = 0
  125. s = 1 # scale
  126. for j in p :
  127. x += s * ord(j)
  128. s <<= 8
  129. yield x
  130. def bitfunge(f, depth, width) :
  131. """A "packer" used to convert single bytes into multiple pixels.
  132. Depth is the pixel depth (< 8), width is the row width in pixels.
  133. """
  134. for block in f :
  135. col = 0
  136. for i in block :
  137. x = ord(i)
  138. for j in range(8/depth) :
  139. yield x >> (8 - depth)
  140. col += 1
  141. if col == width :
  142. # A row-end forces a new byte even if we haven't consumed
  143. # all of the current byte. Effectively rows are bit-padded
  144. # to make a whole number of bytes.
  145. col = 0
  146. break
  147. x <<= depth
  148. # number of bits in each channel
  149. chan = map(int, re.findall(r'\d+', pixel))
  150. # type of each channel
  151. type = re.findall('[a-z]', pixel)
  152. depth = sum(chan)
  153. # According to the value of depth pick a "packer" that either gathers
  154. # multiple bytes into a single pixel (for depth >= 8) or split bytes
  155. # into several pixels (for depth < 8)
  156. if depth >= 8 :
  157. #
  158. assert depth % 8 == 0
  159. packer = deblock
  160. else :
  161. assert 8 % depth == 0
  162. packer = bitfunge
  163. for x in packer(f, depth, width) :
  164. # x is the pixel as an unsigned integer
  165. o = []
  166. # This is a bit yucky. Extract each channel from the _most_
  167. # significant part of x.
  168. for j in range(len(chan)) :
  169. v = (x >> (depth - chan[j])) & mask(chan[j])
  170. x <<= chan[j]
  171. if type[j] != 'x' :
  172. # scale to maxval
  173. v = v * float(maxval) / mask(chan[j])
  174. v = int(v+0.5)
  175. o.append(v)
  176. yield o
  177. def decompress(f) :
  178. """Decompress a Plan 9 image file. Assumes f is already cued past the
  179. initial 'compressed\n' string.
  180. """
  181. r = meta(f.read(60))
  182. return r, decomprest(f, r[4])
  183. def decomprest(f, rows) :
  184. """Iterator that decompresses the rest of a file once the metadata
  185. have been consumed."""
  186. row = 0
  187. while row < rows :
  188. row,o = deblock(f)
  189. yield o
  190. def deblock(f) :
  191. """Decompress a single block from a compressed Plan 9 image file.
  192. Each block starts with 2 decimal strings of 12 bytes each. Yields a
  193. sequence of (row, data) pairs where row is the total number of rows
  194. processed according to the file format and data is the decompressed
  195. data for a set of rows."""
  196. row = int(f.read(12))
  197. size = int(f.read(12))
  198. if not (0 <= size <= 6000) :
  199. raise 'block has invalid size; not a Plan 9 image file?'
  200. # Since each block is at most 6000 bytes we may as well read it all in
  201. # one go.
  202. d = f.read(size)
  203. i = 0
  204. o = []
  205. while i < size :
  206. x = ord(d[i])
  207. i += 1
  208. if x & 0x80 :
  209. x = (x & 0x7f) + 1
  210. lit = d[i:i+x]
  211. i += x
  212. o.extend(lit)
  213. continue
  214. # x's high-order bit is 0
  215. l = (x >> 2) + 3
  216. # Offset is made from bottom 2 bits of x and all 8 bits of next
  217. # byte. http://plan9.bell-labs.com/magic/man2html/6/image doesn't
  218. # say whether x's 2 bits are most signiificant or least significant.
  219. # But it is clear from inspecting a random file,
  220. # http://plan9.bell-labs.com/sources/plan9/sys/games/lib/sokoban/images/cargo.bit
  221. # that x's 2 bit are most significant.
  222. #
  223. offset = (x & 3) << 8
  224. offset |= ord(d[i])
  225. i += 1
  226. # Note: complement operator neatly maps (0 to 1023) to (-1 to
  227. # -1024). Adding len(o) gives a (non-negative) offset into o from
  228. # which to start indexing.
  229. offset = ~offset + len(o)
  230. if offset < 0 :
  231. raise 'byte offset indexes off the begininning of the output buffer; not a Plan 9 image file?'
  232. for j in range(l) :
  233. o.append(o[offset+j])
  234. return row,''.join(o)
  235. def main(argv=None) :
  236. if argv is None :
  237. argv = sys.argv
  238. if len(sys.argv) <= 1 :
  239. return convert(sys.stdin)
  240. else :
  241. return convert(open(argv[1], 'rb'))
  242. if __name__ == '__main__' :
  243. sys.exit(main())