#! /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()


