mathtext patch

This is a long one.

The problem with the last patch I submitted was a no brainer.
Instead of:
math_parse_s_ft2font_ps = math_parse_s_ft2font_common('PS')
should have been:
math_parse_s_ps = math_parse_s_ft2font_common('PS')

I am now in the process of creating a UnicodeFonts class, and a
derived BakomaUnicodeFonts, which is a hack to add "unicode support"
to the
Bakoma fonts. Is BakomaUnicodeFonts worth of implementing?

I must say that I had a lot of problems in reading/understanding the code
of the current Fonts classes, especially regarding the ambiguous usage
of the word "font". I also tried to make a common parent class in order to
abstract the common properties of the BakomaTrueTypeFonts and
BakomaPSFonts classes (something along the way of what I did with the
math_parse_s_ft2font_common class), but I hit the wall - there was a lot
of black magic under the hood, IMHO.

So I decided to make naming changes in the new Unicode aware classes,
and that's the purpose of writing this email before I got the new classes to
work. So please John, do comment on this, because I don't know whether
I'm breaking something, and whether the names are chosen appropriately.

The changes I made, make no effect on the current code - that is, everything
still works. I'm sending you a patch against the SVN tree. I also added
the automatically genarated tex2uni etc. files, and they are now incorporated
in the patch.

What should we do with the script that generates those file. Should I try to
integrate it into the setup/build process?

These are the new imports:

from matplotlib.tex2uni import tex2uni
from matplotlib.type12uni import type12uni

These are the new top-level functions I defined in mathtext:

def font_open(filename):
    ext = filename.rsplit('.',1)[1]
    if ext == 'afm':
        return AFM(filename)
    else:
        return FT2Font(filename)

def get_unicode_index(symbol):
    """get_unicode_index(symbol) -> integer

Return the integer index from the Unicode table of an Unicode symbol
"""
    try:# This will succeed if symbol is a single unicode char
        return ord(symbol)
    except TypeError:
        pass
    try:# Is symbol a TeX symbol (i.e. \alpha)
        return tex2uni[symbol]
    except KeyError:
        pass
    try:# Is symbol a Type1 name (i.e. degree)? If not raise error
        return type12uni[symbol]
    except KeyError:
        message = """'%(symbol)s' is not a valid Unicode character or
TeX/Type1 symbol"""%locals()
        raise ValueError, message

And finally, these are the new classes:

svn.patch (103 KB)

···

====
class UnicodeFonts(Fonts):
    """An abstract base class for handling Unicode fonts.

Specific terminology:
* fontface: an FT2Font object, corresponding to a facename
* facename: a string that defines the (type)face's name - 'rm', 'it' etc.
* filename: a string that is used for generating a fontface object
* symbol*: a single Unicode character or a TeX command,
    or to be precise, a TeX symbol command like \alpha (but not \frac) or
    even a Type1/PS name
* filenamesdict: a dict that maps the face's name to the filename:
    filenamesdict = { 'cal' : 'fontnamecal.ext',
                  'rm' : 'fontnamerm.ext',
                  'tt' : 'fontnamett.ext',
                  'it' : 'fontnameit.ext',
                  None : 'fontnamesmth.ext'}
    filenamesdict should be declared as a class atribute
* glyphdict: a dict used for caching of glyph specific data
* fontsdict: a dict of facename -> fontface pairs
* charmaps: a dict of facename -> charmap pairs
* glyphmaps: a dict of facename -> glyphmap pairs. A glyphmap is an
    inverted charmap
* output: a string in ['BMP','SVG','PS'], coresponding to the backends
* index: Fontfile specific index of a glyph/char. Taken from a charmap.

"""

    # The path to the dir with the fontfiles
    basepath = get_data_path()

    def __init__(self, output='BMP'):
        if output:
            self.output = output
        # dict self.glyphdict[key] = facename, metrics, glyph, offset
        self.glyphdict = {}

        self.fontsdict = dict(
            [ (facename, font_open(os.path.join(self.basepath,
                filenamesdict[facename]))) for facename in
                                                self.facenames])
        # a dict of glyphindex -> charcode pairs
        self.charmaps = dict(
            [ (facename, self.fontsdict[facename].get_charmap())\
                for facename in self.facenames])
        # a dict of charcode -> glyphindex pairs
        self.glyphmaps = {}
        for facename in self.facenames:
            charmap = self.charmaps[facename]
            self.glyphmaps[facename] = dict([(charcode, glyphind)
                for glyphind, charcode in charmap.items()])
        for fontface in self.fontsdict.values():
            fontface.clear()
        if self.output == 'SVG':
            # a list of "glyphs" we need to render this thing in SVG
            self.svg_glyphs=[]

    def set_canvas_size(self, w, h, pswriter=None):
        'Dimension the drawing canvas; may be a noop'
        # self.width = int(w)
        # self.height = int(h)
        # I don't know why this was different than the PS version
        self.width = w
        self.height = h
        if pswriter:
            self.pswriter = pswriter
        else:
            for fontface in self.fontsdict.values():
                fontface.set_bitmap_size(int(w), int(h))

    def render(self, ox, oy, facename, symbol, fontsize, dpi):
        filename = self.filenamesdict[facename]
        metrics, glyph, offset = self._get_info(facename, symbol, fontsize,
                                                                dpi)
        index = self._get_unicode_index(symbol)
        if self.output == 'SVG':
            oy += offset - 512/2048.*10.
            self.svg_glyphs.append((filename, fontsize, index, ox, oy,
                                                            metrics))
        elif self.output == 'PS':
            # This should be changed to check for math mode or smth.
            #if filename == 'cmex10.ttf':
            # oy += offset - 512/2048.*10.
            psfontname = self.fontsdict[facename].postscript_name
            ps = """/%(psfontname)s findfont
%(fontsize)s scalefont
setfont
%(ox)f %(oy)f moveto
/%(glyphname)s glyphshow
""" % locals()
            self.pswriter.write(ps)
        else: # BMP
            fontface = self.fontsdict[facename]
            fontface.draw_glyph_to_bitmap(
            int(ox), int(self.height - oy - metrics.ymax), glyph)

    def get_metrics(self, facename, symbol, fontsize, dpi):
        metrics, glyph, offset = \
                self._get_info(facename, symbol, fontsize, dpi)
        return metrics

    def _get_unicode_index(self, symbol):
        return get_unicode_index(symbol)

    # Methods that must be overridden
    def _get_info(self, facename, symbol, fontsize, dpi):
        raise NotImplementedError('Derived must override')

class BakomaUnicodeFonts(UnicodeFonts):
    """A class that simulates Unicode support in the BaKoMa fonts"""

    filenamesdict = { 'cal' : 'cmsy10.ttf',
                'rm' : 'cmr10.ttf',
                'tt' : 'cmtt10.ttf',
                'it' : 'cmmi10.ttf',
                }

    def _get_info(self, facename, symbol, fontsize, dpi):
        'load the facename, metrics and glyph'
        #print hex(index), symbol, filename, facename
        key = facename, symbol, fontsize, dpi
        tup = self.glyphdict.get(key)
        if tup is not None:
            return tup

        filename = self.fontmap[facename]
        # This is black magic to me (Edin)
        if self.output == 'PS':
            if filename not in bakoma_fonts:
                bakoma_fonts.append(filename)
        fontface = self.fontsdict[facename]
        fontface.set_size(fontsize, dpi)
        head = fontface.get_sfnt_table('head')
        index = self._get_index(symbol)
        glyph = fontface.load_char(index)
        xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox]
        # This is black magic to me (Edin)
        if filename == 'cmex10.ttf':
            if self.output == 'PS':
                offset = -(head['yMin']+512)/head['unitsPerEm']*10.
            else:
                offset = glyph.height/64.0/2 + 256.0/64.0*dpi/72.0
        else:
            offset = 0.
        metrics = Bunch(
            advance = glyph.linearHoriAdvance/65536.0,
            height = glyph.height/64.0,
            width = glyph.width/64.0,
            xmin = xmin,
            xmax = xmax,
            ymin = ymin+offset,
            ymax = ymax+offset,
            )
        self.glyphdict[key] = metrics, glyph, offset
        return self.glyphdict[key]

    # We override the UnicodeFonts method
    def _get_unicode_index(self, symbol):
        pass

Cheers,
Edin

At last, some real progress!

I don't have a fresh SVN co at home (and I have a dial up), but
tomorrow morning I'll send a patch against the SVN.

I fixed a bit the UnicodeFonts class so now the raster (Agg) and SVG
outputs work. SVG needs a little more fixing.

I basicaly defined a new class, based on the Arial Unicode MS font.
class ArialUnicodeFonts(UnicodeFonts):
    basepath = r'C:\WINDOWS\Fonts'
    facefiledict = { 'cal' : 'KUNSTLER.TTF',
                'rm' : 'times.ttf',
                'tt' : 'cour.ttf',
                'it' : 'ARIALUNI.TTF',
                None : 'ARIALUNI.TTF',
                }

...and just that! Everything else is inherited from the UnicodeFonts class.

Then I ran the mathtext_demo.py. The results are attached.

mathtext_demo.png

mathtext_demo.svg

···

On 6/25/06, Edin Salković <edin.salkovic@...149...> wrote:

This is a long one.

I got the SVG backend to work properly. The PS backend seems to be
very tightly integrated with the current implementation of the Fonts
classes so I haven't realy digged into it yet.

This patch only adds four new files for converting TeX/Type1 <->
Unicode, and the modified mathtext.py file.

Feel free to setup the class MyUnicodeFonts with your Unicode math
font(s), and then uncomment the coresponding lines in the
math_parse_s_ft2font_common class to actually enable the
MyUnicodeFonts class to do something useful.

Cheers
Edin

mathtext.patch (90.9 KB)

···

On 6/26/06, Edin Salković <edin.salkovic@...149...> wrote:

At last, some real progress!

I don't have a fresh SVN co at home (and I have a dial up), but
tomorrow morning I'll send a patch against the SVN.

I fixed a bit the UnicodeFonts class so now the raster (Agg) and SVG
outputs work. SVG needs a little more fixing.

I basicaly defined a new class, based on the Arial Unicode MS font.
class ArialUnicodeFonts(UnicodeFonts):
    basepath = r'C:\WINDOWS\Fonts'
    facefiledict = { 'cal' : 'KUNSTLER.TTF',
                'rm' : 'times.ttf',
                'tt' : 'cour.ttf',
                'it' : 'ARIALUNI.TTF',
                None : 'ARIALUNI.TTF',
                }

...and just that! Everything else is inherited from the UnicodeFonts class.

Then I ran the mathtext_demo.py. The results are attached.

On 6/25/06, Edin Salković <edin.salkovic@...149...> wrote:
> This is a long one.