# -*- coding: utf-8 -*- # # Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com) # # Jockey is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any # later version. # # Jockey is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # import math import sys import time import re import font import pychart_util import version from scaling import * def _compute_bounding_box(points): """Given the list of coordinates (x,y), this procedure computes the smallest rectangle that covers all the points.""" (xmin, ymin, xmax, ymax) = (999999, 999999, -999999, -999999) for p in points: xmin = min(xmin, p[0]) xmax = max(xmax, p[0]) ymin = min(ymin, p[1]) ymax = max(ymax, p[1]) return (xmin, ymin, xmax, ymax) def _intersect_box(b1, b2): xmin = max(b1[0], b2[0]) ymin = max(b1[1], b2[1]) xmax = min(b1[2], b2[2]) ymax = min(b1[3], b2[3]) return (xmin, ymin, xmax, ymax) def invisible_p(x, y): """Return true if the point (X, Y) is visible in the canvas.""" if x < -499999 or y < -499999: return 1 return 0 def to_radian(deg): return deg*2*math.pi / 360.0 def midpoint(p1, p2): return ( (p1[0]+p2[0])/2.0, (p1[1]+p2[1])/2.0 ) active_canvases = [] InvalidCoord = 999999 class T(object): def __init__(self): global active_canvases self.__xmax = -InvalidCoord self.__xmin = InvalidCoord self.__ymax = -InvalidCoord self.__ymin = InvalidCoord self.__clip_box = (-InvalidCoord, -InvalidCoord, InvalidCoord, InvalidCoord) self.__clip_stack = [] self.__nr_gsave = 0 self.title = re.sub("(.*)\\.py$", "\\1", sys.argv[0]) self.creator = "pychart %s" % (version.version,) self.creation_date = time.strftime("(%m/%d/%y) (%I:%M %p)") self.aux_comments = "" self.author = None active_canvases.append(self) def set_title(self, s): """Define the string to shown in EPS/PDF "Title" field. The default value is the name of the script that creates the EPS/PDF file.""" self.title = s def set_creator(self, tag): """Define the string to be shown in EPS %%Creator or PDF Producer field. The default value is "pychart".""" self.creator = tag def set_creation_date(self, s): """Define the string to shown in EPS/PDF "CreationDate" field. Defalt value of this field is the current time.""" self.creation_date = s def set_author(self, s): """Set the author string. Unless this method is called, the Author field is not output in EPS or PDF.""" self.author = s def add_aux_comments(self, s): """Define an auxiliary comments to be output to the file, just after the required headers""" self.aux_comments += s def close(self): """This method closes the canvas and writes contents to the associated file. Calling this procedure is optional, because Pychart calls this procedure for every open canvas on normal exit.""" for i in range(0, len(active_canvases)): if active_canvases[i] == self: del active_canvases[i] return def open_output(self, fname): """Open the output file FNAME. Returns tuple (FD, NEED_CLOSE), where FD is a file (or file-like) object, and NEED_CLOSE is a boolean flag that tells whether FD.close() should be called after finishing writing to the file. FNAME can be one of the three things: (1) None, in which case (sys.stdout, False) is returned. (2) A file-like object, in which case (fname, False) is returned. (3) A string, in which case this procedure opens the file and returns (fd, True).""" if not fname: return (sys.stdout, False) elif isinstance(fname, str): return (file(fname, "wb"), True) else: if not hasattr(fname, "write"): raise Exception, "Expecting either a filename or a file-like object, but got %s" % fname return (fname, False) def setbb(self, x, y): """Call this method when point (X,Y) is to be drawn in the canvas. This methods expands the bounding box to include this point.""" self.__xmin = min(self.__xmin, max(x, self.__clip_box[0])) self.__xmax = max(self.__xmax, min(x, self.__clip_box[2])) self.__ymin = min(self.__ymin, max(y, self.__clip_box[1])) self.__ymax = max(self.__ymax, min(y, self.__clip_box[3])) def fill_with_pattern(self, pat, x1, y1, x2, y2): if invisible_p(x2, y2): return self.comment("FILL pat=%s (%d %d)-(%d %d)\n" % (pat, x1, y1, x2, y2)) self.set_fill_color(pat.bgcolor) self._path_polygon([(x1, y1), (x1, y2), (x2, y2), (x2, y1)]) self.fill() pat.draw(self, x1, y1, x2, y2) self.comment("end FILL.\n") def _path_polygon(self, points): "Low-level polygon-drawing routine." (xmin, ymin, xmax, ymax) = _compute_bounding_box(points) if invisible_p(xmax, ymax): return self.setbb(xmin, ymin) self.setbb(xmax, ymax) self.newpath() self.moveto(xscale(points[0][0]), yscale(points[0][1])) for point in points[1:]: self.lineto(xscale(point[0]), yscale(point[1])) self.closepath() def polygon(self, edge_style, pat, points, shadow = None): """Draw a polygon with EDGE_STYLE, fill with PAT, and the edges POINTS. POINTS is a sequence of coordinates, e.g., ((10,10), (15,5), (20,8)). SHADOW is either None or a tuple (XDELTA, YDELTA, fillstyle). If non-null, a shadow of FILLSTYLE is drawn beneath the polygon at the offset of (XDELTA, YDELTA).""" if pat: self.comment("POLYGON points=[%s] pat=[%s]" % (str(points), str(pat))) (xmin, ymin, xmax, ymax) = _compute_bounding_box(points) if shadow: xoff, yoff, shadow_pat = shadow self.gsave() self._path_polygon(map(lambda p, xoff=xoff, yoff=yoff: (p[0]+xoff, p[1]+yoff), points)) self.clip_sub() self.fill_with_pattern(shadow_pat, xmin+xoff, ymin+yoff, xmax+xoff, ymax+yoff) self.grestore() self.gsave() self._path_polygon(points) self.clip_sub() self.fill_with_pattern(pat, xmin, ymin, xmax, ymax) self.grestore() if edge_style: self.comment("POLYGON points=[%s] edge=[%s]" % (str(points), str(edge_style))) self.set_line_style(edge_style) self._path_polygon(points) self.stroke() def set_background(self, pat, x1, y1, x2, y2): xmax, xmin, ymax, ymin = self.__xmax, self.__xmin, self.__ymax, self.__ymin self.rectangle(None, pat, x1, y1, x2, y2) self.__xmax, self.__xmin, self.__ymax, self.__ymin = xmax, xmin, ymax, ymin def rectangle(self, edge_style, pat, x1, y1, x2, y2, shadow = None): """Draw a rectangle with EDGE_STYLE, fill with PAT, and the bounding box (X1, Y1, X2, Y2). SHADOW is either None or a tuple (XDELTA, YDELTA, fillstyle). If non-null, a shadow of FILLSTYLE is drawn beneath the polygon at the offset of (XDELTA, YDELTA).""" self.polygon(edge_style, pat, [(x1,y1), (x1,y2), (x2,y2), (x2, y1)], shadow) def _path_ellipsis(self, x, y, radius, ratio, start_angle, end_angle): self.setbb(x - radius, y - radius*ratio) self.setbb(x + radius, y + radius*ratio) oradius = nscale(radius) centerx, centery = xscale(x), yscale(y) startx, starty = centerx+oradius * math.cos(to_radian(start_angle)), \ centery+oradius * math.sin(to_radian(start_angle)) self.moveto(centerx, centery) if start_angle % 360 != end_angle % 360: self.moveto(centerx, centery) self.lineto(startx, starty) else: self.moveto(startx, starty) self.path_arc(xscale(x), yscale(y), nscale(radius), ratio, start_angle, end_angle) self.closepath() def ellipsis(self, line_style, pattern, x, y, radius, ratio = 1.0, start_angle=0, end_angle=360, shadow=None): """Draw an ellipsis with line_style and fill PATTERN. The center is \ (X, Y), X radius is RADIUS, and Y radius is RADIUS*RATIO, whose \ default value is 1.0. SHADOW is either None or a tuple (XDELTA, YDELTA, fillstyle). If non-null, a shadow of FILLSTYLE is drawn beneath the polygon at the offset of (XDELTA, YDELTA).""" if invisible_p(x + radius, y + radius*ratio): return if pattern: if shadow: x_off, y_off, shadow_pat = shadow self.gsave() self.newpath() self._path_ellipsis(x+x_off, y+y_off, radius, ratio, start_angle, end_angle) self.clip_sub() self.fill_with_pattern(shadow_pat, x-radius*2+x_off, y-radius*ratio*2+y_off, x+radius*2+x_off, y+radius*ratio*2+y_off) self.grestore() self.gsave() self.newpath() self._path_ellipsis(x, y, radius, ratio, start_angle, end_angle) self.clip_sub() self.fill_with_pattern(pattern, (x-radius*2), (y-radius*ratio*2), (x+radius*2), (y+radius*ratio*2)) self.grestore() if line_style: self.set_line_style(line_style) self.newpath() self._path_ellipsis(x, y, radius, ratio, start_angle, end_angle) self.stroke() def clip_ellipsis(self, x, y, radius, ratio = 1.0): """Create an elliptical clip region. You must call endclip() after you completed drawing. See also the ellipsis method.""" self.gsave() self.newpath() self.moveto(xscale(x)+nscale(radius), yscale(y)) self.path_arc(xscale(x), yscale(y), nscale(radius), ratio, 0, 360) self.closepath() self.__clip_stack.append(self.__clip_box) self.clip_sub() def clip_polygon(self, points): """Create a polygonal clip region. You must call endclip() after you completed drawing. See also the polygon method.""" self.gsave() self._path_polygon(points) self.__clip_stack.append(self.__clip_box) self.__clip_box = _intersect_box(self.__clip_box, _compute_bounding_box(points)) self.clip_sub() def clip(self, x1, y1, x2, y2): """Activate a rectangular clip region, (X1, Y1) - (X2, Y2). You must call endclip() after you completed drawing. canvas.clip(x,y,x2,y2) draw something ... canvas.endclip() """ self.__clip_stack.append(self.__clip_box) self.__clip_box = _intersect_box(self.__clip_box, (x1, y1, x2, y2)) self.gsave() self.newpath() self.moveto(xscale(x1), yscale(y1)) self.lineto(xscale(x1), yscale(y2)) self.lineto(xscale(x2), yscale(y2)) self.lineto(xscale(x2), yscale(y1)) self.closepath() self.clip_sub() def endclip(self): """End the current clip region. When clip calls are nested, it ends the most recently created crip region.""" self.__clip_box = self.__clip_stack[-1] del self.__clip_stack[-1] self.grestore() def curve(self, style, points): for p in points: self.setbb(p[0], p[1]) self.newpath() self.set_line_style(style) self.moveto(xscale(points[0][0]), xscale(points[0][1])) i = 1 n = 1 while i < len(points): if n == 1: x2 = points[i] n += 1 elif n == 2: x3 = points[i] n += 1 elif n == 3: x4 = midpoint(x3, points[i]) self.curveto(xscale(x2[0]), xscale(x2[1]), xscale(x3[0]), xscale(x3[1]), xscale(x4[0]), xscale(x4[1])) n = 1 i += 1 if n == 1: pass if n == 2: self.lineto(xscale(x2[0]), xscale(x2[1])) if n == 3: self.curveto(xscale(x2[0]), xscale(x2[1]), xscale(x2[0]), xscale(x2[1]), xscale(x3[0]), xscale(x3[1])) self.stroke() def line(self, style, x1, y1, x2, y2): if not style: return if invisible_p(x2, y2) and invisible_p(x1, y1): return self.setbb(x1, y1) self.setbb(x2, y2) self.newpath() self.set_line_style(style) self.moveto(xscale(x1), yscale(y1)) self.lineto(xscale(x2), yscale(y2)) self.stroke() def lines(self, style, segments): if not style: return (xmin, ymin, xmax, ymax) = _compute_bounding_box(segments) if invisible_p(xmax, ymax): return self.setbb(xmin, ymin) self.setbb(xmax, ymax) self.newpath() self.set_line_style(style) self.moveto(xscale(segments[0][0]), xscale(segments[0][1])) for i in range(1, len(segments)): self.lineto(xscale(segments[i][0]), yscale(segments[i][1])) self.stroke() def _path_round_rectangle(self, x1, y1, x2, y2, radius): self.moveto(xscale(x1 + radius), yscale(y1)) self.lineto(xscale(x2 - radius), yscale(y1)) self.path_arc(xscale(x2-radius), yscale(y1+radius), nscale(radius), 1, 270, 360) self.lineto(xscale(x2), yscale(y2-radius)) self.path_arc(xscale(x2-radius), yscale(y2-radius), nscale(radius), 1, 0, 90) self.lineto(xscale(x1+radius), yscale(y2)) self.path_arc(xscale(x1 + radius), yscale(y2 - radius), nscale(radius), 1, 90, 180) self.lineto(xscale(x1), xscale(y1+radius)) self.path_arc(xscale(x1 + radius), yscale(y1 + radius), nscale(radius), 1, 180, 270) def round_rectangle(self, style, fill, x1, y1, x2, y2, radius, shadow=None): """Draw a rectangle with rounded four corners. Parameter specifies the radius of each corner.""" if invisible_p(x2, y2): return self.setbb(x1, y1) self.setbb(x2, y2) if fill: if shadow: x_off, y_off, shadow_fill = shadow self.gsave(); self.newpath() self._path_round_rectangle(x1+x_off, y1+y_off, x2+x_off, y2+y_off, radius) self.closepath() self.clip_sub() self.fill_with_pattern(shadow_fill, x1+x_off, y1+y_off, x2+x_off, y2+y_off) self.grestore() self.gsave(); self.newpath() self._path_round_rectangle(x1, y1, x2, y2, radius) self.closepath() self.clip_sub() self.fill_with_pattern(fill, x1, y1, x2, y2) self.grestore() if style: self.set_line_style(style) self.newpath() self._path_round_rectangle(x1, y1, x2, y2, radius) self.closepath() self.stroke() def show(self, x, y, str): global out y_org = y org_str = str if invisible_p(x, y): return (xmin, xmax, ymin, ymax) = font.get_dimension(str) # rectangle(line_style.default, None, x+xmin, y+ymin, x+xmax, y+ymax) # ellipsis(line_style.default, None, x, y, 1) self.setbb(x+xmin, y+ymin) self.setbb(x+xmax, y+ymax) (halign, valign, angle) = font.get_align(str) base_x = x base_y = y # Handle vertical alignment if valign == "B": y = font.unaligned_text_height(str) elif valign == "T": y = 0 elif valign == "M": y = font.unaligned_text_height(str) / 2.0 (xmin, xmax, ymin, ymax) = font.get_dimension(org_str) # print org_str, xmin, xmax, ymin, ymax, x, y_org, y self.setbb(x+xmin, y_org+y+ymin) self.setbb(x+xmax, y_org+y+ymax) itr = font.text_iterator(None) max_width = 0 lines = [] for line in str.split('\n'): cur_width = 0 cur_height = 0 itr.reset(line) strs = [] while 1: elem = itr.next() if not elem: break (font_name, size, line_height, color, _h, _v, _a, str) = elem cur_width += font.line_width(font_name, size, str) max_width = max(cur_width, max_width) cur_height = max(cur_height, line_height) # replace '(' -> '\(', ')' -> '\)' to make # Postscript string parser happy. str = str.replace("(", "\\(") str = str.replace(")", "\\)") strs.append((font_name, size, color, str)) lines.append((cur_width, cur_height, strs)) for line in lines: cur_width, cur_height, strs = line cur_y = y - cur_height y = y - cur_height self.comment("cury: %d hei %d str %s\n" % (cur_y, cur_height, strs)) if halign == 'C': cur_x = -cur_width/2.0 elif halign == 'R': cur_x = -cur_width else: cur_x = 0 rel_x, rel_y = pychart_util.rotate(cur_x, cur_y, angle) self.text_begin() self.text_moveto(xscale(base_x + rel_x), yscale(base_y + rel_y), angle) for segment in strs: font_name, size, color, str = segment self.text_show(font_name, nscale(size), color, str) self.text_end()