summaryrefslogtreecommitdiffstats
path: root/build/pypng/pipdither
diff options
context:
space:
mode:
Diffstat (limited to 'build/pypng/pipdither')
-rw-r--r--build/pypng/pipdither181
1 files changed, 181 insertions, 0 deletions
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()