#
# 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 color
import string
import pychart_util
import re
import theme
import afm.dir
__doc__ = """The module for manipulating texts and their attributes.
Pychart supports extensive sets of attributes in texts. All attributes
are specified via "escape sequences", starting from letter "/". For
example, the below examples draws string "Hello" using a 12-point font
at 60-degree angle:
/12/a60{}Hello
List of attributes:
/hA
Specifies horizontal alignment of the text. A is one of L (left
alignment), R (right alignment), or C (center alignment).
/vA
Specifies vertical alignment of the text. A is one of "B"
(bottom), "T" (top), " M" (middle).
/F{FONT}
Switch to FONT font family.
/T
Shorthand of /F{Times-Roman}.
/H
Shorthand of /F{Helvetica}.
/C
Shorthand of /F{Courier}.
/B
Shorthand of /F{Bookman-Demi}.
/A
Shorthand of /F{AvantGarde-Book}.
/P
Shorthand of /F{Palatino}.
/S
Shorthand of /F{Symbol}.
/b
Switch to bold typeface.
/i
Switch to italic typeface.
/o
Switch to oblique typeface.
/DD
Set font size to DD points.
/20{}2001 space odyssey!
/cDD
Set gray-scale to 0.DD. Gray-scale of 00 means black, 99 means white.
//, /{, /}
Display `/', `{', or `}'.
{ ... }
Limit the effect of escape sequences. For example, the below
example draws "Foo" at 12pt, "Bar" at 8pt, and "Baz" at 12pt.
/12Foo{/8Bar}Baz
\n
Break the line.
"""
# List of fonts for which their absence have been already warned.
_undefined_font_warned = {}
class FontException(Exception):
def __init__(self, msg, str):
self.__msg = msg
self.__str = str
def __str__(self):
return """%s (got "%s"). Write "//". "/{", "/}" to display "/", "{", "}", respectively.""" % \
(self.__msg, self.__str)
def _intern_afm(font, text):
global _undefined_font_warned
r = afm.dir.afm.get(font, None)
if r: return r
font2 = _font_aliases.get(font, None)
if font2:
r = afm.dir.afm.get(font2, None)
if r: return r
try:
exec("import pychart.afm.%s" % re.sub("-", "_", font))
return afm.dir.afm[font]
except:
if not font2 and not _undefined_font_warned.has_key(font):
pychart_util.warn('Warning: unknown font "%s" while parsing "%s"'
% (font, text))
_undefined_font_warned[font] = 1
if font2:
try:
exec('import pychart.afm.%s' % re.sub('-', '_', font2))
return afm.dir.afm[font2]
except:
if not _undefined_font_warned.has_key(font):
pychart_util.warn('Warning: unknown font "%s" while parsing "%s"' % (font, text))
_undefined_font_warned[font] = 1
return None
def line_width(font, size, text):
table = _intern_afm(font, text)
if not table:
return 0
width = 0
for ch in text:
code = ord(ch)
if code < len(table):
width += table[code]
else:
# Invalid char. Make up a number.
width += table[0]
width = float(width) * size / 1000.0
return width
_font_family_map = {'T': 'Times',
'H': 'Helvetica',
'C': 'Courier',
'N': 'Helvetica-Narrow',
'B': 'Bookman-Demi',
'A': 'AvantGarde-Book',
'P': 'Palatino',
'S': 'Symbol'}
# Aliases for ghostscript font names.
_font_aliases = {
'Bookman-Demi': 'URWBookmanL-DemiBold%I',
'Bookman-DemiItalic': 'URWBookmanL-DemiBoldItal',
'Bookman-Demi-Italic': 'URWBookmanL-DemiBoldItal',
'Bookman-Light': 'URWBookmanL-Ligh',
'Bookman-LightItalic': 'URWBookmanL-LighItal',
'Bookman-Light-Italic': 'URWBookmanL-LighItal',
'Courier': 'NimbusMonL-Regu',
'Courier-Oblique': 'NimbusMonL-ReguObli',
'Courier-Bold': 'NimbusMonL-Bold',
'Courier-BoldOblique': 'NimbusMonL-BoldObli',
'AvantGarde-Book': 'URWGothicL-Book',
'AvantGarde-BookOblique': 'URWGothicL-BookObli',
'AvantGarde-Book-Oblique': 'URWGothicL-BookObli',
'AvantGarde-Demi': 'URWGothicL-Demi',
'AvantGarde-DemiOblique': 'URWGothicL-DemiObli',
'AvantGarde-Demi-Oblique': 'URWGothicL-DemiObli',
'Helvetica': 'NimbusSanL-Regu',
'Helvetica-Oblique': 'NimbusSanL-ReguItal',
'Helvetica-Bold': 'NimbusSanL-Bold',
'Helvetica-BoldOblique': 'NimbusSanL-BoldItal',
'Helvetica-Narrow': 'NimbusSanL-ReguCond',
'Helvetica-Narrow-Oblique': 'NimbusSanL-ReguCondItal',
'Helvetica-Narrow-Bold': 'NimbusSanL-BoldCond',
'Helvetica-Narrow-BoldOblique': 'NimbusSanL-BoldCondItal',
'Palatino-Roman': 'URWPalladioL-Roma',
'Palatino': 'URWPalladioL-Roma',
'Palatino-Italic': 'URWPalladioL-Ital',
'Palatino-Bold': 'URWPalladioL-Bold',
'Palatino-BoldItalic': 'URWPalladioL-BoldItal',
'NewCenturySchlbk-Roman': 'CenturySchL-Roma',
'NewCenturySchlbk': 'CenturySchL-Roma',
'NewCenturySchlbk-Italic': 'CenturySchL-Ital',
'NewCenturySchlbk-Bold': 'CenturySchL-Bold',
'NewCenturySchlbk-BoldItalic': 'CenturySchL-BoldItal',
'Times-Roman': 'NimbusRomNo9L-Regu',
'Times': 'NimbusRomNo9L-Regu',
'Times-Italic': 'NimbusRomNo9L-ReguItal',
'Times-Bold': 'NimbusRomNo9L-Medi',
'Times-BoldItalic': 'NimbusRomNo9L-MediItal',
'Symbol': 'StandardSymL',
'ZapfChancery-MediumItalic': 'URWChanceryL-MediItal',
'ZapfChancery-Medium-Italic': 'URWChanceryL-MediItal',
'ZapfDingbats': 'Dingbats'
}
class text_state:
def copy(self):
ts = text_state()
ts.family = self.family
ts.modifiers = list(self.modifiers)
ts.size = self.size
ts.line_height = self.line_height
ts.color = self.color
ts.halign = self.halign
ts.valign = self.valign
ts.angle = self.angle
return ts
def __init__(self):
self.family = theme.default_font_family
self.modifiers = [] # 'b' for bold, 'i' for italic, 'o' for oblique.
self.size = theme.default_font_size
self.line_height = theme.default_line_height or theme.default_font_size
self.color = color.default
self.halign = theme.default_font_halign
self.valign = theme.default_font_valign
self.angle = theme.default_font_angle
class text_iterator:
def __init__(self, s):
self.str = unicode(s)
self.i = 0
self.ts = text_state()
self.stack = []
def reset(self, s):
self.str = unicode(s)
self.i = 0
def __return_state(self, ts, str):
font_name = ts.family
if ts.modifiers != []:
is_bold = 0
if 'b' in ts.modifiers:
is_bold = 1
font_name += '-Bold'
if 'o' in ts.modifiers:
if not is_bold:
font_name += '-'
font_name += 'Oblique'
elif 'i' in ts.modifiers:
if not is_bold:
font_name += '-'
font_name += 'Italic'
elif font_name in ('Palatino', 'Times', 'NewCenturySchlbk'):
font_name += '-Roman'
return (font_name, ts.size, ts.line_height, ts.color,
ts.halign, ts.valign, ts.angle, str)
def __parse_float(self):
istart = self.i
while self.i < len(self.str) and self.str[self.i] in string.digits or self.str[self.i] == '.':
self.i += 1
return float(self.str[istart:self.i])
def __parse_int(self):
istart = self.i
while self.i < len(self.str) and \
(self.str[self.i] in string.digits or
self.str[self.i] == '-'):
self.i += 1
return int(self.str[istart:self.i])
def next(self):
"Get the next text segment. Return an 8-element array: (FONTNAME, SIZE, LINEHEIGHT, COLOR, H_ALIGN, V_ALIGN, ANGLE, STR."
l = []
changed = 0
self.old_state = self.ts.copy()
while self.i < len(self.str):
if self.str[self.i] == '/':
self.i = self.i+1
ch = self.str[self.i]
self.i = self.i+1
self.old_state = self.ts.copy()
if ch == '/' or ch == '{' or ch == '}':
l.append(ch)
elif _font_family_map.has_key(ch):
self.ts.family = _font_family_map[ch]
changed = 1
elif ch == 'F':
# /F{font-family}
if self.str[self.i] != '{':
raise FontException('"{" must follow /F', self.str)
self.i += 1
istart = self.i
while self.str[self.i] != '}':
self.i += 1
if self.i >= len(self.str):
raise FontException('Expecting "/F{...}"', self.str)
self.ts.family = self.str[istart:self.i]
self.i += 1
changed = 1
elif ch in string.digits:
self.i -= 1
self.ts.size = self.__parse_int()
self.ts.line_height = self.ts.size
changed = 1
elif ch == 'l':
self.ts.line_height = self.__parse_int()
changed = 1
elif ch == 'b':
self.ts.modifiers.append('b')
changed = 1
elif ch == 'i':
self.ts.modifiers.append('i')
changed = 1
elif ch == 'o':
self.ts.modifiers.append('q')
changed = 1
elif ch == 'c':
self.ts.color = color.gray_scale(self.__parse_float())
elif ch == 'v':
if self.str[self.i] not in 'BTM':
raise FontException('Undefined escape sequence "/v%c"' %
self.str[self.i], self.str)
self.ts.valign = self.str[self.i]
self.i += 1
changed = 1
elif ch == 'h':
if self.str[self.i] not in 'LRC':
raise FontException('Undefined escape sequence "/h%c"' %
self.str[self.i], self.str)
self.ts.halign = self.str[self.i]
self.i += 1
changed = 1
elif ch == 'a':
self.ts.angle = self.__parse_int()
changed = 1
else:
raise FontException('Undefined escape sequence: "/%c"' % ch,
self.str)
elif self.str[self.i] == '{':
self.stack.append(self.ts.copy())
self.i += 1
elif self.str[self.i] == '}':
if len(self.stack) == 0:
raise FontError('Unmatched "}"', self.str)
self.ts = self.stack[-1]
del self.stack[-1]
self.i += 1
changed = 1
else:
l.append(self.str[self.i])
self.i += 1
if changed and len(l) > 0:
return self.__return_state(self.old_state, ''.join(l))
else:
# font change in the beginning of the sequence doesn't count.
self.old_state = self.ts.copy()
changed = 0
if len(l) > 0:
return self.__return_state(self.old_state, ''.join(l))
else:
return None
#
#
def unaligned_get_dimension(text):
"""Return the bounding box of the text, assuming that the left-bottom corner
of the first letter of the text is at (0, 0). This procedure ignores
/h, /v, and /a directives when calculating the BB; it just returns the
alignment specifiers as a part of the return value. The return value is a
tuple (width, height, halign, valign, angle)."""
xmax = 0
ymax = 0
ymax = 0
angle = None
halign = None
valign = None
itr = text_iterator(None)
for line in unicode(text).split('\n'):
cur_height = 0
cur_width = 0
itr.reset(line)
while 1:
elem = itr.next()
if not elem:
break
(font, size, line_height, color, new_h, new_v, new_a, chunk) = elem
if halign != None and new_h != halign:
raise FontException('Only one "/h" can appear in a string.',
unicode(text))
if valign != None and new_v != valign:
raise FontException('Only one "/v" can appear in a string.',
unicode(text))
if angle != None and new_a != angle:
raise FontException('Only one "/a" can appear in a string.',
unicode(text))
halign = new_h
valign = new_v
angle = new_a
cur_width += line_width(font, size, chunk)
cur_height = max(cur_height, line_height)
xmax = max(cur_width, xmax)
ymax += cur_height
return (xmax, ymax,
halign or theme.default_font_halign,
valign or theme.default_font_valign,
angle or theme.default_font_angle)
def get_dimension(text):
"""Return the bounding box of the <text>,
assuming that the left-bottom corner
of the first letter of the text is at (0, 0). This procedure ignores
/h, /v, and /a directives when calculating the boundingbox; it just returns the
alignment specifiers as a part of the return value. The return value is a
tuple (width, height, halign, valign, angle)."""
(xmax, ymax, halign, valign, angle) = unaligned_get_dimension(text)
xmin = ymin = 0
if halign == 'C':
xmin = -xmax / 2.0
xmax = xmax / 2.0
elif halign == 'R':
xmin = -xmax
xmax = 0
if valign == 'M':
ymin = -ymax / 2.0
ymax = ymax / 2.0
elif valign == 'T':
ymin = -ymax
ymax = 0
if angle != 0:
(x0, y0) = pychart_util.rotate(xmin, ymin, angle)
(x1, y1) = pychart_util.rotate(xmax, ymin, angle)
(x2, y2) = pychart_util.rotate(xmin, ymax, angle)
(x3, y3) = pychart_util.rotate(xmax, ymax, angle)
xmax = max(x0, x1, x2, x3)
xmin = min(x0, x1, x2, x3)
ymax = max(y0, y1, y2, y3)
ymin = min(y0, y1, y2, y3)
return (xmin, xmax, ymin, ymax)
return (xmin, xmax, ymin, ymax)
def unaligned_text_width(text):
x = unaligned_get_dimension(text)
return x[0]
def text_width(text):
"""Return the width of the <text> in points."""
(xmin, xmax, d1, d2) = get_dimension(text)
return xmax-xmin
def unaligned_text_height(text):
x = unaligned_get_dimension(text)
return x[1]
def text_height(text):
"""Return the total height of the <text> and the length from the
base point to the top of the text box."""
(d1, d2, ymin, ymax) = get_dimension(text)
return (ymax-ymin, ymax)
def get_align(text):
"Return (halign, valign, angle) of the <text>."
(x1, x2, h, v, a) = unaligned_get_dimension(text)
return (h, v, a)
def quotemeta(text):
"""Quote letters with special meanings in pychart so that <text> will display
as-is when passed to canvas.show().
>>> font.quotemeta("foo/bar")
"foo//bar"
"""
text = re.sub(r'/', '//', text)
text = re.sub(r'\\{', '/{', text)
text = re.sub(r'\\}', '/}', text)
return text
|