# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2000, 2003 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Font management...
#
import os, re, operator
from string import split,strip,atoi,atof,lower,translate,maketrans
import streamfilter
from Sketch import _,config,const,Point,TrafoType,Scale,SketchError,\
SketchInternalError, Subscribe, CreatePath, CreateFontMetric, SKCache
from Sketch.Lib import encoding
from Sketch.warn import warn,INTERNAL,USER,pdebug
from Sketch.Lib.util import find_in_path,find_files_in_path
minus_tilde = maketrans('-', '~')
def xlfd_matrix(trafo):
mat = '[%f %f %f %f]' % trafo.matrix()
return translate(mat, minus_tilde)
def _str(val):
return strip(val)
def _bb(val):
return tuple(map(int, map(round, map(atof, split(strip(val))))))
def _number(val):
return int(round(atof(val)))
converters = {
'EncodingScheme': _str,
'Ascender': _number,
'Descender': _number,
'ItalicAngle': atof,
'FontBBox': _bb,
'StartCharMetrics': None,
'EndFontMetrics': None
}
StandardEncoding = 'AdobeStandardEncoding'
def read_char_metrics(afm):
# read the individual char metrics. Assumes that each line contains
# the keys C, W, N and B. Of these keys, only C (or CH, but that's
# not implemented here) is really required by the AFM specification.
charmetrics = {encoding.notdef: (0, 0,0,0,0)}
font_encoding = [encoding.notdef] * 256
while 1:
line = afm.readline()
if line == 'EndCharMetrics\n':
break
items = filter(None, map(strip, split(line, ';')))
if not items:
continue
code = name = width = bbox = None
for item in items:
[key, value] = split(item, None, 1)
if key == 'C':
code = atoi(value)
elif key == 'WX':
width = int(round(atof(value)))
elif key == 'N':
name = value
elif key == 'B':
bbox = tuple(map(int,map(round,map(atof,split(value)))))
charmetrics[name] = (width,) + bbox
font_encoding[code] = name
return charmetrics, font_encoding
def read_afm_file(filename):
afm = streamfilter.LineDecode(open(filename, 'r'))
attribs = {'ItalicAngle': 0.0}
charmetrics = None
font_encoding = [encoding.notdef] * 256
while 1:
line = afm.readline()
if not line:
break
try:
[key, value] = split(line, None, 1)
except ValueError:
# this normally means that a line contained only a keyword
# but no value or that the line was empty
continue
try:
action = converters[key]
except KeyError:
continue
if action:
attribs[key] = action(value)
elif key == 'StartCharMetrics':
charmetrics, font_encoding = read_char_metrics(afm)
break
else:
# EndFontMetrics
break
if not charmetrics:
raise ValueError, \
'AFM files without individual char metrics not yet supported.'
if attribs.get('EncodingScheme', StandardEncoding) == StandardEncoding:
enc = encoding.iso_latin_1
else:
enc = font_encoding
try:
rescharmetrics = map(operator.getitem, [charmetrics] * len(enc), enc)
except KeyError:
# Some iso-latin-1 glyphs are not defined in the font. Try the
# slower way and report missing glyphs.
length = len(enc)
rescharmetrics = [(0, 0,0,0,0)] * length
for idx in range(length):
name = enc[idx]
try:
rescharmetrics[idx] = charmetrics[name]
except KeyError:
# missing character...
warn(INTERNAL, '%s: missing character %s', filename, name)
# some fonts don't define ascender and descender (psyr.afm for
# instance). use the values from the font bounding box instead. This
# is not really a good idea, but how do we solve this?
#
# If psyr.afm is the only afm-file where these values are missing
# (?) we could use the values from the file s050000l.afm shipped
# with ghostscript (or replace psyr.afm with that file).
#
# This is a more general problem since many of the values Sketch
# reads from afm files are only optional (including ascender and
# descender).
if not attribs.has_key('Ascender'):
attribs['Ascender'] = attribs['FontBBox'][3]
if not attribs.has_key('Descender'):
attribs['Descender'] = attribs['FontBBox'][1]
return (CreateFontMetric(attribs['Ascender'], attribs['Descender'],
attribs['FontBBox'], attribs['ItalicAngle'],
rescharmetrics),
enc)
_warned_about_afm = {}
def read_metric(ps_name):
for afm in ps_to_filename[ps_name]:
afm = afm + '.afm'
filename = find_in_path(config.font_path, afm)
if filename:
if __debug__:
import time
start = time.clock()
metric = read_afm_file(filename)
if __debug__:
pdebug('timing', 'time to read afm %s: %g', afm,
time.clock() - start)
return metric
else:
if not _warned_about_afm.get(afm):
warn(USER,
_("I cannot find the metrics for the font %(ps_name)s.\n"
"The file %(afm)s is not in the font_path.\n"
"I'll use the metrics for %(fallback)s instead."),
ps_name = ps_name, afm = afm,
fallback = config.preferences.fallback_font)
_warned_about_afm[afm] = 1
if ps_name != config.preferences.fallback_font:
return read_metric(config.preferences.fallback_font)
else:
raise SketchError("Can't load metrics for fallback font %s",
config.preferences.fallback_font)
def font_file_name(ps_name):
names = []
for basename in ps_to_filename[ps_name]:
names.append(basename + '.pfb')
names.append(basename + '.pfa')
filename = find_files_in_path(config.font_path, names)
return filename
def read_outlines(ps_name):
filename = font_file_name(ps_name)
if filename:
if __debug__:
pdebug('font', 'read_outlines: %s', filename)
import Sketch.Lib.type1
return Sketch.Lib.type1.read_outlines(filename)
else:
raise SketchInternalError('Cannot find file for font %s' % ps_name)
def convert_outline(outline):
paths = []
trafo = Scale(0.001)
for closed, sub in outline:
if closed:
sub.append(sub[0])
path = CreatePath()
paths.append(path)
for item in sub:
if len(item) == 2:
apply(path.AppendLine, item)
else:
apply(path.AppendBezier, item)
if closed:
path.load_close()
for path in paths:
path.Transform(trafo)
return tuple(paths)
fontlist = []
fontmap = {}
ps_to_filename = {}
def _add_ps_filename(ps_name, filename):
filename = (filename,)
if ps_to_filename.has_key(ps_name):
filename = ps_to_filename[ps_name] + filename
ps_to_filename[ps_name] = filename
def read_font_dirs():
#print 'read_font_dirs'
if __debug__:
import time
start = time.clock()
rx_sfd = re.compile(r'^.*\.sfd$')
for directory in config.font_path:
#print directory
try:
filenames = os.listdir(directory)
except os.error, exc:
warn(USER, _("Cannot list directory %s:%s\n"
"ignoring it in font_path"),
directory, str(exc))
continue
dirfiles = filter(rx_sfd.match, filenames)
for filename in dirfiles:
filename = os.path.join(directory, filename)
#print filename
try:
file = open(filename, 'r')
line_nr = 0
for line in file.readlines():
line_nr = line_nr + 1
line = strip(line)
if not line or line[0] == '#':
continue
info = map(intern, split(line, ','))
if len(info) == 6:
psname = info[0]
fontlist.append(tuple(info[:-1]))
_add_ps_filename(psname, info[-1])
fontmap[psname] = tuple(info[1:-1])
elif len(info) == 2:
psname, basename = info
_add_ps_filename(psname, basename)
else:
warn(INTERNAL,'%s:%d: line must have exactly 6 fields',
filename, line_nr)
file.close()
except IOError, value:
warn(USER, _("Cannot load sfd file %(filename)s:%(message)s;"
"ignoring it"),
filename = filename, message = value.strerror)
if __debug__:
pdebug('timing', 'time to read font dirs: %g', time.clock() - start)
def make_family_to_fonts():
families = {}
for item in fontlist:
family = item[1]
fontname = item[0]
if families.has_key(family):
families[family] = families[family] + (fontname,)
else:
families[family] = (fontname,)
return families
xlfd_template = "%s--%s-*-*-*-*-*-%s"
font_cache = SKCache()
class Font:
def __init__(self, name):
self.name = name
info = fontmap[name]
family, font_attrs, xlfd_start, encoding_name = info
self.family = family
self.font_attrs = font_attrs
self.xlfd_start = lower(xlfd_start)
self.encoding_name = encoding_name
self.metric, self.encoding = read_metric(self.PostScriptName())
self.outlines = None
self.ref_count = 0
font_cache[self.name] = self
def __del__(self):
if font_cache is not None and font_cache.has_key(self.name):
del font_cache[self.name]
def __repr__(self):
return "<Font %s>" % self.name
def GetXLFD(self, size_trafo):
if type(size_trafo) == TrafoType:
if size_trafo.m11 == size_trafo.m22 > 0\
and size_trafo.m12 == size_trafo.m21 == 0:
# a uniform scaling. Special case for better X11R5
# compatibility
return xlfd_template % (self.xlfd_start,
int(round(size_trafo.m11)),
self.encoding_name)
return xlfd_template % (self.xlfd_start, xlfd_matrix(size_trafo),
self.encoding_name)
return xlfd_template % (self.xlfd_start, int(round(size_trafo)),
self.encoding_name)
def PostScriptName(self):
return self.name
def TextBoundingBox(self, text, size):
# Return the bounding rectangle of TEXT when set in this font
# with a size of SIZE. The coordinates of the rectangle are
# relative to the origin of the first character.
llx, lly, urx, ury = self.metric.string_bbox(text)
size = size / 1000.0
return (llx * size, lly * size, urx * size, ury * size)
def TextCoordBox(self, text, size):
# Return the coord rectangle of TEXT when set in this font with
# a size of SIZE. The coordinates of the rectangle are relative
# to the origin of the first character.
metric = self.metric
width = metric.string_width(text)
size = size / 1000.0
return (0, metric.descender * size,
width * size, metric.ascender * size)
def TextCaretData(self, text, pos, size):
from math import tan,pi
size = size / 1000.0
x = self.metric.string_width(text, pos) * size
lly = self.metric.lly * size
ury = self.metric.ury * size
t = tan(self.metric.italic_angle * pi / 180.0);
up = ury - lly
return Point(x - t * lly, lly), Point(-t * up, up)
def TypesetText(self, text):
return self.metric.typeset_string(text)
def IsPrintable(self, char):
return self.encoding[ord(char)] != encoding.notdef
def GetOutline(self, char):
if self.outlines is None:
self.char_strings, self.cs_interp \
= read_outlines(self.PostScriptName())
self.outlines = {}
char_name = self.encoding[ord(char)]
outline = self.outlines.get(char_name)
if outline is None:
self.cs_interp.execute(self.char_strings[char_name])
outline = convert_outline(self.cs_interp.paths)
self.outlines[char_name] = outline
self.cs_interp.reset()
copy = []
for path in outline:
path = path.Duplicate()
copy.append(path)
return tuple(copy)
def FontFileName(self):
return font_file_name(self.PostScriptName())
_warned_about_font = {}
def GetFont(fontname):
if font_cache.has_key(fontname):
return font_cache[fontname]
if not fontmap.has_key(fontname):
if not _warned_about_font.get(fontname):
warn(USER, _("I can't find font %(fontname)s. "
"I'll use %(fallback)s instead"),
fontname = fontname,
fallback = config.preferences.fallback_font)
_warned_about_font[fontname] = 1
if fontname != config.preferences.fallback_font:
return GetFont(config.preferences.fallback_font)
raise ValueError, 'Cannot find font %s.' % fontname
return Font(fontname)
#
# Initialisation on import
#
Subscribe(const.INITIALIZE, read_font_dirs)
|