# -*- coding: ISO-8859-1 -*-
#
#
# Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
# Copyright (C) 2003-2004,2006,2007 Michael Schindler <m-schindler@users.sourceforge.net>
# Copyright (C) 2002-2006 André Wobst <wobsta@users.sourceforge.net>
#
# This file is part of PyX (http://pyx.sourceforge.net/).
#
# PyX 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.
#
# PyX 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 PyX; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

import cStringIO, exceptions, re, struct, string, sys, warnings, math
import unit, epsfile, bbox, canvas, color, trafo, path, pykpathsea, type1font


class binfile:

    def __init__(self, filename, mode="r"):
        self.file = open(filename, mode)

    def close(self):
        self.file.close()

    def tell(self):
        return self.file.tell()

    def eof(self):
        return self.file.eof()

    def read(self, bytes):
        return self.file.read(bytes)

    def readint(self, bytes=4, signed=0):
        first = 1
        result = 0
        while bytes:
            value = ord(self.file.read(1))
            if first and signed and value > 127:
                value -= 256
            first = 0
            result = 256 * result + value
            bytes -= 1
        return result

    def readint32(self):
        return struct.unpack(">l", self.file.read(4))[0]

    def readuint32(self):
        return struct.unpack(">L", self.file.read(4))[0]

    def readint24(self):
        # XXX: checkme
        return struct.unpack(">l", "\0"+self.file.read(3))[0]

    def readuint24(self):
        # XXX: checkme
        return struct.unpack(">L", "\0"+self.file.read(3))[0]

    def readint16(self):
        return struct.unpack(">h", self.file.read(2))[0]

    def readuint16(self):
        return struct.unpack(">H", self.file.read(2))[0]

    def readchar(self):
        return struct.unpack("b", self.file.read(1))[0]

    def readuchar(self):
        return struct.unpack("B", self.file.read(1))[0]

    def readstring(self, bytes):
        l = self.readuchar()
        assert l <= bytes-1, "inconsistency in file: string too long"
        return self.file.read(bytes-1)[:l]

class stringbinfile(binfile):

    def __init__(self, s):
        self.file = cStringIO.StringIO(s)




##############################################################################
# TFM file handling
##############################################################################

class TFMError(exceptions.Exception): pass


class char_info_word:
    def __init__(self, word):
        self.width_index  = int((word & 0xFF000000L) >> 24) #make sign-safe
        self.height_index = (word & 0x00F00000) >> 20
        self.depth_index  = (word & 0x000F0000) >> 16
        self.italic_index = (word & 0x0000FC00) >> 10
        self.tag          = (word & 0x00000300) >> 8
        self.remainder    = (word & 0x000000FF)


class tfmfile:
    def __init__(self, name, debug=0):
        self.file = binfile(name, "rb")

        #
        # read pre header
        #

        self.lf = self.file.readint16()
        self.lh = self.file.readint16()
        self.bc = self.file.readint16()
        self.ec = self.file.readint16()
        self.nw = self.file.readint16()
        self.nh = self.file.readint16()
        self.nd = self.file.readint16()
        self.ni = self.file.readint16()
        self.nl = self.file.readint16()
        self.nk = self.file.readint16()
        self.ne = self.file.readint16()
        self.np = self.file.readint16()

        if not (self.bc-1 <= self.ec <= 255 and
                self.ne <= 256 and
                self.lf == 6+self.lh+(self.ec-self.bc+1)+self.nw+self.nh+self.nd
                +self.ni+self.nl+self.nk+self.ne+self.np):
            raise TFMError, "error in TFM pre-header"

        if debug:
            print "lh=%d" % self.lh

        #
        # read header
        #

        self.checksum = self.file.readint32()
        self.designsize = self.file.readint32()
        assert self.designsize > 0, "invald design size"
        if self.lh > 2:
            assert self.lh > 11, "inconsistency in TFM file: incomplete field"
            self.charcoding = self.file.readstring(40)
        else:
            self.charcoding = None

        if self.lh > 12:
            assert self.lh > 16, "inconsistency in TFM file: incomplete field"
            self.fontfamily = self.file.readstring(20)
        else:
            self.fontfamily = None

        if debug:
            print "(FAMILY %s)" % self.fontfamily
            print "(CODINGSCHEME %s)" % self.charcoding
            print "(DESINGSIZE R %f)" % 16.0*self.designsize/16777216L

        if self.lh > 17:
            self.sevenbitsave = self.file.readuchar()
            # ignore the following two bytes
            self.file.readint16()
            facechar = self.file.readuchar()
            # decode ugly face specification into the Knuth suggested string
            if facechar < 18:
                if facechar >= 12:
                    self.face = "E"
                    facechar -= 12
                elif facechar >= 6:
                    self.face = "C"
                    facechar -= 6
                else:
                    self.face = "R"

                if facechar >= 4:
                    self.face = "L" + self.face
                    facechar -= 4
                elif facechar >= 2:
                    self.face = "B" + self.face
                    facechar -= 2
                else:
                    self.face = "M" + self.face

                if facechar == 1:
                    self.face = self.face[0] + "I" + self.face[1]
                else:
                    self.face = self.face[0] + "R" + self.face[1]

            else:
                self.face = None
        else:
            self.sevenbitsave = self.face = None

        if self.lh > 18:
            # just ignore the rest
            print self.file.read((self.lh-18)*4)

        #
        # read char_info
        #

        self.char_info = [None]*(self.ec+1)
        for charcode in range(self.bc, self.ec+1):
            self.char_info[charcode] = char_info_word(self.file.readint32())
            if self.char_info[charcode].width_index == 0:
                # disable character if width_index is zero
                self.char_info[charcode] = None

        #
        # read widths
        #

        self.width = [None for width_index in range(self.nw)]
        for width_index in range(self.nw):
            self.width[width_index] = self.file.readint32()

        #
        # read heights
        #

        self.height = [None for height_index in range(self.nh)]
        for height_index in range(self.nh):
            self.height[height_index] = self.file.readint32()

        #
        # read depths
        #

        self.depth = [None for depth_index in range(self.nd)]
        for depth_index in range(self.nd):
            self.depth[depth_index] = self.file.readint32()

        #
        # read italic
        #

        self.italic = [None for italic_index in range(self.ni)]
        for italic_index in range(self.ni):
            self.italic[italic_index] = self.file.readint32()

        #
        # read lig_kern
        #

        # XXX decode to lig_kern_command

        self.lig_kern = [None for lig_kern_index in range(self.nl)]
        for lig_kern_index in range(self.nl):
            self.lig_kern[lig_kern_index] = self.file.readint32()

        #
        # read kern
        #

        self.kern = [None for kern_index in range(self.nk)]
        for kern_index in range(self.nk):
            self.kern[kern_index] = self.file.readint32()

        #
        # read exten
        #

        # XXX decode to extensible_recipe

        self.exten = [None for exten_index in range(self.ne)]
        for exten_index in range(self.ne):
            self.exten[exten_index] = self.file.readint32()

        #
        # read param
        #

        # XXX decode

        self.param = [None for param_index in range(self.np)]
        for param_index in range(self.np):
            self.param[param_index] = self.file.readint32()

        self.file.close()



##############################################################################
# Font handling
##############################################################################

#
# PostScript font selection and output primitives
#

class UnsupportedFontFormat(Exception):
    pass

class UnsupportedPSFragment(Exception):
    pass

class fontmapping:

    tokenpattern = re.compile(r'"(.*?)("\s+|"$|$)|(.*?)(\s+|$)')

    def __init__(self, s):
        """ construct font mapping from line s of font mapping file """
        self.texname = self.basepsname = self.fontfile = None

        # standard encoding
        self.encodingfile = None

        # supported postscript fragments occuring in psfonts.map
        self.reencodefont = self.extendfont = self.slantfont = None

        tokens = []
        while len(s):
            match = self.tokenpattern.match(s)
            if match:
                if match.groups()[0] is not None:
                    tokens.append('"%s"' % match.groups()[0])
                else:
                    tokens.append(match.groups()[2])
                s = s[match.end():]
            else:
                raise RuntimeError("Cannot tokenize string '%s'" % s)

        for token in tokens:
            if token.startswith("<"):
                if token.startswith("<<"):
                    # XXX: support non-partial download here
                    self.fontfile = token[2:]
                elif token.startswith("<["):
                    self.encodingfile = token[2:]
                elif token.endswith(".pfa") or token.endswith(".pfb"):
                    self.fontfile = token[1:]
                elif token.endswith(".enc"):
                    self.encodingfile = token[1:]
                elif token.endswith(".ttf"):
                    raise UnsupportedFontFormat("TrueType font")
                else:
                    raise RuntimeError("Unknown token '%s'" % token)
            elif token.startswith('"'):
                pscode = token[1:-1].split()
                # parse standard postscript code fragments
                while pscode:
                    try:
                        arg, cmd = pscode[:2]
                    except:
                        raise UnsupportedPSFragment("Unsupported Postscript fragment '%s'" % pscode)
                    pscode = pscode[2:]
                    if cmd == "ReEncodeFont":
                        self.reencodefont = arg
                    elif cmd == "ExtendFont":
                        self.extendfont = arg
                    elif cmd == "SlantFont":
                        self.slantfont = arg
                    else:
                        raise UnsupportedPSFragment("Unsupported Postscript fragment '%s %s'" % (arg, cmd))
            else:
                if self.texname is None:
                    self.texname = token
                else:
                    self.basepsname = token
        if self.basepsname is None:
            self.basepsname = self.texname

    def __str__(self):
        return ("'%s' is '%s' read from '%s' encoded as '%s'" %
                (self.texname, self.basepsname, self.fontfile, repr(self.encodingfile)))

# generate fontmap

def readfontmap(filenames):
    """ read font map from filename (without path) """
    fontmap = {}
    for filename in filenames:
        mappath = pykpathsea.find_file(filename, pykpathsea.kpse_fontmap_format)
        # try also the oft-used registration as dvips config file
        if not mappath:
            mappath = pykpathsea.find_file(filename, pykpathsea.kpse_dvips_config_format)
        if not mappath:
            raise RuntimeError("cannot find font mapping file '%s'" % filename)
        mapfile = open(mappath, "rU")
        lineno = 0
        for line in mapfile.readlines():
            lineno += 1
            line = line.rstrip()
            if not (line=="" or line[0] in (" ", "%", "*", ";" , "#")):
                try:
                    fm = fontmapping(line)
                except (RuntimeError, UnsupportedPSFragment), e:
                    warnings.warn("Ignoring line %i in mapping file '%s': %s" % (lineno, mappath, e))
                except UnsupportedFontFormat, e:
                    pass
                else:
                    fontmap[fm.texname] = fm
        mapfile.close()
    return fontmap


class font:

    def __init__(self, name, c, q, d, tfmconv, pyxconv, fontmap, debug=0):
        self.name = name
        self.q = q                  # desired size of font (fix_word) in TeX points
        self.d = d                  # design size of font (fix_word) in TeX points
        self.tfmconv = tfmconv      # conversion factor from tfm units to dvi units
        self.pyxconv = pyxconv      # conversion factor from dvi units to PostScript points
        self.fontmap = fontmap
        tfmpath = pykpathsea.find_file("%s.tfm" % self.name, pykpathsea.kpse_tfm_format)
        if not tfmpath:
            raise TFMError("cannot find %s.tfm" % self.name)
        self.tfmfile = tfmfile(tfmpath, debug)

        # We only check for equality of font checksums if none of them
        # is zero. The case c == 0 happend in some VF files and
        # according to the VFtoVP documentation, paragraph 40, a check
        # is only performed if tfmfile.checksum > 0. Anyhow, being
        # more generous here seems to be reasonable
        if self.tfmfile.checksum != c and self.tfmfile.checksum != 0 and c != 0:
            raise DVIError("check sums do not agree: %d vs. %d" %
                           (self.tfmfile.checksum, c))

        # Check whether the given design size matches the one defined in the tfm file
        if abs(self.tfmfile.designsize - d) > 2:
            raise DVIError("design sizes do not agree: %d vs. %d" % (self.tfmfile.designsize, d))
        #if q < 0 or q > 134217728:
        #    raise DVIError("font '%s' not loaded: bad scale" % self.name)
        if d < 0 or d > 134217728:
            raise DVIError("font '%s' not loaded: bad design size" % self.name)

        self.scale = 1.0*q/d

    def fontinfo(self):
        class fontinfo:
            pass

        # The following code is a very crude way to obtain the information
        # required for the PDF font descritor. (TODO: The correct way would
        # be to read the information from the AFM file.)
        fontinfo = fontinfo()
        try:
            fontinfo.fontbbox = (0,
                                 -self.getdepth_ds(ord("y")),
                                 self.getwidth_ds(ord("W")),
                                 self.getheight_ds(ord("H")))
        except:
            fontinfo.fontbbox = (0, -10, 100, 100)
        try:
            fontinfo.italicangle = -180/math.pi*math.atan(self.tfmfile.param[0]/65536.0)
        except IndexError:
            fontinfo.italicangle = 0
        fontinfo.ascent = fontinfo.fontbbox[3]
        fontinfo.descent = fontinfo.fontbbox[1]
        try:
            fontinfo.capheight = self.getheight_ds(ord("h"))
        except:
            fontinfo.capheight = 100
        try:
            fontinfo.vstem = self.getwidth_ds(ord("."))/3
        except:
            fontinfo.vstem = 5
        return fontinfo

    def __str__(self):
        return "font %s designed at %g TeX pts used at %g TeX pts" % (self.name, 
                                                                      16.0*self.d/16777216L,
                                                                      16.0*self.q/16777216L)
    __repr__ = __str__

    def getsize_pt(self):
        """ return size of font in (PS) points """
        # The factor 16L/16777216L=2**(-20) converts a fix_word (here self.q)
        # to the corresponding float. Furthermore, we have to convert from TeX
        # points to points, hence the factor 72/72.27.
        return 16L*self.q/16777216L*72/72.27

    def _convert_tfm_to_dvi(self, length):
        # doing the integer math with long integers will lead to different roundings
        # return 16*length*int(round(self.q*self.tfmconv))/16777216

        # Knuth instead suggests the following algorithm based on 4 byte integer logic only
        # z = int(round(self.q*self.tfmconv))
        # b0, b1, b2, b3 = [ord(c) for c in struct.pack(">L", length)]
        # assert b0 == 0 or b0 == 255
        # shift = 4
        # while z >= 8388608:
        #     z >>= 1
        #     shift -= 1
        # assert shift >= 0
        # result = ( ( ( ( ( b3 * z ) >> 8 ) + ( b2 * z ) ) >> 8 ) + ( b1 * z ) ) >> shift
        # if b0 == 255:
        #     result = result - (z << (8-shift))

        # however, we can simplify this using a single long integer multiplication,
        # but take into account the transformation of z
        z = int(round(self.q*self.tfmconv))
        assert -16777216 <= length < 16777216 # -(1 << 24) <= length < (1 << 24)
        assert z < 134217728 # 1 << 27
        shift = 20 # 1 << 20
        while z >= 8388608: # 1 << 23
            z >>= 1
            shift -= 1
        # length*z is a long integer, but the result will be a regular integer
        return int(length*long(z) >> shift)

    def _convert_tfm_to_ds(self, length):
        return (16*long(round(length*float(self.q)*self.tfmconv))/16777216) * self.pyxconv * 1000 / self.getsize_pt()
    
    def _convert_tfm_to_pt(self, length):
        return (16*long(round(length*float(self.q)*self.tfmconv))/16777216) * self.pyxconv

    # routines returning lengths as integers in dvi units

    def getwidth_dvi(self, charcode):
        return self._convert_tfm_to_dvi(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])

    def getheight_dvi(self, charcode):
        return self._convert_tfm_to_dvi(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])

    def getdepth_dvi(self, charcode):
        return self._convert_tfm_to_dvi(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])

    def getitalic_dvi(self, charcode):
        return self._convert_tfm_to_dvi(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])

    # routines returning lengths as integers in design size (AFM) units 

    def getwidth_ds(self, charcode):
        return self._convert_tfm_to_ds(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])

    def getheight_ds(self, charcode):
        return self._convert_tfm_to_ds(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])

    def getdepth_ds(self, charcode):
        return self._convert_tfm_to_ds(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])

    def getitalic_ds(self, charcode):
        return self._convert_tfm_to_ds(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])

    # routines returning lengths as floats in PostScript points

    def getwidth_pt(self, charcode):
        return self._convert_tfm_to_pt(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])

    def getheight_pt(self, charcode):
        return self._convert_tfm_to_pt(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])

    def getdepth_pt(self, charcode):
        return self._convert_tfm_to_pt(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])

    def getitalic_pt(self, charcode):
        return self._convert_tfm_to_pt(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])


class virtualfont(font):
    def __init__(self, name, c, q, d, tfmconv, pyxconv, fontmap, debug=0):
        fontpath = pykpathsea.find_file(name, pykpathsea.kpse_vf_format)
        if fontpath is None or not len(fontpath):
            raise RuntimeError
        font.__init__(self, name, c, q, d, tfmconv, pyxconv, fontmap, debug)
        self.vffile = vffile(fontpath, self.scale, tfmconv, pyxconv, fontmap, debug > 1)

    def getfonts(self):
        """ return fonts used in virtual font itself """
        return self.vffile.getfonts()

    def getchar(self, cc):
        """ return dvi chunk corresponding to char code cc """
        return self.vffile.getchar(cc)


##############################################################################
# DVI file handling
##############################################################################

_DVI_CHARMIN     =   0 # typeset a character and move right (range min)
_DVI_CHARMAX     = 127 # typeset a character and move right (range max)
_DVI_SET1234     = 128 # typeset a character and move right
_DVI_SETRULE     = 132 # typeset a rule and move right
_DVI_PUT1234     = 133 # typeset a character
_DVI_PUTRULE     = 137 # typeset a rule
_DVI_NOP         = 138 # no operation
_DVI_BOP         = 139 # beginning of page
_DVI_EOP         = 140 # ending of page
_DVI_PUSH        = 141 # save the current positions (h, v, w, x, y, z)
_DVI_POP         = 142 # restore positions (h, v, w, x, y, z)
_DVI_RIGHT1234   = 143 # move right
_DVI_W0          = 147 # move right by w
_DVI_W1234       = 148 # move right and set w
_DVI_X0          = 152 # move right by x
_DVI_X1234       = 153 # move right and set x
_DVI_DOWN1234    = 157 # move down
_DVI_Y0          = 161 # move down by y
_DVI_Y1234       = 162 # move down and set y
_DVI_Z0          = 166 # move down by z
_DVI_Z1234       = 167 # move down and set z
_DVI_FNTNUMMIN   = 171 # set current font (range min)
_DVI_FNTNUMMAX   = 234 # set current font (range max)
_DVI_FNT1234     = 235 # set current font
_DVI_SPECIAL1234 = 239 # special (dvi extention)
_DVI_FNTDEF1234  = 243 # define the meaning of a font number
_DVI_PRE         = 247 # preamble
_DVI_POST        = 248 # postamble beginning
_DVI_POSTPOST    = 249 # postamble ending

_DVI_VERSION     = 2   # dvi version

# position variable indices
_POS_H           = 0
_POS_V           = 1
_POS_W           = 2
_POS_X           = 3
_POS_Y           = 4
_POS_Z           = 5

# reader states
_READ_PRE       = 1
_READ_NOPAGE    = 2
_READ_PAGE      = 3
_READ_POST      = 4 # XXX not used
_READ_POSTPOST  = 5 # XXX not used
_READ_DONE      = 6


class DVIError(exceptions.Exception): pass

# save and restore colors

class _savecolor(canvas.canvasitem):
    def processPS(self, file, writer, context, registry, bbox):
        file.write("currentcolor currentcolorspace\n")

    def processPDF(self, file, writer, context, registry, bbox):
        file.write("q\n")


class _restorecolor(canvas.canvasitem):
    def processPS(self, file, writer, context, registry, bbox):
        file.write("setcolorspace setcolor\n")

    def processPDF(self, file, writer, context, registry, bbox):
        file.write("Q\n")

class _savetrafo(canvas.canvasitem):
    def processPS(self, file, writer, context, registry, bbox):
        file.write("matrix currentmatrix\n")

    def processPDF(self, file, writer, context, registry, bbox):
        file.write("q\n")


class _restoretrafo(canvas.canvasitem):
    def processPS(self, file, writer, context, registry, bbox):
        file.write("setmatrix\n")

    def processPDF(self, file, writer, context, registry, bbox):
        file.write("Q\n")


class dvifile:

    def __init__(self, filename, fontmap, debug=0, debugfile=sys.stdout):
        """ opens the dvi file and reads the preamble """
        self.filename = filename
        self.fontmap = fontmap
        self.debug = debug
        self.debugfile = debugfile
        self.debugstack = []

        self.fonts = {}
        self.activefont = None

        # stack of fonts and fontscale currently used (used for VFs)
        self.fontstack = []
        self.stack = []

        # pointer to currently active page
        self.actpage = None

        # stack for self.file, self.fonts and self.stack, needed for VF inclusion
        self.statestack = []

        self.file = binfile(self.filename, "rb")

        # currently read byte in file (for debugging output)
        self.filepos = None

        self._read_pre()

    # helper routines

    def flushtext(self):
        """ finish currently active text object """
        if self.debug and self.activetext:
            self.debugfile.write("[%s]\n" % "".join([chr(char) for char in self.activetext.chars]))
        self.activetext = None

    def putrule(self, height, width, advancepos=1):
        self.flushtext()
        x1 =  self.pos[_POS_H] * self.pyxconv
        y1 = -self.pos[_POS_V] * self.pyxconv
        w = width * self.pyxconv
        h = height * self.pyxconv

        if height > 0 and width > 0:
            if self.debug:
                self.debugfile.write("%d: %srule height %d, width %d (???x??? pixels)\n" %
                                     (self.filepos, advancepos and "set" or "put", height, width))
            self.actpage.fill(path.rect_pt(x1, y1, w, h))
        else:
            if self.debug:
                self.debugfile.write("%d: %srule height %d, width %d (invisible)\n" %
                                     (self.filepos, advancepos and "set" or "put", height, width))

        if advancepos:
            if self.debug:
                self.debugfile.write(" h:=%d+%d=%d, hh:=???\n" %
                                     (self.pos[_POS_H], width, self.pos[_POS_H]+width))
            self.pos[_POS_H] += width

    def putchar(self, char, advancepos=1, id1234=0):
        dx = advancepos and self.activefont.getwidth_dvi(char) or 0

        if self.debug:
            self.debugfile.write("%d: %s%s%d h:=%d+%d=%d, hh:=???\n" %
                                 (self.filepos,
                                  advancepos and "set" or "put",
                                  id1234 and "%i " % id1234 or "char",
                                  char,
                                  self.pos[_POS_H], dx, self.pos[_POS_H]+dx))

        if isinstance(self.activefont, virtualfont):
            # virtual font handling
            afterpos = list(self.pos)
            afterpos[_POS_H] += dx
            self._push_dvistring(self.activefont.getchar(char), self.activefont.getfonts(), afterpos,
                                 self.activefont.getsize_pt())
        else:
            if self.activetext is None:
                if not self.fontmap.has_key(self.activefont.name):
                    raise RuntimeError("missing font information for '%s'; check fontmapping file(s)" % self.activefont.name)
                fontmapinfo = self.fontmap[self.activefont.name]

                encodingname = fontmapinfo.reencodefont
                if encodingname is not None:
                    encodingfilename = pykpathsea.find_file(fontmapinfo.encodingfile, pykpathsea.kpse_tex_ps_header_format)
                    if not encodingfilename:
                        raise RuntimeError("cannot find font encoding file %s" % fontmapinfo.encodingfile)
                    fontencoding = type1font.encoding(encodingname, encodingfilename)
                else:
                    fontencoding = None

                fontbasefontname = fontmapinfo.basepsname
                if fontmapinfo.fontfile is not None:
                    fontfilename = pykpathsea.find_file(fontmapinfo.fontfile, pykpathsea.kpse_type1_format)
                    if not fontfilename:
                        raise RuntimeError("cannot find type 1 font %s" % fontmapinfo.fontfile)
                else:
                    fontfilename = None

                fontslant = fontmapinfo.slantfont
                if fontslant is not None:
                    fontslant = float(fontslant)

                # XXX we currently misuse use self.activefont as metric 
                font = type1font.font(fontbasefontname, fontfilename, fontencoding, fontslant, self.activefont)

                self.activetext = type1font.text_pt(self.pos[_POS_H] * self.pyxconv, -self.pos[_POS_V] * self.pyxconv, font)
                self.actpage.insert(self.activetext)
            self.activetext.addchar(char)
            self.pos[_POS_H] += dx

        if not advancepos:
            self.flushtext()

    def usefont(self, fontnum, id1234=0):
        self.flushtext()
        self.activefont = self.fonts[fontnum]
        if self.debug:
            self.debugfile.write("%d: fnt%s%i current font is %s\n" %
                                 (self.filepos,
                                  id1234 and "%i " % id1234 or "num",
                                  fontnum,
                                  self.fonts[fontnum].name))


    def definefont(self, cmdnr, num, c, q, d, fontname):
        # cmdnr: type of fontdef command (only used for debugging output)
        # c:     checksum
        # q:     scaling factor (fix_word)
        #        Note that q is actually s in large parts of the documentation.
        # d:     design size (fix_word)

        try:
            afont = virtualfont(fontname, c, q/self.tfmconv, d/self.tfmconv, self.tfmconv, self.pyxconv, self.fontmap, self.debug > 1)
        except (TypeError, RuntimeError):
            afont = font(fontname, c, q/self.tfmconv, d/self.tfmconv, self.tfmconv, self.pyxconv, self.fontmap, self.debug > 1)

        self.fonts[num] = afont

        if self.debug:
            self.debugfile.write("%d: fntdef%d %i: %s\n" % (self.filepos, cmdnr, num, fontname))

#            scale = round((1000.0*self.conv*q)/(self.trueconv*d))
#            m = 1.0*q/d
#            scalestring = scale!=1000 and " scaled %d" % scale or ""
#            print ("Font %i: %s%s---loaded at size %d DVI units" %
#                   (num, fontname, scalestring, q))
#            if scale!=1000:
#                print " (this font is magnified %d%%)" % round(scale/10)

    def special(self, s):
        x =  self.pos[_POS_H] * self.pyxconv
        y = -self.pos[_POS_V] * self.pyxconv
        if self.debug:
            self.debugfile.write("%d: xxx '%s'\n" % (self.filepos, s))
        if not s.startswith("PyX:"):
            warnings.warn("ignoring special '%s'" % s)
            return

        # it is in general not safe to continue using the currently active font because
        # the specials may involve some gsave/grestore operations
        self.flushtext()

        command, args = s[4:].split()[0], s[4:].split()[1:]
        if command == "color_begin":
            if args[0] == "cmyk":
                c = color.cmyk(float(args[1]), float(args[2]), float(args[3]), float(args[4]))
            elif args[0] == "gray":
                c = color.gray(float(args[1]))
            elif args[0] == "hsb":
                c = color.hsb(float(args[1]), float(args[2]), float(args[3]))
            elif args[0] == "rgb":
                c = color.rgb(float(args[1]), float(args[2]), float(args[3]))
            elif args[0] == "RGB":
                c = color.rgb(int(args[1])/255.0, int(args[2])/255.0, int(args[3])/255.0)
            elif args[0] == "texnamed":
                try:
                    c = getattr(color.cmyk, args[1])
                except AttributeError:
                    raise RuntimeError("unknown TeX color '%s', aborting" % args[1])
            elif args[0] == "pyxcolor":
                # pyx.color.cmyk.PineGreen or
                # pyx.color.cmyk(0,0,0,0.0)
                pat = re.compile(r"(pyx\.)?(color\.)?(?P<model>(cmyk)|(rgb)|(grey)|(gray)|(hsb))[\.]?(?P<arg>.*)")
                sd = pat.match(" ".join(args[1:]))
                if sd:
                    sd = sd.groupdict()
                    if sd["arg"][0] == "(":
                        numpat = re.compile(r"[+-]?((\d+\.\d*)|(\d*\.\d+)|(\d+))([eE][+-]\d+)?")
                        arg = tuple([float(x[0]) for x in numpat.findall(sd["arg"])])
                        try:
                            c = getattr(color, sd["model"])(*arg)
                        except TypeError or AttributeError:
                            raise RuntimeError("cannot access PyX color '%s' in TeX, aborting" % " ".join(args[1:]))
                    else:
                        try:
                            c = getattr(getattr(color, sd["model"]), sd["arg"])
                        except AttributeError:
                            raise RuntimeError("cannot access PyX color '%s' in TeX, aborting" % " ".join(args[1:]))
                else:
                    raise RuntimeError("cannot access PyX color '%s' in TeX, aborting" % " ".join(args[1:]))
            else:
                raise RuntimeError("color model '%s' cannot be handled by PyX, aborting" % args[0])
            self.actpage.insert(_savecolor())
            self.actpage.insert(c)
        elif command == "color_end":
            self.actpage.insert(_restorecolor())
        elif command == "rotate_begin":
            self.actpage.insert(_savetrafo())
            self.actpage.insert(trafo.rotate_pt(float(args[0]), x, y))
        elif command == "rotate_end":
            self.actpage.insert(_restoretrafo())
        elif command == "scale_begin":
            self.actpage.insert(_savetrafo())
            self.actpage.insert(trafo.scale_pt(float(args[0]), float(args[1]), x, y))
        elif command == "scale_end":
            self.actpage.insert(_restoretrafo())
        elif command == "epsinclude":
            # parse arguments
            argdict = {}
            for arg in args:
                name, value = arg.split("=")
                argdict[name] = value

            # construct kwargs for epsfile constructor
            epskwargs = {}
            epskwargs["filename"] = argdict["file"]
            epskwargs["bbox"] = bbox.bbox_pt(float(argdict["llx"]), float(argdict["lly"]),
                                           float(argdict["urx"]), float(argdict["ury"]))
            if argdict.has_key("width"):
                epskwargs["width"] = float(argdict["width"]) * unit.t_pt
            if argdict.has_key("height"):
                epskwargs["height"] = float(argdict["height"]) * unit.t_pt
            if argdict.has_key("clip"):
               epskwargs["clip"] = int(argdict["clip"])
            self.actpage.insert(epsfile.epsfile(x * unit.t_pt, y * unit.t_pt, **epskwargs))
        elif command == "marker":
            if len(args) != 1:
                raise RuntimeError("marker contains spaces")
            for c in args[0]:
                if c not in string.digits + string.letters + "@":
                    raise RuntimeError("marker contains invalid characters")
            if self.actpage.markers.has_key(args[0]):
                raise RuntimeError("marker name occurred several times")
            self.actpage.markers[args[0]] = x * unit.t_pt, y * unit.t_pt
        else:
            raise RuntimeError("unknown PyX special '%s', aborting" % command)

    # routines for pushing and popping different dvi chunks on the reader

    def _push_dvistring(self, dvi, fonts, afterpos, fontsize):
        """ push dvi string with defined fonts on top of reader
        stack. Every positions gets scaled relatively by the factor
        scale. After the interpreting of the dvi chunk has been finished,
        continue with self.pos=afterpos. The designsize of the virtual
        font is passed as a fix_word

        """

        #if self.debug:
        #    self.debugfile.write("executing new dvi chunk\n")
        self.debugstack.append(self.debug)
        self.debug = 0

        self.statestack.append((self.file, self.fonts, self.activefont, afterpos, self.stack, self.pyxconv, self.tfmconv))

        # units in vf files are relative to the size of the font and given as fix_words
        # which can be converted to floats by diving by 2**20
        oldpyxconv = self.pyxconv
        self.pyxconv = fontsize/2**20
        rescale = self.pyxconv/oldpyxconv

        self.file = stringbinfile(dvi)
        self.fonts = fonts
        self.stack = []
        self.filepos = 0

        # rescale self.pos in order to be consistent with the new scaling
        self.pos = map(lambda x, rescale=rescale:1.0*x/rescale, self.pos)

        # since tfmconv converts from tfm units to dvi units, rescale it as well
        self.tfmconv /= rescale

        self.usefont(0)

    def _pop_dvistring(self):
        self.flushtext()
        #if self.debug:
        #    self.debugfile.write("finished executing dvi chunk\n")
        self.debug = self.debugstack.pop()

        self.file.close()
        self.file, self.fonts, self.activefont, self.pos, self.stack, self.pyxconv, self.tfmconv = self.statestack.pop()

    # routines corresponding to the different reader states of the dvi maschine

    def _read_pre(self):
        afile = self.file
        while 1:
            self.filepos = afile.tell()
            cmd = afile.readuchar()
            if cmd == _DVI_NOP:
                pass
            elif cmd == _DVI_PRE:
                if afile.readuchar() != _DVI_VERSION: raise DVIError
                num = afile.readuint32()
                den = afile.readuint32()
                self.mag = afile.readuint32()

                # For the interpretation of the lengths in dvi and tfm files, 
                # three conversion factors are relevant:
                # - self.tfmconv: tfm units -> dvi units
                # - self.pyxconv: dvi units -> (PostScript) points
                # - self.conv:    dvi units -> pixels
                self.tfmconv = (25400000.0/num)*(den/473628672.0)/16.0

                # calculate conv as described in the DVIType docu using 
                # a given resolution in dpi
                self.resolution = 300.0
                self.conv = (num/254000.0)*(self.resolution/den)

                # self.pyxconv is the conversion factor from the dvi units
                # to (PostScript) points. It consists of
                # - self.mag/1000.0:   magstep scaling
                # - self.conv:         conversion from dvi units to pixels
                # - 1/self.resolution: conversion from pixels to inch
                # - 72               : conversion from inch to points
                self.pyxconv = self.mag/1000.0*self.conv/self.resolution*72

                comment = afile.read(afile.readuchar())
                return
            else:
                raise DVIError

    def readpage(self, pageid=None):
        """ reads a page from the dvi file

        This routine reads a page from the dvi file which is
        returned as a canvas. When there is no page left in the
        dvifile, None is returned and the file is closed properly."""

        while 1:
            self.filepos = self.file.tell()
            cmd = self.file.readuchar()
            if cmd == _DVI_NOP:
                pass
            elif cmd == _DVI_BOP:
                ispageid = [self.file.readuint32() for i in range(10)]
                if pageid is not None and ispageid != pageid:
                    raise DVIError("invalid pageid")
                if self.debug:
                    self.debugfile.write("%d: beginning of page %i\n" % (self.filepos, ispageid[0]))
                self.file.readuint32()
                break
            elif cmd == _DVI_POST:
                self.file.close()
                return None # nothing left
            else:
                raise DVIError

        self.actpage = canvas.canvas()
        self.actpage.markers = {}
        self.pos = [0, 0, 0, 0, 0, 0]

        # currently active output: text instance currently used
        self.activetext = None

        while 1:
            afile = self.file
            self.filepos = afile.tell()
            try:
                cmd = afile.readuchar()
            except struct.error:
                # we most probably (if the dvi file is not corrupt) hit the end of a dvi chunk,
                # so we have to continue with the rest of the dvi file
                self._pop_dvistring()
                continue
            if cmd == _DVI_NOP:
                pass
            if cmd >= _DVI_CHARMIN and cmd <= _DVI_CHARMAX:
                self.putchar(cmd)
            elif cmd >= _DVI_SET1234 and cmd < _DVI_SET1234 + 4:
                self.putchar(afile.readint(cmd - _DVI_SET1234 + 1), id1234=cmd-_DVI_SET1234+1)
            elif cmd == _DVI_SETRULE:
                self.putrule(afile.readint32(), afile.readint32())
            elif cmd >= _DVI_PUT1234 and cmd < _DVI_PUT1234 + 4:
                self.putchar(afile.readint(cmd - _DVI_PUT1234 + 1), advancepos=0, id1234=cmd-_DVI_SET1234+1)
            elif cmd == _DVI_PUTRULE:
                self.putrule(afile.readint32(), afile.readint32(), 0)
            elif cmd == _DVI_EOP:
                self.flushtext()
                if self.debug:
                    self.debugfile.write("%d: eop\n \n" % self.filepos)
                return self.actpage
            elif cmd == _DVI_PUSH:
                self.stack.append(list(self.pos))
                if self.debug:
                    self.debugfile.write("%s: push\n"
                                         "level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=???,vv=???)\n" %
                                         ((self.filepos, len(self.stack)-1) + tuple(self.pos)))
            elif cmd == _DVI_POP:
                self.flushtext()
                self.pos = self.stack.pop()
                if self.debug:
                    self.debugfile.write("%s: pop\n"
                                         "level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=???,vv=???)\n" %
                                         ((self.filepos, len(self.stack)) + tuple(self.pos)))
            elif cmd >= _DVI_RIGHT1234 and cmd < _DVI_RIGHT1234 + 4:
                self.flushtext()
                dh = afile.readint(cmd - _DVI_RIGHT1234 + 1, 1)
                if self.debug:
                    self.debugfile.write("%d: right%d %d h:=%d%+d=%d, hh:=???\n" %
                                         (self.filepos,
                                          cmd - _DVI_RIGHT1234 + 1,
                                          dh,
                                          self.pos[_POS_H],
                                          dh,
                                          self.pos[_POS_H]+dh))
                self.pos[_POS_H] += dh
            elif cmd == _DVI_W0:
                self.flushtext()
                if self.debug:
                    self.debugfile.write("%d: w0 %d h:=%d%+d=%d, hh:=???\n" %
                                         (self.filepos,
                                          self.pos[_POS_W],
                                          self.pos[_POS_H],
                                          self.pos[_POS_W],
                                          self.pos[_POS_H]+self.pos[_POS_W]))
                self.pos[_POS_H] += self.pos[_POS_W]
            elif cmd >= _DVI_W1234 and cmd < _DVI_W1234 + 4:
                self.flushtext()
                self.pos[_POS_W] = afile.readint(cmd - _DVI_W1234 + 1, 1)
                if self.debug:
                    self.debugfile.write("%d: w%d %d h:=%d%+d=%d, hh:=???\n" %
                                         (self.filepos,
                                          cmd - _DVI_W1234 + 1,
                                          self.pos[_POS_W],
                                          self.pos[_POS_H],
                                          self.pos[_POS_W],
                                          self.pos[_POS_H]+self.pos[_POS_W]))
                self.pos[_POS_H] += self.pos[_POS_W]
            elif cmd == _DVI_X0:
                self.flushtext()
                if self.debug:
                    self.debugfile.write("%d: x0 %d h:=%d%+d=%d, hh:=???\n" %
                                         (self.filepos,
                                          self.pos[_POS_X],
                                          self.pos[_POS_H],
                                          self.pos[_POS_X],
                                          self.pos[_POS_H]+self.pos[_POS_X]))
                self.pos[_POS_H] += self.pos[_POS_X]
            elif cmd >= _DVI_X1234 and cmd < _DVI_X1234 + 4:
                self.flushtext()
                self.pos[_POS_X] = afile.readint(cmd - _DVI_X1234 + 1, 1)
                if self.debug:
                    self.debugfile.write("%d: x%d %d h:=%d%+d=%d, hh:=???\n" %
                                         (self.filepos,
                                          cmd - _DVI_X1234 + 1,
                                          self.pos[_POS_X],
                                          self.pos[_POS_H],
                                          self.pos[_POS_X],
                                          self.pos[_POS_H]+self.pos[_POS_X]))
                self.pos[_POS_H] += self.pos[_POS_X]
            elif cmd >= _DVI_DOWN1234 and cmd < _DVI_DOWN1234 + 4:
                self.flushtext()
                dv = afile.readint(cmd - _DVI_DOWN1234 + 1, 1)
                if self.debug:
                    self.debugfile.write("%d: down%d %d v:=%d%+d=%d, vv:=???\n" %
                                         (self.filepos,
                                          cmd - _DVI_DOWN1234 + 1,
                                          dv,
                                          self.pos[_POS_V],
                                          dv,
                                          self.pos[_POS_V]+dv))
                self.pos[_POS_V] += dv
            elif cmd == _DVI_Y0:
                self.flushtext()
                if self.debug:
                    self.debugfile.write("%d: y0 %d v:=%d%+d=%d, vv:=???\n" %
                                         (self.filepos,
                                          self.pos[_POS_Y],
                                          self.pos[_POS_V],
                                          self.pos[_POS_Y],
                                          self.pos[_POS_V]+self.pos[_POS_Y]))
                self.pos[_POS_V] += self.pos[_POS_Y]
            elif cmd >= _DVI_Y1234 and cmd < _DVI_Y1234 + 4:
                self.flushtext()
                self.pos[_POS_Y] = afile.readint(cmd - _DVI_Y1234 + 1, 1)
                if self.debug:
                    self.debugfile.write("%d: y%d %d v:=%d%+d=%d, vv:=???\n" %
                                         (self.filepos,
                                          cmd - _DVI_Y1234 + 1,
                                          self.pos[_POS_Y],
                                          self.pos[_POS_V],
                                          self.pos[_POS_Y],
                                          self.pos[_POS_V]+self.pos[_POS_Y]))
                self.pos[_POS_V] += self.pos[_POS_Y]
            elif cmd == _DVI_Z0:
                self.flushtext()
                if self.debug:
                    self.debugfile.write("%d: z0 %d v:=%d%+d=%d, vv:=???\n" %
                                         (self.filepos,
                                          self.pos[_POS_Z],
                                          self.pos[_POS_V],
                                          self.pos[_POS_Z],
                                          self.pos[_POS_V]+self.pos[_POS_Z]))
                self.pos[_POS_V] += self.pos[_POS_Z]
            elif cmd >= _DVI_Z1234 and cmd < _DVI_Z1234 + 4:
                self.flushtext()
                self.pos[_POS_Z] = afile.readint(cmd - _DVI_Z1234 + 1, 1)
                if self.debug:
                    self.debugfile.write("%d: z%d %d v:=%d%+d=%d, vv:=???\n" %
                                         (self.filepos,
                                          cmd - _DVI_Z1234 + 1,
                                          self.pos[_POS_Z],
                                          self.pos[_POS_V],
                                          self.pos[_POS_Z],
                                          self.pos[_POS_V]+self.pos[_POS_Z]))
                self.pos[_POS_V] += self.pos[_POS_Z]
            elif cmd >= _DVI_FNTNUMMIN and cmd <= _DVI_FNTNUMMAX:
                self.usefont(cmd - _DVI_FNTNUMMIN, 0)
            elif cmd >= _DVI_FNT1234 and cmd < _DVI_FNT1234 + 4:
                # note that according to the DVI docs, for four byte font numbers,
                # the font number is signed. Don't ask why!
                fntnum = afile.readint(cmd - _DVI_FNT1234 + 1, cmd == _DVI_FNT1234 + 3)
                self.usefont(fntnum, id1234=cmd-_DVI_FNT1234+1)
            elif cmd >= _DVI_SPECIAL1234 and cmd < _DVI_SPECIAL1234 + 4:
                self.special(afile.read(afile.readint(cmd - _DVI_SPECIAL1234 + 1)))
            elif cmd >= _DVI_FNTDEF1234 and cmd < _DVI_FNTDEF1234 + 4:
                if cmd == _DVI_FNTDEF1234:
                    num = afile.readuchar()
                elif cmd == _DVI_FNTDEF1234+1:
                    num = afile.readuint16()
                elif cmd == _DVI_FNTDEF1234+2:
                    num = afile.readuint24()
                elif cmd == _DVI_FNTDEF1234+3:
                    # Cool, here we have according to docu a signed int. Why?
                    num = afile.readint32()
                self.definefont(cmd-_DVI_FNTDEF1234+1,
                                num,
                                afile.readint32(),
                                afile.readint32(),
                                afile.readint32(),
                                afile.read(afile.readuchar()+afile.readuchar()))
            else:
                raise DVIError


##############################################################################
# VF file handling
##############################################################################

_VF_LONG_CHAR  = 242              # character packet (long version)
_VF_FNTDEF1234 = _DVI_FNTDEF1234  # font definition
_VF_PRE        = _DVI_PRE         # preamble
_VF_POST       = _DVI_POST        # postamble

_VF_ID         = 202              # VF id byte

class VFError(exceptions.Exception): pass

class vffile:
    def __init__(self, filename, scale, tfmconv, pyxconv, fontmap, debug=0):
        self.filename = filename
        self.scale = scale
        self.tfmconv = tfmconv
        self.pyxconv = pyxconv
        self.fontmap = fontmap
        self.debug = debug
        self.fonts = {}            # used fonts
        self.widths = {}           # widths of defined chars
        self.chardefs = {}         # dvi chunks for defined chars

        afile = binfile(self.filename, "rb")

        cmd = afile.readuchar()
        if cmd == _VF_PRE:
            if afile.readuchar() != _VF_ID: raise VFError
            comment = afile.read(afile.readuchar())
            self.cs = afile.readuint32()
            self.ds = afile.readuint32()
        else:
            raise VFError

        while 1:
            cmd = afile.readuchar()
            if cmd >= _VF_FNTDEF1234 and cmd < _VF_FNTDEF1234 + 4:
                # font definition
                if cmd == _VF_FNTDEF1234:
                    num = afile.readuchar()
                elif cmd == _VF_FNTDEF1234+1:
                    num = afile.readuint16()
                elif cmd == _VF_FNTDEF1234+2:
                    num = afile.readuint24()
                elif cmd == _VF_FNTDEF1234+3:
                    num = afile.readint32()
                c = afile.readint32()
                s = afile.readint32()     # relative scaling used for font (fix_word)
                d = afile.readint32()     # design size of font
                fontname = afile.read(afile.readuchar()+afile.readuchar())

                # rescaled size of font: s is relative to the scaling
                # of the virtual font itself.  Note that realscale has
                # to be a fix_word (like s)
                # XXX: check rounding
                reals = int(round(self.scale * (16*self.ds/16777216L) * s))

                # print ("defining font %s -- VF scale: %g, VF design size: %d, relative font size: %d => real size: %d" %
                #        (fontname, self.scale, self.ds, s, reals)
                #        )

                # XXX allow for virtual fonts here too
                self.fonts[num] =  font(fontname, c, reals, d, self.tfmconv, self.pyxconv, self.fontmap, self.debug > 1)
            elif cmd == _VF_LONG_CHAR:
                # character packet (long form)
                pl = afile.readuint32()   # packet length
                cc = afile.readuint32()   # char code (assumed unsigned, but anyhow only 0 <= cc < 255 is actually used)
                tfm = afile.readuint24()  # character width
                dvi = afile.read(pl)      # dvi code of character
                self.widths[cc] = tfm
                self.chardefs[cc] = dvi
            elif cmd < _VF_LONG_CHAR:
                # character packet (short form)
                cc = afile.readuchar()    # char code
                tfm = afile.readuint24()  # character width
                dvi = afile.read(cmd)
                self.widths[cc] = tfm
                self.chardefs[cc] = dvi
            elif cmd == _VF_POST:
                break
            else:
                raise VFError

        afile.close()

    def getfonts(self):
        return self.fonts

    def getchar(self, cc):
        return self.chardefs[cc]


