123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- #!/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())
|