From 9d65a3fdc49858895dcefecd7b53534c297a25c4 Mon Sep 17 00:00:00 2001 From: Peter Ward Date: Thu, 29 Oct 2009 20:29:54 +1100 Subject: Added PNG charts. --- pngcanvas.py | 291 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pngchart.py | 53 +++++++++++ stats.py | 3 +- 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 pngcanvas.py create mode 100644 pngchart.py diff --git a/pngcanvas.py b/pngcanvas.py new file mode 100644 index 0000000..394ff4f --- /dev/null +++ b/pngcanvas.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python + +"""Simple PNG Canvas for Python""" +__version__ = "0.8" +__author__ = "Rui Carmo (http://the.taoofmac.com)" +__copyright__ = "CC Attribution-NonCommercial-NoDerivs 2.0 Rui Carmo" +__contributors__ = ["http://collaboa.weed.rbse.com/repository/file/branches/pgsql/lib/spark_pr.rb"], ["Eli Bendersky"] + +import zlib, struct + +signature = struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10) + +# alpha blends two colors, using the alpha given by c2 +def blend(c1, c2): + return [c1[i]*(0xFF-c2[3]) + c2[i]*c2[3] >> 8 for i in range(3)] + +# calculate a new alpha given a 0-0xFF intensity +def intensity(c,i): + return [c[0],c[1],c[2],(c[3]*i) >> 8] + +# calculate perceptive grayscale value +def grayscale(c): + return int(c[0]*0.3 + c[1]*0.59 + c[2]*0.11) + +# calculate gradient colors +def gradientList(start,end,steps): + delta = [end[i] - start[i] for i in range(4)] + grad = [] + for i in range(steps+1): + grad.append([start[j] + (delta[j]*i)/steps for j in range(4)]) + return grad + +class PNGCanvas: + def __init__(self, width, height,bgcolor=[0xff,0xff,0xff,0xff],color=[0,0,0,0xff]): + self.canvas = [] + self.width = width + self.height = height + self.color = color #rgba + bgcolor = bgcolor[0:3] # we don't need alpha for background + for i in range(height): + self.canvas.append([bgcolor] * width) + + def point(self,x,y,color=None): + if x<0 or y<0 or x>self.width-1 or y>self.height-1: return + if color == None: color = self.color + self.canvas[y][x] = blend(self.canvas[y][x],color) + + def _rectHelper(self,x0,y0,x1,y1): + x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) + if x0 > x1: x0, x1 = x1, x0 + if y0 > y1: y0, y1 = y1, y0 + return [x0,y0,x1,y1] + + def verticalGradient(self,x0,y0,x1,y1,start,end): + x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) + grad = gradientList(start,end,y1-y0) + for x in range(x0, x1+1): + for y in range(y0, y1+1): + self.point(x,y,grad[y-y0]) + + def rectangle(self,x0,y0,x1,y1): + x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) + self.polyline([[x0,y0],[x1,y0],[x1,y1],[x0,y1],[x0,y0]]) + + def filledRectangle(self,x0,y0,x1,y1): + x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) + for x in range(x0, x1+1): + for y in range(y0, y1+1): + self.point(x,y,self.color) + + def copyRect(self,x0,y0,x1,y1,dx,dy,destination): + x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) + for x in range(x0, x1+1): + for y in range(y0, y1+1): + destination.canvas[dy+y-y0][dx+x-x0] = self.canvas[y][x] + + def blendRect(self,x0,y0,x1,y1,dx,dy,destination,alpha=0xff): + x0, y0, x1, y1 = self._rectHelper(x0,y0,x1,y1) + for x in range(x0, x1+1): + for y in range(y0, y1+1): + rgba = self.canvas[y][x] + [alpha] + destination.point(dx+x-x0,dy+y-y0,rgba) + + # draw a line using Xiaolin Wu's antialiasing technique + def line(self,x0, y0, x1, y1): + # clean params + x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1) + if y0>y1: + y0, y1, x0, x1 = y1, y0, x1, x0 + dx = x1-x0 + if dx < 0: + sx = -1 + else: + sx = 1 + dx *= sx + dy = y1-y0 + + # 'easy' cases + if dy == 0: + for x in range(x0,x1,sx): + self.point(x, y0) + return + if dx == 0: + for y in range(y0,y1): + self.point(x0, y) + self.point(x1, y1) + return + if dx == dy: + for x in range(x0,x1,sx): + self.point(x, y0) + y0 = y0 + 1 + return + + # main loop + self.point(x0, y0) + e_acc = 0 + if dy > dx: # vertical displacement + e = (dx << 16) / dy + for i in range(y0,y1-1): + e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xFFFF + if (e_acc <= e_acc_temp): + x0 = x0 + sx + w = 0xFF-(e_acc >> 8) + self.point(x0, y0, intensity(self.color,(w))) + y0 = y0 + 1 + self.point(x0 + sx, y0, intensity(self.color,(0xFF-w))) + self.point(x1, y1) + return + + # horizontal displacement + e = (dy << 16) / dx + for i in range(x0,x1-sx,sx): + e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xFFFF + if (e_acc <= e_acc_temp): + y0 = y0 + 1 + w = 0xFF-(e_acc >> 8) + self.point(x0, y0, intensity(self.color,(w))) + x0 = x0 + sx + self.point(x0, y0 + 1, intensity(self.color,(0xFF-w))) + self.point(x1, y1) + + def polyline(self,arr): + for i in range(0,len(arr)-1): + self.line(arr[i][0],arr[i][1],arr[i+1][0], arr[i+1][1]) + + def dump(self): + raw_list = [] + for y in range(self.height): + raw_list.append(chr(0)) # filter type 0 (None) + for x in range(self.width): + raw_list.append(struct.pack("!3B",*self.canvas[y][x])) + raw_data = ''.join(raw_list) + + # 8-bit image represented as RGB tuples + # simple transparency, alpha is pure white + return signature + \ + self.pack_chunk('IHDR', struct.pack("!2I5B",self.width,self.height,8,2,0,0,0)) + \ + self.pack_chunk('tRNS', struct.pack("!6B",0xFF,0xFF,0xFF,0xFF,0xFF,0xFF)) + \ + self.pack_chunk('IDAT', zlib.compress(raw_data,9)) + \ + self.pack_chunk('IEND', '') + + def pack_chunk(self,tag,data): + to_check = tag + data + return struct.pack("!I",len(data)) + to_check + struct.pack("!I", zlib.crc32(to_check) & 0xFFFFFFFF) + + def load(self,f): + assert f.read(8) == signature + self.canvas=[] + for tag, data in self.chunks(f): + if tag == "IHDR": + ( width, + height, + bitdepth, + colortype, + compression, filter, interlace ) = struct.unpack("!2I5B",data) + self.width = width + self.height = height + if (bitdepth,colortype,compression, filter, interlace) != (8,2,0,0,0): + raise TypeError('Unsupported PNG format') + # we ignore tRNS because we use pure white as alpha anyway + elif tag == 'IDAT': + raw_data = zlib.decompress(data) + rows = [] + i = 0 + for y in range(height): + filtertype = ord(raw_data[i]) + i = i + 1 + cur = [ord(x) for x in raw_data[i:i+width*3]] + if y == 0: + rgb = self.defilter(cur,None,filtertype) + else: + rgb = self.defilter(cur,prev,filtertype) + prev = cur + i = i+width*3 + row = [] + j = 0 + for x in range(width): + pixel = rgb[j:j+3] + row.append(pixel) + j = j + 3 + self.canvas.append(row) + + def defilter(self,cur,prev,filtertype,bpp=3): + if filtertype == 0: # No filter + return cur + elif filtertype == 1: # Sub + xp = 0 + for xc in range(bpp,len(cur)): + cur[xc] = (cur[xc] + cur[xp]) % 256 + xp = xp + 1 + elif filtertype == 2: # Up + for xc in range(len(cur)): + cur[xc] = (cur[xc] + prev[xc]) % 256 + elif filtertype == 3: # Average + xp = 0 + for xc in range(len(cur)): + cur[xc] = (cur[xc] + (cur[xp] + prev[xc])/2) % 256 + xp = xp + 1 + elif filtertype == 4: # Paeth + xp = 0 + for i in range(bpp): + cur[i] = (cur[i] + prev[i]) % 256 + for xc in range(bpp,len(cur)): + a = cur[xp] + b = prev[xc] + c = prev[xp] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + value = a + elif pb <= pc: + value = b + else: + value = c + cur[xc] = (cur[xc] + value) % 256 + xp = xp + 1 + else: + raise TypeError('Unrecognized scanline filter type') + return cur + + def chunks(self,f): + while 1: + try: + length = struct.unpack("!I",f.read(4))[0] + tag = f.read(4) + data = f.read(length) + crc = struct.unpack("!i",f.read(4))[0] + except: + return + if zlib.crc32(tag + data) != crc: + raise IOError + yield [tag,data] + +if __name__ == '__main__': + width = 128 + height = 64 + print "Creating Canvas..." + c = PNGCanvas(width,height) + c.color = [0xff,0,0,0xff] + c.rectangle(0,0,width-1,height-1) + print "Generating Gradient..." + c.verticalGradient(1,1,width-2, height-2,[0xff,0,0,0xff],[0x20,0,0xff,0x80]) + print "Drawing Lines..." + c.color = [0,0,0,0xff] + c.line(0,0,width-1,height-1) + c.line(0,0,width/2,height-1) + c.line(0,0,width-1,height/2) + # Copy Rect to Self + print "Copy Rect" + c.copyRect(1,1,width/2-1,height/2-1,0,height/2,c) + # Blend Rect to Self + print "Blend Rect" + c.blendRect(1,1,width/2-1,height/2-1,width/2,0,c) + # Write test + print "Writing to file..." + f = open("test.png", "wb") + f.write(c.dump()) + f.close() + # Read test + print "Reading from file..." + f = open("test.png", "rb") + c.load(f) + f.close() + # Write back + print "Writing to new file..." + f = open("recycle.png","wb") + f.write(c.dump()) + f.close() + diff --git a/pngchart.py b/pngchart.py new file mode 100644 index 0000000..5428718 --- /dev/null +++ b/pngchart.py @@ -0,0 +1,53 @@ +from pngcanvas import PNGCanvas + +try: + from itertools import izip as zip +except ImportError: + pass + +class SimpleLineChart(object): + def __init__(self, width, height, colours=None, legend=None): + self.canvas = PNGCanvas(width, height) + + self.width = width + self.height = height + + self.colours = colours + self.legend = legend + + self.series = [] + + def add_data(self, series): + self.series.append(series) + + def render(self): + max_width = max(map(len, self.series)) + max_height = max(map(max, self.series)) + x_scale = float(self.width) / max_width + y_scale = float(self.height) / max_height + + data = zip(self.series, self.colours or [], self.legend or []) + for series, colour, legend in data: + colour = int(colour, 16) + self.canvas.color = ( + colour>>16 & 0xff, + colour>>8 & 0xff, + colour & 0xff, + 0xff, + ) + last = None + for x, y in enumerate(series): + if y is not None: + y = self.height - y * y_scale + if last is not None: + x *= x_scale + self.canvas.line(x - x_scale, last, x, y) + last = y + + def download(self, filename): + self.render() + + f = open(filename, 'wb') + f.write(self.canvas.dump()) + f.close() + diff --git a/stats.py b/stats.py index 30032b5..aaacfcf 100644 --- a/stats.py +++ b/stats.py @@ -2,7 +2,8 @@ import sys from collections import defaultdict -from pygooglechart import SimpleLineChart +from pngchart import SimpleLineChart +#from pygooglechart import SimpleLineChart from colour import hash_colour WIDTH = 800 -- cgit v1.2.3