[IMP] base : Improved the typos.
[odoo/odoo.git] / openerp / pychart / font.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
4
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
8 # later version.
9 #
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
13 # for more details.
14 #
15 import color
16 import string
17 import pychart_util
18 import re
19 import theme
20 import afm.dir
21
22
23 __doc__ = """The module for manipulating texts and their attributes.
24
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
28 at 60-degree angle:
29
30 /12/a60{}Hello
31
32 List of attributes:
33
34 /hA
35     Specifies horizontal alignment of the text.  A is one of L (left
36     alignment), R (right alignment), or C (center alignment).
37 /vA
38     Specifies vertical alignment of the text.  A is one of "B"
39     (bottom), "T" (top), " M" (middle).
40
41 /F{FONT}
42     Switch to FONT font family.
43 /T
44     Shorthand of /F{Times-Roman}.
45 /H
46     Shorthand of /F{Helvetica}.
47 /C
48     Shorthand of /F{Courier}.
49 /B
50     Shorthand of /F{Bookman-Demi}.
51 /A
52     Shorthand of /F{AvantGarde-Book}.
53 /P
54     Shorthand of /F{Palatino}.
55 /S
56     Shorthand of /F{Symbol}.
57 /b
58     Switch to bold typeface.
59 /i
60     Switch to italic typeface.
61 /o
62     Switch to oblique typeface.
63 /DD
64     Set font size to DD points.
65
66     /20{}2001 space odyssey!
67
68 /cDD
69     Set gray-scale to 0.DD. Gray-scale of 00 means black, 99 means white.
70
71 //, /{, /}
72     Display `/', `@', or `@{'.
73     
74 { ... }
75     Limit the effect of escape sequences. For example, the below
76     example draws "Foo" at 12pt, "Bar" at 8pt, and "Baz" at 12pt.
77
78     /12Foo{/8Bar}Baz
79 \n
80     Break the line.
81 """
82
83 # List of fonts for which their absence have been already warned.
84 _undefined_font_warned = {}
85
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]
93
94     try:        
95         exec("import pychart.afm.%s" % re.sub("-", "_", font))
96         return afm.dir.afm[font]
97     except:
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
101     
102     if font2:
103         try:
104             exec("import pychart.afm.%s" % re.sub("-", "_", font2))
105             return afm.dir.afm[font2]
106         except:
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
110     return None    
111 def line_width(font, size, text):
112     table = _intern_afm(font, text)
113     if not table:
114         return 0
115
116     width = 0
117     for ch in text:
118         code = ord(ch)
119         if code < len(table):
120             width += table[code]
121         else:
122             width += 10000
123             
124     width = float(width) * size / 1000.0
125     return width
126
127 _font_family_map = {'T': "Times",
128                     'H': "Helvetica",
129                     'C': "Courier",
130                     'N': "Helvetica-Narrow",
131                     'B': "Bookman-Demi", 
132                     'A': "AvantGarde-Book",
133                     'P': "Palatino",
134                     'S': "Symbol"}
135
136 # Aliases for ghostscript font names.
137
138 _font_aliases = {
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"
182 }
183
184
185 class text_state:
186     def copy(self):
187         ts = text_state()
188         ts.family = self.family
189         ts.modifiers = list(self.modifiers)
190         ts.size = self.size
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
196         return ts
197     def __init__(self):
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
206         
207 class text_iterator:
208     def __init__(self, s):
209         self.str = str(s)
210         self.i = 0
211         self.ts = text_state()
212         self.stack = []
213     def reset(self, s):
214         self.str = str(s)
215         self.i = 0
216
217     def __return_state(self, ts, str):
218         font_name = ts.family
219
220         if ts.modifiers != []:
221             is_bold = 0
222             if 'b' in ts.modifiers:
223                 is_bold = 1
224                 font_name += "-Bold"
225             if 'o' in ts.modifiers:
226                 if not is_bold:
227                     font_name += "-"
228                 font_name += "Oblique"
229             elif 'i' in ts.modifiers:
230                 if not is_bold:
231                     font_name += "-"
232                 font_name += "Italic"
233         elif font_name in ("Palatino", "Times", "NewCenturySchlbk"):
234             font_name += "-Roman"
235                 
236         return (font_name, ts.size, ts.line_height, ts.color,
237                 ts.halign, ts.valign, ts.angle, str)
238     def __parse_float(self):
239         istart = self.i
240         while self.i < len(self.str) and self.str[self.i] in string.digits or self.str[self.i] == '.':
241             self.i += 1
242         return float(self.str[istart:self.i])
243             
244     def __parse_int(self):
245         istart = self.i
246         while self.i < len(self.str) and \
247               (self.str[self.i] in string.digits or
248                self.str[self.i] == '-'):
249             self.i += 1
250         return int(self.str[istart:self.i])
251     def next(self):
252         "Get the next text segment. Return an 8-element array: (FONTNAME, SIZE, LINEHEIGHT, COLOR, H_ALIGN, V_ALIGN, ANGLE, STR."
253         l = []
254         changed = 0
255         self.old_state = self.ts.copy()
256         
257         while self.i < len(self.str):
258             if self.str[self.i] == '/':
259                 self.i = self.i+1
260                 ch = self.str[self.i]
261                 self.i = self.i+1
262                 self.old_state = self.ts.copy()
263                 if ch == '/' or ch == '{' or ch == '}':
264                     l.append(ch)
265                 elif _font_family_map.has_key(ch):
266                     self.ts.family = _font_family_map[ch]
267                     changed = 1
268                 elif ch == 'F':
269                     # /F{font-family}
270                     if self.str[self.i] != '{':
271                         raise Exception, "'{' must follow /F in \"%s\"" % self.str
272                     self.i += 1
273                     istart = self.i
274                     while self.str[self.i] != '}':
275                         self.i += 1
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]
279                     self.i += 1
280                     changed = 1
281                     
282                 elif ch in string.digits:
283                     self.i -= 1
284                     self.ts.size = self.__parse_int()
285                     self.ts.line_height = self.ts.size
286                     changed = 1
287                 elif ch == 'l':
288                     self.ts.line_height = self.__parse_int()
289                     changed = 1
290                 elif ch == 'b':
291                     self.ts.modifiers.append('b')
292                     changed = 1
293                 elif ch == 'i':
294                     self.ts.modifiers.append('i')
295                     changed = 1
296                 elif ch == 'o':
297                     self.ts.modifiers.append('q')
298                     changed = 1
299                 elif ch == 'c':
300                     self.ts.color = color.gray_scale(self.__parse_float())
301                 elif ch == 'v':
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]
305                     self.i += 1
306                     changed = 1
307                 elif ch == 'h':
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]
311                     self.i += 1
312                     changed = 1
313                 elif ch == 'a':
314                     self.ts.angle = self.__parse_int()
315                     changed = 1
316                 else:
317                     raise Exception, "Undefined escape sequence: /%c (%s)" % (ch, self.str)
318             elif self.str[self.i] == '{':
319                 self.stack.append(self.ts.copy())
320                 self.i += 1
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]
325                 del self.stack[-1]
326                 self.i += 1
327                 changed = 1
328             else:
329                 l.append(self.str[self.i])
330                 self.i += 1
331
332             if changed and len(l) > 0:
333                 return self.__return_state(self.old_state, ''.join(l))
334             else:
335                 # font change in the beginning of the sequence doesn't count.
336                 self.old_state = self.ts.copy()
337                 changed = 0
338         if len(l) > 0:
339             return self.__return_state(self.old_state, ''.join(l))
340         else:
341             return None
342
343 #
344 #
345
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)."""
352
353     xmax = 0
354     ymax = 0
355     ymax = 0
356     angle = None
357     halign = None
358     valign = None
359
360     itr = text_iterator(None)
361     for line in str(text).split("\n"):
362         cur_height = 0
363         cur_width = 0
364         itr.reset(line)
365         while 1:
366             elem = itr.next()
367             if not elem:
368                 break
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)
376             halign = new_h
377             valign = new_v
378             angle = new_a
379             cur_width += line_width(font, size, chunk)
380             cur_height = max(cur_height, line_height)
381         xmax = max(cur_width, xmax)
382         ymax += cur_height
383     return (xmax, ymax,
384             halign or theme.default_font_halign,
385             valign or theme.default_font_valign,
386             angle or theme.default_font_angle)
387
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)
396     xmin = ymin = 0
397     if halign == "C":
398         xmin = -xmax / 2.0
399         xmax = xmax / 2.0
400     elif halign == "R":
401         xmin = -xmax
402         xmax = 0
403     if valign == "M":
404         ymin = -ymax / 2.0
405         ymax = ymax / 2.0
406     elif valign == "T":
407         ymin = -ymax
408         ymax = 0
409     if angle != 0:
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)
420
421 def unaligned_text_width(text):
422     x = unaligned_get_dimension(text)
423     return x[0]
424
425 def text_width(text):
426     """Return the width of the <text> in points."""
427     (xmin, xmax, d1, d2) = get_dimension(text)
428     return xmax-xmin
429
430 def unaligned_text_height(text):
431     x = unaligned_get_dimension(text)
432     return x[1]
433
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)
439
440 def get_align(text):
441     "Return (halign, valign, angle) of the <text>."
442     (x1, x2, h, v, a) = unaligned_get_dimension(text)
443     return (h, v, a)
444
445 def quotemeta(text):
446     """Quote letters with special meanings in pychart so that <text> will display
447     as-is when passed to canvas.show(). 
448
449 >>> font.quotemeta("foo/bar")
450 "foo//bar"
451 """
452     text = re.sub("/", "//", text)
453     text = re.sub("\\{", "/{", text)
454     text = re.sub("\\}", "/}", text)
455     return text