############################################################################## # Module: MiniPNG # Description: nano-scale PNG graphics library for Python # Author: Dan Sandler -- http://dsandler.org/ # Source: http://dsandler.org/soft/python/minipng.py # Homepage: http://dsandler.org/wp/archives/2006/03/05/minipng-for-python # Version: 1.1 ############################################################################## # CHANGELOG: # 1.1 [2007-11-30] - fix struct.pack warning in 2.5: # DeprecationWarning: struct integer overflow masking is deprecated # 1.0 [2006-03-05] - first version # inspired by code by jzp and why, from # http://redhanded.hobix.com/inspect/sparklinesForMinimalists.html ############################################################################## import zlib, struct, types def _flatten(l): """Recursively flatten a list. Ruby 1, Python 0.""" r = [] for x in l: if type(x) is types.ListType: r += _flatten(x) else: r.append(x) return r PNG_CTYPE_GRAYSCALE = 0 PNG_CTYPE_RGB = 2 PNG_CTYPE_INDEXED = 3 PNG_CTYPE_GRAY_ALPHA = 4 PNG_CTYPE_RGB_ALPHA = 6 def build_png_chunk(chunk_type,data): """build_png_chunk(chunktype, data) -- Construct a PNG chunk. (Internal.) The PNG chunk is extremely simple: +------------------+ 0 | data length | (32-bit unsigned big-endian) +------------------+ 4 | chunk type | (4-byte code, e.g. 'IDAT') +------------------+ 8 | data | | ... | +------------------+ | CRC32(type+data) | (32-bit unsigned big-endian) +------------------+ """ to_check = chunk_type + data return struct.pack('>L', len(data)) \ + to_check \ + struct.pack('>L', 0xFFFFFFFF & zlib.crc32(to_check)) # clamp to uint32 def build_png(image_rows, transparent_color = None): """build_png(image_rows[, transparent_color]) -- Return a string containing a PNG representing the given pixel rows. image_rows: a sequence of pixel rows; each row is a sequence of pixels; each pixel an (R,G,B) triple. transparent_color: (optional) an (R,G,B) triple to use as the single transparent color for the image. A PNG image is an 8-byte header followed by a series of chunks; the only required chunks are: IHDR: encodes the image size and some other stuff Width: 4 bytes Height: 4 bytes Bit depth: 1 byte Color type: 1 byte 0 1,2,4,8,16 Each pixel is a grayscale sample. 2 8,16 Each pixel is an R,G,B triple. 3 1,2,4,8 Each pixel is a palette index; a PLTE chunk must appear. 4 8,16 Each pixel is a grayscale sample, followed by an alpha sample. 6 8,16 Each pixel is an R,G,B triple, followed by an alpha sample. Compression method: 1 byte Filter method: 1 byte Interlace method: 1 byte IDAT: image data chunk For RGB, this is just raw bytes (e.g. for 8-bit RGB, there's a byte of R, one of G, and one of B) from the topleft of the image to the bottom right. The complete image data is DEFLATEd before being encoded in the chunk. IEND: we're done! [ Source: http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html ] To support simple transparency, we also throw in a tRNS (if requested) that contains the (R,G,B) of the single color that should be transparent. (Inexplicably, PNG requires that each component be stored as a 16-bit value, even if the depth of the image is 8.) """ width = len(image_rows[0]) height = len(image_rows) hdrbytes = [137, 80, 78, 71, 13, 10, 26, 10] header = apply(struct.pack, ["%dB" % len(hdrbytes)] + hdrbytes) raw_data = apply(struct.pack, ["%dB" % ((3*width+1)*height)] \ + _flatten([[0] + row for row in image_rows])) ihdr_data = struct.pack(">LLBBBBB", width,height,8,PNG_CTYPE_RGB,0,0,0) ihdr = build_png_chunk("IHDR", ihdr_data) if transparent_color: trns = build_png_chunk("tRNS", apply(struct.pack, [">3H"] + transparent_color)) else: trns = '' idat = build_png_chunk("IDAT", zlib.compress(raw_data)) iend = build_png_chunk("IEND", "") return header + ihdr + trns + idat + iend class Color: """Namespace for some handy colors.""" WHITE = [0xFF]*3 BLACK = [0x00]*3 GRAY = [0x7F]*3 RED = [0xFF,0x00,0x00] ORANGE = [0xFF,0x7F,0x00] YELLOW = [0xFF,0xFF,0x00] GREEN = [0x00,0xFF,0x00] CYAN = [0x00,0xFF,0xFF] BLUE = [0x00,0x00,0xFF] INDIGO = [0x7F,0x00,0xFF] VIOLET = PURPLE = [0xFF,0x00,0xFF] class MiniPNG: """Slightly object-oriented representation of a PNG image. Sample code: # should result in a black smiley face on yellow background mp = MiniPNG(9,6,Color.YELLOW) for pt in [(2,1), (6,1), (1,3), (7,3), (2,4), (3,4), (4,4), (5,4), (6,4)]: mp.plot(pt, Color.BLACK) pngdata = mp.to_png() """ def __init__(self, width, height, initcolor=Color.WHITE, transparent_color=None): """MiniPNG(width, height[, ]) Keyword args: initcolor: initial color (R,G,B) for all pixels (default Color.WHITE) transparent_color: color (R,G,B) to use as transparent (default None) """ self.width = width self.height = height self.pixels = [[initcolor] * width for i in range(height)] self.transparent_color = transparent_color def set_transparent_color(self, c): """Set the transparent color (R,G,B) of the image.""" self.transparent_color = c def plot(self,pt,color): """Plot color (R,G,B) at pt (x,y) on the image.""" if not map(type,color) == [types.IntType]*3: raise ValueError, "not a valid color" x,y = pt if not (x >=0 and x < self.width and y >= 0 and y < self.height): raise ValueError, "point off canvas" self.pixels[y][x] = color def to_png(self): """Return a string containing PNG data for the current image.""" return build_png(self.pixels, self.transparent_color) def __str__(self): return "MiniPNG(%d x %d)" % (self.width,self.height) def _runtests(): """Some crude unit tests.""" print "testing..." # test raw png functions pix=[ [{'.':[0xFF,0xFF,0x00], 'X':[0x00,0x00,0x00]}[i] for i in row] for row in '''\ ......... ..X...X.. ......... .X.....X. ..XXXXX.. .........'''.split('\n')] pngdata = build_png(pix) # test MiniPNG class mp = MiniPNG(9,6,Color.YELLOW) for pt in [(2,1), (6,1), (1,3), (7,3), (2,4), (3,4), (4,4), (5,4), (6,4) ]: mp.plot(pt, Color.BLACK) pngdata2 = mp.to_png() # reference output reference = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\t\x00\x00\x00\x06\x08\x02\x00\x00\x00\x9e\xa5#\x92\x00\x00\x00"IDATx\x9cc\xf8\xff\x9f\x01\x17\x82Q\x0cHB\x0c\xa8r8\xf51`\xa8\x80\x88 8\xc8\x80\xb0\x99\x00\xcc\x81Y\xa7\x1a\xcc\xda\xc7\x00\x00\x00\x00IEND\xaeB`\x82' open('minipng_reference.png','w').write(reference) print 'build_png output ' + ['failed','ok'][pngdata==reference] open('minipng_test_1.png','w').write(pngdata) print 'MiniPNG output ' + ['failed','ok'][pngdata2==reference] open('minipng_test_2.png','w').write(pngdata2) if __name__ == '__main__': _runtests()