#! /usr/bin/env python
# mkfontdb.py - Generate 'font databases' from Type 1 AFM-files.
# Copyright (C) 1997, 1998, 1999, 2000 by Bernhard Herzog
#
# This program 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 of the License, or (at
# your option) any later version.
#
# This program 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
# Script to extract information from afm-files and to create `font
# databases' for Sketch and X.
#
# X manages a font database (which basically maps font file names to X
# font names) in fonts.dir files in each directory in the font path. For
# Type1 fonts (usually in the Type1 directory) this file is a copy of
# fonts.scale in the same directory. The fonts.dir files are generated
# by mkfontdir(1) while the fonts.scale file is meant to be edited by
# the system administrator. This script can create the entries for that
# file from the afm files.
#
# An X font name (X Logical Font Description, XLFD) for scalable fonts
# has the form:
#
# -foundry-family-weight-slant-setwidth--0-0-0-0-spacing-0-char-set
#
# where the fields have the following meanings (simplifying a bit):
#
# foundry: The manufacturer of the font. (e.g. adobe)
#
# family: The font family. (e.g. times)
#
# weight: The `boldness'. This can not only be medium and bold
# but also light, demibold or black, for instance.
# Synonyms for medium are normal or regular.
#
# slant: Either r (roman), i (italic) or o (oblique).
#
# setwidth: A name for average width. Often just normal.
# Other setwidths are for instance narrow or extended.
#
# spacing: Whether the font is monospace (m) (i.e. all characters
# have the same width) or proportional (p)
#
# char-set: How character codes are mapped to glyphs.
# Often iso8859-1. Another example is adobe-fontspecific
#
# Part of this information (family, weight and spacing) can be read
# directly from the afm file. The other fields have to be deduced from
# the information available. This script uses the following strategy:
#
#
# foundry
#
# The foundry is often mentioned in a copyright note. Try to find the
# name of a known foundry in the note and use that foundry. The known
# foundries are listed in the variable foundries.
#
# family, weight, spacing
#
# This information is listed directly in the afm file. Just translate
# IsFixedPitch to m or p. There can be some problems with the family
# name, though. See below.
#
# slant
#
# The afm file has a field ItalicAngle. If this angle is 0, we assume a
# roman font. Otherwise it can be italic or oblique. To decide which to
# use, we search the FullName for the word `Oblique'. If we find it we
# assume oblique, italic otherwise.
#
# setwidth
#
# This is not mentioned in the afm file. It is usually available in the
# FullName, so we search it for common setwidth names. If we find
# something we use that, otherwise we assume normal. The known setwidths
# are listed in the variable setwidths. They are possibly preceded by a
# modifier from the list modifiers.
#
# char-set
#
# The afm file has a field EncodingScheme. If this is FontSpecific, we
# use adobe-fontspecific for the XLFD, iso8859-1 otherwise.
#
# The XFree86 servers I have used so far don't seem to accept char-sets
# like e.g. bitstream-fontspecific so we have to use adobe here.
#
# Problems
#
# Many of the fields in an afm-file are optional. This script tries to
# use reasonable default values. Fortunately, many of the optional
# fields are provided by the afm-files I tested this with.
#
# Another problem when generating x font names is that some fonts have
# somewhat unusual attribute names. The bitstream font
# ShelleyAndanteBT-Regular, for instance, is the variant Andante from
# the Shelley family. Other variants of this family are Allegro and
# Volante. How do we fit this into x's scheme where variants involve
# just weight, slant and setwidth?
#
# This script tries to form the x family name by removing those parts of
# the full font name that describe weight, slant and setwidth.
# Whatever's left is taken as the family name if it is longer than the
# family name indicated in the afm file. The family name of
# ShelleyAndanteBT-Regular becomes "Shelley Andante", for instance.
#
#
# Sketch
#
# Sketch's font directories (.sfd files) contain similar information,
# which has to be extracted from the afm files. This script can create
# sfd files.
#
__version__ = "1.3"
import sys, os
import glob, string, re
from string import atof,lower,find,split,join,strip,translate
# Most of these lists are far from complete.
foundries = ['adobe', 'bitstream', 'urw', 'softmaker', 'letraset', 'corel',
'monotype']
slants = ['Italic', 'Oblique', 'Roman']
weights = ['Medium', 'Normal', 'Bold', 'Black', 'Light']
setwidths = ['Extended', 'Condensed', 'Narrow', 'Compressed']
modifiers = ['Ultra', 'Extra', 'Semi', 'Demi']
# create a dict with the words that sould be removed from the full name
# to get the x family name. See 'Problems' above.
remove_words = {}
for word in setwidths + weights + slants + modifiers:
remove_words[word] = 1
rx_oblique = re.compile('Oblique')
rx_setwidth = re.compile('((' + join(modifiers, '|') + ')[ \t]+)?'
'(' + join(setwidths, '|') + ')')
trans_minus = string.maketrans('-', ' ')
_alphanum = string.letters + string.digits + ' '
def makealnum(s):
return filter(lambda c: c in _alphanum, s)
def _str(val):
return strip(val)
def _bool(s):
return strip(s) == 'true'
converters = {
'FontName': _str,
'FullName': _str,
'FamilyName': _str,
'Weight': _str,
'EncodingScheme': _str,
'Comment': _str,
'Notice': _str,
'ItalicAngle': atof,
'IsFixedPitch': _bool,
'StartCharMetrics': None,
'EndFontMetrics': None
}
class FontInfo:
def __init__(self, filename):
self.filename = filename
self.basename = os.path.splitext(os.path.split(filename)[1])[0]
self.foundry = default_foundry
self.family = ''
self.x_family = ''
self.weight = 'normal'
self.slant = 'r'
self.spacing = 'm'
self.encoding = 'iso8859-1'
self.fontname = ''
debug_print_fullname = 0
verbosity = 0
guess_family_from_fullname = 1
# the default foundry used if it can't be determined from the afm file.
# can be set via the command line.
default_foundry = 'adobe'
def read_afm_file(afm_file):
# read the afm file AFM_FILE and return a tuple containing the info needed
# to make a fonts.scale or sfd entry.
file = open(afm_file)
info = FontInfo(afm_file)
slant = 0
fullname = ''
lines = file.readlines()
while lines:
line = lines[0]
del lines[0]
temp = split(line, None, 1)
if not temp:
continue
if len(temp) == 1:
key = temp[0]
value = ''
else:
key, value = temp
try:
action = converters[key]
except KeyError:
continue
if action:
value = action(value)
if key == 'FamilyName':
info.family = value
elif key == 'FontName':
info.fontname = value
elif key == 'FullName':
fullname = value
if debug_print_fullname:
print fullname,
elif key == 'Weight':
info.weight = lower(value)
remove_words[value] = 1
elif key == 'ItalicAngle':
slant = value != 0.0
elif key == 'IsFixedPitch':
if value:
info.spacing = 'm'
else:
info.spacing = 'p'
elif key == 'EncodingScheme':
if value == 'FontSpecific':
info.encoding = 'adobe-fontspecific'
else:
# this should be probably configurable
info.encoding = 'iso8859-1'
elif key == 'Comment' or key == 'Notice':
value = lower(value)
if value[:9] == 'copyright':
for name in foundries:
if find(value, name) != -1:
info.foundry = name
else:
# EndFontMetrics or StartCharMetrics
break
file.close()
# translate the slant to roman, italic or oblique
if slant:
if rx_oblique.search(fullname):
info.slant = 'o'
else:
info.slant = 'i'
if info.weight == 'roman':
info.weight = 'medium'
# try to find the setwidth
match = rx_setwidth.search(fullname)
if match:
info.x_setwidth = lower(match.group(0))
else:
info.x_setwidth = 'normal'
# The font attributes displayed in Sketch's font dialog is
# everything in the full name of the font apart of the family name
if info.family == fullname[:len(info.family)]:
info.sfd_attrs = strip(translate(fullname[len(info.family):],
trans_minus))
if not info.sfd_attrs:
info.sfd_attrs = 'Roman'
else:
sys.stderr.write("%s: fullname doesn't start with family\n"
% afm_file)
info.sfd_attrs = 'Roman'
# The family name used in the X font names shouldn't contain any
# funny characters, especially no '-'.
# See 'Problems' above for this special case
parts = split(translate(fullname, trans_minus))
parts = filter(lambda n: not remove_words.has_key(n), parts)
fullname = join(parts)
if guess_family_from_fullname and len(fullname) > len(info.family):
info.x_family = makealnum(fullname)
else:
info.x_family = makealnum(info.family)
info.x_fontname_start = ('-%(foundry)s-%(x_family)s-%(weight)s-'
'%(slant)s-%(x_setwidth)s') % info.__dict__
return info
def write_sfd_line(file, info):
file.write('%(fontname)s,%(family)s,%(sfd_attrs)s,%(x_fontname_start)s,'
'%(encoding)s,%(basename)s\n' % info.__dict__)
def write_fonts_scale_header(file, files):
file.write("%d\n" % len(files))
def write_fonts_scale_line(file, info):
fontname = '--0-0-0-0-%(spacing)s-0-%(encoding)s' % info.__dict__
pfb = info.basename + '.pfb'
file.write('%s %s%s\n' % (pfb, info.x_fontname_start, fontname))
def write_fontmap_line(file, info):
file.write('/%(fontname)s\t(%(basename)s.pfb)\t;\n' % info.__dict__)
def process_files(dir, files, handlers):
for file in files:
if verbosity > 0:
print file
info = read_afm_file(os.path.join(dir, file))
for file, write_line in handlers:
write_line(file, info)
def print_debug_infos(dir, files):
global debug_print_fullname
debug_print_fullname = 1
for file in files:
print file,
info = read_afm_file(os.path.join(dir, file))
print ';\t', info.fontname, info.family, info.weight, \
info.slant, info.x_setwidth
SFD = 'sfd'
FONTSSCALE = 'fonts.scale'
FONTMAP = 'Fontmap'
format_handlers = {
SFD: (write_sfd_line, None),
FONTSSCALE: (write_fonts_scale_line, write_fonts_scale_header),
FONTMAP: (write_fontmap_line, None)
}
format_filenames = {
SFD: 'std.sfd',
FONTSSCALE: 'fonts.scale',
FONTMAP: 'Fontmap',
}
def main():
# set defaults of options
output_formats = []
outfilename = ''
dir = '.'
debug = 0
import getopt
try:
opts, args = getopt.getopt(sys.argv[1:], 'df:gho:svx')
except getopt.error:
print_usage()
return
for optchar, value in opts:
if optchar == '-h':
print_usage()
return
elif optchar == '-d':
debug = 1
elif optchar == '-v':
global verbosity
verbosity = 1
elif optchar == '-s':
output_formats.append(SFD)
elif optchar == '-x':
output_formats.append(FONTSSCALE)
elif optchar == '-g':
output_formats.append(FONTMAP)
elif optchar == '-o':
outfilename = value
elif optchar == '-f':
global default_foundry
default_foundry = lower(value)
if args and len(args) > 1:
files = args
else:
if args:
dir = args[0]
if os.path.isdir(dir):
files = glob.glob(os.path.join(dir, '*.afm'))
else:
files = args
dir = '.'
if debug:
print_debug_infos(dir, files)
else:
if len(output_formats) == 0:
output_formats = [FONTSSCALE]
if len(output_formats) == 1 and outfilename:
format_filenames[output_formats[0]] = outfilename
for i in range(len(output_formats)):
file = open(format_filenames[output_formats[i]], 'w')
write_line, write_header = format_handlers[output_formats[i]]
if write_header:
write_header(file, files)
output_formats[i] = (file, write_line)
process_files(dir, files, handlers = output_formats)
usage_msg = """\
usage:
%(scriptname)s [-h] [-d] [-f] [-v] [-s] [-x] [-o file] [dir | file1 file2...]
Read afm files for Type 1 fonts and produce one or more `font database'
files. Currently three formats are supported: X11 fonts.scale (the
default), Ghostscript Fontmap and Sketch font directories (.sfd-files).
The program can generate files for several formats at once.
Arguments can be either a directory or .afm-files. If no arguments other
than options are given, assume '.' as argument.
Options:
-h print this help message (and do nothing else)
-d (debug) print some info on the files. Do nothing else.
-x Create a fonts.scale file for X
Default filename is %(default_x)s
-s Create a Sketch font directory.
Default filename is %(default_sfd)s
-g Create a Fontmap file for ghostscript
Default filename is %(default_gs)s
-o <file> the name of the output file. This option is only valid
if only one output format is chosen.
-f <foundry> Foundry used if it can't be determined from the afm file.
Default: %(default_foundry)s
-v verbose. print each filename as it is read
If neither -x nor -s nor -g is given, the output format defaults to -x.
If more than one of -x, -s or -g is given, the -o option is ignored.
"""
def print_usage():
scriptname = os.path.basename(sys.argv[0])
print usage_msg % {'scriptname': scriptname,
'default_sfd': format_filenames[SFD],
'default_x': format_filenames[FONTSSCALE],
'default_gs': format_filenames[FONTMAP],
'default_foundry': default_foundry}
if __name__ == '__main__':
main()
|