3
# $URL: http://pypng.googlecode.com/svn/trunk/code/plan9topng.py $
5
# Imported from //depot/prj/plan9topam/master/code/plan9topam.py#4 on
8
"""Command line tool to convert from Plan 9 image format to PNG format.
10
Plan 9 image format description:
11
http://plan9.bell-labs.com/magic/man2html/6/image
14
# http://www.python.org/doc/2.3.5/lib/module-itertools.html
16
# http://www.python.org/doc/2.3.5/lib/module-re.html
18
# http://www.python.org/doc/2.3.5/lib/module-sys.html
22
# See http://www.python.org/doc/2.6.2/library/functions.html#zip
23
return zip(*[iter(s)]*n)
25
def convert(f, output=sys.stdout) :
26
"""Convert Plan 9 file to PNG format. Works with either uncompressed
31
if r == 'compressed\n' :
32
png(output, *decompress(f))
34
png(output, *glue(f, r))
38
"""Return (metadata, stream) pair where `r` is the initial portion of
39
the metadata that has already been read from the stream `f`.
42
r = r + f.read(60-len(r))
46
"""Convert 60 character string `r`, the metadata from an image file.
47
Returns a 5-tuple (*chan*,*minx*,*miny*,*limx*,*limy*). 5-tuples may
48
settle into lists in transit.
50
As per http://plan9.bell-labs.com/magic/man2html/6/image the metadata
51
comprises 5 words separated by blanks. As it happens each word starts
52
at an index that is a multiple of 12, but this routine does not care
56
# :todo: raise FormatError
58
r = [r[0]] + map(int, r[1:])
61
def bitdepthof(pixel) :
62
"""Return the bitdepth for a Plan9 pixel format string."""
65
for c in re.findall(r'[a-z]\d*', pixel) :
67
maxd = max(maxd, int(c[1:]))
71
"""Return the netpbm MAXVAL for a Plan9 pixel format string."""
73
bitdepth = bitdepthof(pixel)
74
return (2**bitdepth)-1
76
def pixmeta(metadata, f) :
77
"""Convert (uncompressed) Plan 9 image file to pair of (*metadata*,
78
*pixels*). This is intended to be used by PyPNG format. *metadata*
79
is the metadata returned in a dictionary, *pixels* is an iterator that
80
yields each row in boxed row flat pixel format.
82
`f`, the input file, should be cued up to the start of the image data.
85
chan,minx,miny,limx,limy = metadata
88
nchans = len(re.findall('[a-wyz]', chan))
90
# Iverson's convention for the win!
91
ncolour = nchans - alpha
92
greyscale = ncolour == 1
93
bitdepth = bitdepthof(chan)
94
maxval = 2**bitdepth - 1
96
meta=dict(size=(width,rows), bitdepth=bitdepthof(chan),
97
greyscale=greyscale, alpha=alpha, planes=nchans)
99
return itertools.imap(lambda x: itertools.chain(*x),
100
block(unpack(f, rows, width, chan, maxval), width)), meta
102
def png(out, metadata, f):
103
"""Convert to PNG format. `metadata` should be a Plan9 5-tuple; `f`
104
the input file (see :meth:`pixmeta`).
109
pixels,meta = pixmeta(metadata, f)
110
p = png.Writer(**meta)
114
"""Not really spam, but old PAM code, which is in limbo."""
116
if nchans == 3 or nchans == 1 :
117
# PGM (P5) or PPM (P6) format.
118
output.write('P%d\n%d %d %d\n' % (5+(nchans==3), width, rows, maxval))
126
""" % (width, rows, nchans, maxval))
128
def unpack(f, rows, width, pixel, maxval) :
129
"""Unpack `f` into pixels. Assumes the pixel format is such that the depth
130
is either a multiple or a divisor of 8.
131
`f` is assumed to be an iterator that returns blocks of input such
132
that each block contains a whole number of pixels. An iterator is
133
returned that yields each pixel as an n-tuple. `pixel` describes the
134
pixel format using the Plan9 syntax ("k8", "r8g8b8", and so on).
138
"""An integer, to be used as a mask, with bottom `w` bits set to 1."""
142
def deblock(f, depth, width) :
143
"""A "packer" used to convert multiple bytes into single pixels.
144
`depth` is the pixel depth in bits (>= 8), `width` is the row width in
151
for i in range(len(block)//w) :
152
p = block[w*i:w*(i+1)]
154
# Convert p to little-endian integer, x
162
def bitfunge(f, depth, width) :
163
"""A "packer" used to convert single bytes into multiple pixels.
164
Depth is the pixel depth (< 8), width is the row width in pixels.
171
for j in range(8/depth) :
172
yield x >> (8 - depth)
175
# A row-end forces a new byte even if we haven't consumed
176
# all of the current byte. Effectively rows are bit-padded
177
# to make a whole number of bytes.
182
# number of bits in each channel
183
chan = map(int, re.findall(r'\d+', pixel))
184
# type of each channel
185
type = re.findall('[a-z]', pixel)
189
# According to the value of depth pick a "packer" that either gathers
190
# multiple bytes into a single pixel (for depth >= 8) or split bytes
191
# into several pixels (for depth < 8)
194
assert depth % 8 == 0
197
assert 8 % depth == 0
200
for x in packer(f, depth, width) :
201
# x is the pixel as an unsigned integer
203
# This is a bit yucky. Extract each channel from the _most_
204
# significant part of x.
205
for j in range(len(chan)) :
206
v = (x >> (depth - chan[j])) & mask(chan[j])
210
v = v * float(maxval) / mask(chan[j])
217
"""Decompress a Plan 9 image file. Assumes f is already cued past the
218
initial 'compressed\n' string.
222
return r, decomprest(f, r[4])
225
def decomprest(f, rows) :
226
"""Iterator that decompresses the rest of a file once the metadata
227
have been consumed."""
236
"""Decompress a single block from a compressed Plan 9 image file.
237
Each block starts with 2 decimal strings of 12 bytes each. Yields a
238
sequence of (row, data) pairs where row is the total number of rows
239
processed according to the file format and data is the decompressed
240
data for a set of rows."""
242
row = int(f.read(12))
243
size = int(f.read(12))
244
if not (0 <= size <= 6000) :
245
raise 'block has invalid size; not a Plan 9 image file?'
247
# Since each block is at most 6000 bytes we may as well read it all in
262
# x's high-order bit is 0
264
# Offset is made from bottom 2 bits of x and all 8 bits of next
265
# byte. http://plan9.bell-labs.com/magic/man2html/6/image doesn't
266
# say whether x's 2 bits are most signiificant or least significant.
267
# But it is clear from inspecting a random file,
268
# http://plan9.bell-labs.com/sources/plan9/sys/games/lib/sokoban/images/cargo.bit
269
# that x's 2 bit are most significant.
271
offset = (x & 3) << 8
274
# Note: complement operator neatly maps (0 to 1023) to (-1 to
275
# -1024). Adding len(o) gives a (non-negative) offset into o from
276
# which to start indexing.
277
offset = ~offset + len(o)
279
raise 'byte offset indexes off the begininning of the output buffer; not a Plan 9 image file?'
281
o.append(o[offset+j])
282
return row,''.join(o)
284
def main(argv=None) :
287
if len(sys.argv) <= 1 :
288
return convert(sys.stdin)
290
return convert(open(argv[1], 'rb'))
292
if __name__ == '__main__' :