summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Ward <peteraward@gmail.com>2009-10-29 20:29:54 +1100
committerPeter Ward <peteraward@gmail.com>2009-10-29 20:29:54 +1100
commit9d65a3fdc49858895dcefecd7b53534c297a25c4 (patch)
tree79ff102f5faa1f8c8b82a92a84bcffc96fd82e77
parentcf517eb700405758f091451f8d66fef2a2cf7108 (diff)
Added PNG charts.
-rw-r--r--pngcanvas.py291
-rw-r--r--pngchart.py53
-rw-r--r--stats.py3
3 files changed, 346 insertions, 1 deletions
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