summaryrefslogtreecommitdiff
path: root/build/pypng
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2019-12-16 13:30:48 -0500
committerMatt A. Tobin <email@mattatobin.com>2019-12-16 13:30:48 -0500
commit3d5a69a191906dfe349e679e3586f60805427b02 (patch)
tree0d6c3134fc678684bb0db9abe2eaa248d704efab /build/pypng
parentdb902e77e2442e5fdc4a54d4de6cdf2b65c80320 (diff)
downloadbasilisk-3d5a69a191906dfe349e679e3586f60805427b02.tar.gz
Add Comm Build System
Diffstat (limited to 'build/pypng')
-rw-r--r--build/pypng/check-sync-exceptions3
-rw-r--r--build/pypng/exnumpy.py128
-rw-r--r--build/pypng/iccp.py537
-rw-r--r--build/pypng/mkiccp.py45
-rw-r--r--build/pypng/pdsimgtopng99
-rw-r--r--build/pypng/pipasgrey73
-rw-r--r--build/pypng/pipcat44
-rw-r--r--build/pypng/pipcolours56
-rw-r--r--build/pypng/pipcomposite121
-rw-r--r--build/pypng/pipdither181
-rw-r--r--build/pypng/piprgb36
-rw-r--r--build/pypng/pipscalez53
-rw-r--r--build/pypng/pipstack127
-rw-r--r--build/pypng/pipwindow67
-rw-r--r--build/pypng/plan9topng.py293
-rw-r--r--build/pypng/pngchunk172
-rw-r--r--build/pypng/pnghist79
-rw-r--r--build/pypng/pnglsch31
-rw-r--r--build/pypng/texttopng151
19 files changed, 2296 insertions, 0 deletions
diff --git a/build/pypng/check-sync-exceptions b/build/pypng/check-sync-exceptions
new file mode 100644
index 0000000..b326f7c
--- /dev/null
+++ b/build/pypng/check-sync-exceptions
@@ -0,0 +1,3 @@
+# Nothing in this directory needs to be in sync with mozilla
+# The contents are used only in c-c
+* \ No newline at end of file
diff --git a/build/pypng/exnumpy.py b/build/pypng/exnumpy.py
new file mode 100644
index 0000000..82daf0a
--- /dev/null
+++ b/build/pypng/exnumpy.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/exnumpy.py $
+# $Rev: 126 $
+
+# Numpy example.
+# Original code created by Mel Raab, modified by David Jones.
+
+'''
+ Example code integrating RGB PNG files, PyPNG and NumPy
+ (abstracted from Mel Raab's functioning code)
+'''
+
+# http://www.python.org/doc/2.4.4/lib/module-itertools.html
+import itertools
+
+import numpy
+import png
+
+
+''' If you have a PNG file for an RGB image,
+ and want to create a numpy array of data from it.
+'''
+# Read the file "picture.png" from the current directory. The `Reader`
+# class can take a filename, a file-like object, or the byte data
+# directly; this suggests alternatives such as using urllib to read
+# an image from the internet:
+# png.Reader(file=urllib.urlopen('http://www.libpng.org/pub/png/PngSuite/basn2c16.png'))
+pngReader=png.Reader(filename='picture.png')
+# Tuple unpacking, using multiple assignment, is very useful for the
+# result of asDirect (and other methods).
+# See
+# http://docs.python.org/tutorial/introduction.html#first-steps-towards-programming
+row_count, column_count, pngdata, meta = pngReader.asDirect()
+bitdepth=meta['bitdepth']
+plane_count=meta['planes']
+
+# Make sure we're dealing with RGB files
+assert plane_count == 3
+
+''' Boxed row flat pixel:
+ list([R,G,B, R,G,B, R,G,B],
+ [R,G,B, R,G,B, R,G,B])
+ Array dimensions for this example: (2,9)
+
+ Create `image_2d` as a two-dimensional NumPy array by stacking a
+ sequence of 1-dimensional arrays (rows).
+ The NumPy array mimics PyPNG's (boxed row flat pixel) representation;
+ it will have dimensions ``(row_count,column_count*plane_count)``.
+'''
+# The use of ``numpy.uint16``, below, is to convert each row to a NumPy
+# array with data type ``numpy.uint16``. This is a feature of NumPy,
+# discussed further in
+# http://docs.scipy.org/doc/numpy/user/basics.types.html .
+# You can use avoid the explicit conversion with
+# ``numpy.vstack(pngdata)``, but then NumPy will pick the array's data
+# type; in practice it seems to pick ``numpy.int32``, which is large enough
+# to hold any pixel value for any PNG image but uses 4 bytes per value when
+# 1 or 2 would be enough.
+# --- extract 001 start
+image_2d = numpy.vstack(itertools.imap(numpy.uint16, pngdata))
+# --- extract 001 end
+# Do not be tempted to use ``numpy.asarray``; when passed an iterator
+# (`pngdata` is often an iterator) it will attempt to create a size 1
+# array with the iterator as its only element.
+# An alternative to the above is to create the target array of the right
+# shape, then populate it row by row:
+if 0:
+ image_2d = numpy.zeros((row_count,plane_count*column_count),
+ dtype=numpy.uint16)
+ for row_index, one_boxed_row_flat_pixels in enumerate(pngdata):
+ image_2d[row_index,:]=one_boxed_row_flat_pixels
+
+del pngReader
+del pngdata
+
+
+''' Reconfigure for easier referencing, similar to
+ Boxed row boxed pixel:
+ list([ (R,G,B), (R,G,B), (R,G,B) ],
+ [ (R,G,B), (R,G,B), (R,G,B) ])
+ Array dimensions for this example: (2,3,3)
+
+ ``image_3d`` will contain the image as a three-dimensional numpy
+ array, having dimensions ``(row_count,column_count,plane_count)``.
+'''
+# --- extract 002 start
+image_3d = numpy.reshape(image_2d,
+ (row_count,column_count,plane_count))
+# --- extract 002 end
+
+
+''' ============= '''
+
+''' Convert NumPy image_3d array to PNG image file.
+
+ If the data is three-dimensional, as it is above, the best thing
+ to do is reshape it into a two-dimensional array with a shape of
+ ``(row_count, column_count*plane_count)``. Because a
+ two-dimensional numpy array is an iterator, it can be passed
+ directly to the ``png.Writer.write`` method.
+'''
+
+row_count, column_count, plane_count = image_3d.shape
+assert plane_count==3
+
+pngfile = open('picture_out.png', 'wb')
+try:
+ # This example assumes that you have 16-bit pixel values in the data
+ # array (that's what the ``bitdepth=16`` argument is for).
+ # If you don't, then the resulting PNG file will likely be
+ # very dark. Hey, it's only an example.
+ pngWriter = png.Writer(column_count, row_count,
+ greyscale=False,
+ alpha=False,
+ bitdepth=16)
+ # As of 2009-04-13 passing a numpy array that has an element type
+ # that is a numpy integer type (for example, the `image_3d` array has an
+ # element type of ``numpy.uint16``) generates a deprecation warning.
+ # This is probably a bug in numpy; it may go away in the future.
+ # The code still works despite the warning.
+ # See http://code.google.com/p/pypng/issues/detail?id=44
+# --- extract 003 start
+ pngWriter.write(pngfile,
+ numpy.reshape(image_3d, (-1, column_count*plane_count)))
+# --- extract 003 end
+finally:
+ pngfile.close()
+
diff --git a/build/pypng/iccp.py b/build/pypng/iccp.py
new file mode 100644
index 0000000..190db73
--- /dev/null
+++ b/build/pypng/iccp.py
@@ -0,0 +1,537 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/iccp.py $
+# $Rev: 182 $
+
+# iccp
+#
+# International Color Consortium Profile
+#
+# Tools for manipulating ICC profiles.
+#
+# An ICC profile can be extracted from a PNG image (iCCP chunk).
+#
+#
+# Non-standard ICCP tags.
+#
+# Apple use some (widespread but) non-standard tags. These can be
+# displayed in Apple's ColorSync Utility.
+# - 'vcgt' (Video Card Gamma Tag). Table to load into video
+# card LUT to apply gamma.
+# - 'ndin' Apple display native information.
+# - 'dscm' Apple multi-localized description strings.
+# - 'mmod' Apple display make and model information.
+#
+
+# References
+#
+# [ICC 2001] ICC Specification ICC.1:2001-04 (Profile version 2.4.0)
+# [ICC 2004] ICC Specification ICC.1:2004-10 (Profile version 4.2.0.0)
+
+import struct
+
+import png
+
+class FormatError(Exception):
+ pass
+
+class Profile:
+ """An International Color Consortium Profile (ICC Profile)."""
+
+ def __init__(self):
+ self.rawtagtable = None
+ self.rawtagdict = {}
+ self.d = dict()
+
+ def fromFile(self, inp, name='<unknown>'):
+
+ # See [ICC 2004]
+ profile = inp.read(128)
+ if len(profile) < 128:
+ raise FormatError("ICC Profile is too short.")
+ size, = struct.unpack('>L', profile[:4])
+ profile += inp.read(d['size'] - len(profile))
+ return self.fromString(profile, name)
+
+ def fromString(self, profile, name='<unknown>'):
+ self.d = dict()
+ d = self.d
+ if len(profile) < 128:
+ raise FormatError("ICC Profile is too short.")
+ d.update(
+ zip(['size', 'preferredCMM', 'version',
+ 'profileclass', 'colourspace', 'pcs'],
+ struct.unpack('>L4sL4s4s4s', profile[:24])))
+ if len(profile) < d['size']:
+ warnings.warn(
+ 'Profile size declared to be %d, but only got %d bytes' %
+ (d['size'], len(profile)))
+ d['version'] = '%08x' % d['version']
+ d['created'] = readICCdatetime(profile[24:36])
+ d.update(
+ zip(['acsp', 'platform', 'flag', 'manufacturer', 'model'],
+ struct.unpack('>4s4s3L', profile[36:56])))
+ if d['acsp'] != 'acsp':
+ warnings.warn('acsp field not present (not an ICC Profile?).')
+ d['deviceattributes'] = profile[56:64]
+ d['intent'], = struct.unpack('>L', profile[64:68])
+ d['pcsilluminant'] = readICCXYZNumber(profile[68:80])
+ d['creator'] = profile[80:84]
+ d['id'] = profile[84:100]
+ ntags, = struct.unpack('>L', profile[128:132])
+ d['ntags'] = ntags
+ fmt = '4s2L' * ntags
+ # tag table
+ tt = struct.unpack('>' + fmt, profile[132:132+12*ntags])
+ tt = group(tt, 3)
+
+ # Could (should) detect 2 or more tags having the same sig. But
+ # we don't. Two or more tags with the same sig is illegal per
+ # the ICC spec.
+
+ # Convert (sig,offset,size) triples into (sig,value) pairs.
+ rawtag = map(lambda x: (x[0], profile[x[1]:x[1]+x[2]]), tt)
+ self.rawtagtable = rawtag
+ self.rawtagdict = dict(rawtag)
+ tag = dict()
+ # Interpret the tags whose types we know about
+ for sig, v in rawtag:
+ if sig in tag:
+ warnings.warn("Duplicate tag %r found. Ignoring." % sig)
+ continue
+ v = ICCdecode(v)
+ if v is not None:
+ tag[sig] = v
+ self.tag = tag
+ return self
+
+ def greyInput(self):
+ """Adjust ``self.d`` dictionary for greyscale input device.
+ ``profileclass`` is 'scnr', ``colourspace`` is 'GRAY', ``pcs``
+ is 'XYZ '.
+ """
+
+ self.d.update(dict(profileclass='scnr',
+ colourspace='GRAY', pcs='XYZ '))
+ return self
+
+ def maybeAddDefaults(self):
+ if self.rawtagdict:
+ return
+ self._addTags(
+ cprt='Copyright unknown.',
+ desc='created by $URL: http://pypng.googlecode.com/svn/trunk/code/iccp.py $ $Rev: 182 $',
+ wtpt=D50(),
+ )
+
+ def addTags(self, **k):
+ self.maybeAddDefaults()
+ self._addTags(**k)
+
+ def _addTags(self, **k):
+ """Helper for :meth:`addTags`."""
+
+ for tag, thing in k.items():
+ if not isinstance(thing, (tuple, list)):
+ thing = (thing,)
+ typetag = defaulttagtype[tag]
+ self.rawtagdict[tag] = encode(typetag, *thing)
+ return self
+
+ def write(self, out):
+ """Write ICC Profile to the file."""
+
+ if not self.rawtagtable:
+ self.rawtagtable = self.rawtagdict.items()
+ tags = tagblock(self.rawtagtable)
+ self.writeHeader(out, 128 + len(tags))
+ out.write(tags)
+ out.flush()
+
+ return self
+
+ def writeHeader(self, out, size=999):
+ """Add default values to the instance's `d` dictionary, then
+ write a header out onto the file stream. The size of the
+ profile must be specified using the `size` argument.
+ """
+
+ def defaultkey(d, key, value):
+ """Add ``[key]==value`` to the dictionary `d`, but only if
+ it does not have that key already.
+ """
+
+ if key in d:
+ return
+ d[key] = value
+
+ z = '\x00' * 4
+ defaults = dict(preferredCMM=z,
+ version='02000000',
+ profileclass=z,
+ colourspace=z,
+ pcs='XYZ ',
+ created=writeICCdatetime(),
+ acsp='acsp',
+ platform=z,
+ flag=0,
+ manufacturer=z,
+ model=0,
+ deviceattributes=0,
+ intent=0,
+ pcsilluminant=encodefuns()['XYZ'](*D50()),
+ creator=z,
+ )
+ for k,v in defaults.items():
+ defaultkey(self.d, k, v)
+
+ hl = map(self.d.__getitem__,
+ ['preferredCMM', 'version', 'profileclass', 'colourspace',
+ 'pcs', 'created', 'acsp', 'platform', 'flag',
+ 'manufacturer', 'model', 'deviceattributes', 'intent',
+ 'pcsilluminant', 'creator'])
+ # Convert to struct.pack input
+ hl[1] = int(hl[1], 16)
+
+ out.write(struct.pack('>L4sL4s4s4s12s4s4sL4sLQL12s4s', size, *hl))
+ out.write('\x00' * 44)
+ return self
+
+def encodefuns():
+ """Returns a dictionary mapping ICC type signature sig to encoding
+ function. Each function returns a string comprising the content of
+ the encoded value. To form the full value, the type sig and the 4
+ zero bytes should be prefixed (8 bytes).
+ """
+
+ def desc(ascii):
+ """Return textDescription type [ICC 2001] 6.5.17. The ASCII part is
+ filled in with the string `ascii`, the Unicode and ScriptCode parts
+ are empty."""
+
+ ascii += '\x00'
+ l = len(ascii)
+
+ return struct.pack('>L%ds2LHB67s' % l,
+ l, ascii, 0, 0, 0, 0, '')
+
+ def text(ascii):
+ """Return textType [ICC 2001] 6.5.18."""
+
+ return ascii + '\x00'
+
+ def curv(f=None, n=256):
+ """Return a curveType, [ICC 2001] 6.5.3. If no arguments are
+ supplied then a TRC for a linear response is generated (no entries).
+ If an argument is supplied and it is a number (for *f* to be a
+ number it means that ``float(f)==f``) then a TRC for that
+ gamma value is generated.
+ Otherwise `f` is assumed to be a function that maps [0.0, 1.0] to
+ [0.0, 1.0]; an `n` element table is generated for it.
+ """
+
+ if f is None:
+ return struct.pack('>L', 0)
+ try:
+ if float(f) == f:
+ return struct.pack('>LH', 1, int(round(f*2**8)))
+ except (TypeError, ValueError):
+ pass
+ assert n >= 2
+ table = []
+ M = float(n-1)
+ for i in range(n):
+ x = i/M
+ table.append(int(round(f(x) * 65535)))
+ return struct.pack('>L%dH' % n, n, *table)
+
+ def XYZ(*l):
+ return struct.pack('>3l', *map(fs15f16, l))
+
+ return locals()
+
+# Tag type defaults.
+# Most tags can only have one or a few tag types.
+# When encoding, we associate a default tag type with each tag so that
+# the encoding is implicit.
+defaulttagtype=dict(
+ A2B0='mft1',
+ A2B1='mft1',
+ A2B2='mft1',
+ bXYZ='XYZ',
+ bTRC='curv',
+ B2A0='mft1',
+ B2A1='mft1',
+ B2A2='mft1',
+ calt='dtim',
+ targ='text',
+ chad='sf32',
+ chrm='chrm',
+ cprt='desc',
+ crdi='crdi',
+ dmnd='desc',
+ dmdd='desc',
+ devs='',
+ gamt='mft1',
+ kTRC='curv',
+ gXYZ='XYZ',
+ gTRC='curv',
+ lumi='XYZ',
+ meas='',
+ bkpt='XYZ',
+ wtpt='XYZ',
+ ncol='',
+ ncl2='',
+ resp='',
+ pre0='mft1',
+ pre1='mft1',
+ pre2='mft1',
+ desc='desc',
+ pseq='',
+ psd0='data',
+ psd1='data',
+ psd2='data',
+ psd3='data',
+ ps2s='data',
+ ps2i='data',
+ rXYZ='XYZ',
+ rTRC='curv',
+ scrd='desc',
+ scrn='',
+ tech='sig',
+ bfd='',
+ vued='desc',
+ view='view',
+)
+
+def encode(tsig, *l):
+ """Encode a Python value as an ICC type. `tsig` is the type
+ signature to (the first 4 bytes of the encoded value, see [ICC 2004]
+ section 10.
+ """
+
+ fun = encodefuns()
+ if tsig not in fun:
+ raise "No encoder for type %r." % tsig
+ v = fun[tsig](*l)
+ # Padd tsig out with spaces.
+ tsig = (tsig + ' ')[:4]
+ return tsig + '\x00'*4 + v
+
+def tagblock(tag):
+ """`tag` should be a list of (*signature*, *element*) pairs, where
+ *signature* (the key) is a length 4 string, and *element* is the
+ content of the tag element (another string).
+
+ The entire tag block (consisting of first a table and then the
+ element data) is constructed and returned as a string.
+ """
+
+ n = len(tag)
+ tablelen = 12*n
+
+ # Build the tag table in two parts. A list of 12-byte tags, and a
+ # string of element data. Offset is the offset from the start of
+ # the profile to the start of the element data (so the offset for
+ # the next element is this offset plus the length of the element
+ # string so far).
+ offset = 128 + tablelen + 4
+ # The table. As a string.
+ table = ''
+ # The element data
+ element = ''
+ for k,v in tag:
+ table += struct.pack('>4s2L', k, offset + len(element), len(v))
+ element += v
+ return struct.pack('>L', n) + table + element
+
+def iccp(out, inp):
+ profile = Profile().fromString(*profileFromPNG(inp))
+ print >>out, profile.d
+ print >>out, map(lambda x: x[0], profile.rawtagtable)
+ print >>out, profile.tag
+
+def profileFromPNG(inp):
+ """Extract profile from PNG file. Return (*profile*, *name*)
+ pair."""
+ r = png.Reader(file=inp)
+ _,chunk = r.chunk('iCCP')
+ i = chunk.index('\x00')
+ name = chunk[:i]
+ compression = chunk[i+1]
+ assert compression == chr(0)
+ profile = chunk[i+2:].decode('zlib')
+ return profile, name
+
+def iccpout(out, inp):
+ """Extract ICC Profile from PNG file `inp` and write it to
+ the file `out`."""
+
+ out.write(profileFromPNG(inp)[0])
+
+def fs15f16(x):
+ """Convert float to ICC s15Fixed16Number (as a Python ``int``)."""
+
+ return int(round(x * 2**16))
+
+def D50():
+ """Return D50 illuminant as an (X,Y,Z) triple."""
+
+ # See [ICC 2001] A.1
+ return (0.9642, 1.0000, 0.8249)
+
+
+def writeICCdatetime(t=None):
+ """`t` should be a gmtime tuple (as returned from
+ ``time.gmtime()``). If not supplied, the current time will be used.
+ Return an ICC dateTimeNumber in a 12 byte string.
+ """
+
+ import time
+ if t is None:
+ t = time.gmtime()
+ return struct.pack('>6H', *t[:6])
+
+def readICCdatetime(s):
+ """Convert from 12 byte ICC representation of dateTimeNumber to
+ ISO8601 string. See [ICC 2004] 5.1.1"""
+
+ return '%04d-%02d-%02dT%02d:%02d:%02dZ' % struct.unpack('>6H', s)
+
+def readICCXYZNumber(s):
+ """Convert from 12 byte ICC representation of XYZNumber to (x,y,z)
+ triple of floats. See [ICC 2004] 5.1.11"""
+
+ return s15f16l(s)
+
+def s15f16l(s):
+ """Convert sequence of ICC s15Fixed16 to list of float."""
+ # Note: As long as float has at least 32 bits of mantissa, all
+ # values are preserved.
+ n = len(s)//4
+ t = struct.unpack('>%dl' % n, s)
+ return map((2**-16).__mul__, t)
+
+# Several types and their byte encodings are defined by [ICC 2004]
+# section 10. When encoded, a value begins with a 4 byte type
+# signature. We use the same 4 byte type signature in the names of the
+# Python functions that decode the type into a Pythonic representation.
+
+def ICCdecode(s):
+ """Take an ICC encoded tag, and dispatch on its type signature
+ (first 4 bytes) to decode it into a Python value. Pair (*sig*,
+ *value*) is returned, where *sig* is a 4 byte string, and *value* is
+ some Python value determined by the content and type.
+ """
+
+ sig = s[0:4].strip()
+ f=dict(text=RDtext,
+ XYZ=RDXYZ,
+ curv=RDcurv,
+ vcgt=RDvcgt,
+ sf32=RDsf32,
+ )
+ if sig not in f:
+ return None
+ return (sig, f[sig](s))
+
+def RDXYZ(s):
+ """Convert ICC XYZType to rank 1 array of trimulus values."""
+
+ # See [ICC 2001] 6.5.26
+ assert s[0:4] == 'XYZ '
+ return readICCXYZNumber(s[8:])
+
+def RDsf32(s):
+ """Convert ICC s15Fixed16ArrayType to list of float."""
+ # See [ICC 2004] 10.18
+ assert s[0:4] == 'sf32'
+ return s15f16l(s[8:])
+
+def RDmluc(s):
+ """Convert ICC multiLocalizedUnicodeType. This types encodes
+ several strings together with a language/country code for each
+ string. A list of (*lc*, *string*) pairs is returned where *lc* is
+ the 4 byte language/country code, and *string* is the string
+ corresponding to that code. It seems unlikely that the same
+ language/country code will appear more than once with different
+ strings, but the ICC standard does not prohibit it."""
+ # See [ICC 2004] 10.13
+ assert s[0:4] == 'mluc'
+ n,sz = struct.unpack('>2L', s[8:16])
+ assert sz == 12
+ record = []
+ for i in range(n):
+ lc,l,o = struct.unpack('4s2L', s[16+12*n:28+12*n])
+ record.append(lc, s[o:o+l])
+ # How are strings encoded?
+ return record
+
+def RDtext(s):
+ """Convert ICC textType to Python string."""
+ # Note: type not specified or used in [ICC 2004], only in older
+ # [ICC 2001].
+ # See [ICC 2001] 6.5.18
+ assert s[0:4] == 'text'
+ return s[8:-1]
+
+def RDcurv(s):
+ """Convert ICC curveType."""
+ # See [ICC 2001] 6.5.3
+ assert s[0:4] == 'curv'
+ count, = struct.unpack('>L', s[8:12])
+ if count == 0:
+ return dict(gamma=1)
+ table = struct.unpack('>%dH' % count, s[12:])
+ if count == 1:
+ return dict(gamma=table[0]*2**-8)
+ return table
+
+def RDvcgt(s):
+ """Convert Apple CMVideoCardGammaType."""
+ # See
+ # http://developer.apple.com/documentation/GraphicsImaging/Reference/ColorSync_Manager/Reference/reference.html#//apple_ref/c/tdef/CMVideoCardGammaType
+ assert s[0:4] == 'vcgt'
+ tagtype, = struct.unpack('>L', s[8:12])
+ if tagtype != 0:
+ return s[8:]
+ if tagtype == 0:
+ # Table.
+ channels,count,size = struct.unpack('>3H', s[12:18])
+ if size == 1:
+ fmt = 'B'
+ elif size == 2:
+ fmt = 'H'
+ else:
+ return s[8:]
+ l = len(s[18:])//size
+ t = struct.unpack('>%d%s' % (l, fmt), s[18:])
+ t = group(t, count)
+ return size, t
+ return s[8:]
+
+
+def group(s, n):
+ # See
+ # http://www.python.org/doc/2.6/library/functions.html#zip
+ return zip(*[iter(s)]*n)
+
+
+def main(argv=None):
+ import sys
+ from getopt import getopt
+ if argv is None:
+ argv = sys.argv
+ argv = argv[1:]
+ opt,arg = getopt(argv, 'o:')
+ if len(arg) > 0:
+ inp = open(arg[0], 'rb')
+ else:
+ inp = sys.stdin
+ for o,v in opt:
+ if o == '-o':
+ f = open(v, 'wb')
+ return iccpout(f, inp)
+ return iccp(sys.stdout, inp)
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/mkiccp.py b/build/pypng/mkiccp.py
new file mode 100644
index 0000000..08e8df6
--- /dev/null
+++ b/build/pypng/mkiccp.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/mkiccp.py $
+# $Rev: 182 $
+# Make ICC Profile
+
+# References
+#
+# [ICC 2001] ICC Specification ICC.1:2001-04 (Profile version 2.4.0)
+# [ICC 2004] ICC Specification ICC.1:2004-10 (Profile version 4.2.0.0)
+
+import struct
+
+# Local module.
+import iccp
+
+def black(m):
+ """Return a function that maps all values from [0.0,m] to 0, and maps
+ the range [m,1.0] into [0.0, 1.0] linearly.
+ """
+
+ m = float(m)
+
+ def f(x):
+ if x <= m:
+ return 0.0
+ return (x-m)/(1.0-m)
+ return f
+
+# For monochrome input the required tags are (See [ICC 2001] 6.3.1.1):
+# profileDescription [ICC 2001] 6.4.32
+# grayTRC [ICC 2001] 6.4.19
+# mediaWhitePoint [ICC 2001] 6.4.25
+# copyright [ICC 2001] 6.4.13
+
+def agreyprofile(out):
+ it = iccp.Profile().greyInput()
+ it.addTags(kTRC=black(0.07))
+ it.write(out)
+
+def main():
+ import sys
+ agreyprofile(sys.stdout)
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pdsimgtopng b/build/pypng/pdsimgtopng
new file mode 100644
index 0000000..975db93
--- /dev/null
+++ b/build/pypng/pdsimgtopng
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pdsimgtopng $
+# $Rev: 154 $
+# PDS Image to PNG
+
+import re
+import struct
+
+import png
+
+class FormatError(Exception):
+ pass
+
+def pdskey(s, k):
+ """Lookup key `k` in string `s`. Returns value (as a string), or
+ raises exception if not found.
+ """
+
+ assert re.match(r' *\^?[:\w]+$', k)
+ safere = '^' + re.escape(k) +r' *= *(\w+)'
+ m = re.search(safere, s, re.MULTILINE)
+ if not m:
+ raise FormatError("Can't find %s." % k)
+ return m.group(1)
+
+def img(inp):
+ """Open the PDS IMG file `inp` and return (*pixels*, *info*).
+ *pixels* is an iterator over the rows, *info* is the information
+ dictionary.
+ """
+
+ err = __import__('sys').stderr
+
+ consumed = 1024
+
+ s = inp.read(consumed)
+ record_type = pdskey(s, 'RECORD_TYPE')
+ if record_type != 'FIXED_LENGTH':
+ raise FormatError(
+ "Can only deal with FIXED_LENGTH record type (found %s)" %
+ record_type)
+ record_bytes = int(pdskey(s,'RECORD_BYTES'))
+ file_records = int(pdskey(s, 'FILE_RECORDS'))
+ label_records = int(pdskey(s, 'LABEL_RECORDS'))
+ remaining = label_records * record_bytes - consumed
+ s += inp.read(remaining)
+ consumed += remaining
+
+ image_pointer = int(pdskey(s, '^IMAGE'))
+ # "^IMAGE" locates a record. Records are numbered starting from 1.
+ image_index = image_pointer - 1
+ image_offset = image_index * record_bytes
+ gap = image_offset - consumed
+ assert gap >= 0
+ if gap:
+ inp.read(gap)
+ # This assumes there is only one OBJECT in the file, and it is the
+ # IMAGE.
+ height = int(pdskey(s, ' LINES'))
+ width = int(pdskey(s, ' LINE_SAMPLES'))
+ sample_type = pdskey(s, ' SAMPLE_TYPE')
+ sample_bits = int(pdskey(s, ' SAMPLE_BITS'))
+ # For Messenger MDIS, SAMPLE_BITS is reported as 16, but only values
+ # from 0 ot 4095 are used.
+ bitdepth = 12
+ if sample_type == 'MSB_UNSIGNED_INTEGER':
+ fmt = '>H'
+ else:
+ raise 'Unknown sample type: %s.' % sample_type
+ sample_bytes = (1,2)[bitdepth > 8]
+ row_bytes = sample_bytes * width
+ fmt = fmt[:1] + str(width) + fmt[1:]
+ def rowiter():
+ for y in range(height):
+ yield struct.unpack(fmt, inp.read(row_bytes))
+ info = dict(greyscale=True, alpha=False, bitdepth=bitdepth,
+ size=(width,height), gamma=1.0)
+ return rowiter(), info
+
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+ argv = argv[1:]
+ arg = argv
+ if len(arg) >= 1:
+ f = open(arg[0], 'rb')
+ else:
+ f = sys.stdin
+ pixels,info = img(f)
+ w = png.Writer(**info)
+ w.write(sys.stdout, pixels)
+
+if __name__ == '__main__':
+ main()
+
+
diff --git a/build/pypng/pipasgrey b/build/pypng/pipasgrey
new file mode 100644
index 0000000..2b3727f
--- /dev/null
+++ b/build/pypng/pipasgrey
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pipasgrey $
+# $Rev: 187 $
+
+# pipasgrey
+
+# Convert image to grey (L, or LA), but only if that involves no colour
+# change.
+
+def asgrey(out, inp, quiet=False):
+ """Convert image to greyscale, but only when no colour change. This
+ works by using the input G channel (green) as the output L channel
+ (luminance) and checking that every pixel is grey as we go. A non-grey
+ pixel will raise an error, but if `quiet` is true then the grey pixel
+ check is suppressed.
+ """
+
+ from array import array
+
+ import png
+
+ r = png.Reader(file=inp)
+ _,_,pixels,info = r.asDirect()
+ if info['greyscale']:
+ w = png.Writer(**info)
+ return w.write(out, pixels)
+ planes = info['planes']
+ targetplanes = planes - 2
+ alpha = info['alpha']
+ width = info['size'][0]
+ typecode = 'BH'[info['bitdepth'] > 8]
+ # Values per target row
+ vpr = width * (targetplanes)
+ def iterasgrey():
+ for i,row in enumerate(pixels):
+ row = array(typecode, row)
+ targetrow = array(typecode, [0]*vpr)
+ # Copy G (and possibly A) channel.
+ green = row[0::planes]
+ if alpha:
+ targetrow[0::2] = green
+ targetrow[1::2] = row[3::4]
+ else:
+ targetrow = green
+ # Check R and B channel match.
+ if not quiet and (
+ green != row[0::planes] or green != row[2::planes]):
+ raise ValueError('Row %i contains non-grey pixel.' % i)
+ yield targetrow
+ info['greyscale'] = True
+ del info['planes']
+ w = png.Writer(**info)
+ w.write(out, iterasgrey())
+
+def main(argv=None):
+ from getopt import getopt
+ import sys
+ if argv is None:
+ argv = sys.argv
+ argv = argv[1:]
+ opt,argv = getopt(argv, 'q')
+ quiet = False
+ for o,v in opt:
+ if o == '-q':
+ quiet = True
+ if len(argv) > 0:
+ f = open(argv[0], 'rb')
+ else:
+ f = sys.stdin
+ return asgrey(sys.stdout, f, quiet)
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pipcat b/build/pypng/pipcat
new file mode 100644
index 0000000..e0d0805
--- /dev/null
+++ b/build/pypng/pipcat
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pipcat $
+# $Rev: 77 $
+
+# http://www.python.org/doc/2.4.4/lib/module-itertools.html
+import itertools
+import sys
+
+import png
+
+def cat(out, l):
+ """Concatenate the list of images. All input images must be same
+ height and have the same number of channels. They are concatenated
+ left-to-right. `out` is the (open file) destination for the
+ output image. `l` should be a list of open files (the input
+ image files).
+ """
+
+ l = map(lambda f: png.Reader(file=f), l)
+ # Ewgh, side effects.
+ map(lambda r: r.preamble(), l)
+ # The reference height; from the first image.
+ height = l[0].height
+ # The total target width
+ width = 0
+ for i,r in enumerate(l):
+ if r.height != height:
+ raise Error('Image %d, height %d, does not match %d.' %
+ (i, r.height, height))
+ width += r.width
+ pixel,info = zip(*map(lambda r: r.asDirect()[2:4], l))
+ tinfo = dict(info[0])
+ del tinfo['size']
+ w = png.Writer(width, height, **tinfo)
+ def itercat():
+ for row in itertools.izip(*pixel):
+ yield itertools.chain(*row)
+ w.write(out, itercat())
+
+def main(argv):
+ return cat(sys.stdout, map(lambda n: open(n, 'rb'), argv[1:]))
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/build/pypng/pipcolours b/build/pypng/pipcolours
new file mode 100644
index 0000000..7c76df8
--- /dev/null
+++ b/build/pypng/pipcolours
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pipcolours $
+# $Rev: 96 $
+
+# pipcolours - extract all colours present in source image.
+
+def colours(out, inp):
+ import itertools
+ import png
+
+ r = png.Reader(file=inp)
+ _,_,pixels,info = r.asDirect()
+ planes = info['planes']
+ col = set()
+ for row in pixels:
+ # Ewgh, side effects on col
+ map(col.add, png.group(row, planes))
+ col,planes = channel_reduce(col, planes)
+ col = list(col)
+ col.sort()
+ col = list(itertools.chain(*col))
+ width = len(col)//planes
+ greyscale = planes in (1,2)
+ alpha = planes in (2,4)
+ bitdepth = info['bitdepth']
+ w = png.Writer(width, 1,
+ bitdepth=bitdepth, greyscale=greyscale, alpha=alpha)
+ w.write(out, [col])
+
+def channel_reduce(col, planes):
+ """Attempt to reduce the number of channels in the set of
+ colours."""
+ if planes >= 3:
+ def isgrey(c):
+ return c[0] == c[1] == c[2]
+ if min(map(isgrey, col)) == True:
+ # Every colour is grey.
+ col = set(map(lambda x: x[0::3], col))
+ planes -= 2
+ return col,planes
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+
+ argv = argv[1:]
+ if len(argv) > 0:
+ f = open(argv[0], 'rb')
+ else:
+ f = sys.stdin
+ return colours(sys.stdout, f)
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pipcomposite b/build/pypng/pipcomposite
new file mode 100644
index 0000000..21dd283
--- /dev/null
+++ b/build/pypng/pipcomposite
@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pipcomposite $
+# $Rev: 208 $
+# pipcomposite
+# Image alpha compositing.
+
+"""
+pipcomposite [--background #rrggbb] file.png
+
+Composite an image onto a background and output the result. The
+background colour is specified with an HTML-style triple (3, 6, or 12
+hex digits), and defaults to black (#000).
+
+The output PNG has no alpha channel.
+
+It is valid for the input to have no alpha channel, but it doesn't
+make much sense: the output will equal the input.
+"""
+
+import sys
+
+def composite(out, inp, background):
+ import png
+
+ p = png.Reader(file=inp)
+ w,h,pixel,info = p.asRGBA()
+
+ outinfo = dict(info)
+ outinfo['alpha'] = False
+ outinfo['planes'] -= 1
+ outinfo['interlace'] = 0
+
+ # Convert to tuple and normalise to same range as source.
+ background = rgbhex(background)
+ maxval = float(2**info['bitdepth'] - 1)
+ background = map(lambda x: int(0.5 + x*maxval/65535.0),
+ background)
+ # Repeat background so that it's a whole row of sample values.
+ background *= w
+
+ def iterrow():
+ for row in pixel:
+ # Remove alpha from row, then create a list with one alpha
+ # entry _per channel value_.
+ # Squirrel the alpha channel away (and normalise it).
+ t = map(lambda x: x/maxval, row[3::4])
+ row = list(row)
+ del row[3::4]
+ alpha = row[:]
+ for i in range(3):
+ alpha[i::3] = t
+ assert len(alpha) == len(row) == len(background)
+ yield map(lambda a,v,b: int(0.5 + a*v + (1.0-a)*b),
+ alpha, row, background)
+
+ w = png.Writer(**outinfo)
+ w.write(out, iterrow())
+
+def rgbhex(s):
+ """Take an HTML style string of the form "#rrggbb" and return a
+ colour (R,G,B) triple. Following the initial '#' there can be 3, 6,
+ or 12 digits (for 4-, 8- or 16- bits per channel). In all cases the
+ values are expanded to a full 16-bit range, so the returned values
+ are all in range(65536).
+ """
+
+ assert s[0] == '#'
+ s = s[1:]
+ assert len(s) in (3,6,12)
+
+ # Create a target list of length 12, and expand the string s to make
+ # it length 12.
+ l = ['z']*12
+ if len(s) == 3:
+ for i in range(4):
+ l[i::4] = s
+ if len(s) == 6:
+ for i in range(2):
+ l[i::4] = s[i::2]
+ l[i+2::4] = s[i::2]
+ if len(s) == 12:
+ l[:] = s
+ s = ''.join(l)
+ return map(lambda x: int(x, 16), (s[:4], s[4:8], s[8:]))
+
+class Usage(Exception):
+ pass
+
+def main(argv=None):
+ import getopt
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+
+ argv = argv[1:]
+
+ try:
+ try:
+ opt,arg = getopt.getopt(argv, '',
+ ['background='])
+ except getopt.error, msg:
+ raise Usage(msg)
+ background = '#000'
+ for o,v in opt:
+ if o in ['--background']:
+ background = v
+ except Usage, err:
+ print >>sys.stderr, __doc__
+ print >>sys.stderr, str(err)
+ return 2
+
+ if len(arg) > 0:
+ f = open(arg[0], 'rb')
+ else:
+ f = sys.stdin
+ return composite(sys.stdout, f, background)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pipdither b/build/pypng/pipdither
new file mode 100644
index 0000000..c14c76c
--- /dev/null
+++ b/build/pypng/pipdither
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pipdither $
+# $Rev: 150 $
+
+# pipdither
+# Error Diffusing image dithering.
+# Now with serpentine scanning.
+
+# See http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
+
+# http://www.python.org/doc/2.4.4/lib/module-bisect.html
+from bisect import bisect_left
+
+import png
+
+def dither(out, inp,
+ bitdepth=1, linear=False, defaultgamma=1.0, targetgamma=None,
+ cutoff=0.75):
+ """Dither the input PNG `inp` into an image with a smaller bit depth
+ and write the result image onto `out`. `bitdepth` specifies the bit
+ depth of the new image.
+
+ Normally the source image gamma is honoured (the image is
+ converted into a linear light space before being dithered), but
+ if the `linear` argument is true then the image is treated as
+ being linear already: no gamma conversion is done (this is
+ quicker, and if you don't care much about accuracy, it won't
+ matter much).
+
+ Images with no gamma indication (no ``gAMA`` chunk) are normally
+ treated as linear (gamma = 1.0), but often it can be better
+ to assume a different gamma value: For example continuous tone
+ photographs intended for presentation on the web often carry
+ an implicit assumption of being encoded with a gamma of about
+ 0.45 (because that's what you get if you just "blat the pixels"
+ onto a PC framebuffer), so ``defaultgamma=0.45`` might be a
+ good idea. `defaultgamma` does not override a gamma value
+ specified in the file itself: It is only used when the file
+ does not specify a gamma.
+
+ If you (pointlessly) specify both `linear` and `defaultgamma`,
+ `linear` wins.
+
+ The gamma of the output image is, by default, the same as the input
+ image. The `targetgamma` argument can be used to specify a
+ different gamma for the output image. This effectively recodes the
+ image to a different gamma, dithering as we go. The gamma specified
+ is the exponent used to encode the output file (and appears in the
+ output PNG's ``gAMA`` chunk); it is usually less than 1.
+
+ """
+
+ # Encoding is what happened when the PNG was made (and also what
+ # happens when we output the PNG). Decoding is what we do to the
+ # source PNG in order to process it.
+
+ # The dithering algorithm is not completely general; it
+ # can only do bit depth reduction, not arbitrary palette changes.
+ import operator
+ maxval = 2**bitdepth - 1
+ r = png.Reader(file=inp)
+ # If image gamma is 1 or gamma is not present and we are assuming a
+ # value of 1, then it is faster to pass a maxval parameter to
+ # asFloat (the multiplications get combined). With gamma, we have
+ # to have the pixel values from 0.0 to 1.0 (as long as we are doing
+ # gamma correction here).
+ # Slightly annoyingly, we don't know the image gamma until we've
+ # called asFloat().
+ _,_,pixels,info = r.asDirect()
+ planes = info['planes']
+ assert planes == 1
+ width = info['size'][0]
+ sourcemaxval = 2**info['bitdepth'] - 1
+ if linear:
+ gamma = 1
+ else:
+ gamma = info.get('gamma') or defaultgamma
+ # Convert gamma from encoding gamma to the required power for
+ # decoding.
+ decode = 1.0/gamma
+ # Build a lookup table for decoding; convert from pixel values to linear
+ # space:
+ sourcef = 1.0/sourcemaxval
+ incode = map(sourcef.__mul__, range(sourcemaxval+1))
+ if decode != 1.0:
+ incode = map(decode.__rpow__, incode)
+ # Could be different, later on. targetdecode is the assumed gamma
+ # that is going to be used to decoding the target PNG. It is the
+ # reciprocal of the exponent that we use to encode the target PNG.
+ # This is the value that we need to build our table that we use for
+ # converting from linear to target colour space.
+ if targetgamma is None:
+ targetdecode = decode
+ else:
+ targetdecode = 1.0/targetgamma
+ # The table we use for encoding (creating the target PNG), still
+ # maps from pixel value to linear space, but we use it inverted, by
+ # searching through it with bisect.
+ targetf = 1.0/maxval
+ outcode = map(targetf.__mul__, range(maxval+1))
+ if targetdecode != 1.0:
+ outcode = map(targetdecode.__rpow__, outcode)
+ # The table used for choosing output codes. These values represent
+ # the cutoff points between two adjacent output codes.
+ choosecode = zip(outcode[1:], outcode)
+ p = cutoff
+ choosecode = map(lambda x: x[0]*p+x[1]*(1.0-p), choosecode)
+ def iterdither():
+ # Errors diffused downwards (into next row)
+ ed = [0.0]*width
+ flipped = False
+ for row in pixels:
+ row = map(incode.__getitem__, row)
+ row = map(operator.add, ed, row)
+ if flipped:
+ row = row[::-1]
+ targetrow = [0] * width
+ for i,v in enumerate(row):
+ # Clamp. Necessary because previously added errors may take
+ # v out of range.
+ v = max(0.0, min(v, 1.0))
+ # `it` will be the index of the chosen target colour;
+ it = bisect_left(choosecode, v)
+ t = outcode[it]
+ targetrow[i] = it
+ # err is the error that needs distributing.
+ err = v - t
+ # Sierra "Filter Lite" distributes * 2
+ # as per this diagram. 1 1
+ ef = err/2.0
+ # :todo: consider making rows one wider at each end and
+ # removing "if"s
+ if i+1 < width:
+ row[i+1] += ef
+ ef /= 2.0
+ ed[i] = ef
+ if i:
+ ed[i-1] += ef
+ if flipped:
+ ed = ed[::-1]
+ targetrow = targetrow[::-1]
+ yield targetrow
+ flipped = not flipped
+ info['bitdepth'] = bitdepth
+ info['gamma'] = 1.0/targetdecode
+ w = png.Writer(**info)
+ w.write(out, iterdither())
+
+
+def main(argv=None):
+ # http://www.python.org/doc/2.4.4/lib/module-getopt.html
+ from getopt import getopt
+ import sys
+ if argv is None:
+ argv = sys.argv
+ opt,argv = getopt(argv[1:], 'b:c:g:lo:')
+ k = {}
+ for o,v in opt:
+ if o == '-b':
+ k['bitdepth'] = int(v)
+ if o == '-c':
+ k['cutoff'] = float(v)
+ if o == '-g':
+ k['defaultgamma'] = float(v)
+ if o == '-l':
+ k['linear'] = True
+ if o == '-o':
+ k['targetgamma'] = float(v)
+ if o == '-?':
+ print >>sys.stderr, "pipdither [-b bits] [-c cutoff] [-g assumed-gamma] [-l] [in.png]"
+
+ if len(argv) > 0:
+ f = open(argv[0], 'rb')
+ else:
+ f = sys.stdin
+
+ return dither(sys.stdout, f, **k)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/piprgb b/build/pypng/piprgb
new file mode 100644
index 0000000..fbe1082
--- /dev/null
+++ b/build/pypng/piprgb
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/piprgb $
+# $Rev: 131 $
+# piprgb
+#
+# Convert input image to RGB or RGBA format. Output will be colour type
+# 2 or 6, and will not have a tRNS chunk.
+
+import png
+
+def rgb(out, inp):
+ """Convert to RGB/RGBA."""
+
+ r = png.Reader(file=inp)
+ r.preamble()
+ if r.alpha or r.trns:
+ get = r.asRGBA
+ else:
+ get = r.asRGB
+ pixels,info = get()[2:4]
+ w = png.Writer(**info)
+ w.write(out, pixels)
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+ if len(argv) > 1:
+ f = open(argv[1], 'rb')
+ else:
+ f = sys.stdin
+ return rgb(sys.stdout, f)
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pipscalez b/build/pypng/pipscalez
new file mode 100644
index 0000000..c60762d
--- /dev/null
+++ b/build/pypng/pipscalez
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pipscalez $
+# $Rev: 131 $
+
+# pipscalez
+# Enlarge an image by an integer factor horizontally and vertically.
+
+def rescale(inp, out, xf, yf):
+ from array import array
+ import png
+
+ r = png.Reader(file=inp)
+ _,_,pixels,meta = r.asDirect()
+ typecode = 'BH'[meta['bitdepth'] > 8]
+ planes = meta['planes']
+ # We are going to use meta in the call to Writer, so expand the
+ # size.
+ x,y = meta['size']
+ x *= xf
+ y *= yf
+ meta['size'] = (x,y)
+ del x
+ del y
+ # Values per row, target row.
+ vpr = meta['size'][0] * planes
+ def iterscale():
+ for row in pixels:
+ bigrow = array(typecode, [0]*vpr)
+ row = array(typecode, row)
+ for c in range(planes):
+ channel = row[c::planes]
+ for i in range(xf):
+ bigrow[i*planes+c::xf*planes] = channel
+ for _ in range(yf):
+ yield bigrow
+ w = png.Writer(**meta)
+ w.write(out, iterscale())
+
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+ xf = int(argv[1])
+ if len(argv) > 2:
+ yf = int(argv[2])
+ else:
+ yf = xf
+ return rescale(sys.stdin, sys.stdout, xf, yf)
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pipstack b/build/pypng/pipstack
new file mode 100644
index 0000000..5523670
--- /dev/null
+++ b/build/pypng/pipstack
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pipstack $
+# $Rev: 190 $
+
+# pipstack
+# Combine input PNG files into a multi-channel output PNG.
+
+"""
+pipstack file1.png [file2.png ...]
+
+pipstack can be used to combine 3 greyscale PNG files into a colour, RGB,
+PNG file. In fact it is slightly more general than that. The number of
+channels in the output PNG is equal to the sum of the numbers of
+channels in the input images. It is an error if this sum exceeds 4 (the
+maximum number of channels in a PNG image is 4, for an RGBA image). The
+output colour model corresponds to the number of channels: 1 -
+greyscale; 2 - greyscale+alpha; 3 - RGB; 4 - RGB+alpha.
+
+In this way it is possible to combine 3 greyscale PNG files into an RGB
+PNG (a common expected use) as well as more esoteric options: rgb.png +
+grey.png = rgba.png; grey.png + grey.png = greyalpha.png.
+
+Color Profile, Gamma, and so on.
+
+[This is not implemented yet]
+
+If an input has an ICC Profile (``iCCP`` chunk) then the output will
+have an ICC Profile, but only if it is possible to combine all the input
+ICC Profiles. It is possible to combine all the input ICC Profiles
+only when: they all use the same Profile Connection Space; the PCS white
+point is the same (specified in the header; should always be D50);
+possibly some other things I haven't thought of yet.
+
+If some of the inputs have a ``gAMA`` chunk (specifying gamma) and
+an output ICC Profile is being generated, then the gamma information
+will be incorporated into the ICC Profile.
+
+When the output is an RGB colour type and the output ICC Profile is
+synthesized, it is necessary to supply colorant tags (``rXYZ`` and so
+on). These are taken from ``sRGB``.
+
+If the input images have ``gAMA`` chunks and no input image has an ICC
+Profile then the output image will have a ``gAMA`` chunk, but only if
+all the ``gAMA`` chunks specify the same value. Otherwise a warning
+will be emitted and no ``gAMA`` chunk. It is possible to add or replace
+a ``gAMA`` chunk using the ``pipchunk`` tool.
+
+gAMA, pHYs, iCCP, sRGB, tIME, any other chunks.
+"""
+
+class Error(Exception):
+ pass
+
+def stack(out, inp):
+ """Stack the input PNG files into a single output PNG."""
+
+ from array import array
+ import itertools
+ # Local module
+ import png
+
+ if len(inp) < 1:
+ raise Error("Required input is missing.")
+
+ l = map(png.Reader, inp)
+ # Let data be a list of (pixel,info) pairs.
+ data = map(lambda p: p.asDirect()[2:], l)
+ totalchannels = sum(map(lambda x: x[1]['planes'], data))
+
+ if not (0 < totalchannels <= 4):
+ raise Error("Too many channels in input.")
+ alpha = totalchannels in (2,4)
+ greyscale = totalchannels in (1,2)
+ bitdepth = []
+ for b in map(lambda x: x[1]['bitdepth'], data):
+ try:
+ if b == int(b):
+ bitdepth.append(b)
+ continue
+ except (TypeError, ValueError):
+ pass
+ # Assume a tuple.
+ bitdepth += b
+ # Currently, fail unless all bitdepths equal.
+ if len(set(bitdepth)) > 1:
+ raise NotImplemented("Cannot cope when bitdepths differ - sorry!")
+ bitdepth = bitdepth[0]
+ arraytype = 'BH'[bitdepth > 8]
+ size = map(lambda x: x[1]['size'], data)
+ # Currently, fail unless all images same size.
+ if len(set(size)) > 1:
+ raise NotImplemented("Cannot cope when sizes differ - sorry!")
+ size = size[0]
+ # Values per row
+ vpr = totalchannels * size[0]
+ def iterstack():
+ # the izip call creates an iterator that yields the next row
+ # from all the input images combined into a tuple.
+ for irow in itertools.izip(*map(lambda x: x[0], data)):
+ row = array(arraytype, [0]*vpr)
+ # output channel
+ och = 0
+ for i,arow in enumerate(irow):
+ # ensure incoming row is an array
+ arow = array(arraytype, arow)
+ n = data[i][1]['planes']
+ for j in range(n):
+ row[och::totalchannels] = arow[j::n]
+ och += 1
+ yield row
+ w = png.Writer(size[0], size[1],
+ greyscale=greyscale, alpha=alpha, bitdepth=bitdepth)
+ w.write(out, iterstack())
+
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+ argv = argv[1:]
+ arg = argv[:]
+ return stack(sys.stdout, arg)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pipwindow b/build/pypng/pipwindow
new file mode 100644
index 0000000..2f8c7a2
--- /dev/null
+++ b/build/pypng/pipwindow
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pipwindow $
+# $Rev: 173 $
+
+# pipwindow
+# Tool to crop/expand an image to a rectangular window. Come the
+# revolution this tool will allow the image and the window to be placed
+# arbitrarily (in particular the window can be bigger than the picture
+# and/or overlap it only partially) and the image can be OpenGL style
+# border/repeat effects (repeat, mirrored repeat, clamp, fixed
+# background colour, background colour from source file). For now it
+# only acts as crop. The window must be no greater than the image in
+# both x and y.
+
+def window(tl, br, inp, out):
+ """Place a window onto the image and cut-out the resulting
+ rectangle. The window is an axis aligned rectangle opposite corners
+ at *tl* and *br* (each being an (x,y) pair). *inp* specifies the
+ input file which should be a PNG image.
+ """
+
+ import png
+
+ r = png.Reader(file=inp)
+ x,y,pixels,meta = r.asDirect()
+ if not (0 <= tl[0] < br[0] <= x):
+ raise NotImplementedError()
+ if not (0 <= tl[1] < br[1] <= y):
+ raise NotImplementedError()
+ # Compute left and right bounds for each row
+ l = tl[0] * meta['planes']
+ r = br[0] * meta['planes']
+ def itercrop():
+ """An iterator to perform the crop."""
+
+ for i,row in enumerate(pixels):
+ if i < tl[1]:
+ continue
+ if i >= br[1]:
+ # Same as "raise StopIteration"
+ return
+ yield row[l:r]
+ meta['size'] = (br[0]-tl[0], br[1]-tl[1])
+ w = png.Writer(**meta)
+ w.write(out, itercrop())
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+ argv = argv[1:]
+
+ tl = (0,0)
+ br = tuple(map(int, argv[:2]))
+ if len(argv) >= 4:
+ tl = br
+ br = tuple(map(int, argv[2:4]))
+ if len(argv) in (2, 4):
+ f = sys.stdin
+ else:
+ f = open(argv[-1], 'rb')
+
+ return window(tl, br, f, sys.stdout)
+
+if __name__ == '__main__':
+ main()
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())
diff --git a/build/pypng/pngchunk b/build/pypng/pngchunk
new file mode 100644
index 0000000..b00e4b1
--- /dev/null
+++ b/build/pypng/pngchunk
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pngchunk $
+# $Rev: 156 $
+# pngchunk
+# Chunk editing/extraction tool.
+
+import struct
+import warnings
+
+# Local module.
+import png
+
+"""
+pngchunk [--gamma g] [--iccprofile file] [--sigbit b] [-c cHNK!] [-c cHNK:foo] [-c cHNK<file]
+
+The ``-c`` option is used to add or remove chunks. A chunk is specified
+by its 4 byte chunk type. If this is followed by a ``!`` then that
+chunk is removed from the PNG file. If the chunk type is followed by a
+``:`` then the chunk is replaced with the contents of the rest of the
+argument (this is probably only useful if the content is mostly ASCII,
+otherwise it's a pain to quote the contents, otherwise see...). A ``<``
+can be used to take the contents of the chunk from the named file.
+"""
+
+
+def chunk(out, inp, l):
+ """Process the input PNG file to the output, chunk by chunk. Chunks
+ can be inserted, removed, replaced, or sometimes edited. Generally,
+ chunks are not inspected, so pixel data (in the ``IDAT`` chunks)
+ cannot be modified. `l` should be a list of (*chunktype*,
+ *content*) pairs. *chunktype* is usually the type of the PNG chunk,
+ specified as a 4-byte Python string, and *content* is the chunk's
+ content, also as a string; if *content* is ``None`` then *all*
+ chunks of that type will be removed.
+
+ This function *knows* about certain chunk types and will
+ automatically convert from Python friendly representations to
+ string-of-bytes.
+
+ chunktype
+ 'gamma' 'gAMA' float
+ 'sigbit' 'sBIT' int, or tuple of length 1,2 or 3
+
+ Note that the length of the strings used to identify *friendly*
+ chunk types is greater than 4, hence they cannot be confused with
+ canonical chunk types.
+
+ Chunk types, if specified using the 4-byte syntax, need not be
+ official PNG chunks at all. Non-standard chunks can be created.
+ """
+
+ def canonical(p):
+ """Take a pair (*chunktype*, *content*), and return canonical
+ representation (*chunktype*, *content*) where `chunktype` is the
+ 4-byte PNG chunk type and `content` is a string.
+ """
+
+ t,v = p
+ if len(t) == 4:
+ return t,v
+ if t == 'gamma':
+ t = 'gAMA'
+ v = int(round(1e5*v))
+ v = struct.pack('>I', v)
+ elif t == 'sigbit':
+ t = 'sBIT'
+ try:
+ v[0]
+ except TypeError:
+ v = (v,)
+ v = struct.pack('%dB' % len(v), *v)
+ elif t == 'iccprofile':
+ t = 'iCCP'
+ # http://www.w3.org/TR/PNG/#11iCCP
+ v = 'a color profile\x00\x00' + v.encode('zip')
+ else:
+ warnings.warn('Unknown chunk type %r' % t)
+ return t[:4],v
+
+ l = map(canonical, l)
+ # Some chunks automagically replace ones that are present in the
+ # source PNG. There can only be one of each of these chunk types.
+ # Create a 'replace' dictionary to record these chunks.
+ add = []
+ delete = set()
+ replacing = set(['gAMA', 'sBIT', 'PLTE', 'tRNS', 'sPLT', 'IHDR'])
+ replace = dict()
+ for t,v in l:
+ if v is None:
+ delete.add(t)
+ elif t in replacing:
+ replace[t] = v
+ else:
+ add.append((t,v))
+ del l
+ r = png.Reader(file=inp)
+ chunks = r.chunks()
+ def iterchunks():
+ for t,v in chunks:
+ if t in delete:
+ continue
+ if t in replace:
+ yield t,replace[t]
+ del replace[t]
+ continue
+ if t == 'IDAT' and replace:
+ # Insert into the output any chunks that are on the
+ # replace list. We haven't output them yet, because we
+ # didn't see an original chunk of the same type to
+ # replace. Thus the "replace" is actually an "insert".
+ for u,w in replace.items():
+ yield u,w
+ del replace[u]
+ if t == 'IDAT' and add:
+ for item in add:
+ yield item
+ del add[:]
+ yield t,v
+ return png.write_chunks(out, iterchunks())
+
+class Usage(Exception):
+ pass
+
+def main(argv=None):
+ import getopt
+ import re
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+
+ argv = argv[1:]
+
+ try:
+ try:
+ opt,arg = getopt.getopt(argv, 'c:',
+ ['gamma=', 'iccprofile=', 'sigbit='])
+ except getopt.error, msg:
+ raise Usage(msg)
+ k = []
+ for o,v in opt:
+ if o in ['--gamma']:
+ k.append(('gamma', float(v)))
+ if o in ['--sigbit']:
+ k.append(('sigbit', int(v)))
+ if o in ['--iccprofile']:
+ k.append(('iccprofile', open(v, 'rb').read()))
+ if o in ['-c']:
+ type = v[:4]
+ if not re.match('[a-zA-Z]{4}', type):
+ raise Usage('Chunk type must consist of 4 letters.')
+ if v[4] == '!':
+ k.append((type, None))
+ if v[4] == ':':
+ k.append((type, v[5:]))
+ if v[4] == '<':
+ k.append((type, open(v[5:], 'rb').read()))
+ except Usage, err:
+ print >>sys.stderr, (
+ "usage: pngchunk [--gamma d.dd] [--sigbit b] [-c cHNK! | -c cHNK:text-string]")
+ print >>sys.stderr, err.message
+ return 2
+
+ if len(arg) > 0:
+ f = open(arg[0], 'rb')
+ else:
+ f = sys.stdin
+ return chunk(sys.stdout, f, k)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pnghist b/build/pypng/pnghist
new file mode 100644
index 0000000..4fbbd0a
--- /dev/null
+++ b/build/pypng/pnghist
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pnghist $
+# $Rev: 153 $
+# PNG Histogram
+# Only really works on grayscale images.
+
+from array import array
+import getopt
+
+import png
+
+def decidemax(level):
+ """Given an array of levels, decide the maximum value to use for the
+ histogram. This is normally chosen to be a bit bigger than the 99th
+ percentile, but if the 100th percentile is not much more (within a
+ factor of 2) then the 100th percentile is chosen.
+ """
+
+ truemax = max(level)
+ sl = level[:]
+ sl.sort(reverse=True)
+ i99 = int(round(len(level)*0.01))
+ if truemax <= 2*sl[i99]:
+ return truemax
+ return 1.05*sl[i99]
+
+def hist(out, inp, verbose=None):
+ """Open the PNG file `inp` and generate a histogram."""
+
+ r = png.Reader(file=inp)
+ x,y,pixels,info = r.asDirect()
+ bitdepth = info['bitdepth']
+ level = [0]*2**bitdepth
+ for row in pixels:
+ for v in row:
+ level[v] += 1
+ maxlevel = decidemax(level)
+
+ h = 100
+ outbitdepth = 8
+ outmaxval = 2**outbitdepth - 1
+ def genrow():
+ for y in range(h):
+ y = h-y-1
+ # :todo: vary typecode according to outbitdepth
+ row = array('B', [0]*len(level))
+ fl = y*maxlevel/float(h)
+ ce = (y+1)*maxlevel/float(h)
+ for x in range(len(row)):
+ if level[x] <= fl:
+ # Relies on row being initialised to all 0
+ continue
+ if level[x] >= ce:
+ row[x] = outmaxval
+ continue
+ frac = (level[x] - fl)/(ce - fl)
+ row[x] = int(round(outmaxval*frac))
+ yield row
+ w = png.Writer(len(level), h, gamma=1.0,
+ greyscale=True, alpha=False, bitdepth=outbitdepth)
+ w.write(out, genrow())
+ if verbose: print >>verbose, level
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+ argv = argv[1:]
+ opt,arg = getopt.getopt(argv, '')
+
+ if len(arg) < 1:
+ f = sys.stdin
+ else:
+ f = open(arg[0])
+ hist(sys.stdout, f)
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/pnglsch b/build/pypng/pnglsch
new file mode 100644
index 0000000..d10d238
--- /dev/null
+++ b/build/pypng/pnglsch
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/pnglsch $
+# $Rev: 107 $
+# pnglsch
+# PNG List Chunks
+
+import png
+
+def list(out, inp):
+ r = png.Reader(file=inp)
+ for t,v in r.chunks():
+ add = ''
+ if len(v) <= 28:
+ add = ' ' + v.encode('hex')
+ print >>out, "%s %10d%s" % (t, len(v), add)
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+ arg = argv[1:]
+
+ if len(arg) > 0:
+ f = open(arg[0], 'rb')
+ else:
+ f = sys.stdin
+ return list(sys.stdout, f)
+
+if __name__ == '__main__':
+ main()
diff --git a/build/pypng/texttopng b/build/pypng/texttopng
new file mode 100644
index 0000000..ab0c690
--- /dev/null
+++ b/build/pypng/texttopng
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+# $URL: http://pypng.googlecode.com/svn/trunk/code/texttopng $
+# $Rev: 132 $
+# Script to renders text as a PNG image.
+
+from array import array
+import itertools
+
+font = {
+ 32: '0000000000000000',
+ 33: '0010101010001000',
+ 34: '0028280000000000',
+ 35: '0000287c287c2800',
+ 36: '00103c5038147810',
+ 37: '0000644810244c00',
+ 38: '0020502054483400',
+ 39: '0010100000000000',
+ 40: '0008101010101008',
+ 41: '0020101010101020',
+ 42: '0010543838541000',
+ 43: '000010107c101000',
+ 44: '0000000000301020',
+ 45: '000000007c000000',
+ 46: '0000000000303000',
+ 47: '0000040810204000',
+ 48: '0038445454443800',
+ 49: '0008180808080800',
+ 50: '0038043840407c00',
+ 51: '003c041804043800',
+ 52: '00081828487c0800',
+ 53: '0078407804047800',
+ 54: '0038407844443800',
+ 55: '007c040810101000',
+ 56: '0038443844443800',
+ 57: '0038443c04040400',
+ 58: '0000303000303000',
+ 59: '0000303000301020',
+ 60: '0004081020100804',
+ 61: '0000007c007c0000',
+ 62: '0040201008102040',
+ 63: '0038440810001000',
+ 64: '00384c545c403800',
+ 65: '0038447c44444400',
+ 66: '0078447844447800',
+ 67: '0038444040443800',
+ 68: '0070484444487000',
+ 69: '007c407840407c00',
+ 70: '007c407840404000',
+ 71: '003844405c443c00',
+ 72: '0044447c44444400',
+ 73: '0038101010103800',
+ 74: '003c040404443800',
+ 75: '0044487048444400',
+ 76: '0040404040407c00',
+ 77: '006c545444444400',
+ 78: '004464544c444400',
+ 79: '0038444444443800',
+ 80: '0078447840404000',
+ 81: '0038444444443c02',
+ 82: '0078447844444400',
+ 83: '0038403804047800',
+ 84: '007c101010101000',
+ 85: '0044444444443c00',
+ 86: '0044444444281000',
+ 87: '0044445454543800',
+ 88: '0042241818244200',
+ 89: '0044443810101000',
+ 90: '007c081020407c00',
+ 91: '0038202020202038',
+ 92: '0000402010080400',
+ 93: '0038080808080838',
+ 94: '0010284400000000',
+ 95: '000000000000fe00',
+ 96: '0040200000000000',
+ 97: '000038043c443c00',
+ 98: '0040784444447800',
+ 99: '0000384040403800',
+ 100: '00043c4444443c00',
+ 101: '000038447c403c00',
+ 102: '0018203820202000',
+ 103: '00003c44443c0438',
+ 104: '0040784444444400',
+ 105: '0010003010101000',
+ 106: '0010003010101020',
+ 107: '0040404870484400',
+ 108: '0030101010101000',
+ 109: '0000385454444400',
+ 110: '0000784444444400',
+ 111: '0000384444443800',
+ 112: '0000784444784040',
+ 113: '00003c44443c0406',
+ 114: '00001c2020202000',
+ 115: '00003c4038047800',
+ 116: '0020203820201800',
+ 117: '0000444444443c00',
+ 118: '0000444444281000',
+ 119: '0000444454543800',
+ 120: '0000442810284400',
+ 121: '00004444443c0438',
+ 122: '00007c0810207c00',
+ 123: '0018202060202018',
+ 124: '0010101000101010',
+ 125: '003008080c080830',
+ 126: '0020540800000000',
+}
+
+def char(i):
+ """Get image data for the character `i` (a one character string).
+ Returned as a list of rows. Each row is a tuple containing the
+ packed pixels.
+ """
+
+ i = ord(i)
+ if i not in font:
+ return [(0,)]*8
+ return map(lambda row: (ord(row),), font[i].decode('hex'))
+
+def texttoraster(m):
+ """Convert string *m* to a raster image (by rendering it using the
+ font in *font*). A triple of (*width*, *height*, *pixels*) is
+ returned; *pixels* is in boxed row packed pixel format.
+ """
+
+ # Assumes monospaced font.
+ x = 8*len(m)
+ y = 8
+ return x,y,itertools.imap(lambda row: itertools.chain(*row),
+ zip(*map(char, m)))
+
+
+def render(message, out):
+ import png
+
+ x,y,pixels = texttoraster(message)
+ w = png.Writer(x, y, greyscale=True, bitdepth=1)
+ w.write_packed(out, pixels)
+ out.flush()
+
+def main(argv=None):
+ import sys
+
+ if argv is None:
+ argv = sys.argv
+ if len(argv) > 1:
+ message = argv[1]
+ else:
+ message = sys.stdin.read()
+ render(message, sys.stdout)
+
+if __name__ == '__main__':
+ main()