Package lib :: Module xcairo
[hide private]
[frames] | no frames]

Source Code for Module lib.xcairo

  1  # -*- coding: utf-8 -*- 
  2   
  3  #    callirhoe - high quality calendar rendering 
  4  #    Copyright (C) 2012 George M. Tzoumas 
  5   
  6  #    This program is free software: you can redistribute it and/or modify 
  7  #    it under the terms of the GNU General Public License as published by 
  8  #    the Free Software Foundation, either version 3 of the License, or 
  9  #    (at your option) any later version. 
 10  # 
 11  #    This program is distributed in the hope that it will be useful, 
 12  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  #    GNU General Public License for more details. 
 15  # 
 16  #    You should have received a copy of the GNU General Public License 
 17  #    along with this program.  If not, see http://www.gnu.org/licenses/ 
 18   
 19  # ******************************************************************** 
 20  #                                                                    # 
 21  """ general-purpose drawing routines & higher-level CAIRO routines """ 
 22  #                                                                    # 
 23  # ******************************************************************** 
 24   
 25  import cairo 
 26  import math 
 27  import random 
 28  from os.path import splitext 
 29  from geom import * 
 30   
 31  XDPI = 72.0 
 32  """dots per inch of output device""" 
 33   
 34  # decreasing order 
 35  # [1188, 840, 594, 420, 297, 210, 148, 105, 74, 52, 37] 
 36  ISOPAGE = map(lambda x: int(210*math.sqrt(2)**x+0.5), range(5,-6,-1)) 
 37  """ISO page height list, index k for height of Ak paper""" 
 38   
39 -def page_spec(spec = None):
40 """return tuple of page dimensions (width,height) in mm for I{spec} 41 42 @param spec: paper type. 43 Paper type can be an ISO paper type (a0..a9 or a0w..a9w) or of the 44 form W:H; positive values correspond to W or H mm, negative values correspond to 45 -W or -H pixels; 'w' suffix swaps width & height; None defaults to A4 paper 46 47 @rtype: (int,int) 48 """ 49 if not spec: 50 return (ISOPAGE[5], ISOPAGE[4]) 51 if len(spec) == 2 and spec[0].lower() == 'a': 52 k = int(spec[1]) 53 if k > 9: k = 9 54 return (ISOPAGE[k+1], ISOPAGE[k]) 55 if len(spec) == 3 and spec[0].lower() == 'a' and spec[2].lower() == 'w': 56 k = int(spec[1]) 57 if k > 9: k = 9 58 return (ISOPAGE[k], ISOPAGE[k+1]) 59 if ':' in spec: 60 s = spec.split(':') 61 w, h = float(s[0]), float(s[1]) 62 if w < 0: w = dots_to_mm(-w) 63 if h < 0: h = dots_to_mm(-h) 64 return (w,h)
65
66 -def mm_to_dots(mm):
67 """convert millimeters to dots 68 69 @rtype: float 70 """ 71 return mm/25.4 * XDPI
72
73 -def dots_to_mm(dots):
74 """convert dots to millimeters 75 76 @rtype: float 77 """ 78 return dots*25.4/XDPI
79
80 -class Page(object):
81 """class holding Page properties 82 83 @type Size_mm: tuple (width,height) 84 @ivar Size_mm: page dimensions in mm 85 @type landscape: bool 86 @ivar landscape: landscape mode (for landscape, Size_mm will have swapped elements) 87 @type Size: tuple (width,height) 88 @ivar Size: page size in dots/pixels 89 @type Margins: tuple (top,left,bottom,right) 90 @ivar Margins: page margins in pixels 91 @type Text_rect: tuple (x,y,w,h) 92 @ivar Text_rect: text rectangle 93 """
94 - def __init__(self, landscape, w, h, b, raster):
95 """initialize Page properties object 96 97 @type landscape: bool 98 @param landscape: landscape mode 99 @param w: page physical width in mm 100 @param h: page physical height in mm, M{h>w}, even in landscape mode 101 @param b: page border in mm (uniform) 102 @type raster: bool 103 @param raster: raster mode (not vector) 104 """ 105 if not landscape: 106 self.Size_mm = (w, h) # (width, height) in mm 107 else: 108 self.Size_mm = (h, w) 109 self.landscape = landscape 110 self.Size = (mm_to_dots(self.Size_mm[0]), mm_to_dots(self.Size_mm[1])) # size in dots/pixels 111 self.raster = raster 112 self.Margins = (mm_to_dots(b),)*4 113 txs = (self.Size[0] - self.Margins[1] - self.Margins[3], 114 self.Size[1] - self.Margins[0] - self.Margins[2]) 115 self.Text_rect = (self.Margins[1], self.Margins[0], txs[0], txs[1])
116
117 -class InvalidFormat(Exception):
118 """exception thrown when an invalid output format is requested""" 119 pass
120
121 -class PageWriter(Page):
122 """class to output multiple pages in raster (png) or vector (pdf) format 123 124 125 @ivar base: out filename (without extension) 126 @ivar ext: filename extension (with dot) 127 @type curpage: int 128 @ivar curpage: current page 129 @ivar format: output format: L{PDF} or L{PNG} 130 @type keep_transparency: bool 131 @ivar keep_transparency: C{True} to use transparent instead of white fill color 132 @ivar img_format: C{cairo.FORMAT_ARGB32} or C{cairo.FORMAT_RGB24} depending on 133 L{keep_transparency} 134 @ivar Surface: cairo surface (set by L{_setup_surface_and_context}) 135 @ivar cr: cairo context (set by L{_setup_surface_and_context}) 136 """ 137 138 PDF = 0 139 PNG = 1
140 - def __init__(self, filename, pagespec = None, keep_transparency = True, landscape = False, b = 0.0):
141 """initialize PageWriter object 142 143 see also L{Page.__init__} 144 @param filename: output filename (extension determines format PDF or PNG) 145 @param pagespec: iso page spec, see L{page_spec} 146 @param keep_transparency: see L{keep_transparency} 147 """ 148 self.base,self.ext = splitext(filename) 149 self.filename = filename 150 self.curpage = 1 151 if self.ext.lower() == ".pdf": self.format = PageWriter.PDF 152 elif self.ext.lower() == ".png": self.format = PageWriter.PNG 153 else: 154 raise InvalidFormat(self.ext) 155 self.keep_transparency = keep_transparency 156 if keep_transparency: 157 self.img_format = cairo.FORMAT_ARGB32 158 else: 159 self.img_format = cairo.FORMAT_RGB24 160 w, h = page_spec(pagespec) 161 if landscape and self.format == PageWriter.PNG: 162 w, h = h, w 163 landscape = False 164 super(PageWriter,self).__init__(landscape, w, h, b, self.format == PageWriter.PNG) 165 self._setup_surface_and_context()
166
168 """setup cairo surface taking into account raster mode, transparency and landscape mode""" 169 z = int(self.landscape) 170 if self.format == PageWriter.PDF: 171 self.Surface = cairo.PDFSurface(self.filename, self.Size[z], self.Size[1-z]) 172 else: 173 self.Surface = cairo.ImageSurface(self.img_format, int(self.Size[z]), int(self.Size[1-z])) 174 175 self.cr = cairo.Context(self.Surface) 176 if self.landscape: 177 self.cr.translate(0,self.Size[0]) 178 self.cr.rotate(-math.pi/2) 179 if not self.keep_transparency: 180 self.cr.set_source_rgb(1,1,1) 181 self.cr.move_to(0,0) 182 self.cr.line_to(0,int(self.Size[1])) 183 self.cr.line_to(int(self.Size[0]),int(self.Size[1])) 184 self.cr.line_to(int(self.Size[0]),0) 185 self.cr.close_path() 186 self.cr.fill()
187
188 - def end_page(self):
189 """in PNG mode, output a separate file for each page""" 190 if self.format == PageWriter.PNG: 191 outfile = self.filename if self.curpage < 2 else self.base + "%02d" % (self.curpage) + self.ext 192 self.Surface.write_to_png(outfile)
193
194 - def new_page(self):
195 """setup next page""" 196 if self.format == PageWriter.PDF: 197 self.cr.show_page() 198 else: 199 self.curpage += 1 200 self._setup_surface_and_context()
201 202
203 -def set_color(cr, rgba):
204 """set stroke color 205 206 @param cr: cairo context 207 @type rgba: tuple 208 @param rgba: (r,g,b) or (r,g,b,a) 209 """ 210 if len(rgba) == 3: 211 cr.set_source_rgb(*rgba) 212 else: 213 cr.set_source_rgba(*rgba)
214
215 -def extract_font_name(f):
216 """extract the font name from a string or from a tuple (fontname, slant, weight) 217 218 @rtype: str 219 """ 220 return f if type(f) is str else f[0]
221
222 -def make_sloppy_rect(cr, rect, sdx = 0.0, sdy = 0.0, srot = 0.0):
223 """slightly rotate and translate a rect to give it a sloppy look 224 225 @param cr: cairo context 226 @param sdx: maximum x-offset, true offset will be uniformly distibuted 227 @param sdy: maximum y-offset 228 @param sdy: maximum rotation 229 """ 230 x, y, w, h = rect 231 cr.save() 232 cr.translate(x,y) 233 if sdx != 0.0 or sdy != 0.0 or srot != 0.0: 234 cr.translate(w/2, h/2) 235 cr.translate(w*(random.random() - 0.5)*sdx, h*(random.random() - 0.5)*sdy) 236 cr.rotate((random.random() - 0.5)*srot) 237 cr.translate(-w/2.0, -h/2.0)
238
239 -def draw_shadow(cr, rect, thickness = None, shadow_color = (0,0,0,0.3)):
240 """draw a shadow at the bottom-right corner of a rect 241 242 @param cr: cairo context 243 @param rect: tuple (x,y,w,h) 244 @param thickness: if C{None} nothing is drawn 245 @param shadow_color: shadow color 246 """ 247 if thickness is None: return 248 fx = mm_to_dots(thickness[0]) 249 fy = mm_to_dots(thickness[1]) 250 x1, y1, x3, y3 = rect_to_abs(rect) 251 x2, y2 = x1, y3 252 x4, y4 = x3, y1 253 u1, v1 = cr.user_to_device(x1,y1) 254 u2, v2 = cr.user_to_device(x2,y2) 255 u3, v3 = cr.user_to_device(x3,y3) 256 u4, v4 = cr.user_to_device(x4,y4) 257 u1 += fx; v1 += fy; u2 += fx; v2 += fy; 258 u3 += fx; v3 += fy; u4 += fx; v4 += fy; 259 x1, y1 = cr.device_to_user(u1, v1) 260 x2, y2 = cr.device_to_user(u2, v2) 261 x3, y3 = cr.device_to_user(u3, v3) 262 x4, y4 = cr.device_to_user(u4, v4) 263 cr.move_to(x1, y1) 264 cr.line_to(x2, y2); cr.line_to(x3, y3); cr.line_to(x4, y4) 265 set_color(cr, shadow_color) 266 cr.close_path(); cr.fill();
267
268 -def draw_line(cr, rect, stroke_rgba = None, stroke_width = 1.0):
269 """draw a line from (x,y) to (x+w,y+h), where rect=(x,y,w,h) 270 271 @param cr: cairo context 272 @param rect: tuple (x,y,w,h) 273 @param stroke_rgba: stroke color 274 @param stroke_width: stroke width, if <= 0 nothing is drawn 275 """ 276 if (stroke_width <= 0): return 277 x, y, w, h = rect 278 cr.move_to(x, y) 279 cr.rel_line_to(w, h) 280 cr.close_path() 281 if stroke_rgba: 282 set_color(cr, stroke_rgba) 283 cr.set_line_width(stroke_width) 284 cr.stroke()
285
286 -def draw_box(cr, rect, stroke_rgba = None, fill_rgba = None, stroke_width = 1.0, shadow = None):
287 """draw a box (rectangle) with optional shadow 288 289 @param cr: cairo context 290 @param rect: box rectangle as tuple (x,y,w,h) 291 @param stroke_rgba: stroke color (set if not C{None}) 292 @param fill_rgba: fill color (set if not C{None}) 293 @param stroke_width: stroke width 294 @param shadow: shadow thickness 295 """ 296 if (stroke_width <= 0): return 297 draw_shadow(cr, rect, shadow) 298 x, y, w, h = rect 299 cr.move_to(x, y) 300 cr.rel_line_to(w, 0) 301 cr.rel_line_to(0, h) 302 cr.rel_line_to(-w, 0) 303 cr.close_path() 304 if fill_rgba: 305 set_color(cr, fill_rgba) 306 cr.fill_preserve() 307 if stroke_rgba: 308 set_color(cr, stroke_rgba) 309 cr.set_line_width(stroke_width) 310 cr.stroke()
311
312 -def draw_str(cr, text, rect, scaling = -1, stroke_rgba = None, align = (2,0), bbox = False, 313 font = "Times", measure = None, shadow = None):
314 """draw text 315 316 @param cr: cairo context 317 @param text: text string to be drawn 318 @type scaling: int 319 @param scaling: text scaling mode 320 321 - -1: auto select x-scaling or y-scaling (whatever fills the rect) 322 - 0: no scaling 323 - 1: x-scaling, scale so that text fills rect horizontally, preserving ratio 324 - 2: y-scaling, scale so that text fills rect vertically, preserving ratio 325 - 3: xy-scaling, stretch so that text fills rect completely, does not preserve ratio 326 327 @param stroke_rgba: stroke color 328 @type align: tuple 329 @param align: alignment mode as (int,int) tuple for horizontal/vertical alignment 330 331 - 0: left/top alignment 332 - 1: right/bottom alignment 333 - 2: center/middle alignment 334 335 @param bbox: draw text bounding box (for debugging) 336 @param font: font name as string or (font,slant,weight) tuple 337 @param measure: use this string for measurement instead of C{text} 338 @param shadow: draw text shadow as tuple (dx,dy) 339 """ 340 x, y, w, h = rect 341 cr.save() 342 slant = weight = 0 343 if type(font) is str: fontname = font 344 elif len(font) == 3: fontname, slant, weight = font 345 elif len(font) == 2: fontname, slant = font 346 elif len(font) == 1: fontname = font[0] 347 cr.select_font_face(fontname, slant, weight) 348 if measure is None: measure = text 349 te = cr.text_extents(measure) 350 mw, mh = te[2], te[3] 351 if mw < 5: 352 mw = 5. 353 if mh < 5: 354 mh = 5. 355 #ratio, tratio = w*1.0/h, mw*1.0/mh; 356 xratio, yratio = mw*1.0/w, mh*1.0/h; 357 if scaling < 0: scaling = 1 if xratio >= yratio else 2 358 if scaling == 0: crs = (1,1) 359 elif scaling == 1: crs = (1.0/xratio, 1.0/xratio) 360 elif scaling == 2: crs = (1.0/yratio, 1.0/yratio) 361 elif scaling == 3: crs = (1.0/xratio, 1.0/yratio) 362 te = cr.text_extents(text) 363 tw,th = te[2], te[3] 364 tw *= crs[0] 365 th *= crs[1] 366 px, py = x, y + h 367 if align[0] == 1: px += w - tw 368 elif align[0] == 2: px += (w-tw)/2.0 369 if align[1] == 1: py -= h - th 370 elif align[1] == 2: py -= (h-th)/2.0 371 372 cr.translate(px,py) 373 cr.scale(*crs) 374 if shadow is not None: 375 sx = mm_to_dots(shadow[0]) 376 sy = mm_to_dots(shadow[1]) 377 cr.set_source_rgba(0, 0, 0, 0.5) 378 u1, v1 = cr.user_to_device(0, 0) 379 u1 += sx; v1 += sy 380 x1, y1 = cr.device_to_user(u1, v1) 381 cr.move_to(x1, y1) 382 cr.show_text(text) 383 cr.move_to(0, 0) 384 if stroke_rgba: set_color(cr, stroke_rgba) 385 cr.show_text(text) 386 cr.restore() 387 if bbox: 388 draw_box(cr, (x, y, w, h), stroke_rgba) 389 #draw_box(cr, (x, y+h, mw*crs[0], -mh*crs[1]), stroke_rgba) 390 draw_box(cr, (px, py, tw, -th), stroke_rgba)
391