# Copyright 1997-1998 by Corporation for National Research Initiatives.
# See the file LICENSE for details.

"""Object-oriented interface to the t1lib Type1 font rasterizer.

This is the interface that should actually be used; _t1lib is the actual
C extension which exposes the low-level interface.

Constants
---------

LOG_ERROR
    Log only serious errors.  This can include such events as memory
    allocation failures.

LOG_WARNING
    Log anything that might be useful in diagnosing misbehavior in a debugged
    application.  This can include such information as missing font metrics
    (AFM) files.

LOG_STATISTIC
    Various informational messages are included in the log; this can include
    memory consumption information.

LOG_DEBUG
    Logging level to support debugging.  All sorts of mess gets included in
    the log.  This should be used primarily when debugging t1lib or tracking
    really nasty memory problems.
"""

__version__ = "0.3"

# Note that the docstrings in this module are heavily stylized to allow
# document generation using a Python program I've been using for such things;
# see http://weyr.cnri.reston.va.us/~fdrake/progenv/ for more information.

import _t1lib
import string

from _t1lib import \
     LOG_ERROR, \
     LOG_WARNING, \
     LOG_STATISTIC, \
     LOG_DEBUG, \
     PFAB_PATH, \
     AFM_PATH, \
     ENC_PATH


def init(logging=0):
    """Initialize the library.

    logging -- flag to indicate whether logging should be enabled

    Use of this function is only necessary to enable logging.  In that case,
    this must be called before any other calls to this module have been made.
    """
    _t1lib.InitLib(logging)


def printLog(message, level=LOG_WARNING):
    """Add an entry to the t1lib.log file.

    message -- string containing the text to write out
    level -- severity level; one of LOG_ERROR, LOG_WARNING, LOG_STATISTIC,
	     or LOG_DEBUG

    The name of the calling function or method is determined automatically
    and cannot be supplied by the caller.
    """
    # This is a little evil, buts gets it right while reducing the clutter
    # in the caller.
    funcname = sys.exc_info()[2].tb_frame.f_back.f_code.co_name
    _t1lib.PrintLog(funcname, message, level)


def setLogLevel(level):
    """Set the level of severity at which log entries are written to the log.

    level -- severity level; one of LOG_ERROR, LOG_WARNING, LOG_STATISTIC,
	     or LOG_DEBUG
    """
    _t1lib.SetLogLevel(level)


def setDeviceResolutions(xres, yres):
    """Set the resolution of the output device in dots-per-inch.

    xres -- horizontal DPI
    yres -- vertical DPI
    """
    _t1lib.SetDeviceResolutions(xres, yres)


_init_gray = 0

def setAAGrayValues(white, gray75, gray50, gray25, black):
    """Set the values used for each level of gray for anti-aliasing.

    Each value is interpreted as an unsigned 32-bit integer.  Long integers
    may be used.
    """
    global _init_gray
    _t1lib.AASetGrayValues(white, gray75, gray50, gray25, black)
    _init_gray = 1


def _init_gray_values():
    global _init_gray_values
    if not _init_gray:
	setAAGrayValues(0xffffffff, 0xa8a8a8a8, 0x77777777, 0x38383838, 0)
    _init_gray_values = _noinit_gray_values

def _noinit_gray_values():
    # faster version to replace the original after initialization
    pass


class T1Error(Exception):
    # This is really a dummy class, so that the documentation can be
    # generated for the exception object.
    """Raised by primitive t1lib operations that fail.

    The string value describes what went wrong.
    """
    pass

# This is the real thing; we don't really want to mask this one,
# but keep the docstring around.
_t1lib.T1Error.__doc__ = T1Error.__doc__
T1Error = _t1lib.T1Error


class UnknownFontError(T1Error):
    """Raised when a requested fontname is not recognized.

    Public Members
    --------------

    fontname -- requested font name
    """
    def __init__(self, fontname):
	"""UNDOCUMENTED"""
	self.fontname = fontname
	T1Error.__init__(self, fontname)

class EncodingFormatError(T1Error):
    """Raised when a font encoding file has a formatting error."""
    pass


class _Unpackable:
    """Mixin class that allows objects to behave as sequences.

    This is mostly interesting to allow unpacking a BBox or MetricsInfo
    instance into the component values rather than offer multiple interfaces
    on the FontSetter class for each output treatment.
    """
    def __init__(self, stuff):
	self.__stuff = stuff

    def __getitem__(self, index):
	return self.__stuff[index]


class BBox(_Unpackable):
    """A bounding box.

    Public Members
    --------------

    llx -- lower-left x coordinate
    lly -- lower-left y coordinate
    urx -- upper-right x coordinate
    ury -- upper-right y coordinate

    All values are measured in character space units.
    """
    def __init__(self, bbox):
	self.llx, self.lly, self.urx, self.ury = bbox
	_Unpackable.__init__(self, bbox)

    def getHeight(self):
	"""Return the height of the region."""
	return self.ury - self.lly

    def getWidth(self):
	"""Return the width of the region."""
	return self.urx - self.llx


class MetricsInfo(_Unpackable):
    """Metrics information derived from AFM data.

    Public Members
    --------------

    width -- 
    bbox -- bounding box of the glyph which would be generated for string
    numchars -- number of characters in the string measured
    charpos -- vector of numchars integers; see below
    string -- the string measured

    All values are measured in character space units.

    The charpos vector contains values which represent the horizontal
    escapement of string[:i + 1], where i is an index into string.
    """
    def __init__(self, metrics, string):
	self.width, self.bbox, self.numchars, self.charpos = metrics
	self.string = string
	if type(self.bbox) is type(()):
	    self.bbox = BBox(self.bbox)
	metrics = self.width, self.bbox, self.numchars, self.charpos
	_Unpackable.__init__(self, metrics)


class Glyph:
    """A rasterized bitmap.

    Public Members
    --------------

    bits -- string representing the bitmap as generated by t1lib
    ascent -- number of pixels image rises above the baseline
    descent -- number of pixels image descends below the baseline
    leftSideBearing -- number of pixels between the origin of the glyph
		       and the leftmost bitmap pixel
    rightSideBearing -- number of pixels between the origin of the glyph
			and the rightmost bitmap pixel
    characterWidth -- width of the glyph, including character margins;
		      this is the amount of horizontal movement needed
		      before placing a subsequent bitmap
    bpp -- number of data bits per pixel
    """

    def __init__(self, bits, metrics, bpp):
	"""Initialize a Glyph instance.

	bits -- string containing the bitmaps as generated by t1lib
	metrics -- 5-tuple representing the metrics field of a t1lib
		   GLYPH structure
	bpp -- bits per pixel

	See the t1lib documentation for information on the format of the
	bits field.
	"""
	self.bits = bits
	self.bpp = bpp
	self.ascent, self.descent, \
                     self.leftSideBearing, self.rightSideBearing, \
                     self.advanceX, self.advanceY = metrics
        # backward compatibility
        self.characterWidth = self.advanceX

    def getHeight(self):
	"""Return the height of the bitmap.

	Character margin areas are not included.
	"""
	return self.ascent + self.descent

    def getWidth(self):
	"""Return the width of the bitmap.

	Character margin areas are not included.
	"""
	return self.rightSideBearing - self.leftSideBearing


_font_cache = {}

def getFont(fontname):
    """Return a font object for a named font.

    fontname -- PostScript name of the font to be located

    If the named font is not found, an UnknownFontError exception is
    raised.  The fontname value is case-sensitive.

    Font objects returned by this function will always refer to 'physical'
    fonts in t1lib terminology; logical fonts generated from the same
    physical base may exist.
    """
    if _font_cache.has_key(fontname):
	return _font_cache[fontname]
    # determine the FontID that is associated with this font:
    for id in range(_t1lib.Get_no_fonts()):
	try:
	    fname = _t1lib.GetFontName(id)
	except T1Error:
	    # attempt to set a character to get the font loaded
	    _t1lib.LoadFont(id)
	    fname = _t1lib.GetFontName(id)
	if fname == fontname:
	    font = T1Font(id, id, fontname)
	    _font_cache[fontname] = font
	    return font
    raise UnknownFontError(fontname)


class T1Font:
    """A font.

    Instances of this class should not be created explicitly in user code.
    """
    def __init__(self, id, phys_id, fontname, peers=None,
		 extension=0.0, slant=0.0, encoding=None):
	"""UNDOCUMENTED"""
	self.id = id
	self.phys_id = phys_id
	self.fontname = fontname
	if peers is None:
	    peers = []
	self.__peers = peers
	self.__extension = extension
	self.__slant = slant
	self.__encoding = encoding

    def __repr__(self):
	"""UNDOCUMENTED"""
	return "<%s.%s for %s (id=%s)>" \
	       % (self.__module__, self.__class__.__name__,
		  self.fontname, self.id)

    def isLogical(self):
	"""Return true iff this font is a t1lib 'logical' font."""
	return self.phys_id != self.id

    def isPhysical(self):
	"""Return true iff this font is a t1lib 'physical' font."""
	return self.phys_id == self.id

    def getLogicalPeers(self):
	"""Return sequence of all logical fonts generated from the physical
	font associated with this font.
	"""
	return tuple(self.__peers)

    def getPhysicalFont(self):
	"""Return the t1lib 'physical' font associated with this font."""
	if self.id == self.phys_id:
	    return self
	return getFont(self.fontname)

    def getCharName(self, char):
	"""Return the PostScript name of a character in the current encoding.

	char -- character for which the name is desired
	"""
	return _t1lib.GetCharName(self.id, char)

    def getEncodingIndex(self, charname):
	"""Return the index of a character in the current encoding.

	charname -- PostScript character name

	If the character is not present in the current encoding, a T1Error
	exception is raised.
	"""
	return _t1lib.GetEncodingIndex(self.id, charname)

    def getLigatures(self, char):
	return _t1lib.QueryLigs(self.id, char)

    def getKerning(self, char1, char2):
	"""Return the kerning for a character pair.

	The value returned is measured in character space units.  If no
	AFM data is available, 0 is returned.
	"""
	return _t1lib.GetKerning(self.id, char1, char2)

    def getEncoding(self):
	"""Return the current encoding vector."""
	if self.__encoding is None:
	    self.__encoding = tuple(
		map(_t1lib.GetCharName, [self.id] * 256, map(chr, range(256))))
	return self.__encoding

    def setEncoding(self, encoding=None):
	"""Change the encoding vector.

	encoding -- new encoding vector

	If the new encoding vector is different from the old vector, the
	bitmap cache is cleared.
	"""
	if encoding != self.__encoding:
	    self.reset()
	    _t1lib.ReencodeFont(self.id, encoding)
	    self.__encoding = encoding

    def getExtension(self):
	"""Return the current extension factor for the font."""
	return self.__extension

    def setExtension(self, extend):
	"""Change the extension of the font.

	extend -- the new extension factor

	The new slant replaces the old extension; it is not a modifier.
	If the new extension factor is different from the old factor, the
	bitmap cache is cleared.
	"""
	if extend != self.__entension:
	    self.reset()
	    _t1lib.ExtendFont(self.id, extend)
	    self.__extension = extend

    def getSlant(self):
	"""Return the current slant factor for the font."""
	return self.__slant

    def setSlant(self, slant):
	"""Change the slant of the font.

	slant -- the new slant factor

	The new slant replaces the old slant; it is not a modifier.  If
	the new slant factor is different from the old facter, the bitmap
	cache is cleared.
	"""
	if slant != self.__slant:
	    self.reset()
	    _t1lib.SlantFont(self.id, slant)
	    self.__slant = slant

    def copy(self):
	"""Return a new logical font based on the same physical font as
	this font.
	"""
	# If this is a logical font, things may look a little different
	# than they do for the physical font.  Update these attributes
	# in the new font object.
	if self.id != self.phys_id:
	    font = getFont(self.fontname).copy()
	    font.setEncoding(self.getEncoding())
	    font.setExtension(self.getExtension())
	    font.setSlant(self.getSlant())
	else:
	    id = _t1lib.CopyFont(self.phys_id)
	    font = T1Font(id, self.phys_id, self.fontname, self.__peers,
			  self.__extension, self.__slant, self.__encoding)
	    self.__peers.append(font)
	return font

    def reset(self):
	"""Delete all cached bitmaps for this font."""
	_t1lib.DeleteAllSizes(self.id)

    def delete(self):
	"""Attempt to free all memory used by this font.

	If this is a physical font still referenced by any logical fonts,
	a T1Error exception will be raised.
	"""
	# not recommended...
	_t1lib.DeleteFont(self.id)
	if self.id == self.phys_id:
	    del _font_cache[self.fontname]
	else:
	    self.__peers.remove(self)

    def getInfo(self):
	"""Retrieve a FontInfo instance for this font."""
	return FontInfo(self)


class FontInfo:
    """Font information for a specific font.

    Public Members
    --------------

    bbox -- bounding box for the font
    familyName -- font family name.  This may be shared among roman, italic,
		  or other varieties of a font.
    fontName -- PostScript name of the font
    fullName -- full name of the font, for human consumption
    isFixedPitch -- true if the font is monospaced
    italicAngle -- angle off of vertical for italic fonts
    notice -- copyright notice
    underlinePosition -- recommended underlining position raltive to the
			 baseline, in character space units
    underlineThickness -- recommended underlining thickness, in character
			  space units
    version -- version string from a Type 1 font
    weight -- string describing the font weight
    """
    def __init__(self, font):
	id = font.id
	self.bbox = BBox(_t1lib.GetFontBBox(id))
	self.extension = font.getExtension()
	self.familyName = _t1lib.GetFamilyName(id)
	self.fontName = self.fontname = font.fontname
	self.fullName = _t1lib.GetFullName(id)
	self.isFixedPitch = _t1lib.GetIsFixedPitch(id)
	self.italicAngle = _t1lib.GetItalicAngle(id)
	self.notice = _t1lib.GetNotice(id)
	self.slant = font.getSlant()
	self.underlinePosition = _t1lib.GetUnderlinePosition(id)
	self.underlineThickness = _t1lib.GetUnderlineThickness(id)
	self.version = _t1lib.GetVersion(id)
	self.weight = _t1lib.GetWeight(id)


class FontSetter:
    def __init__(self, font, size, angle=0.0, spaceoff=0.0, kerning=0, bpp=1):
	"""Initialize a FontSetter instance.

	font -- T1Font instance
	size -- point size of the rasterized characters
	angle -- rotation angle of the rasterized image (measured
		 counter-clockwise)
	spaceoff -- 
	kerning -- flag indicating whether pairwise kerning should be used
	bpp -- bits-per-pixel for the generated rasterizations
	"""
	self.font = font
	self.size = float(size)
	self.angle = float(angle)
	self.spaceoff = float(spaceoff)
	self.kerning = kerning and 1 or 0
	if bpp not in (1, 8, 16, 24, 32):
	    raise ValueError, "illegal bit depth"
	self.bpp = bpp

    def __repr__(self):
	"""UNDOCUMENTED"""
	s = "<%s.%s for %s (id=%s) at %spt" % \
	    (self.__module__, self.__class__.__name__, self.font.fontname,
	     self.font.id, self.size)
	if self.kerning:
	    s = s + ", kerned"
	# how to indicate non-zero spaceoff?
	return s + ">"

    def setChar(self, char):
	"""Rasterize a single character.

	char -- the character to rasterize

	Returns a Glyph instance.
	"""
	if self.bpp == 1:
	    stuff = _t1lib.SetChar(self.font.id, char, self.size, self.angle)
	else:
	    _init_gray_values()
	    _setAADepth(self.bpp)
	    stuff = _t1lib.AASetChar(self.font.id, char, self.size, self.angle)
	bits, metrics, bpp = stuff
	return self.newGlyph(bits, metrics, bpp)

    def setString(self, string):
	"""Rasterize a string.

	string -- the string to rasterize

	Returns a Glyph instance.
	"""
	if self.bpp == 1:
	    stuff = _t1lib.SetString(self.font.id, string, self.spaceoff,
				     self.kerning, self.size, self.angle)
	else:
	    _init_gray_values()
	    _setAADepth(self.bpp)
	    stuff = _t1lib.AASetString(self.font.id, string, self.spaceoff,
				       self.kerning, self.size, self.angle)
	bits, metrics, bpp = stuff
	return self.newGlyph(bits, metrics, bpp)

    def getMetrics(self, string):
	"""Return metrics information for a string.

	string -- the string to compute metrics information for.
	"""
	stuff = _t1lib.GetMetricsInfo(self.font.id, string,
				      self.spaceoff, self.kerning)
	return MetricsInfo(stuff,string)

    def getWidth(self, string):
	"""Return the width of a string in character space units.

	string -- the string to measure
	"""
	return _t1lib.GetStringWidth(self.font.id, string,
				     self.spaceoff, self.kerning)

    def getBBox(self, string):
	"""Return the bounding box for a string.

	string -- string to compute the bounding box for

	Returns a BBox instance.
	"""
	stuff = _t1lib.GetStringBBox(self.font.id, string,
				     self.spaceoff, self.kerning)
	return BBox(stuff)

    def setBitsPerPixel(self, bpp):
	"""Set the number of bits per pixel for the resulting glyphs.

	bpp -- new bit depth

	If bpp is not 1, 8, 16, 24, or 32, ValueError is raised.
	"""
	if bpp == self.bpp:
	    return
	if bpp not in (1, 8, 16, 24, 32):
	    raise ValueError, "illegal bit depth"
	self.bpp = bpp

    def newGlyph(self, bits, metrics, bpp):
	"""Return a glyph object for the specified information.

	bits -- string representing the raw bitmap generated by t1lib
	metrics --
	bpp -- bits-per-pixel for the bitmap
	"""
	return Glyph(bits, metrics, bpp)

    def resetSize(self):
	"""Clear bitmap cache in underlying t1lib."""
	_t1lib.DeleteSize(self.font.id, self.size)


_prev_aa_depth = 0

def _setAADepth(bpp):
    global _prev_aa_depth
    if _prev_aa_depth != bpp:
	_t1lib.AASetBitsPerPixel(bpp)
	_prev_aa_depth = bpp


def loadEncoding(fp_or_filename):
    """Return an encoding vector read from a file.

    fp_or_filename -- either a file-like object or the name of a disk file

    If a string is passed in, it will be used as a filename and passed to
    open(), otherwise fp_or_filename will be treated as any file-like
    object and assumed to have a readline() method.
    """
    if type(fp_or_filename) is type(''):
	fp = open(fp_or_filename)
    else:
	fp = fp_or_filename
    encoding = []
    started = 0
    while len(encoding) < 256:
	line = fp.readline()
	if not line:
	    raise EncodingFormatError(
		"EOF reached before encoding was defined")
	if started:
	    stuff = string.split(line)
	    if not stuff:
		raise EncodingFormatError(
		    "encountered line without character name in encoding")
	    encoding.append(stuff[0])
	elif len(line) > 9:
	    if string.lower(line[:9]) == "encoding=" \
	       and line[9] in string.whitespace:
		started = 1
    return tuple(encoding)


class SearchPath:
    __type_map = {
	_t1lib.AFM_PATH: 'afm',
	_t1lib.PFAB_PATH: 'font',
	_t1lib.ENC_PATH: 'encoding',
	}
    def __init__(self, which):
	try:
	    self.__type = self.__type_map[which]
	except KeyError:
	    raise ValueError, "illegal t1lib path type"
	self.__which = which

    def append(self, entry):
	_t1lib.AddToFileSearchPath(self.__which, _t1lib.APPEND_PATH, entry)

    def insert(self, index, entry):
	list = self.__get()
	list.insert(index, entry)
	self.__set(list)

    def count(self, entry):
	return self.__get().count(entry)

    def index(self, entry):
	return self.__get().index(entry)

    def remove(self, entry):
	list = self.__get()
	list.remove(entry)
	self.__set(list)

    def __len__(self):
	return len(self.__get())

    def __getitem__(self, index):
	return self.__get()[index]

    def __setitem__(self, index, entry):
	list = self.__get()
	list[index] = entry
	self.__set(list)

    def __delitem__(self, index):
	list = self.__get()
	del list[index]
	self.__set(list)

    def __getslice__(self, start, stop):
	return self.__get()[start:stop]

    def __setslice__(self, start, stop, entries):
	list = self.__get()
	list[start:stop] = entries
	self.__set(list)

    def __delslice__(self, start, stop):
	list = self.__get()
	del list[start:stop]
	self.__set(list)

    def __repr__(self):
	return "<t1lib.SearchPath for %s files>" % self.__type

    # These methods don't really make sense, but we'll implement them for
    # completeness.

    def reverse(self):
	pass

    def sort(self):
	pass

    # These are internal only.

    def __get(self):
	return string.split(_t1lib.GetFileSearchPath(self.__which), ':')

    def __set(self, list):
	v = string.join(list, ':')
	print "new path =", `v`
	_t1lib.SetFileSearchPath(self.__which, v)

pfab_path = SearchPath(_t1lib.PFAB_PATH)
afm_path = SearchPath(_t1lib.AFM_PATH)
enc_path = SearchPath(_t1lib.ENC_PATH)
