#! /usr/bin/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. #SJH #default_foundry = 'adobe' default_foundry = 'unknown' 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': # print value 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 = fullname #SJH: was = a default instead # 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) #SJH # info.x_fontname_start = ('-%(foundry)s-%(x_family)s-%(weight)s-' # '%(slant)s-%(x_setwidth)s') % info.__dict__ info.x_fontname_start = ('-%(foundry)s-%(fontname)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 the name of the output file. This option is only valid if only one output format is chosen. -f 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()