1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
5 # Jockey is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2, or (at your option) any
10 # Jockey is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 __doc__ = """The module for manipulating texts and their attributes.
25 Pychart supports extensive sets of attributes in texts. All attributes
26 are specified via "escape sequences", starting from letter "/". For
27 example, the below examples draws string "Hello" using a 12-point font
35 Specifies horizontal alignment of the text. A is one of L (left
36 alignment), R (right alignment), or C (center alignment).
38 Specifies vertical alignment of the text. A is one of "B"
39 (bottom), "T" (top), " M" (middle).
42 Switch to FONT font family.
44 Shorthand of /F{Times-Roman}.
46 Shorthand of /F{Helvetica}.
48 Shorthand of /F{Courier}.
50 Shorthand of /F{Bookman-Demi}.
52 Shorthand of /F{AvantGarde-Book}.
54 Shorthand of /F{Palatino}.
56 Shorthand of /F{Symbol}.
58 Switch to bold typeface.
60 Switch to italic typeface.
62 Switch to oblique typeface.
64 Set font size to DD points.
66 /20{}2001 space odyssey!
69 Set gray-scale to 0.DD. Gray-scale of 00 means black, 99 means white.
72 Display `/', `@', or `@{'.
75 Limit the effect of escape sequences. For example, the below
76 example draws "Foo" at 12pt, "Bar" at 8pt, and "Baz" at 12pt.
83 # List of fonts for which their absence have been already warned.
84 _undefined_font_warned = {}
86 def _intern_afm(font, text):
87 global _undefined_font_warned
88 font2 = _font_aliases.has_key(font) and _font_aliases[font]
89 if afm.dir.afm.has_key(font):
90 return afm.dir.afm[font]
91 if afm.dir.afm.has_key(font2):
92 return afm.dir.afm[font2]
95 exec("import pychart.afm.%s" % re.sub("-", "_", font))
96 return afm.dir.afm[font]
98 if not font2 and not _undefined_font_warned.has_key(font):
99 pychart_util.warn("Warning: unknown font '%s' while parsing '%s'" % (font, text))
100 _undefined_font_warned[font] = 1
104 exec("import pychart.afm.%s" % re.sub("-", "_", font2))
105 return afm.dir.afm[font2]
107 if not _undefined_font_warned.has_key(font):
108 pychart_util.warn("Warning: unknown font '%s' while parsing '%s'" % (font, text))
109 _undefined_font_warned[font] = 1
111 def line_width(font, size, text):
112 table = _intern_afm(font, text)
119 if code < len(table):
124 width = float(width) * size / 1000.0
127 _font_family_map = {'T': "Times",
130 'N': "Helvetica-Narrow",
132 'A': "AvantGarde-Book",
136 # Aliases for ghostscript font names.
139 'Bookman-Demi': "URWBookmanL-DemiBold%I",
140 'Bookman-DemiItalic': "URWBookmanL-DemiBoldItal",
141 'Bookman-Demi-Italic': "URWBookmanL-DemiBoldItal",
142 'Bookman-Light': "URWBookmanL-Ligh",
143 'Bookman-LightItalic': "URWBookmanL-LighItal",
144 'Bookman-Light-Italic': "URWBookmanL-LighItal",
145 'Courier': "NimbusMonL-Regu",
146 'Courier-Oblique': "NimbusMonL-ReguObli",
147 'Courier-Bold': "NimbusMonL-Bold",
148 'Courier-BoldOblique': "NimbusMonL-BoldObli",
149 'AvantGarde-Book': "URWGothicL-Book",
150 'AvantGarde-BookOblique': "URWGothicL-BookObli",
151 'AvantGarde-Book-Oblique': "URWGothicL-BookObli",
152 'AvantGarde-Demi': "URWGothicL-Demi",
153 'AvantGarde-DemiOblique': "URWGothicL-DemiObli",
154 'AvantGarde-Demi-Oblique': "URWGothicL-DemiObli",
155 'Helvetica': "NimbusSanL-Regu",
156 'Helvetica-Oblique': "NimbusSanL-ReguItal",
157 'Helvetica-Bold': "NimbusSanL-Bold",
158 'Helvetica-BoldOblique': "NimbusSanL-BoldItal",
159 'Helvetica-Narrow': "NimbusSanL-ReguCond",
160 'Helvetica-Narrow-Oblique': "NimbusSanL-ReguCondItal",
161 'Helvetica-Narrow-Bold': "NimbusSanL-BoldCond",
162 'Helvetica-Narrow-BoldOblique': "NimbusSanL-BoldCondItal",
163 'Palatino-Roman': "URWPalladioL-Roma",
164 'Palatino': "URWPalladioL-Roma",
165 'Palatino-Italic': "URWPalladioL-Ital",
166 'Palatino-Bold': "URWPalladioL-Bold",
167 'Palatino-BoldItalic': "URWPalladioL-BoldItal",
168 'NewCenturySchlbk-Roman': "CenturySchL-Roma",
169 'NewCenturySchlbk': "CenturySchL-Roma",
170 'NewCenturySchlbk-Italic': "CenturySchL-Ital",
171 'NewCenturySchlbk-Bold': "CenturySchL-Bold",
172 'NewCenturySchlbk-BoldItalic': "CenturySchL-BoldItal",
173 'Times-Roman': "NimbusRomNo9L-Regu",
174 'Times': "NimbusRomNo9L-Regu",
175 'Times-Italic': "NimbusRomNo9L-ReguItal",
176 'Times-Bold': "NimbusRomNo9L-Medi",
177 'Times-BoldItalic': "NimbusRomNo9L-MediItal",
178 'Symbol': "StandardSymL",
179 'ZapfChancery-MediumItalic': "URWChanceryL-MediItal",
180 'ZapfChancery-Medium-Italic': "URWChanceryL-MediItal",
181 'ZapfDingbats': "Dingbats"
188 ts.family = self.family
189 ts.modifiers = list(self.modifiers)
191 ts.line_height = self.line_height
192 ts.color = self.color
193 ts.halign = self.halign
194 ts.valign = self.valign
195 ts.angle = self.angle
198 self.family = theme.default_font_family
199 self.modifiers = [] # 'b' for bold, 'i' for italic, 'o' for oblique.
200 self.size = theme.default_font_size
201 self.line_height = theme.default_line_height or theme.default_font_size
202 self.color = color.default
203 self.halign = theme.default_font_halign
204 self.valign = theme.default_font_valign
205 self.angle = theme.default_font_angle
208 def __init__(self, s):
211 self.ts = text_state()
217 def __return_state(self, ts, str):
218 font_name = ts.family
220 if ts.modifiers != []:
222 if 'b' in ts.modifiers:
225 if 'o' in ts.modifiers:
228 font_name += "Oblique"
229 elif 'i' in ts.modifiers:
232 font_name += "Italic"
233 elif font_name in ("Palatino", "Times", "NewCenturySchlbk"):
234 font_name += "-Roman"
236 return (font_name, ts.size, ts.line_height, ts.color,
237 ts.halign, ts.valign, ts.angle, str)
238 def __parse_float(self):
240 while self.i < len(self.str) and self.str[self.i] in string.digits or self.str[self.i] == '.':
242 return float(self.str[istart:self.i])
244 def __parse_int(self):
246 while self.i < len(self.str) and \
247 (self.str[self.i] in string.digits or
248 self.str[self.i] == '-'):
250 return int(self.str[istart:self.i])
252 "Get the next text segment. Return an 8-element array: (FONTNAME, SIZE, LINEHEIGHT, COLOR, H_ALIGN, V_ALIGN, ANGLE, STR."
255 self.old_state = self.ts.copy()
257 while self.i < len(self.str):
258 if self.str[self.i] == '/':
260 ch = self.str[self.i]
262 self.old_state = self.ts.copy()
263 if ch == '/' or ch == '{' or ch == '}':
265 elif _font_family_map.has_key(ch):
266 self.ts.family = _font_family_map[ch]
270 if self.str[self.i] != '{':
271 raise Exception, "'{' must follow /F in \"%s\"" % self.str
274 while self.str[self.i] != '}':
276 if self.i >= len(self.str):
277 raise Exception, "Expecting /F{...}. in \"%s\"" % self.str
278 self.ts.family = self.str[istart:self.i]
282 elif ch in string.digits:
284 self.ts.size = self.__parse_int()
285 self.ts.line_height = self.ts.size
288 self.ts.line_height = self.__parse_int()
291 self.ts.modifiers.append('b')
294 self.ts.modifiers.append('i')
297 self.ts.modifiers.append('q')
300 self.ts.color = color.gray_scale(self.__parse_float())
302 if self.str[self.i] not in "BTM":
303 raise Exception, "Undefined escape sequence: /v%c (%s)" % (self.str[self.i], self.str)
304 self.ts.valign = self.str[self.i]
308 if self.str[self.i] not in "LRC":
309 raise Exception, "Undefined escape sequence: /h%c (%s)" % (self.str[self.i], self.str)
310 self.ts.halign = self.str[self.i]
314 self.ts.angle = self.__parse_int()
317 raise Exception, "Undefined escape sequence: /%c (%s)" % (ch, self.str)
318 elif self.str[self.i] == '{':
319 self.stack.append(self.ts.copy())
321 elif self.str[self.i] == '}':
322 if len(self.stack) == 0:
323 raise ValueError, "unmatched '}' in \"%s\"" % (self.str)
324 self.ts = self.stack[-1]
329 l.append(self.str[self.i])
332 if changed and len(l) > 0:
333 return self.__return_state(self.old_state, ''.join(l))
335 # font change in the beginning of the sequence doesn't count.
336 self.old_state = self.ts.copy()
339 return self.__return_state(self.old_state, ''.join(l))
346 def unaligned_get_dimension(text):
347 """Return the bounding box of the text, assuming that the left-bottom corner
348 of the first letter of the text is at (0, 0). This procedure ignores
349 /h, /v, and /a directives when calculating the BB; it just returns the
350 alignment specifiers as a part of the return value. The return value is a
351 tuple (width, height, halign, valign, angle)."""
360 itr = text_iterator(None)
361 for line in str(text).split("\n"):
369 (font, size, line_height, color, new_h, new_v, new_a, chunk) = elem
370 if halign != None and new_h != halign:
371 raise Exception, "Only one /h can appear in string '%s'." % str(text)
372 if valign != None and new_v != valign:
373 raise Exception, "Only one /v can appear in string '%s'." % str(text)
374 if angle != None and new_a != angle:
375 raise Exception, "Only one /a can appear in string '%s'." % str(text)
379 cur_width += line_width(font, size, chunk)
380 cur_height = max(cur_height, line_height)
381 xmax = max(cur_width, xmax)
384 halign or theme.default_font_halign,
385 valign or theme.default_font_valign,
386 angle or theme.default_font_angle)
388 def get_dimension(text):
389 """Return the bounding box of the <text>,
390 assuming that the left-bottom corner
391 of the first letter of the text is at (0, 0). This procedure ignores
392 /h, /v, and /a directives when calculating the boundingbox; it just returns the
393 alignment specifiers as a part of the return value. The return value is a
394 tuple (width, height, halign, valign, angle)."""
395 (xmax, ymax, halign, valign, angle) = unaligned_get_dimension(text)
410 (x0, y0) = pychart_util.rotate(xmin, ymin, angle)
411 (x1, y1) = pychart_util.rotate(xmax, ymin, angle)
412 (x2, y2) = pychart_util.rotate(xmin, ymax, angle)
413 (x3, y3) = pychart_util.rotate(xmax, ymax, angle)
414 xmax = max(x0, x1, x2, x3)
415 xmin = min(x0, x1, x2, x3)
416 ymax = max(y0, y1, y2, y3)
417 ymin = min(y0, y1, y2, y3)
418 return (xmin, xmax, ymin, ymax)
419 return (xmin, xmax, ymin, ymax)
421 def unaligned_text_width(text):
422 x = unaligned_get_dimension(text)
425 def text_width(text):
426 """Return the width of the <text> in points."""
427 (xmin, xmax, d1, d2) = get_dimension(text)
430 def unaligned_text_height(text):
431 x = unaligned_get_dimension(text)
434 def text_height(text):
435 """Return the total height of the <text> and the length from the
436 base point to the top of the text box."""
437 (d1, d2, ymin, ymax) = get_dimension(text)
438 return (ymax-ymin, ymax)
441 "Return (halign, valign, angle) of the <text>."
442 (x1, x2, h, v, a) = unaligned_get_dimension(text)
446 """Quote letters with special meanings in pychart so that <text> will display
447 as-is when passed to canvas.show().
449 >>> font.quotemeta("foo/bar")
452 text = re.sub("/", "//", text)
453 text = re.sub("\\{", "/{", text)
454 text = re.sub("\\}", "/}", text)