From: <md...@us...> - 2007-07-26 14:45:59
|
Revision: 3617 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3617&view=rev Author: mdboom Date: 2007-07-26 07:45:57 -0700 (Thu, 26 Jul 2007) Log Message: ----------- Merging mathtext changes into trunk. Modified Paths: -------------- trunk/matplotlib/examples/mathtext_demo.py trunk/matplotlib/lib/matplotlib/_mathtext_data.py trunk/matplotlib/lib/matplotlib/afm.py trunk/matplotlib/lib/matplotlib/backends/backend_agg.py trunk/matplotlib/lib/matplotlib/backends/backend_cairo.py trunk/matplotlib/lib/matplotlib/backends/backend_gdk.py trunk/matplotlib/lib/matplotlib/backends/backend_pdf.py trunk/matplotlib/lib/matplotlib/backends/backend_ps.py trunk/matplotlib/lib/matplotlib/backends/backend_svg.py trunk/matplotlib/lib/matplotlib/mathtext.py trunk/matplotlib/lib/matplotlib/pyparsing.py trunk/matplotlib/lib/matplotlib/text.py trunk/matplotlib/src/ft2font.cpp trunk/matplotlib/src/mplutils.h Added Paths: ----------- trunk/matplotlib/examples/mathtext_examples.py trunk/matplotlib/lib/matplotlib/mpl-data/fonts/ttf/cmb10.ttf trunk/matplotlib/lib/matplotlib/mpl-data/fonts/ttf/cmss10.ttf Modified: trunk/matplotlib/examples/mathtext_demo.py =================================================================== --- trunk/matplotlib/examples/mathtext_demo.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/examples/mathtext_demo.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -1,28 +1,25 @@ #!/usr/bin/env python """ +Use matplotlib's internal LaTex parser and layout engine. For true +latex rendering, see the text.usetex option +""" +import numpy as npy +from pylab import figure, show +fig = figure() +fig.subplots_adjust(bottom=0.2) -In order to use mathtext, you must build matplotlib.ft2font. This is -built by default in the windows installer. +ax = fig.add_subplot(111, axisbg='y') +ax.plot([1,2,3], 'r') +x = npy.arange(0.0, 3.0, 0.1) -For other platforms, edit setup.py and set +ax.grid(True) +ax.set_xlabel(r'$\Delta_i^j$', fontsize=20) +ax.set_ylabel(r'$\Delta_{i+1}^j$', fontsize=20) +tex = r'$\mathcal{R}\prod_{i=\alpha_{i+1}}^\infty a_i\sin(2 \pi f x_i)$' -BUILD_FT2FONT = True +ax.text(1, 1.6, tex, fontsize=20, va='bottom') -""" -from pylab import * -subplot(111, axisbg='y') -plot([1,2,3], 'r') -x = arange(0.0, 3.0, 0.1) - -grid(True) -xlabel(r'$\Delta_i^j$', fontsize=20) -ylabel(r'$\Delta_{i+1}^j$', fontsize=20) -tex = r'$\cal{R}\prod_{i=\alpha_{i+1}}^\infty a_i\rm{sin}(2 \pi f x_i)$' -text(1, 1.6, tex, fontsize=20) - #title(r'$\Delta_i^j \hspace{0.4} \rm{versus} \hspace{0.4} \Delta_{i+1}^j$', fontsize=20) -savefig('mathtext_demo') +fig.savefig('mathtext_demo') - - show() Copied: trunk/matplotlib/examples/mathtext_examples.py (from rev 3616, branches/mathtext_mgd/examples/mathtext_examples.py) =================================================================== --- trunk/matplotlib/examples/mathtext_examples.py (rev 0) +++ trunk/matplotlib/examples/mathtext_examples.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +import os, sys + +stests = [ + r'Kerning: AVA $AVA$', + r'$x y$', + r'$x+y\ x=y\ x<y\ x:y\ x,y\ x@y$', + r'$100\%y\ x*y\ x/y x\$y$', + r'$x\leftarrow y\ x\forall y$', + r'$x \sf x \bf x {\cal X} \rm x$', + r'$\{ \rm braces \}$', + r'$\left[\left\lfloor\frac{5}{\frac{\left(3\right)}{4}} y\right)\right]$', + r'$\left(x\right)$', + r'$\sin(x)$', + r'$x_2$', + r'$x^2$', + r'$x^2_y$', + r'$x_y^2$', + r'$\prod_{i=\alpha_{i+1}}^\infty$', + r'$x = \frac{x+\frac{5}{2}}{\frac{y+3}{8}}$', + r'$dz/dt \/ = \/ \gamma x^2 \/ + \/ {\rm sin}(2\pi y+\phi)$', + r'Foo: $\alpha_{i+1}^j \/ = \/ {\rm sin}(2\pi f_j t_i) e^{-5 t_i/\tau}$', + r'$\mathcal{R}\prod_{i=\alpha_{i+1}}^\infty a_i \sin(2 \pi f x_i)$', +# r'$\bigodot \bigoplus {\sf R} a_i{\rm sin}(2 \pi f x_i)$', + r'Variable $i$ is good', + r'$\Delta_i^j$', + r'$\Delta^j_{i+1}$', + r'$\ddot{o}\acute{e}\grave{e}\hat{O}\breve{\imath}\tilde{n}\vec{q}$', + r'$_i$', + r"$\arccos((x^i))$", + r"$\gamma = \frac{x=\frac{6}{8}}{y} \delta$", + r'$\"o\ddot o \'e\`e\~n\.x\^y$', + + ] + +from pylab import * + +if '--latex' in sys.argv: + fd = open("mathtext_examples.ltx", "w") + fd.write("\\documentclass{article}\n") + fd.write("\\begin{document}\n") + fd.write("\\begin{enumerate}\n") + + for i, s in enumerate(stests): + fd.write("\\item %s\n" % s) + + fd.write("\\end{enumerate}\n") + fd.write("\\end{document}\n") + fd.close() + + os.system("pdflatex mathtext_examples.ltx") +else: + for i, s in enumerate(stests): + print "%02d: %s" % (i, s) + plot([1,2,3], 'r') + x = arange(0.0, 3.0, 0.1) + + grid(True) + text(1, 1.6, s, fontsize=20) + + savefig('mathtext_example%02d' % i) + figure() + Property changes on: trunk/matplotlib/examples/mathtext_examples.py ___________________________________________________________________ Name: svn:executable + * Modified: trunk/matplotlib/lib/matplotlib/_mathtext_data.py =================================================================== --- trunk/matplotlib/lib/matplotlib/_mathtext_data.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/_mathtext_data.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -38,8 +38,10 @@ r'\SQRT' : ('cmex10', 53), r'\leftbrace' : ('cmex10', 92), r'{' : ('cmex10', 92), + r'\{' : ('cmex10', 92), r'\rightbrace' : ('cmex10', 130), r'}' : ('cmex10', 130), + r'\}' : ('cmex10', 130), r'\leftangle' : ('cmex10', 97), r'\rightangle' : ('cmex10', 64), r'\Leftparen' : ('cmex10', 112), @@ -112,7 +114,7 @@ r'\phi' : ('cmmi10', 42), r'\chi' : ('cmmi10', 17), r'\psi' : ('cmmi10', 31), - + r'(' : ('cmr10', 119), r'\leftparen' : ('cmr10', 119), r'\rightparen' : ('cmr10', 68), @@ -135,7 +137,11 @@ r'[' : ('cmr10', 62), r'\rightbracket' : ('cmr10', 72), r']' : ('cmr10', 72), - + r'\%' : ('cmr10', 48), + r'%' : ('cmr10', 48), + r'\$' : ('cmr10', 99), + r'@' : ('cmr10', 111), + # these are mathml names, I think. I'm just using them for the # tex methods noted r'\circumflexaccent' : ('cmr10', 124), # for \hat @@ -749,7 +755,17 @@ r'\langle' : ('psyr', 225), r'\Sigma' : ('psyr', 229), r'\sum' : ('psyr', 229), - + # these are mathml names, I think. I'm just using them for the + # tex methods noted + r'\circumflexaccent' : ('pncri8a', 124), # for \hat + r'\combiningbreve' : ('pncri8a', 81), # for \breve + r'\combininggraveaccent' : ('pncri8a', 114), # for \grave + r'\combiningacuteaccent' : ('pncri8a', 63), # for \accute + r'\combiningdiaeresis' : ('pncri8a', 91), # for \ddot + r'\combiningtilde' : ('pncri8a', 75), # for \tilde + r'\combiningrightarrowabove' : ('pncri8a', 110), # for \vec + r'\combiningdotabove' : ('pncri8a', 26), # for \dot + r'\imath' : ('pncri8a', 105) } # Automatically generated. Modified: trunk/matplotlib/lib/matplotlib/afm.py =================================================================== --- trunk/matplotlib/lib/matplotlib/afm.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/afm.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -394,7 +394,14 @@ "Return the fontangle as float" return self._header['ItalicAngle'] + def get_xheight(self): + "Return the xheight as float" + return self._header['XHeight'] + def get_underline_thickness(self): + "Return the underline thickness as float" + return self._header['UnderlineThickness'] + if __name__=='__main__': #pathname = '/usr/local/lib/R/afm/' Modified: trunk/matplotlib/lib/matplotlib/backends/backend_agg.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_agg.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/backends/backend_agg.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -173,10 +173,9 @@ """ if __debug__: verbose.report('RendererAgg.draw_mathtext', 'debug-annoying') - size = prop.get_size_in_points() - width, height, fonts = math_parse_s_ft2font( - s, self.dpi.get(), size, angle) - + width, height, fonts, used_characters = math_parse_s_ft2font( + s, self.dpi.get(), prop, angle) + if angle == 90: width, height = height, width for font in fonts: @@ -225,7 +224,6 @@ # texmanager more efficient. It is not meant to be used # outside the backend """ - if ismath=='TeX': # todo: handle props size = prop.get_size_in_points() @@ -235,8 +233,8 @@ return n,m if ismath: - width, height, fonts = math_parse_s_ft2font( - s, self.dpi.get(), prop.get_size_in_points()) + width, height, fonts, used_characters = math_parse_s_ft2font( + s, self.dpi.get(), prop) return width, height font = self._get_agg_font(prop) font.set_text(s, 0.0) # the width and height of unrotated string Modified: trunk/matplotlib/lib/matplotlib/backends/backend_cairo.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_cairo.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/backends/backend_cairo.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -314,9 +314,8 @@ # "_draw_mathtext()") # return - size = prop.get_size_in_points() - width, height, fonts = math_parse_s_ft2font( - s, self.dpi.get(), size) + width, height, fonts, used_characters = math_parse_s_ft2font( + s, self.dpi.get(), prop) if angle==90: width, height = height, width @@ -372,8 +371,8 @@ def get_text_width_height(self, s, prop, ismath): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: - width, height, fonts = math_parse_s_ft2font( - s, self.dpi.get(), prop.get_size_in_points()) + width, height, fonts, used_characters = math_parse_s_ft2font( + s, self.dpi.get(), prop) return width, height ctx = self.text_ctx Modified: trunk/matplotlib/lib/matplotlib/backends/backend_gdk.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_gdk.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/backends/backend_gdk.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -198,9 +198,8 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): - size = prop.get_size_in_points() - width, height, fonts = math_parse_s_ft2font( - s, self.dpi.get(), size) + width, height, fonts, used_characters = math_parse_s_ft2font( + s, self.dpi.get(), prop) if angle==90: width, height = height, width @@ -342,8 +341,8 @@ def get_text_width_height(self, s, prop, ismath): if ismath: - width, height, fonts = math_parse_s_ft2font( - s, self.dpi.get(), prop.get_size_in_points()) + width, height, fonts, used_characters = math_parse_s_ft2font( + s, self.dpi.get(), prop) return width, height layout, inkRect, logicalRect = self._get_pango_layout(s, prop) Modified: trunk/matplotlib/lib/matplotlib/backends/backend_pdf.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_pdf.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/backends/backend_pdf.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -457,8 +457,9 @@ fontdictObject = self._write_afm_font(filename) else: realpath, stat_key = get_realpath_and_stat(filename) - fontdictObject = self.embedTTF( - *self.used_characters[stat_key]) + chars = self.used_characters.get(stat_key) + if chars is not None and len(chars[1]): + fontdictObject = self.embedTTF(realpath, chars[1]) fonts[Fx] = fontdictObject #print >>sys.stderr, filename self.writeObject(self.fontObject, fonts) @@ -1092,37 +1093,36 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): # TODO: fix positioning and encoding - fontsize = prop.get_size_in_points() width, height, pswriter, used_characters = \ - math_parse_s_pdf(s, 72, fontsize, 0) + math_parse_s_pdf(s, 72, prop, 0) self.merge_used_characters(used_characters) - + self.check_gc(gc, gc._rgb) self.file.output(Op.begin_text) prev_font = None, None oldx, oldy = 0, 0 - for ox, oy, fontname, fontsize, glyph in pswriter: - #print ox, oy, glyph - fontname = fontname.lower() - a = angle / 180.0 * pi - newx = x + cos(a)*ox - sin(a)*oy - newy = y + sin(a)*ox + cos(a)*oy - self._setup_textpos(newx, newy, angle, oldx, oldy) - oldx, oldy = newx, newy - if (fontname, fontsize) != prev_font: - self.file.output(self.file.fontName(fontname), fontsize, - Op.selectfont) - prev_font = fontname, fontsize + for record in pswriter: + if record[0] == 'glyph': + rec_type, ox, oy, fontname, fontsize, glyph = record + a = angle / 180.0 * pi + newx = x + cos(a)*ox - sin(a)*oy + newy = y + sin(a)*ox + cos(a)*oy + self._setup_textpos(newx, newy, angle, oldx, oldy) + oldx, oldy = newx, newy + if (fontname, fontsize) != prev_font: + self.file.output(self.file.fontName(fontname), fontsize, + Op.selectfont) + prev_font = fontname, fontsize - #if fontname.endswith('cmsy10.ttf') or \ - #fontname.endswith('cmmi10.ttf') or \ - #fontname.endswith('cmex10.ttf'): - # string = '\0' + chr(glyph) - - string = chr(glyph) - self.file.output(string, Op.show) + string = chr(glyph) + self.file.output(string, Op.show) self.file.output(Op.end_text) + for record in pswriter: + if record[0] == 'rect': + rec_type, ox, oy, width, height = record + self.file.output(Op.gsave, x + ox, y + oy, width, height, Op.rectangle, Op.fill, Op.grestore) + def _draw_tex(self, gc, x, y, s, prop, angle): # Rename to draw_tex to enable, but note the following: # TODO: @@ -1208,9 +1208,7 @@ s = s.encode('cp1252', 'replace') if ismath: - fontsize = prop.get_size_in_points() - w, h, pswriter, used_characters = math_parse_s_pdf( - s, 72, fontsize, 0) + w, h, pswriter, used_characters = math_parse_s_pdf(s, 72, prop, 0) elif rcParams['pdf.use14corefonts']: font = self._get_font_afm(prop) Modified: trunk/matplotlib/lib/matplotlib/backends/backend_ps.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_ps.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/backends/backend_ps.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -278,7 +278,7 @@ if ismath: width, height, pswriter, used_characters = math_parse_s_ps( - s, 72, prop.get_size_in_points(), 0) + s, 72, prop, 0) return width, height if rcParams['ps.useafm']: @@ -813,11 +813,9 @@ if debugPS: self._pswriter.write("% mathtext\n") - fontsize = prop.get_size_in_points() width, height, pswriter, used_characters = \ - math_parse_s_ps(s, 72, fontsize, angle) + math_parse_s_ps(s, 72, prop, angle) self.merge_used_characters(used_characters) - self.set_color(*gc.get_rgb()) thetext = pswriter.getvalue() ps = """gsave @@ -1038,13 +1036,14 @@ print >>fh, l.strip() if not rcParams['ps.useafm']: for font_filename, chars in renderer.used_characters.values(): - font = FT2Font(font_filename) - cmap = font.get_charmap() - glyph_ids = [] - for c in chars: - gind = cmap.get(ord(c)) or 0 - glyph_ids.append(gind) - convert_ttf_to_ps(font_filename, fh, rcParams['ps.fonttype'], glyph_ids) + if len(chars): + font = FT2Font(font_filename) + cmap = font.get_charmap() + glyph_ids = [] + for c in chars: + gind = cmap.get(ord(c)) or 0 + glyph_ids.append(gind) + convert_ttf_to_ps(font_filename, fh, rcParams['ps.fonttype'], glyph_ids) print >>fh, "end" print >>fh, "%%EndProlog" Modified: trunk/matplotlib/lib/matplotlib/backends/backend_svg.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_svg.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/backends/backend_svg.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -291,9 +291,12 @@ self._svgwriter.write (svg) def _add_char_def(self, prop, char): - newprop = prop.copy() - newprop.set_size(self.FONT_SCALE) - font = self._get_font(newprop) + if isinstance(prop, FontProperties): + newprop = prop.copy() + font = self._get_font(newprop) + else: + font = prop + font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1,0,0,6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) if char_id in self._char_defs: @@ -332,10 +335,10 @@ """ Draw math text using matplotlib.mathtext """ - fontsize = prop.get_size_in_points() - width, height, svg_elements = math_parse_s_ft2font_svg(s, 72, fontsize) + width, height, svg_elements, used_characters = \ + math_parse_s_ft2font_svg(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs - svg_lines = svg_elements.svg_lines + svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()) self.open_group("mathtext") @@ -349,10 +352,9 @@ svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') - for fontname, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: - prop = FontProperties(family=fontname, size=fontsize) - charid = self._add_char_def(prop, thetext) - + for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: + charid = self._add_char_def(font, thetext) + svg.append('<use xlink:href="#%s" transform="translate(%s, %s) scale(%s)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE)) svg.append('</g>\n') @@ -366,7 +368,7 @@ curr_x,curr_y = 0.0,0.0 - for fontname, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: + for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: if rcParams["mathtext.mathtext2"]: new_y = new_y_mtc - height else: @@ -392,13 +394,20 @@ svg.append('</text>\n') + if len(svg_rects): + svg.append('<g style="fill: black; stroke: none" transform="') + if angle != 0: + svg.append('translate(%f,%f) rotate(%1.1f)' + % (x,y,-angle) ) + else: + svg.append('translate(%f,%f)' % (x, y)) + svg.append('">\n') + + for x, y, width, height in svg_rects: + svg.append('<rect x="%s" y="%s" width="%s" height="%s" fill="black" stroke="none" />' % (x, -y + height, width, height)) + svg.append("</g>") + self._svgwriter.write (''.join(svg)) - rgbFace = gc.get_rgb() - for xmin, ymin, xmax, ymax in svg_lines: - newx, newy = x + xmin, y + height - ymax#-(ymax-ymin)/2#-height - self.draw_rectangle(gc, rgbFace, newx, newy, xmax-xmin, ymax-ymin) - #~ self.draw_line(gc, x + xmin, y + ymin,# - height, - #~ x + xmax, y + ymax)# - height) self.close_group("mathtext") def finish(self): @@ -417,8 +426,8 @@ def get_text_width_height(self, s, prop, ismath): if ismath: - width, height, trash = math_parse_s_ft2font_svg( - s, 72, prop.get_size_in_points()) + width, height, trash, used_characters = \ + math_parse_s_ft2font_svg(s, 72, prop) return width, height font = self._get_font(prop) font.set_text(s, 0.0) Modified: trunk/matplotlib/lib/matplotlib/mathtext.py =================================================================== --- trunk/matplotlib/lib/matplotlib/mathtext.py 2007-07-26 13:46:56 UTC (rev 3616) +++ trunk/matplotlib/lib/matplotlib/mathtext.py 2007-07-26 14:45:57 UTC (rev 3617) @@ -14,7 +14,7 @@ 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)$' + s = r'$\mathcal{R}\prod_{i=\alpha\mathcal{B}}^\infty a_i\sin(2 \pi f x_i)$' The fonts \cal, \rm, \it, and \tt are allowed. @@ -59,17 +59,10 @@ ^ use raw strings - The $ symbols must be the first and last symbols in the string. Eg, - you cannot do + Math and non-math can be interpresed in the same string. E.g., 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. @@ -77,6 +70,8 @@ Allowed TeX symbols: + [MGDTODO: This list is no longer exhaustive and needs to be updated] + \/ \Delta \Downarrow \Gamma \Im \LEFTangle \LEFTbrace \LEFTbracket \LEFTparen \Lambda \Leftarrow \Leftbrace \Leftbracket \Leftparen \Leftrightarrow \Omega \P \Phi \Pi \Psi \RIGHTangle \RIGHTbrace @@ -119,11 +114,16 @@ 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. + - Certainly there are some... +STATUS: + The *Unicode* classes were incomplete when I found them, and have + not been refactored to support intermingling of regular text and + math text yet. They are most likely broken. -- Michael Droettboom, July 2007 + Author : John Hunter <jdh...@ac...> + Michael Droettboom <md...@st...> + (rewrite based on TeX box layout algorithms) Copyright : John Hunter (2004,2005) License : matplotlib license (PSF compatible) @@ -132,26 +132,32 @@ import os, sys from cStringIO import StringIO from sets import Set +from warnings import warn 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 + StringStart, StringEnd, ParseFatalException, FollowedBy, Regex, \ + operatorPrecedence, opAssoc, ParseResults, Or, Suppress, oneOf from matplotlib.afm import AFM -from matplotlib.cbook import enumerate, iterable, Bunch, get_realpath_and_stat -from matplotlib.ft2font import FT2Font +from matplotlib.cbook import enumerate, iterable, Bunch, get_realpath_and_stat, \ + is_string_like +from matplotlib.ft2font import FT2Font, KERNING_UNFITTED 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 import get_data_path, rcParams +#################### + # symbols that have the sub and superscripts over/under -overunder = { r'\sum' : 1, - r'\int' : 1, - r'\prod' : 1, - r'\coprod' : 1, - } +overunder_symbols = { + 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 @@ -160,6 +166,8 @@ r'\angstrom' : ( ('rm', 'A', 1.0), (None, '\circ', 0.5), 0.0 ), } +############################################################################## +# FONTS def font_open(filename): ext = filename.rsplit('.',1)[1] @@ -224,7 +232,7 @@ 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. @@ -264,7 +272,13 @@ def render(self, ox, oy, facename, sym, fontsize, dpi): pass - + def render_rect_filled(self, x1, y1, x2, y2): + pass + + def get_used_characters(self): + return {} + + class DummyFonts(Fonts): 'dummy class for debugging parser' def get_metrics(self, font, sym, fontsize, dpi): @@ -475,6 +489,8 @@ 'rm' : 'cmr10.ttf', 'tt' : 'cmtt10.ttf', 'it' : 'cmmi10.ttf', + 'bf' : 'cmb10.ttf', + 'sf' : 'cmss10.ttf', None : 'cmmi10.ttf', } @@ -542,78 +558,108 @@ # Old classes -class BakomaTrueTypeFonts(Fonts): +class BakomaFonts(Fonts): """ Use the Bakoma true type fonts for rendering """ - fnames = ('cmmi10', 'cmsy10', 'cmex10', - 'cmtt10', 'cmr10') # allocate a new set of fonts basepath = os.path.join( get_data_path(), 'fonts', 'ttf' ) - fontmap = { 'cal' : 'cmsy10', - 'rm' : 'cmr10', - 'tt' : 'cmtt10', - 'it' : 'cmmi10', - None : 'cmmi10', + fontmap = { 'cal' : 'Cmsy10', + 'rm' : 'Cmr10', + 'tt' : 'Cmtt10', + 'it' : 'Cmmi10', + 'bf' : 'Cmb10', + 'sf' : 'Cmss10', + None : 'Cmmi10', + 'ex' : 'Cmex10' } - def __init__(self, useSVG=False): - self.glyphd = {} - self.fonts = dict( - [ (name, FT2Font(os.path.join(self.basepath, name) + '.ttf')) - for name in self.fnames]) + class CachedFont: + def __init__(self, font): + self.font = font + self.charmap = font.get_charmap() + self.glyphmap = dict( + [(glyphind, ccode) for ccode, glyphind in self.charmap.items()]) + + def __init__(self): + self.glyphd = {} + self.fonts = {} + self.used_characters = {} - self.charmaps = dict( - [ (name, self.fonts[name].get_charmap()) for name in self.fnames]) - # glyphmaps is a dict names to a dict of glyphindex -> charcode - self.glyphmaps = {} - for name in self.fnames: - cmap = self.charmaps[name] - self.glyphmaps[name] = dict([(glyphind, ccode) for ccode, glyphind in cmap.items()]) + def _get_font(self, font): + """Looks up a CachedFont with its charmap and inverse charmap. + font may be a TeX font name (cal, rm, it etc.), a Computer Modern + font name (cmtt10, cmr10, etc.) or an FT2Font object.""" + if isinstance(font, str): + if font not in self.fontmap.values(): + basename = self.fontmap[font] + else: + basename = font + else: + basename = font.postscript_name - 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 + cached_font = self.fonts.get(basename) + if cached_font is None: + if isinstance(font, str): + font = FT2Font(os.path.join(self.basepath, basename.lower() + ".ttf")) + basename = font.postscript_name + cached_font = self.CachedFont(font) + self.fonts[basename] = cached_font + return basename, cached_font + def get_font(self, font): + return self._get_font(font)[1].font + + def get_fonts(self): + return [x.font for x in self.fonts.values()] + def get_metrics(self, font, sym, fontsize, dpi): - cmfont, metrics, glyph, offset = \ + basename, font, metrics, symbol_name, num, glyph, offset = \ self._get_info(font, sym, fontsize, dpi) return metrics + def _get_offset(self, basename, cached_font, glyph, fontsize, dpi): + if basename.lower() == 'cmex10': + return glyph.height/64.0/2 + 256.0/64.0*dpi/72.0 + return 0. + def _get_info (self, font, sym, fontsize, dpi): 'load the cmfont, metrics and glyph with caching' - key = font, sym, fontsize, dpi + if hasattr(font, 'postscript_name'): + fontname = font.postscript_name + else: + fontname = font + + key = fontname, 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): + + if font in self.fontmap and latex_to_bakoma.has_key(sym): basename, num = latex_to_bakoma[sym] - num = self.glyphmaps[basename][num] + basename, cached_font = self._get_font(basename.capitalize()) + symbol_name = cached_font.font.get_glyph_name(num) + num = cached_font.glyphmap[num] elif len(sym) == 1: + basename, cached_font = self._get_font(font) num = ord(sym) + symbol_name = cached_font.font.get_glyph_name(cached_font.charmap[num]) 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) + font = cached_font.font + font.set_size(fontsize, dpi) + glyph = font.load_char(num) + realpath, stat_key = get_realpath_and_stat(font.fname) + used_characters = self.used_characters.setdefault( + stat_key, (realpath, Set())) + used_characters[1].update(unichr(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. + offset = self._get_offset(basename, cached_font, glyph, fontsize, dpi) metrics = Bunch( advance = glyph.linearHoriAdvance/65536.0, height = glyph.height/64.0, @@ -622,211 +668,120 @@ xmax = xmax, ymin = ymin+offset, ymax = ymax+offset, + # iceberg is the equivalent of TeX's "height" + iceberg = glyph.horiBearingY/64.0 + offset ) - self.glyphd[key] = cmfont, metrics, glyph, offset + self.glyphd[key] = basename, font, metrics, symbol_name, num, 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)) + for cached_font in self.fonts.values(): + cached_font.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) + basename, font, metrics, symbol_name, num, 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.glyphmaps[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)) + font.draw_glyph_to_bitmap( + int(ox), int(oy - metrics.ymax), glyph) + def render_rect_filled(self, x1, y1, x2, y2): + assert len(self.fonts) + font = self.fonts.values()[0] + font.font.draw_rect_filled(x1, y1, max(x2 - 1, x1), max(y2 - 1, y1)) + + def get_used_characters(self): + return self.used_characters - def _old_get_kern(self, font, symleft, symright, fontsize, dpi): - """ - Get the kerning distance for font between symleft and symright. + def get_xheight(self, font, fontsize, dpi): + basename, cached_font = self._get_font(font) + cached_font.font.set_size(fontsize, dpi) + pclt = cached_font.font.get_sfnt_table('pclt') + xHeight = pclt['xHeight'] / 64.0 + return xHeight - font is one of tt, it, rm, cal or None + def get_underline_thickness(self, font, fontsize, dpi): + basename, cached_font = self._get_font(font) + cached_font.font.set_size(fontsize, dpi) + return max(1.0, cached_font.font.underline_thickness / 64.0) - 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.glyphmaps[basename][num] - elif len(sym) == 1: - num = ord(sym) - else: - num = 0 - return num - - -class BakomaPSFonts(Fonts): + def get_kern(self, fontleft, symleft, fontsizeleft, + fontright, symright, fontsizeright, dpi): + if fontsizeleft == fontsizeright: + basename, font1, metrics, symbol_name, num1, glyph1, offset = \ + self._get_info(fontleft, symleft, fontsizeleft, dpi) + basename, font2, metrics, symbol_name, num2, glyph2, offset = \ + self._get_info(fontright, symright, fontsizeright, dpi) + if font1 == font2: + basename, font = self._get_font(font1) + return font.font.get_kerning(num1, num2, KERNING_UNFITTED) / 64.0 + return 0.0 + +class BakomaPSFonts(BakomaFonts): """ Use the Bakoma postscript fonts for rendering to backend_ps """ - facenames = ('cmmi10', 'cmsy10', 'cmex10', - 'cmtt10', 'cmr10') - # allocate a new set of fonts - basepath = os.path.join( get_data_path(), 'fonts', 'ttf' ) - 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.facenames]) - - self.glyphmaps = {} - for facename in self.facenames: - charmap = self.fonts[facename].get_charmap() - self.glyphmaps[facename] = dict([(glyphind, charcode) - for charcode, glyphind in charmap.items()]) - for font in self.fonts.values(): - font.clear() - - self.used_characters = {} - - 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.glyphmaps[basename][num] - elif len(sym) == 1: - num = ord(sym) - else: - num = 0 - #sym = '.notdef' - raise ValueError('unrecognized symbol "%s, %d"' % (sym, num)) - - cmfont = self.fonts[basename] - cmfont.set_size(fontsize, dpi) - head = cmfont.get_sfnt_table('head') - glyph = cmfont.load_char(num) - - realpath, stat_key = get_realpath_and_stat(cmfont.fname) - used_characters = self.used_characters.setdefault( - stat_key, (realpath, Set())) - used_characters[1].update(unichr(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 = \ + basename, font, metrics, symbol_name, num, glyph, offset = \ self._get_info(font, sym, fontsize, dpi) - fontname = fontname.capitalize() - if fontname == 'Cmex10': - oy += offset - 512/2048.*10. - - ps = """/%(fontname)s findfont + oy = self.height - oy + offset + + ps = """/%(basename)s findfont %(fontsize)s scalefont setfont %(ox)f %(oy)f moveto -/%(glyphname)s glyphshow +/%(symbol_name)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 - + def render_rect_filled(self, x1, y1, x2, y2): + ps = "%f %f %f %f rectfill" % (x1, self.height - y2, x2 - x1, y2 - y1) + self.pswriter.write(ps) + 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] + def render(self, ox, oy, font, sym, fontsize, dpi): + basename, font, metrics, symbol_name, num, glyph, offset = \ + self._get_info(font, sym, fontsize, dpi) + filename = font.fname + oy = self.height - oy + offset - if latex_to_bakoma.has_key(sym): - basename, num = latex_to_bakoma[sym] - sym = self.fonts[basename].get_glyph_name(num) - num = self.glyphmaps[basename][num] - elif len(sym) == 1: - num = ord(sym) - else: - num = 0 - raise ValueError('unrecognized symbol "%s"' % (sym,)) + self.pswriter.append(('glyph', ox, oy, filename, fontsize, num)) - return os.path.join(self.basepath, basename) + '.ttf', num - + def render_rect_filled(self, x1, y1, x2, y2): + self.pswriter.append(('rect', x1, self.height - y2, x2 - x1, y2 - y1)) + +class BakomaSVGFonts(BakomaFonts): + """Hack of BakomaFonts for SVG support.""" + def __init__(self): + BakomaFonts.__init__(self) + self.svg_glyphs = [] + self.svg_rects = [] + def render(self, ox, oy, font, sym, fontsize, dpi): - fontname, metrics, glyphname, offset = \ + basename, font, metrics, symbol_name, num, glyph, 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)) + oy = self.height - oy + offset + thetext = unichr(num) + thetext.encode('utf-8') + self.svg_glyphs.append((font, fontsize, thetext, ox, oy, metrics)) - + def render_rect_filled(self, x1, y1, x2, y2): + self.svg_rects.append((x1, self.height - y2, x2 - x1, y2 - y1)) + class StandardPSFonts(Fonts): """ Use the standard postscript fonts for rendering to backend_ps @@ -835,21 +790,51 @@ # allocate a new set of fonts basepath = os.path.join( get_data_path(), 'fonts', 'afm' ) - fontmap = { 'cal' : 'pzcmi8a', - 'rm' : 'pncr8a', - 'tt' : 'pcrr8a', - 'it' : 'pncri8a', + fontmap = { 'cal' : 'pzcmi8a', # Zapf Chancery + 'rm' : 'pncr8a', # New Century Schoolbook + 'tt' : 'pcrr8a', # Courier + 'it' : 'pncri8a', # New Century Schoolbook Italic + 'sf' : 'phvr8a', # Helvetica + 'bf' : 'pncb8a', # New Century Schoolbook Bold + None : 'psyr' # Symbol } def __init__(self): self.glyphd = {} - self.fonts = dict( - [ (name, AFM(file(os.path.join(self.basepath, name) + '.afm'))) - for name in self.fnames]) + self.fonts = {} + def _get_font(self, font): + if isinstance(font, str): + if font not in self.fontmap.values(): + basename = self.fontmap[font] + else: + basename = font + else: + basename = font.get_fontname() + + cached_font = self.fonts.get(basename) + if cached_font is None: + if isinstance(font, str): + fname = os.path.join(self.basepath, basename + ".afm") + cached_font = AFM(file(fname, 'r')) + cached_font.fname = fname + basename = cached_font.get_fontname() + else: + cached_font = font + self.fonts[basename] = cached_font + return basename, cached_font + + def get_fonts(self): + return [x.font for x in self.fonts.values()] + def _get_info (self, font, sym, fontsize, dpi): 'load the cmfont, metrics and glyph with caching' - key = font, sym, fontsize, dpi + if hasattr(font, 'get_fontname'): + fontname = font.get_fontname() + else: + fontname = font + + key = fontname, sym, fontsize, dpi tup = self.glyphd.get(key) if tup is not None: @@ -857,41 +842,42 @@ 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) + font, num = latex_to_standard[sym] + glyph = chr(num) elif len(sym) == 1: - char = sym + glyph = sym + num = ord(glyph) else: raise ValueError('unrecognized symbol "%s"' % (sym)) - + basename, font = self._get_font(font) + try: - sym = self.fonts[basename].get_name_char(char) + symbol_name = font.get_name_char(glyph) 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)] + for val in font.get_bbox_char(glyph)] metrics = Bunch( advance = (xmax-xmin), - width = cmfont.get_width_char(char) * scale, - height = cmfont.get_width_char(char) * scale, + width = font.get_width_char(glyph) * scale, + height = font.get_height_char(glyph) * scale, xmin = xmin, xmax = xmax, ymin = ymin+offset, - ymax = ymax+offset + ymax = ymax+offset, + # iceberg is the equivalent of TeX's "height" + iceberg = ymax + offset ) - self.glyphd[key] = fontname, basename, metrics, sym, offset, char - return fontname, basename, metrics, '/'+sym, offset, char + self.glyphd[key] = basename, font, metrics, symbol_name, num, glyph, offset + return self.glyphd[key] def set_canvas_size(self, w, h, pswriter): 'Dimension the drawing canvas; may be a noop' @@ -899,637 +885,1292 @@ self.height = h self.pswriter = pswriter - def render(self, ox, oy, font, sym, fontsize, dpi): - fontname, basename, metrics, glyphname, offset, char = \ + basename, font, metrics, symbol_name, num, glyph, offset = \ self._get_info(font, sym, fontsize, dpi) - ps = """/%(fontname)s findfont + oy = self.height - oy + ps = """/%(basename)s findfont %(fontsize)s scalefont setfont %(ox)f %(oy)f moveto -/%(glyphname)s glyphshow +/%(symbol_name)s glyphshow """ % locals() self.pswriter.write(ps) def get_metrics(self, font, sym, fontsize, dpi): - fontname, basename, metrics, sym, offset, char = \ + basename, font, metrics, symbol_name, num, glyph, offset = \ 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 + def get_kern(self, fontleft, symleft, fontsizeleft, + fontright, symright, fontsizeright, dpi): + if fontsizeleft == fontsizeright: + basename, font1, metrics, symbol_name, num, glyph1, offset = \ + self._get_info(fontleft, symleft, fontsizeleft, dpi) + basename, font2, metrics, symbol_name, num, glyph2, offset = \ + self._get_info(fontright, symright, fontsizeright, dpi) + if font1.get_fontname() == font2.get_fontname(): + basename, font = self._get_font(font1) + return font.get_kern_dist(glyph1, glyph2) * 0.001 * fontsizeleft + return 0.0 -class Element: - fontsize = 12 - dpi = 72 - font = 'it' - _padx, _pady = 2, 2 # the x and y padding in points - _scale = 1.0 + def render_rect_filled(self, x1, y1, x2, y2): + ps = "%f %f %f %f rectfill" % (x1, self.height - y2, x2 - x1, y2 - y1) + self.pswriter.write(ps) - 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 get_xheight(self, font, fontsize, dpi): + basename, cached_font = self._get_font(font) + return cached_font.get_xheight() * 0.001 * fontsize - def advance(self): - 'get the horiz advance' - raise NotImplementedError('derived must override') + def get_underline_thickness(self, font, fontsize, dpi): + basename, cached_font = self._get_font(font) + return cached_font.get_underline_thickness() * 0.001 * fontsize + +############################################################################## +# TeX-LIKE BOX MODEL - def height(self): - 'get the element height: ymax-ymin' - raise NotImplementedError('derived must override') +# The following is based directly on the document 'woven' from the +# TeX82 source code. This information is also available in printed +# form: +# +# Knuth, Donald E.. 1986. Computers and Typesetting, Volume B: +# TeX: The Program. Addison-Wesley Professional. +# +# The most relevant "chapters" are: +# Data structures for boxes and their friends +# Shipping pages out (Ship class) +# Packaging (hpack and vpack) +# Data structures for math mode +# Subroutines for math mode +# Typesetting math formulas +# +# Many of the docstrings below refer to a numbered "node" in that +# book, e.g. @123 +# +# Note that (as TeX) y increases downward, unlike many other parts of +# matplotlib. - def width(self): - 'get the element width: xmax-xmin' - raise NotImplementedError('derived must override') +# How much text shrinks when going to the next-smallest level +SHRINK_FACTOR = 0.7 +# The number of different sizes of chars to use, beyond which they will not +# get any smaller +NUM_SIZE_LEVELS = 3 +# Percentage of x-height that subscripts drop below the baseline +SUBDROP = 0.05 +# Percentage of x-height that superscripts drop below the baseline +SUP1 = 0.2 +# Percentage of x-height that subscripts drop below the baseline +SUB1 = 0.3 +# Percentage of x-height that superscripts are offset relative to the subscript +DELTA = 0.05 + +class MathTextWarning(Warning): + pass + +class Node(object): + """A node in a linked list. + @133 + """ + def __init__(self): + self.link = None + self.size = 0 + + def __repr__(self): + s = self.__internal_repr__() + if self.link: + s += ' ' + self.link.__repr__() + return s - def xmin(self): - 'get the xmin of ink rect' - raise NotImplementedError('derived must override') + def __internal_repr__(self): + return self.__class__.__name__ - def xmax(self): - 'get the xmax of ink rect' - raise NotImplementedError('derived must override') + def get_kerning(self, next): + return 0.0 - def ymin(self): - 'get the ymin of ink rect' - raise NotImplementedError('derived must override') + def set_link(self, other): + self.link = other - def ymax(self): - 'get the ymax of ink rect' - raise NotImplementedError('derived must override') + def pack(self): + if self.link: + self.link.pack() - def set_font(self, font): - 'set the font (one of tt, it, rm , cal)' - raise NotImplementedError('derived must override') + def shrink(self): + """Shrinks one level smaller. There are only three levels of sizes, + after which things will no longer get smaller.""" + if self.link: + self.link.shrink() + self.size += 1 + + def render(self, x, y): + pass - def render(self): - 'render to the fonts canvas' - for element in self.neighbors.values(): - element.render() +class Box(Node): + """Represents any node with a physical location. + @135""" + def __init__(self, width, height, depth): + Node.__init__(self) + self.width = width + self.height = height + self.depth = depth - def set_origin(self, ox, oy): - self.ox, self.oy = ox, oy + def shrink(self): + Node.shrink(self) + if self.size < NUM_SIZE_LEVELS: + if self.width is not None: + self.width *= SHRINK_FACTOR + if self.height is not None: + self.height *= SHRINK_FACTOR + if self.depth is not None: + self.depth *= SHRINK_FACTOR - # 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 + def render(self, x1, y1, x2, y2): + pass - 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()) +class Vbox(Box): + def __init__(self, height, depth): + Box.__init__(self, 0., height, depth) - ny = self.oy - element.set_origin(nx, ny) +class Hbox(Box): + def __init__(self, width): + Box.__init__(self, width, 0., 0.) + +class Char(Node): + """Represents a single character. Unlike TeX, the font + information and metrics are stored with each Char to make it + easier to lookup the font metrics when needed. Note that TeX + boxes have a width, height, and depth, unlike Type1 and Truetype + which use a full bounding box and an advance in the x-direction. + The metrics must be converted to the TeX way, and the advance (if + different from width) must be converted into a Kern node when the + Char is added to its parent Hlist. + @134""" + def __init__(self, c, state): + Node.__init__(self) + self.c = c + self.font_output = state.font_output + self.font = state.font + self.fontsize = state.fontsize + self.dpi = state.dpi + # The real width, height and depth will be set during the + # pack phase, after we know the real fontsize + self._update_metrics() + + def __internal_repr__(self): + return '`%s`' % self.c - 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 _update_metrics(self): + metrics = self._metrics = self.font_output.get_metrics( + self.font, self.c, self.fontsize, self.dpi) + self.width = metrics.width + self.height = metrics.iceberg + self.depth = -(metrics.iceberg - metrics.height) + + def get_kerning(self, next): + """Return the amount of kerning between this and the given + character. Called when characters are strung together into + Hlists to create Kern nodes.""" + advance = self._metrics.advance - self.width + kern = 0. + if isinstance(next, Char): + kern = self.font_output.get_kern(self.font, self.c, self.fontsize, next.font, next.c, next.fontsize, self.dpi) + return advance + kern + + def render(self, x, y): + """Render the character to the canvas""" + self.font_output.render( + x, y, + self.font, self.c, self.fontsize, self.dpi) - def pady(self): - return self.dpi/72.0*self._pady + def shrink(self): + Node.shrink(self) + if self.size < NUM_SIZE_LEVELS: + self.fontsize *= SHRINK_FACTOR + self._update_metrics() - def padx(self): - return self.dpi/72.0*self._padx +class Accent(Char): + """The font metrics need to be dealt with differently for accents, since they + are already offset correctly from the baseline in TrueType fonts.""" + def _update_metrics(self): + metrics = self._metrics = self.font_output.get_metrics( + self.font, self.c, self.fontsize, self.dpi) + self.width = metrics.width + self.height = metrics.ymax - metrics.ymin + self.depth = 0 - def set_padx(self, pad): - 'set the y padding in points' - self._padx = pad + def render(self, x, y): + """Render the character to the canvas""" + self.font_output.render( + x, y + (self._metrics.ymax - self.height), + self.font, self.c, self.fontsize, self.dpi) + +class List(Box): + """A list of nodes (either horizontal or vertical). + @135""" + def __init__(self, elements): + Box.__init__(self, 0., 0., 0.) + self.shift_amount = 0. # An arbitrary offset + self.list_head = None # The head of a linked list of Nodes in this box + # The following parameters are set in the vpack and hpack functions + self.glue_set = 0. # The glue setting of this list + self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching + self.glue_order = 0 # The order of infinity (0 - 3) for the glue + + # Convert the Python list to a linked list + if len(elements): + elem = self.list_head = elements[0] + for next in elements[1:]: + elem.set_link(next) + elem = next - def set_pady(self, pad): - 'set the y padding in points' - self._pady = pad + def ... [truncated message content] |