summaryrefslogtreecommitdiffstats
path: root/build/pypng/plan9topng.py
diff options
context:
space:
mode:
Diffstat (limited to 'build/pypng/plan9topng.py')
-rw-r--r--build/pypng/plan9topng.py293
1 files changed, 293 insertions, 0 deletions
diff --git a/build/pypng/plan9topng.py b/build/pypng/plan9topng.py
new file mode 100644
index 0000000..4600b4c
--- /dev/null
+++ b/build/pypng/plan9topng.py
@@ -0,0 +1,293 @@
+#!/usr/bin/env python
+# $Rev: 184 $
+# $URL: http://pypng.googlecode.com/svn/trunk/code/plan9topng.py $
+
+# Imported from //depot/prj/plan9topam/master/code/plan9topam.py#4 on
+# 2009-06-15.
+
+"""Command line tool to convert from Plan 9 image format to PNG format.
+
+Plan 9 image format description:
+http://plan9.bell-labs.com/magic/man2html/6/image
+"""
+
+# http://www.python.org/doc/2.3.5/lib/module-itertools.html
+import itertools
+# http://www.python.org/doc/2.3.5/lib/module-re.html
+import re
+# http://www.python.org/doc/2.3.5/lib/module-sys.html
+import sys
+
+def block(s, n):
+ # See http://www.python.org/doc/2.6.2/library/functions.html#zip
+ return zip(*[iter(s)]*n)
+
+def convert(f, output=sys.stdout) :
+ """Convert Plan 9 file to PNG format. Works with either uncompressed
+ or compressed files.
+ """
+
+ r = f.read(11)
+ if r == 'compressed\n' :
+ png(output, *decompress(f))
+ else :
+ png(output, *glue(f, r))
+
+
+def glue(f, r) :
+ """Return (metadata, stream) pair where `r` is the initial portion of
+ the metadata that has already been read from the stream `f`.
+ """
+
+ r = r + f.read(60-len(r))
+ return (r, f)
+
+def meta(r) :
+ """Convert 60 character string `r`, the metadata from an image file.
+ Returns a 5-tuple (*chan*,*minx*,*miny*,*limx*,*limy*). 5-tuples may
+ settle into lists in transit.
+
+ As per http://plan9.bell-labs.com/magic/man2html/6/image the metadata
+ comprises 5 words separated by blanks. As it happens each word starts
+ at an index that is a multiple of 12, but this routine does not care
+ about that."""
+
+ r = r.split()
+ # :todo: raise FormatError
+ assert len(r) == 5
+ r = [r[0]] + map(int, r[1:])
+ return r
+
+def bitdepthof(pixel) :
+ """Return the bitdepth for a Plan9 pixel format string."""
+
+ maxd = 0
+ for c in re.findall(r'[a-z]\d*', pixel) :
+ if c[0] != 'x':
+ maxd = max(maxd, int(c[1:]))
+ return maxd
+
+def maxvalof(pixel):
+ """Return the netpbm MAXVAL for a Plan9 pixel format string."""
+
+ bitdepth = bitdepthof(pixel)
+ return (2**bitdepth)-1
+
+def pixmeta(metadata, f) :
+ """Convert (uncompressed) Plan 9 image file to pair of (*metadata*,
+ *pixels*). This is intended to be used by PyPNG format. *metadata*
+ is the metadata returned in a dictionary, *pixels* is an iterator that
+ yields each row in boxed row flat pixel format.
+
+ `f`, the input file, should be cued up to the start of the image data.
+ """
+
+ chan,minx,miny,limx,limy = metadata
+ rows = limy - miny
+ width = limx - minx
+ nchans = len(re.findall('[a-wyz]', chan))
+ alpha = 'a' in chan
+ # Iverson's convention for the win!
+ ncolour = nchans - alpha
+ greyscale = ncolour == 1
+ bitdepth = bitdepthof(chan)
+ maxval = 2**bitdepth - 1
+ # PNG style metadata
+ meta=dict(size=(width,rows), bitdepth=bitdepthof(chan),
+ greyscale=greyscale, alpha=alpha, planes=nchans)
+
+ return itertools.imap(lambda x: itertools.chain(*x),
+ block(unpack(f, rows, width, chan, maxval), width)), meta
+
+def png(out, metadata, f):
+ """Convert to PNG format. `metadata` should be a Plan9 5-tuple; `f`
+ the input file (see :meth:`pixmeta`).
+ """
+
+ import png
+
+ pixels,meta = pixmeta(metadata, f)
+ p = png.Writer(**meta)
+ p.write(out, pixels)
+
+def spam():
+ """Not really spam, but old PAM code, which is in limbo."""
+
+ if nchans == 3 or nchans == 1 :
+ # PGM (P5) or PPM (P6) format.
+ output.write('P%d\n%d %d %d\n' % (5+(nchans==3), width, rows, maxval))
+ else :
+ # PAM format.
+ output.write("""P7
+WIDTH %d
+HEIGHT %d
+DEPTH %d
+MAXVAL %d
+""" % (width, rows, nchans, maxval))
+
+def unpack(f, rows, width, pixel, maxval) :
+ """Unpack `f` into pixels. Assumes the pixel format is such that the depth
+ is either a multiple or a divisor of 8.
+ `f` is assumed to be an iterator that returns blocks of input such
+ that each block contains a whole number of pixels. An iterator is
+ returned that yields each pixel as an n-tuple. `pixel` describes the
+ pixel format using the Plan9 syntax ("k8", "r8g8b8", and so on).
+ """
+
+ def mask(w) :
+ """An integer, to be used as a mask, with bottom `w` bits set to 1."""
+
+ return (1 << w)-1
+
+ def deblock(f, depth, width) :
+ """A "packer" used to convert multiple bytes into single pixels.
+ `depth` is the pixel depth in bits (>= 8), `width` is the row width in
+ pixels.
+ """
+
+ w = depth // 8
+ i = 0
+ for block in f :
+ for i in range(len(block)//w) :
+ p = block[w*i:w*(i+1)]
+ i += w
+ # Convert p to little-endian integer, x
+ x = 0
+ s = 1 # scale
+ for j in p :
+ x += s * ord(j)
+ s <<= 8
+ yield x
+
+ def bitfunge(f, depth, width) :
+ """A "packer" used to convert single bytes into multiple pixels.
+ Depth is the pixel depth (< 8), width is the row width in pixels.
+ """
+
+ for block in f :
+ col = 0
+ for i in block :
+ x = ord(i)
+ for j in range(8/depth) :
+ yield x >> (8 - depth)
+ col += 1
+ if col == width :
+ # A row-end forces a new byte even if we haven't consumed
+ # all of the current byte. Effectively rows are bit-padded
+ # to make a whole number of bytes.
+ col = 0
+ break
+ x <<= depth
+
+ # number of bits in each channel
+ chan = map(int, re.findall(r'\d+', pixel))
+ # type of each channel
+ type = re.findall('[a-z]', pixel)
+
+ depth = sum(chan)
+
+ # According to the value of depth pick a "packer" that either gathers
+ # multiple bytes into a single pixel (for depth >= 8) or split bytes
+ # into several pixels (for depth < 8)
+ if depth >= 8 :
+ #
+ assert depth % 8 == 0
+ packer = deblock
+ else :
+ assert 8 % depth == 0
+ packer = bitfunge
+
+ for x in packer(f, depth, width) :
+ # x is the pixel as an unsigned integer
+ o = []
+ # This is a bit yucky. Extract each channel from the _most_
+ # significant part of x.
+ for j in range(len(chan)) :
+ v = (x >> (depth - chan[j])) & mask(chan[j])
+ x <<= chan[j]
+ if type[j] != 'x' :
+ # scale to maxval
+ v = v * float(maxval) / mask(chan[j])
+ v = int(v+0.5)
+ o.append(v)
+ yield o
+
+
+def decompress(f) :
+ """Decompress a Plan 9 image file. Assumes f is already cued past the
+ initial 'compressed\n' string.
+ """
+
+ r = meta(f.read(60))
+ return r, decomprest(f, r[4])
+
+
+def decomprest(f, rows) :
+ """Iterator that decompresses the rest of a file once the metadata
+ have been consumed."""
+
+ row = 0
+ while row < rows :
+ row,o = deblock(f)
+ yield o
+
+
+def deblock(f) :
+ """Decompress a single block from a compressed Plan 9 image file.
+ Each block starts with 2 decimal strings of 12 bytes each. Yields a
+ sequence of (row, data) pairs where row is the total number of rows
+ processed according to the file format and data is the decompressed
+ data for a set of rows."""
+
+ row = int(f.read(12))
+ size = int(f.read(12))
+ if not (0 <= size <= 6000) :
+ raise 'block has invalid size; not a Plan 9 image file?'
+
+ # Since each block is at most 6000 bytes we may as well read it all in
+ # one go.
+ d = f.read(size)
+ i = 0
+ o = []
+
+ while i < size :
+ x = ord(d[i])
+ i += 1
+ if x & 0x80 :
+ x = (x & 0x7f) + 1
+ lit = d[i:i+x]
+ i += x
+ o.extend(lit)
+ continue
+ # x's high-order bit is 0
+ l = (x >> 2) + 3
+ # Offset is made from bottom 2 bits of x and all 8 bits of next
+ # byte. http://plan9.bell-labs.com/magic/man2html/6/image doesn't
+ # say whether x's 2 bits are most signiificant or least significant.
+ # But it is clear from inspecting a random file,
+ # http://plan9.bell-labs.com/sources/plan9/sys/games/lib/sokoban/images/cargo.bit
+ # that x's 2 bit are most significant.
+ #
+ offset = (x & 3) << 8
+ offset |= ord(d[i])
+ i += 1
+ # Note: complement operator neatly maps (0 to 1023) to (-1 to
+ # -1024). Adding len(o) gives a (non-negative) offset into o from
+ # which to start indexing.
+ offset = ~offset + len(o)
+ if offset < 0 :
+ raise 'byte offset indexes off the begininning of the output buffer; not a Plan 9 image file?'
+ for j in range(l) :
+ o.append(o[offset+j])
+ return row,''.join(o)
+
+def main(argv=None) :
+ if argv is None :
+ argv = sys.argv
+ if len(sys.argv) <= 1 :
+ return convert(sys.stdin)
+ else :
+ return convert(open(argv[1], 'rb'))
+
+if __name__ == '__main__' :
+ sys.exit(main())