r""" OVERVIEW mathtext is a module for parsing TeX expressions and drawing them into a matplotlib.ft2font image buffer. You can draw from this buffer into your backend. A large set of the TeX symbols are provided (see below). Subscripting and superscripting are supported, as well as the over/under style of subscripting with \sum, \int, etc. The module uses pyparsing to parse the TeX expression, an so can handle fairly complex TeX expressions Eg, the following renders correctly s = r'$\cal{R}\prod_{i=\alpha\cal{B}}^\infty a_i\rm{sin}(2 \pi f x_i)$' The fonts \cal, \rm, \it, and \tt are allowed. The following accents are provided: \hat, \breve, \grave, \bar, \acute, \tilde, \vec, \dot, \ddot. All of them have the same syntax, eg to make an overbar you do \bar{o} or to make an o umlaut you do \ddot{o}. The shortcuts are also provided, eg: \"o \'e \`e \~n \.x \^y The spacing elements \ , \/ and \hspace{num} are provided. \/ inserts a small space, and \hspace{num} inserts a fraction of the current fontsize. Eg, if num=0.5 and the fontsize is 12.0, hspace{0.5} inserts 6 points of space If you find TeX expressions that don't parse or render properly, please email me, but please check KNOWN ISSUES below first. REQUIREMENTS mathtext requires matplotlib.ft2font. Set BUILD_FT2FONT=True in setup.py. See BACKENDS below for a summary of availability by backend. LICENSING: The computer modern fonts this package uses are part of the BaKoMa fonts, which are (now) free for commercial and noncommercial use and redistribution; see license/LICENSE_BAKOMA in the matplotlib src distribution for redistribution requirements. USAGE: See http://matplotlib.sourceforge.net/tutorial.html#mathtext for a tutorial introduction. Any text element (xlabel, ylabel, title, text, etc) can use TeX markup, as in xlabel(r'$\Delta_i$') ^ use raw strings The $ symbols must be the first and last symbols in the string. Eg, you cannot do r'My label $x_i$'. but you can change fonts, as in r'$\rm{My label} x_i$' to achieve the same effect. A large set of the TeX symbols are provided. Subscripting and superscripting are supported, as well as the over/under style of subscripting with \sum, \int, etc. Allowed TeX symbols: \/ \Delta \Downarrow \Gamma \Im \LEFTangle \LEFTbrace \LEFTbracket \LEFTparen \Lambda \Leftarrow \Leftbrace \Leftbracket \Leftparen \Leftrightarrow \Omega \P \Phi \Pi \Psi \RIGHTangle \RIGHTbrace \RIGHTbracket \RIGHTparen \Re \Rightarrow \Rightbrace \Rightbracket \Rightparen \S \SQRT \Sigma \Sqrt \Theta \Uparrow \Updownarrow \Upsilon \Vert \Xi \aleph \alpha \approx \angstrom \ast \asymp \backslash \beta \bigcap \bigcirc \bigcup \bigodot \bigoplus \bigotimes \bigtriangledown \bigtriangleup \biguplus \bigvee \bigwedge \bot \bullet \cap \cdot \chi \circ \clubsuit \coprod \cup \dag \dashv \ddag \delta \diamond \diamondsuit \div \downarrow \ell \emptyset \epsilon \equiv \eta \exists \flat \forall \frown \gamma \geq \gg \heartsuit \hspace \imath \in \infty \int \iota \jmath \kappa \lambda \langle \lbrace \lceil \leftangle \leftarrow \leftbrace \leftbracket \leftharpoondown \leftharpoonup \leftparen \leftrightarrow \leq \lfloor \ll \mid \mp \mu \nabla \natural \nearrow \neg \ni \nu \nwarrow \odot \oint \omega \ominus \oplus \oslash \otimes \phi \pi \pm \prec \preceq \prime \prod \propto \psi \rangle \rbrace \rceil \rfloor \rho \rightangle \rightarrow \rightbrace \rightbracket \rightharpoondown \rightharpoonup \rightparen \searrow \sharp \sigma \sim \simeq \slash \smile \spadesuit \sqcap \sqcup \sqrt \sqsubseteq \sqsupseteq \subset \subseteq \succ \succeq \sum \supset \supseteq \swarrow \tau \theta \times \top \triangleleft \triangleright \uparrow \updownarrow \uplus \upsilon \varepsilon \varphi \varphi \varrho \varsigma \vartheta \vdash \vee \vert \wedge \wp \wr \xi \zeta BACKENDS mathtext currently works with GTK, Agg, GTKAgg, TkAgg and WxAgg and PS, though only horizontal and vertical rotations are supported in *Agg mathtext now embeds the TrueType computer modern fonts into the PS file, so what you see on the screen should be what you get on paper. Backends which don't support mathtext will just render the TeX string as a literal. Stay tuned. KNOWN ISSUES: - nested subscripts, eg, x_i_j not working; but you can do x_{i_j} - nesting fonts changes in sub/superscript groups not parsing - I would also like to add a few more layout commands, like \frac. Author : John Hunter Copyright : John Hunter (2004,2005) License : matplotlib license (PSF compatible) """ from __future__ import division import os, sys from cStringIO import StringIO from matplotlib import verbose from matplotlib.pyparsing import Literal, Word, OneOrMore, ZeroOrMore, \ Combine, Group, Optional, Forward, NotAny, alphas, nums, alphanums, \ StringStart, StringEnd, ParseException, FollowedBy, Regex from matplotlib.afm import AFM from matplotlib.cbook import enumerate, iterable, Bunch from matplotlib.ft2font import FT2Font from matplotlib.font_manager import fontManager, FontProperties from matplotlib._mathtext_data import latex_to_bakoma, cmkern, \ latex_to_standard, tex2uni, type12uni, tex2type1, uni2type1 from matplotlib.numerix import absolute from matplotlib import get_data_path, rcParams bakoma_fonts = [] # symbols that have the sub and superscripts over/under overunder = { r'\sum' : 1, r'\int' : 1, r'\prod' : 1, r'\coprod' : 1, } # a character over another character charOverChars = { # The first 2 entires in the tuple are (font, char, sizescale) for # the two symbols under and over. The third entry is the space # between the two symbols in points r'\angstrom' : ( ('rm', 'A', 1.0), (None, '\circ', 0.5), 0.0 ), } def font_open(filename): ext = filename.rsplit('.',1)[1] if ext == 'afm': return AFM(str(filename)) else: return FT2Font(str(filename)) def get_unicode_index(symbol): """get_unicode_index(symbol) -> integer Return the integer index (from the Unicode table) of symbol. symbol can be a single unicode character, a TeX command (i.e. r'\pi'), or a Type1 symbol name (i.e. 'phi'). """ 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.strip("\\")] 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 #Not used, but might turn useful def get_type1_name(symbol): """get_type1_name(symbol) -> string Returns the the Type1 name of symbol. symbol can be a single unicode character, or a TeX command (i.e. r'\pi'). """ try:# This will succeed if symbol is a single unicode char return uni2type1[ord(symbol)] except TypeError: pass try:# Is symbol a TeX symbol (i.e. \alpha) return tex2type1[symbol.strip("\\")] except KeyError: pass # The symbol is already a Type1 name so return it if isinstance(symbol, str): return symbol else: # The user did not suply a valid symbol, show usage raise ValueError, get_type1_name.__doc__ class Fonts: """ An abstract base class for fonts that want to render mathtext The class must be able to take symbol keys and font file names and return the character metrics as well as do the drawing """ def get_kern(self, facename, symleft, symright, fontsize, dpi): """ Get the kerning distance for font between symleft and symright. facename is one of tt, it, rm, cal or None sym is a single symbol(alphanum, punct) or a special symbol like \sigma. """ return 0 def get_metrics(self, facename, sym, fontsize, dpi): """ facename is one of tt, it, rm, cal or None sym is a single symbol(alphanum, punct) or a special symbol like \sigma. fontsize is in points Return object has attributes - see http://www.freetype.org/freetype2/docs/tutorial/step2.html for a pictoral representation of these attributes advance height width xmin, xmax, ymin, ymax - the ink rectangle of the glyph """ raise NotImplementedError('Derived must override') def set_canvas_size(self, w, h): 'Dimension the drawing canvas; may be a noop' self.width, self.height = w, h def render(self, ox, oy, facename, sym, fontsize, dpi): pass class DummyFonts(Fonts): 'dummy class for debugging parser' def get_metrics(self, font, sym, fontsize, dpi): metrics = Bunch( advance = 0, height = 0, width = 0, xmin = 0, xmax = 0, ymin = 0, ymax = 0, ) return metrics 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 * filenamesd: a dict that maps the face's name to the filename: filenamesd = { 'cal' : 'fontnamecal.ext', 'rm' : 'fontnamerm.ext', 'tt' : 'fontnamett.ext', 'it' : 'fontnameit.ext', None : 'fontnamesmth.ext'} filenamesd should be declared as a class atribute * glyphdict: a dict used for caching of glyph specific data * fonts: 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 ['Agg','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 def __init__(self, output='Agg'): self.facenames = self.filenamesd.keys() # Set the filenames to full path for facename in self.filenamesd: self.filenamesd[facename] = self.filenamesd[facename] if output: self.output = output # self.glyphdict[key] = facename, metrics, glyph, offset self.glyphdict = {} self.fonts = dict( [ (facename, font_open(self.filenamesd[facename])) for facename in self.facenames]) # a dict of glyphindex -> charcode pairs self.charmaps = dict( [ (facename, self.fonts[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.fonts.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.fonts.values(): fontface.set_bitmap_size(int(w), int(h)) def render(self, ox, oy, facename, symbol, fontsize, dpi): filename = self.filenamesd[facename] uniindex, metrics, glyph, offset = self._get_info(facename, symbol, fontsize, dpi) if self.output == 'SVG': oy += offset - 512/2048.*10. # TO-DO - make a method for it # This gets the name of the font. familyname = self.fonts[facename].get_sfnt()[(1,0,0,1)] thetext = unichr(uniindex) thetext.encode('utf-8') self.svg_glyphs.append((familyname, fontsize, thetext, 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. # Get the PS name of a glyph (his unicode integer code) # from the font object symbolname = self._get_glyph_name(uniindex, facename) psfontname = self.fonts[facename].postscript_name ps = """/%(psfontname)s findfont %(fontsize)s scalefont setfont %(ox)f %(oy)f moveto /%(symbolname)s glyphshow """ % locals() self.pswriter.write(ps) else: # Agg fontface = self.fonts[facename] fontface.draw_glyph_to_bitmap( int(ox), int(self.height - oy - metrics.ymax), glyph) def get_metrics(self, facename, symbol, fontsize, dpi): uniindex, metrics, glyph, offset = \ self._get_info(facename, symbol, fontsize, dpi) return metrics # Methods that must be overridden for fonts that are not unicode aware def _get_unicode_index(self, symbol): return get_unicode_index(symbol) def _get_glyph_name(self, uniindex, facename): """get_glyph_name(self, uniindex, facename) -> string Returns the name of the glyph directly from the font object. """ font = self.fonts[facename] glyphindex = self.glyphmaps[facename][uniindex] return font.get_glyph_name(glyphindex) 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.filenamesd[facename] # This is used only by the PS backend --- to integrate the fonts # into the resulting PS. TO-DO: fix the PS backend so it doesn't do # these dirty tricks if self.output == 'PS': if filename not in bakoma_fonts: bakoma_fonts.append(filename) fontface = self.fonts[facename] fontface.set_size(fontsize, dpi) head = fontface.get_sfnt_table('head') uniindex = self._get_unicode_index(symbol) glyphindex = self.glyphmaps[facename][uniindex] glyph = fontface.load_char(uniindex) 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] = uniindex, metrics, glyph, offset return self.glyphdict[key] class MyUnicodeFonts(UnicodeFonts): _initialized = False def __init__(self): if not MyUnicodeFonts._initialized: prop = FontProperties() prop.set_family('serif') self.rmfile = fontManager.findfont(prop) prop.set_family('fantasy') self.calfile = fontManager.findfont(prop) prop.set_family('monospace') self.ttfile = fontManager.findfont(prop) prop.set_family('serif') prop.set_style('italic') self.itfile = fontManager.findfont(prop) self.filenamesd = { 'rm' : self.rmfile, 'it' : self.itfile, 'cal' : self.calfile, 'tt' : self.ttfile, } MyUnicodeFonts._initialized = True # TO-DO: pretty much everything class BakomaUnicodeFonts(UnicodeFonts): """A class that simulates Unicode support in the BaKoMa fonts""" filenamesd = { 'cal' : 'cmsy10.ttf', 'rm' : 'cmr10.ttf', 'tt' : 'cmtt10.ttf', 'it' : 'cmmi10.ttf', None : 'cmmi10.ttf', } # We override the UnicodeFonts methods, that depend on Unicode support def _get_unicode_index(self, symbol): uniindex = get_unicode_index(symbol) # Should be deleted def _get_glyph_name(self, uniindex, facename): """get_glyph_name(self, uniindex, facename) -> string Returns the name of the glyph directly from the font object. Because BaKoma fonts don't support Unicode, 'uniindex' is misleading """ font = self.fonts[facename] glyphindex = self.glyphmaps[facename][uniindex] return font.get_glyph_name(glyphindex) 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.filenamesd[facename] # This is used only by the PS backend --- to integrate the fonts # into the resulting PS. if self.output == 'PS': if filename not in bakoma_fonts: bakoma_fonts.append(filename) fontface = self.fonts[facename] fontface.set_size(fontsize, dpi) head = fontface.get_sfnt_table('head') uniindex = self._get_unicode_index(symbol) glyphindex = self.glyphmaps[facename][uniindex] glyph = fontface.load_char(uniindex) 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] = uniindex, metrics, glyph, offset return self.glyphdict[key] # TO-DO: Implement all methods class CMUUnicodeFonts(UnicodeFonts): """A class representing Computer Modern Unicode Fonts, made by Andrey V. Panov panov /at/ canopus. iacp. dvo. ru They are distributed under the X11 License. """ # Old classes class BakomaTrueTypeFonts(Fonts): """ Use the Bakoma true type fonts for rendering """ fnames = ('cmmi10', 'cmsy10', 'cmex10', 'cmtt10', 'cmr10') # allocate a new set of fonts basepath = get_data_path() fontmap = { 'cal' : 'cmsy10', 'rm' : 'cmr10', 'tt' : 'cmtt10', 'it' : 'cmmi10', None : 'cmmi10', } def __init__(self, useSVG=False): self.glyphd = {} self.fonts = dict( [ (name, FT2Font(os.path.join(self.basepath, name) + '.ttf')) for name in self.fnames]) self.charmaps = dict( [ (name, self.fonts[name].get_charmap()) for name in self.fnames]) # glyphmaps is a dict names to a dict of charcode -> glyphindex self.glyphmaps = {} for name in self.fnames: cmap = self.charmaps[name] self.glyphmaps[name] = dict([(ccode, glyphind) for glyphind, ccode in cmap.items()]) for font in self.fonts.values(): font.clear() if useSVG: self.svg_glyphs=[] # a list of "glyphs" we need to render this thing in SVG else: pass self.usingSVG = useSVG def get_metrics(self, font, sym, fontsize, dpi): cmfont, metrics, glyph, offset = \ self._get_info(font, sym, fontsize, dpi) return metrics def _get_info (self, font, sym, fontsize, dpi): 'load the cmfont, metrics and glyph with caching' key = font, sym, fontsize, dpi tup = self.glyphd.get(key) if tup is not None: return tup basename = self.fontmap[font] if latex_to_bakoma.has_key(sym): basename, num = latex_to_bakoma[sym] num = self.charmaps[basename][num] elif len(sym) == 1: num = ord(sym) else: num = 0 raise ValueError('unrecognized symbol "%s"' % sym) #print sym, basename, num cmfont = self.fonts[basename] cmfont.set_size(fontsize, dpi) head = cmfont.get_sfnt_table('head') glyph = cmfont.load_char(num) xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] if basename == 'cmex10': offset = glyph.height/64.0/2 + 256.0/64.0*dpi/72.0 #offset = -(head['yMin']+512)/head['unitsPerEm']*10. 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.glyphd[key] = cmfont, metrics, glyph, offset return self.glyphd[key] def set_canvas_size(self, w, h): 'Dimension the drawing canvas; may be a noop' self.width = int(w) self.height = int(h) for font in self.fonts.values(): font.set_bitmap_size(int(w), int(h)) def render(self, ox, oy, font, sym, fontsize, dpi): cmfont, metrics, glyph, offset = \ self._get_info(font, sym, fontsize, dpi) if not self.usingSVG: cmfont.draw_glyph_to_bitmap( int(ox), int(self.height - oy - metrics.ymax), glyph) else: oy += offset - 512/2048.*10. basename = self.fontmap[font] if latex_to_bakoma.has_key(sym): basename, num = latex_to_bakoma[sym] num = self.charmaps[basename][num] elif len(sym) == 1: num = ord(sym) else: num = 0 print >>sys.stderr, 'unrecognized symbol "%s"' % sym thetext = unichr(num) thetext.encode('utf-8') self.svg_glyphs.append((basename, fontsize, thetext, ox, oy, metrics)) def _old_get_kern(self, font, symleft, symright, fontsize, dpi): """ Get the kerning distance for font between symleft and symright. font is one of tt, it, rm, cal or None sym is a single symbol(alphanum, punct) or a special symbol like \sigma. """ basename = self.fontmap[font] cmfont = self.fonts[basename] cmfont.set_size(fontsize, dpi) kernd = cmkern[basename] key = symleft, symright kern = kernd.get(key,0) #print basename, symleft, symright, key, kern return kern def _get_num(self, font, sym): 'get charcode for sym' basename = self.fontmap[font] if latex_to_bakoma.has_key(sym): basename, num = latex_to_bakoma[sym] num = self.charmaps[basename][num] elif len(sym) == 1: num = ord(sym) else: num = 0 return num class BakomaPSFonts(Fonts): """ Use the Bakoma postscript fonts for rendering to backend_ps """ fnames = ('cmmi10', 'cmsy10', 'cmex10', 'cmtt10', 'cmr10') # allocate a new set of fonts basepath = get_data_path() fontmap = { 'cal' : 'cmsy10', 'rm' : 'cmr10', 'tt' : 'cmtt10', 'it' : 'cmmi10', None : 'cmmi10', } def __init__(self): self.glyphd = {} self.fonts = dict( [ (name, FT2Font(os.path.join(self.basepath, name) + '.ttf')) for name in self.fnames]) self.charmaps = dict( [ (name, self.fonts[name].get_charmap()) for name in self.fnames]) for font in self.fonts.values(): font.clear() def _get_info (self, font, sym, fontsize, dpi): 'load the cmfont, metrics and glyph with caching' key = font, sym, fontsize, dpi tup = self.glyphd.get(key) if tup is not None: return tup basename = self.fontmap[font] if latex_to_bakoma.has_key(sym): basename, num = latex_to_bakoma[sym] sym = self.fonts[basename].get_glyph_name(num) num = self.charmaps[basename][num] elif len(sym) == 1: num = ord(sym) else: num = 0 sym = '.notdef' raise ValueError('unrecognized symbol "%s, %d"' % (sym, num)) filename = os.path.join(self.basepath, basename) + '.ttf' if filename not in bakoma_fonts: bakoma_fonts.append(filename) cmfont = self.fonts[basename] cmfont.set_size(fontsize, dpi) head = cmfont.get_sfnt_table('head') glyph = cmfont.load_char(num) xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] if basename == 'cmex10': offset = -(head['yMin']+512)/head['unitsPerEm']*10. 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.glyphd[key] = basename, metrics, sym, offset return basename, metrics, '/'+sym, offset def set_canvas_size(self, w, h, pswriter): 'Dimension the drawing canvas; may be a noop' self.width = w self.height = h self.pswriter = pswriter def render(self, ox, oy, font, sym, fontsize, dpi): fontname, metrics, glyphname, offset = \ self._get_info(font, sym, fontsize, dpi) fontname = fontname.capitalize() if fontname == 'Cmex10': oy += offset - 512/2048.*10. ps = """/%(fontname)s findfont %(fontsize)s scalefont setfont %(ox)f %(oy)f moveto /%(glyphname)s glyphshow """ % locals() self.pswriter.write(ps) def get_metrics(self, font, sym, fontsize, dpi): basename, metrics, sym, offset = \ self._get_info(font, sym, fontsize, dpi) return metrics class BakomaPDFFonts(BakomaPSFonts): """Hack of BakomaPSFonts for PDF support.""" def _get_filename_and_num (self, font, sym, fontsize, dpi): 'should be part of _get_info' basename = self.fontmap[font] if latex_to_bakoma.has_key(sym): basename, num = latex_to_bakoma[sym] sym = self.fonts[basename].get_glyph_name(num) num = self.charmaps[basename][num] elif len(sym) == 1: num = ord(sym) else: num = 0 raise ValueError('unrecognized symbol "%s"' % (sym,)) return os.path.join(self.basepath, basename) + '.ttf', num def render(self, ox, oy, font, sym, fontsize, dpi): fontname, metrics, glyphname, offset = \ self._get_info(font, sym, fontsize, dpi) filename, num = self._get_filename_and_num(font, sym, fontsize, dpi) if fontname.lower() == 'cmex10': oy += offset - 512/2048.*10. self.pswriter.append((ox, oy, filename, fontsize, num)) class StandardPSFonts(Fonts): """ Use the standard postscript fonts for rendering to backend_ps """ fnames = ('psyr', 'pncri8a', 'pcrr8a', 'pncr8a', 'pzcmi8a') # allocate a new set of fonts basepath = get_data_path() fontmap = { 'cal' : 'pzcmi8a', 'rm' : 'pncr8a', 'tt' : 'pcrr8a', 'it' : 'pncri8a', } def __init__(self): self.glyphd = {} self.fonts = dict( [ (name, AFM(file(os.path.join(self.basepath, name) + '.afm'))) for name in self.fnames]) def _get_info (self, font, sym, fontsize, dpi): 'load the cmfont, metrics and glyph with caching' key = font, sym, fontsize, dpi tup = self.glyphd.get(key) if tup is not None: return tup if sym in "0123456789()" and font == 'it': font = 'rm' basename = self.fontmap[font] if latex_to_standard.has_key(sym): basename, num = latex_to_standard[sym] char = chr(num) elif len(sym) == 1: char = sym else: raise ValueError('unrecognized symbol "%s"' % (sym)) try: sym = self.fonts[basename].get_name_char(char) except KeyError: raise ValueError('unrecognized symbol "%s"' % (sym)) offset = 0 cmfont = self.fonts[basename] fontname = cmfont.get_fontname() scale = 0.001 * fontsize xmin, ymin, xmax, ymax = [val * scale for val in cmfont.get_bbox_char(char)] metrics = Bunch( advance = (xmax-xmin), width = cmfont.get_width_char(char) * scale, height = cmfont.get_width_char(char) * scale, xmin = xmin, xmax = xmax, ymin = ymin+offset, ymax = ymax+offset ) self.glyphd[key] = fontname, basename, metrics, sym, offset, char return fontname, basename, metrics, '/'+sym, offset, char def set_canvas_size(self, w, h, pswriter): 'Dimension the drawing canvas; may be a noop' self.width = w self.height = h self.pswriter = pswriter def render(self, ox, oy, font, sym, fontsize, dpi): fontname, basename, metrics, glyphname, offset, char = \ self._get_info(font, sym, fontsize, dpi) ps = """/%(fontname)s findfont %(fontsize)s scalefont setfont %(ox)f %(oy)f moveto /%(glyphname)s glyphshow """ % locals() self.pswriter.write(ps) def get_metrics(self, font, sym, fontsize, dpi): fontname, basename, metrics, sym, offset, char = \ self._get_info(font, sym, fontsize, dpi) return metrics def get_kern(self, font, symleft, symright, fontsize, dpi): fontname, basename, metrics, sym, offset, char1 = \ self._get_info(font, symleft, fontsize, dpi) fontname, basename, metrics, sym, offset, char2 = \ self._get_info(font, symright, fontsize, dpi) cmfont = self.fonts[basename] return cmfont.get_kern_dist(char1, char2) * 0.001 * fontsize class Element: fontsize = 12 dpi = 72 font = 'it' _padx, _pady = 2, 2 # the x and y padding in points _scale = 1.0 def __init__(self): # a dict mapping the keys above, below, subscript, # superscript, right to Elements in that position self.neighbors = {} self.ox, self.oy = 0, 0 def advance(self): 'get the horiz advance' raise NotImplementedError('derived must override') def height(self): 'get the element height: ymax-ymin' raise NotImplementedError('derived must override') def width(self): 'get the element width: xmax-xmin' raise NotImplementedError('derived must override') def xmin(self): 'get the xmin of ink rect' raise NotImplementedError('derived must override') def xmax(self): 'get the xmax of ink rect' raise NotImplementedError('derived must override') def ymin(self): 'get the ymin of ink rect' raise NotImplementedError('derived must override') def ymax(self): 'get the ymax of ink rect' raise NotImplementedError('derived must override') def set_font(self, font): 'set the font (one of tt, it, rm , cal)' raise NotImplementedError('derived must override') def render(self): 'render to the fonts canvas' for element in self.neighbors.values(): element.render() def set_origin(self, ox, oy): self.ox, self.oy = ox, oy # order matters! right needs to be evaled last keys = ('above', 'below', 'subscript', 'superscript', 'right') for loc in keys: element = self.neighbors.get(loc) if element is None: continue if loc=='above': nx = self.centerx() - element.width()/2.0 ny = self.ymax() + self.pady() + (element.oy - element.ymax() + element.height()) #print element, self.ymax(), element.height(), element.ymax(), element.ymin(), ny elif loc=='below': nx = self.centerx() - element.width()/2.0 ny = self.ymin() - self.pady() - element.height() elif loc=='superscript': nx = self.xmax() ny = self.ymax() - self.pady() elif loc=='subscript': nx = self.xmax() ny = self.oy - 0.5*element.height() elif loc=='right': nx = self.ox + self.advance() if self.neighbors.has_key('subscript'): o = self.neighbors['subscript'] nx = max(nx, o.ox + o.advance()) if self.neighbors.has_key('superscript'): o = self.neighbors['superscript'] nx = max(nx, o.ox + o.advance()) ny = self.oy element.set_origin(nx, ny) def set_size_info(self, fontsize, dpi): self.fontsize = self._scale*fontsize self.dpi = dpi for loc, element in self.neighbors.items(): if loc in ('subscript', 'superscript'): element.set_size_info(0.7*self.fontsize, dpi) else: element.set_size_info(self.fontsize, dpi) def pady(self): return self.dpi/72.0*self._pady def padx(self): return self.dpi/72.0*self._padx def set_padx(self, pad): 'set the y padding in points' self._padx = pad def set_pady(self, pad): 'set the y padding in points' self._pady = pad def set_scale(self, scale): 'scale the element by scale' self._scale = scale def centerx(self): return 0.5 * (self.xmax() + self.xmin() ) def centery(self): return 0.5 * (self.ymax() + self.ymin() ) def __repr__(self): return str(self.__class__) + str(self.neighbors) class SpaceElement(Element): 'blank horizontal space' def __init__(self, space, height=0): """ space is the amount of blank space in fraction of fontsize height is the height of the space in fraction of fontsize """ Element.__init__(self) self.space = space self._height = height def advance(self): 'get the horiz advance' return self.dpi/72.0*self.space*self.fontsize def height(self): 'get the element height: ymax-ymin' return self._height*self.dpi/72.0*self.fontsize def width(self): 'get the element width: xmax-xmin' return self.advance() def xmin(self): 'get the minimum ink in x' return self.ox def xmax(self): 'get the max ink in x' return self.ox + self.advance() def ymin(self): 'get the minimum ink in y' return self.oy def ymax(self): 'get the max ink in y' return self.oy + self.height() def set_font(self, f): # space doesn't care about font, only size pass class SymbolElement(Element): def __init__(self, sym): Element.__init__(self) self.sym = sym self.kern = None self.widthm = 1 # the width of an m; will be resized below def set_font(self, font): 'set the font (one of tt, it, rm , cal)' self.font = font def set_origin(self, ox, oy): Element.set_origin(self, ox, oy) def set_size_info(self, fontsize, dpi): Element.set_size_info(self, fontsize, dpi) self.metrics = Element.fonts.get_metrics( self.font, self.sym, self.fontsize, dpi) mmetrics = Element.fonts.get_metrics( self.font, 'm', self.fontsize, dpi) self.widthm = mmetrics.width #print self.widthm def advance(self): 'get the horiz advance' if self.kern is None: self.kern = 0 if self.neighbors.has_key('right'): sym = None o = self.neighbors['right'] if hasattr(o, 'sym'): sym = o.sym elif isinstance(o, SpaceElement): sym = ' ' if sym is not None: self.kern = Element.fonts.get_kern( self.font, self.sym, sym, self.fontsize, self.dpi) return self.metrics.advance + self.kern #return self.metrics.advance # how to handle cm units?+ self.kern*self.widthm def height(self): 'get the element height: ymax-ymin' return self.metrics.height def width(self): 'get the element width: xmax-xmin' return self.metrics.width def xmin(self): 'get the minimum ink in x' return self.ox + self.metrics.xmin def xmax(self): 'get the max ink in x' return self.ox + self.metrics.xmax def ymin(self): 'get the minimum ink in y' return self.oy + self.metrics.ymin def ymax(self): 'get the max ink in y' return self.oy + self.metrics.ymax def render(self): 'render to the fonts canvas' Element.render(self) Element.fonts.render( self.ox, self.oy, self.font, self.sym, self.fontsize, self.dpi) def __repr__(self): return self.sym class GroupElement(Element): """ A group is a collection of elements """ def __init__(self, elements): Element.__init__(self) self.elements = elements for i in range(len(elements)-1): self.elements[i].neighbors['right'] = self.elements[i+1] def set_font(self, font): 'set the font (one of tt, it, rm , cal)' for element in self.elements: element.set_font(font) #print 'set fonts' for i in range(len(self.elements)-1): if not isinstance(self.elements[i], SymbolElement): continue if not isinstance(self.elements[i+1], SymbolElement): continue symleft = self.elements[i].sym symright = self.elements[i+1].sym self.elements[i].kern = None #self.elements[i].kern = Element.fonts.get_kern(font, symleft, symright, self.fontsize, self.dpi) def set_size_info(self, fontsize, dpi): self.elements[0].set_size_info(self._scale*fontsize, dpi) Element.set_size_info(self, fontsize, dpi) #print 'set size' def set_origin(self, ox, oy): self.elements[0].set_origin(ox, oy) Element.set_origin(self, ox, oy) def advance(self): 'get the horiz advance' return self.elements[-1].xmax() - self.elements[0].ox def height(self): 'get the element height: ymax-ymin' ymax = max([e.ymax() for e in self.elements]) ymin = min([e.ymin() for e in self.elements]) return ymax-ymin def width(self): 'get the element width: xmax-xmin' xmax = max([e.xmax() for e in self.elements]) xmin = min([e.xmin() for e in self.elements]) return xmax-xmin def render(self): 'render to the fonts canvas' Element.render(self) self.elements[0].render() def xmin(self): 'get the minimum ink in x' return min([e.xmin() for e in self.elements]) def xmax(self): 'get the max ink in x' return max([e.xmax() for e in self.elements]) def ymin(self): 'get the minimum ink in y' return max([e.ymin() for e in self.elements]) def ymax(self): 'get the max ink in y' return max([e.ymax() for e in self.elements]) def __repr__(self): return 'Group: [ %s ]' % ' '.join([str(e) for e in self.elements]) class ExpressionElement(GroupElement): """ The entire mathtext expression """ def __repr__(self): return 'Expression: [ %s ]' % ' '.join([str(e) for e in self.elements]) class Handler: symbols = [] def clear(self): self.symbols = [] def expression(self, s, loc, toks): self.expr = ExpressionElement(toks) return loc, [self.expr] def space(self, s, loc, toks): assert(len(toks)==1) if toks[0]==r'\ ': num = 0.30 # 30% of fontsize elif toks[0]==r'\/': num = 0.1 # 10% of fontsize else: # vspace num = float(toks[0][1]) # get the num out of \hspace{num} element = SpaceElement(num) self.symbols.append(element) return loc, [element] def symbol(self, s, loc, toks): assert(len(toks)==1) s = toks[0] #~ print 'sym', toks[0] if charOverChars.has_key(s): under, over, pad = charOverChars[s] font, tok, scale = under sym = SymbolElement(tok) if font is not None: sym.set_font(font) sym.set_scale(scale) sym.set_pady(pad) font, tok, scale = over sym2 = SymbolElement(tok) if font is not None: sym2.set_font(font) sym2.set_scale(scale) sym.neighbors['above'] = sym2 self.symbols.append(sym2) else: sym = SymbolElement(toks[0]) self.symbols.append(sym) return loc, [sym] def composite(self, s, loc, toks): assert(len(toks)==1) where, sym0, sym1 = toks[0] #keys = ('above', 'below', 'subscript', 'superscript', 'right') if where==r'\over': sym0.neighbors['above'] = sym1 elif where==r'\under': sym0.neighbors['below'] = sym1 self.symbols.append(sym0) self.symbols.append(sym1) return loc, [sym0] def accent(self, s, loc, toks): assert(len(toks)==1) accent, sym = toks[0] d = { r'\hat' : r'\circumflexaccent', r'\breve' : r'\combiningbreve', r'\bar' : r'\combiningoverline', r'\grave' : r'\combininggraveaccent', r'\acute' : r'\combiningacuteaccent', r'\ddot' : r'\combiningdiaeresis', r'\tilde' : r'\combiningtilde', r'\dot' : r'\combiningdotabove', r'\vec' : r'\combiningrightarrowabove', r'\"' : r'\combiningdiaeresis', r"\`" : r'\combininggraveaccent', r"\'" : r'\combiningacuteaccent', r'\~' : r'\combiningtilde', r'\.' : r'\combiningdotabove', r'\^' : r'\circumflexaccent', } above = SymbolElement(d[accent]) sym.neighbors['above'] = above sym.set_pady(1) self.symbols.append(above) return loc, [sym] def group(self, s, loc, toks): assert(len(toks)==1) #print 'grp', toks grp = GroupElement(toks[0]) return loc, [grp] def font(self, s, loc, toks): assert(len(toks)==1) name, grp = toks[0] #print 'fontgrp', toks grp.set_font(name[1:]) # suppress the slash return loc, [grp] def subscript(self, s, loc, toks): assert(len(toks)==1) #print 'subsup', toks if len(toks[0])==2: under, next = toks[0] prev = SpaceElement(0) else: prev, under, next = toks[0] if self.is_overunder(prev): prev.neighbors['below'] = next else: prev.neighbors['subscript'] = next return loc, [prev] def is_overunder(self, prev): return isinstance(prev, SymbolElement) and overunder.has_key(prev.sym) def superscript(self, s, loc, toks): assert(len(toks)==1) #print 'subsup', toks if len(toks[0])==2: under, next = toks[0] prev = SpaceElement(0,0.6) else: prev, under, next = toks[0] if self.is_overunder(prev): prev.neighbors['above'] = next else: prev.neighbors['superscript'] = next return loc, [prev] def subsuperscript(self, s, loc, toks): assert(len(toks)==1) #print 'subsup', toks prev, undersym, down, oversym, up = toks[0] if self.is_overunder(prev): prev.neighbors['below'] = down prev.neighbors['above'] = up else: prev.neighbors['subscript'] = down prev.neighbors['superscript'] = up return loc, [prev] handler = Handler() lbrace = Literal('{').suppress() rbrace = Literal('}').suppress() lbrack = Literal('[') rbrack = Literal(']') lparen = Literal('(') rparen = Literal(')') grouping = lbrack | rbrack | lparen | rparen bslash = Literal('\\') langle = Literal('<') rangle = Literal('>') equals = Literal('=') relation = langle | rangle | equals colon = Literal(':') comma = Literal(',') period = Literal('.') semicolon = Literal(';') exclamation = Literal('!') punctuation = colon | comma | period | semicolon at = Literal('@') percent = Literal('%') ampersand = Literal('&') misc = exclamation | at | percent | ampersand over = Literal('over') under = Literal('under') #~ composite = over | under overUnder = over | under accent = Literal('hat') | Literal('check') | Literal('dot') | \ Literal('breve') | Literal('acute') | Literal('ddot') | \ Literal('grave') | Literal('tilde') | Literal('bar') | \ Literal('vec') | Literal('"') | Literal("`") | Literal("'") |\ Literal('~') | Literal('.') | Literal('^') number = Combine(Word(nums) + Optional(Literal('.')) + Optional( Word(nums) )) plus = Literal('+') minus = Literal('-') times = Literal('*') div = Literal('/') binop = plus | minus | times | div roman = Literal('rm') cal = Literal('cal') italics = Literal('it') typewriter = Literal('tt') fontname = roman | cal | italics | typewriter texsym = Combine(bslash + Word(alphanums) + NotAny("{")) char = Word(alphanums + ' ', exact=1).leaveWhitespace() space = FollowedBy(bslash) + (Literal(r'\ ') | Literal(r'\/') | Group(Literal(r'\hspace{') + number + Literal('}'))).setParseAction(handler.space).setName('space') symbol = Regex("("+")|(".join( [ r"\\[a-zA-Z0-9]+(?!{)", r"[a-zA-Z0-9 ]", r"[+\-*/]", r"[<>=]", r"[:,.;!]", r"[!@%&]", r"[[\]()]", ])+")" ).setParseAction(handler.symbol).leaveWhitespace() #~ symbol = (texsym ^ char ^ binop ^ relation ^ punctuation ^ misc ^ grouping ).setParseAction(handler.symbol).leaveWhitespace() _symbol = (texsym | char | binop | relation | punctuation | misc | grouping ).setParseAction(handler.symbol).leaveWhitespace() subscript = Forward().setParseAction(handler.subscript).setName("subscript") superscript = Forward().setParseAction(handler.superscript).setName("superscript") subsuperscript = Forward().setParseAction(handler.subsuperscript).setName("subsuperscript") font = Forward().setParseAction(handler.font).setName("font") accent = Group( Combine(bslash + accent) + Optional(lbrace) + symbol + Optional(rbrace)).setParseAction(handler.accent).setName("accent") group = Group( lbrace + OneOrMore(symbol^subscript^superscript^subsuperscript^space^font^accent) + rbrace).setParseAction(handler.group).setName("group") #~ group = Group( lbrace + OneOrMore(subsuperscript | subscript | superscript | symbol | space ) + rbrace).setParseAction(handler.group).setName("group") #composite = Group( Combine(bslash + composite) + lbrace + symbol + rbrace + lbrace + symbol + rbrace).setParseAction(handler.composite).setName("composite") #~ composite = Group( Combine(bslash + composite) + group + group).setParseAction(handler.composite).setName("composite") composite = Group( Combine(bslash + overUnder) + group + group).setParseAction(handler.composite).setName("composite") symgroup = font | group | symbol subscript << Group( Optional(symgroup) + Literal('_') + symgroup ) superscript << Group( Optional(symgroup) + Literal('^') + symgroup ) subsuperscript << Group( symgroup + Literal('_') + symgroup + Literal('^') + symgroup ) font << Group( Combine(bslash + fontname) + group) expression = OneOrMore( space ^ font ^ accent ^ symbol ^ subscript ^ superscript ^ subsuperscript ^ group ^ composite ).setParseAction(handler.expression).setName("expression") #~ expression = OneOrMore( #~ group | composite | space | font | subsuperscript | subscript | superscript | symbol ).setParseAction(handler.expression).setName("expression") #### class math_parse_s_ft2font_common: """ Parse the math expression s, return the (bbox, fonts) tuple needed to render it. fontsize must be in points return is width, height, fonts """ major, minor1, minor2, tmp, tmp = sys.version_info if major==2 and minor1==2: raise SystemExit('mathtext broken on python2.2. We hope to get this fixed soon') def __init__(self, output): self.output = output self.cache = {} def __call__(self, s, dpi, fontsize, angle=0): cacheKey = (s, dpi, fontsize, angle) s = s[1:-1] # strip the $ from front and back if self.cache.has_key(cacheKey): w, h, fontlike = self.cache[cacheKey] return w, h, fontlike if self.output == 'SVG': self.font_object = BakomaTrueTypeFonts(useSVG=True) #self.font_object = MyUnicodeFonts(output='SVG') Element.fonts = self.font_object elif self.output == 'Agg': self.font_object = BakomaTrueTypeFonts() #self.font_object = MyUnicodeFonts() Element.fonts = self.font_object elif self.output == 'PS': if rcParams['ps.useafm']: self.font_object = StandardPSFonts() Element.fonts = self.font_object else: self.font_object = BakomaPSFonts() #self.font_object = MyUnicodeFonts(output='PS') Element.fonts = self.font_object elif self.output == 'PDF': self.font_object = BakomaPDFFonts() Element.fonts = self.font_object handler.clear() expression.parseString( s ) handler.expr.set_size_info(fontsize, dpi) # set the origin once to allow w, h compution handler.expr.set_origin(0, 0) xmin = min([e.xmin() for e in handler.symbols]) xmax = max([e.xmax() for e in handler.symbols]) ymin = min([e.ymin() for e in handler.symbols]) ymax = max([e.ymax() for e in handler.symbols]) # now set the true origin - doesn't affect with and height w, h = xmax-xmin, ymax-ymin # a small pad for the canvas size w += 2 h += 2 handler.expr.set_origin(0, h-ymax) if self.output in ('SVG', 'Agg'): Element.fonts.set_canvas_size(w,h) elif self.output == 'PS': pswriter = StringIO() Element.fonts.set_canvas_size(w, h, pswriter) elif self.output == 'PDF': pswriter = list() Element.fonts.set_canvas_size(w, h, pswriter) handler.expr.render() handler.clear() if self.output == 'SVG': # The empty list at the end is for lines svg_elements = Bunch(svg_glyphs=self.font_object.svg_glyphs, svg_lines=[]) self.cache[cacheKey] = w, h, svg_elements return w, h, svg_elements elif self.output == 'Agg': self.cache[cacheKey] = w, h, self.font_object.fonts.values() return w, h, self.font_object.fonts.values() elif self.output in ('PS', 'PDF'): self.cache[cacheKey] = w, h, pswriter return w, h, pswriter if rcParams["mathtext.mathtext2"]: from matplotlib.mathtext2 import math_parse_s_ft2font from matplotlib.mathtext2 import math_parse_s_ft2font_svg else: math_parse_s_ft2font = math_parse_s_ft2font_common('Agg') math_parse_s_ft2font_svg = math_parse_s_ft2font_common('SVG') math_parse_s_ps = math_parse_s_ft2font_common('PS') math_parse_s_pdf = math_parse_s_ft2font_common('PDF') if 0: #__name__=='___main__': stests = [ r'$dz/dt \/ = \/ \gamma x^2 \/ + \/ \rm{sin}(2\pi y+\phi)$', r'$dz/dt \/ = \/ \gamma xy^2 \/ + \/ \rm{s}(2\pi y+\phi)$', r'$x^1 2$', r'$\alpha_{i+1}^j \/ = \/ \rm{sin}(2\pi f_j t_i) e^{-5 t_i/\tau}$', r'$\cal{R}\prod_{i=\alpha_{i+1}}^\infty a_i\rm{sin}(2 \pi f x_i)$', r'$\bigodot \bigoplus \cal{R} a_i\rm{sin}(2 \pi f x_i)$', r'$x_i$', r'$5\/\angstrom\hspace{ 2.0 }\pi$', r'$x+1$', r'$i$', r'$i^j$', ] #~ w, h, fonts = math_parse_s_ft2font(s, 20, 72) for s in stests: try: print s print (expression + StringEnd()).parseString( s[1:-1] ) except ParseException, pe: print "*** ERROR ***", pe.msg print s print 'X' + (' '*pe.loc)+'^' # how far did we get? print expression.parseString( s[1:-1] ) print #w, h, fonts = math_parse_s_ps(s, 20, 72) if __name__=='__main__': Element.fonts = DummyFonts() for i in range(5,20): s = '$10^{%02d}$'%i print 'parsing', s w, h, fonts = math_parse_s_ft2font(s, dpi=27, fontsize=12, angle=0) if 0: Element.fonts = DummyFonts() handler.clear() expression.parseString( s ) handler.expr.set_size_info(12, 72) # set the origin once to allow w, h compution handler.expr.set_origin(0, 0) for e in handler.symbols: assert(hasattr(e, 'metrics'))