1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
35
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
67 """convert millimeters to dots
68
69 @rtype: float
70 """
71 return mm/25.4 * XDPI
72
74 """convert dots to millimeters
75
76 @rtype: float
77 """
78 return dots*25.4/XDPI
79
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)
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]))
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
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
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
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
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
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
390 draw_box(cr, (px, py, tw, -th), stroke_rgba)
391