diff options
| author | Peter Ward <peteraward@gmail.com> | 2009-10-29 20:29:54 +1100 | 
|---|---|---|
| committer | Peter Ward <peteraward@gmail.com> | 2009-10-29 20:29:54 +1100 | 
| commit | 9d65a3fdc49858895dcefecd7b53534c297a25c4 (patch) | |
| tree | 79ff102f5faa1f8c8b82a92a84bcffc96fd82e77 | |
| parent | cf517eb700405758f091451f8d66fef2a2cf7108 (diff) | |
Added PNG charts.
| -rw-r--r-- | pngcanvas.py | 291 | ||||
| -rw-r--r-- | pngchart.py | 53 | ||||
| -rw-r--r-- | stats.py | 3 | 
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() + @@ -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  | 
