12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500 |
- r"""
- :mod:`~matplotlib.mathtext` is a module for parsing a subset of the
- TeX math syntax and drawing them to a matplotlib backend.
- For a tutorial of its usage see :doc:`/tutorials/text/mathtext`. This
- document is primarily concerned with implementation details.
- The module uses pyparsing_ to parse the TeX expression.
- .. _pyparsing: http://pyparsing.wikispaces.com/
- The Bakoma distribution of the TeX Computer Modern fonts, and STIX
- fonts are supported. There is experimental support for using
- arbitrary fonts, but results may vary without proper tweaking and
- metrics for those fonts.
- """
- from collections import namedtuple
- import functools
- from io import StringIO
- import logging
- import os
- import types
- import unicodedata
- import numpy as np
- from pyparsing import (
- Combine, Empty, FollowedBy, Forward, Group, Literal, oneOf, OneOrMore,
- Optional, ParseBaseException, ParseFatalException, ParserElement,
- QuotedString, Regex, StringEnd, Suppress, ZeroOrMore)
- from matplotlib import cbook, colors as mcolors, rcParams
- from matplotlib.afm import AFM
- from matplotlib.cbook import get_realpath_and_stat
- from matplotlib.ft2font import FT2Image, KERNING_DEFAULT, LOAD_NO_HINTING
- from matplotlib.font_manager import findfont, FontProperties, get_font
- from matplotlib._mathtext_data import (latex_to_bakoma, latex_to_standard,
- tex2uni, latex_to_cmex,
- stix_virtual_fonts)
- ParserElement.enablePackrat()
- _log = logging.getLogger(__name__)
- ##############################################################################
- # FONTS
- def get_unicode_index(symbol, math=True):
- r"""
- Return the integer index (from the Unicode table) of *symbol*.
- Parameters
- ----------
- symbol : str
- A single unicode character, a TeX command (e.g. r'\pi') or a Type1
- symbol name (e.g. 'phi').
- math : bool, default is True
- If False, always treat as a single unicode character.
- """
- # for a non-math symbol, simply return its unicode index
- if not math:
- return ord(symbol)
- # From UTF #25: U+2212 minus sign is the preferred
- # representation of the unary and binary minus sign rather than
- # the ASCII-derived U+002D hyphen-minus, because minus sign is
- # unambiguous and because it is rendered with a more desirable
- # length, usually longer than a hyphen.
- if symbol == '-':
- return 0x2212
- 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:
- raise ValueError(
- "'{}' is not a valid Unicode character or TeX/Type1 symbol"
- .format(symbol))
- class MathtextBackend:
- """
- The base class for the mathtext backend-specific code. The
- purpose of :class:`MathtextBackend` subclasses is to interface
- between mathtext and a specific matplotlib graphics backend.
- Subclasses need to override the following:
- - :meth:`render_glyph`
- - :meth:`render_rect_filled`
- - :meth:`get_results`
- And optionally, if you need to use a FreeType hinting style:
- - :meth:`get_hinting_type`
- """
- def __init__(self):
- self.width = 0
- self.height = 0
- self.depth = 0
- def set_canvas_size(self, w, h, d):
- 'Dimension the drawing canvas'
- self.width = w
- self.height = h
- self.depth = d
- def render_glyph(self, ox, oy, info):
- """
- Draw a glyph described by *info* to the reference point (*ox*,
- *oy*).
- """
- raise NotImplementedError()
- def render_rect_filled(self, x1, y1, x2, y2):
- """
- Draw a filled black rectangle from (*x1*, *y1*) to (*x2*, *y2*).
- """
- raise NotImplementedError()
- def get_results(self, box):
- """
- Return a backend-specific tuple to return to the backend after
- all processing is done.
- """
- raise NotImplementedError()
- def get_hinting_type(self):
- """
- Get the FreeType hinting type to use with this particular
- backend.
- """
- return LOAD_NO_HINTING
- class MathtextBackendAgg(MathtextBackend):
- """
- Render glyphs and rectangles to an FTImage buffer, which is later
- transferred to the Agg image by the Agg backend.
- """
- def __init__(self):
- self.ox = 0
- self.oy = 0
- self.image = None
- self.mode = 'bbox'
- self.bbox = [0, 0, 0, 0]
- MathtextBackend.__init__(self)
- def _update_bbox(self, x1, y1, x2, y2):
- self.bbox = [min(self.bbox[0], x1),
- min(self.bbox[1], y1),
- max(self.bbox[2], x2),
- max(self.bbox[3], y2)]
- def set_canvas_size(self, w, h, d):
- MathtextBackend.set_canvas_size(self, w, h, d)
- if self.mode != 'bbox':
- self.image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0)))
- def render_glyph(self, ox, oy, info):
- if self.mode == 'bbox':
- self._update_bbox(ox + info.metrics.xmin,
- oy - info.metrics.ymax,
- ox + info.metrics.xmax,
- oy - info.metrics.ymin)
- else:
- info.font.draw_glyph_to_bitmap(
- self.image, ox, oy - info.metrics.iceberg, info.glyph,
- antialiased=rcParams['text.antialiased'])
- def render_rect_filled(self, x1, y1, x2, y2):
- if self.mode == 'bbox':
- self._update_bbox(x1, y1, x2, y2)
- else:
- height = max(int(y2 - y1) - 1, 0)
- if height == 0:
- center = (y2 + y1) / 2.0
- y = int(center - (height + 1) / 2.0)
- else:
- y = int(y1)
- self.image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height)
- def get_results(self, box, used_characters):
- self.mode = 'bbox'
- orig_height = box.height
- orig_depth = box.depth
- ship(0, 0, box)
- bbox = self.bbox
- bbox = [bbox[0] - 1, bbox[1] - 1, bbox[2] + 1, bbox[3] + 1]
- self.mode = 'render'
- self.set_canvas_size(
- bbox[2] - bbox[0],
- (bbox[3] - bbox[1]) - orig_depth,
- (bbox[3] - bbox[1]) - orig_height)
- ship(-bbox[0], -bbox[1], box)
- result = (self.ox,
- self.oy,
- self.width,
- self.height + self.depth,
- self.depth,
- self.image,
- used_characters)
- self.image = None
- return result
- def get_hinting_type(self):
- from matplotlib.backends import backend_agg
- return backend_agg.get_hinting_flag()
- class MathtextBackendBitmap(MathtextBackendAgg):
- def get_results(self, box, used_characters):
- ox, oy, width, height, depth, image, characters = \
- MathtextBackendAgg.get_results(self, box, used_characters)
- return image, depth
- class MathtextBackendPs(MathtextBackend):
- """
- Store information to write a mathtext rendering to the PostScript backend.
- """
- _PSResult = namedtuple(
- "_PSResult", "width height depth pswriter used_characters")
- def __init__(self):
- self.pswriter = StringIO()
- self.lastfont = None
- def render_glyph(self, ox, oy, info):
- oy = self.height - oy + info.offset
- postscript_name = info.postscript_name
- fontsize = info.fontsize
- symbol_name = info.symbol_name
- if (postscript_name, fontsize) != self.lastfont:
- self.lastfont = postscript_name, fontsize
- self.pswriter.write(
- f"/{postscript_name} findfont\n"
- f"{fontsize} scalefont\n"
- f"setfont\n")
- self.pswriter.write(
- f"{ox:f} {oy:f} moveto\n"
- f"/{symbol_name} glyphshow\n")
- def render_rect_filled(self, x1, y1, x2, y2):
- ps = "%f %f %f %f rectfill\n" % (
- x1, self.height - y2, x2 - x1, y2 - y1)
- self.pswriter.write(ps)
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- return self._PSResult(self.width,
- self.height + self.depth,
- self.depth,
- self.pswriter,
- used_characters)
- class MathtextBackendPdf(MathtextBackend):
- """Store information to write a mathtext rendering to the PDF backend."""
- _PDFResult = namedtuple(
- "_PDFResult", "width height depth glyphs rects used_characters")
- def __init__(self):
- self.glyphs = []
- self.rects = []
- def render_glyph(self, ox, oy, info):
- filename = info.font.fname
- oy = self.height - oy + info.offset
- self.glyphs.append(
- (ox, oy, filename, info.fontsize,
- info.num, info.symbol_name))
- def render_rect_filled(self, x1, y1, x2, y2):
- self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1))
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- return self._PDFResult(self.width,
- self.height + self.depth,
- self.depth,
- self.glyphs,
- self.rects,
- used_characters)
- class MathtextBackendSvg(MathtextBackend):
- """
- Store information to write a mathtext rendering to the SVG
- backend.
- """
- def __init__(self):
- self.svg_glyphs = []
- self.svg_rects = []
- def render_glyph(self, ox, oy, info):
- oy = self.height - oy + info.offset
- self.svg_glyphs.append(
- (info.font, info.fontsize, info.num, ox, oy, info.metrics))
- def render_rect_filled(self, x1, y1, x2, y2):
- self.svg_rects.append(
- (x1, self.height - y1 + 1, x2 - x1, y2 - y1))
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- svg_elements = types.SimpleNamespace(svg_glyphs=self.svg_glyphs,
- svg_rects=self.svg_rects)
- return (self.width,
- self.height + self.depth,
- self.depth,
- svg_elements,
- used_characters)
- class MathtextBackendPath(MathtextBackend):
- """
- Store information to write a mathtext rendering to the text path
- machinery.
- """
- def __init__(self):
- self.glyphs = []
- self.rects = []
- def render_glyph(self, ox, oy, info):
- oy = self.height - oy + info.offset
- thetext = info.num
- self.glyphs.append(
- (info.font, info.fontsize, thetext, ox, oy))
- def render_rect_filled(self, x1, y1, x2, y2):
- self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1))
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- return (self.width,
- self.height + self.depth,
- self.depth,
- self.glyphs,
- self.rects)
- class MathtextBackendCairo(MathtextBackend):
- """
- Store information to write a mathtext rendering to the Cairo
- backend.
- """
- def __init__(self):
- self.glyphs = []
- self.rects = []
- def render_glyph(self, ox, oy, info):
- oy = oy - info.offset - self.height
- thetext = chr(info.num)
- self.glyphs.append(
- (info.font, info.fontsize, thetext, ox, oy))
- def render_rect_filled(self, x1, y1, x2, y2):
- self.rects.append(
- (x1, y1 - self.height, x2 - x1, y2 - y1))
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- return (self.width,
- self.height + self.depth,
- self.depth,
- self.glyphs,
- self.rects)
- class Fonts:
- """
- An abstract base class for a system of fonts to use for mathtext.
- The class must be able to take symbol keys and font file names and
- return the character metrics. It also delegates to a backend class
- to do the actual drawing.
- """
- def __init__(self, default_font_prop, mathtext_backend):
- """
- *default_font_prop*: A
- :class:`~matplotlib.font_manager.FontProperties` object to use
- for the default non-math font, or the base font for Unicode
- (generic) font rendering.
- *mathtext_backend*: A subclass of :class:`MathTextBackend`
- used to delegate the actual rendering.
- """
- self.default_font_prop = default_font_prop
- self.mathtext_backend = mathtext_backend
- self.used_characters = {}
- def destroy(self):
- """
- Fix any cyclical references before the object is about
- to be destroyed.
- """
- self.used_characters = None
- def get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi):
- r"""
- Get the kerning distance for font between *sym1* and *sym2*.
- *fontX*: one of the TeX font names::
- tt, it, rm, cal, sf, bf or default/regular (non-math)
- *fontclassX*: TODO
- *symX*: a symbol in raw TeX form. e.g., '1', 'x' or '\sigma'
- *fontsizeX*: the fontsize in points
- *dpi*: the current dots-per-inch
- """
- return 0.
- def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True):
- r"""
- *font*: one of the TeX font names::
- tt, it, rm, cal, sf, bf or default/regular (non-math)
- *font_class*: TODO
- *sym*: a symbol in raw TeX form. e.g., '1', 'x' or '\sigma'
- *fontsize*: font size in points
- *dpi*: current dots-per-inch
- *math*: whether sym is a math character
- Returns an object with the following attributes:
- - *advance*: The advance distance (in points) of the glyph.
- - *height*: The height of the glyph in points.
- - *width*: The width of the glyph in points.
- - *xmin*, *xmax*, *ymin*, *ymax* - the ink rectangle of the glyph
- - *iceberg* - the distance from the baseline to the top of
- the glyph. This corresponds to TeX's definition of
- "height".
- """
- info = self._get_info(font, font_class, sym, fontsize, dpi, math)
- return info.metrics
- def set_canvas_size(self, w, h, d):
- """
- Set the size of the buffer used to render the math expression.
- Only really necessary for the bitmap backends.
- """
- self.width, self.height, self.depth = np.ceil([w, h, d])
- self.mathtext_backend.set_canvas_size(
- self.width, self.height, self.depth)
- def render_glyph(self, ox, oy, facename, font_class, sym, fontsize, dpi):
- """
- Draw a glyph at
- - *ox*, *oy*: position
- - *facename*: One of the TeX face names
- - *font_class*:
- - *sym*: TeX symbol name or single character
- - *fontsize*: fontsize in points
- - *dpi*: The dpi to draw at.
- """
- info = self._get_info(facename, font_class, sym, fontsize, dpi)
- realpath, stat_key = get_realpath_and_stat(info.font.fname)
- used_characters = self.used_characters.setdefault(
- stat_key, (realpath, set()))
- used_characters[1].add(info.num)
- self.mathtext_backend.render_glyph(ox, oy, info)
- def render_rect_filled(self, x1, y1, x2, y2):
- """
- Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*).
- """
- self.mathtext_backend.render_rect_filled(x1, y1, x2, y2)
- def get_xheight(self, font, fontsize, dpi):
- """
- Get the xheight for the given *font* and *fontsize*.
- """
- raise NotImplementedError()
- def get_underline_thickness(self, font, fontsize, dpi):
- """
- Get the line thickness that matches the given font. Used as a
- base unit for drawing lines such as in a fraction or radical.
- """
- raise NotImplementedError()
- def get_used_characters(self):
- """
- Get the set of characters that were used in the math
- expression. Used by backends that need to subset fonts so
- they know which glyphs to include.
- """
- return self.used_characters
- def get_results(self, box):
- """
- Get the data needed by the backend to render the math
- expression. The return value is backend-specific.
- """
- result = self.mathtext_backend.get_results(
- box, self.get_used_characters())
- self.destroy()
- return result
- def get_sized_alternatives_for_symbol(self, fontname, sym):
- """
- Override if your font provides multiple sizes of the same
- symbol. Should return a list of symbols matching *sym* in
- various sizes. The expression renderer will select the most
- appropriate size for a given situation from this list.
- """
- return [(fontname, sym)]
- class TruetypeFonts(Fonts):
- """
- A generic base class for all font setups that use Truetype fonts
- (through FT2Font).
- """
- def __init__(self, default_font_prop, mathtext_backend):
- Fonts.__init__(self, default_font_prop, mathtext_backend)
- self.glyphd = {}
- self._fonts = {}
- filename = findfont(default_font_prop)
- default_font = get_font(filename)
- self._fonts['default'] = default_font
- self._fonts['regular'] = default_font
- def destroy(self):
- self.glyphd = None
- Fonts.destroy(self)
- def _get_font(self, font):
- if font in self.fontmap:
- basename = self.fontmap[font]
- else:
- basename = font
- cached_font = self._fonts.get(basename)
- if cached_font is None and os.path.exists(basename):
- cached_font = get_font(basename)
- self._fonts[basename] = cached_font
- self._fonts[cached_font.postscript_name] = cached_font
- self._fonts[cached_font.postscript_name.lower()] = cached_font
- return cached_font
- def _get_offset(self, font, glyph, fontsize, dpi):
- if font.postscript_name == 'Cmex10':
- return ((glyph.height/64.0/2.0) + (fontsize/3.0 * dpi/72.0))
- return 0.
- def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True):
- key = fontname, font_class, sym, fontsize, dpi
- bunch = self.glyphd.get(key)
- if bunch is not None:
- return bunch
- font, num, symbol_name, fontsize, slanted = \
- self._get_glyph(fontname, font_class, sym, fontsize, math)
- font.set_size(fontsize, dpi)
- glyph = font.load_char(
- num,
- flags=self.mathtext_backend.get_hinting_type())
- xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox]
- offset = self._get_offset(font, glyph, fontsize, dpi)
- metrics = types.SimpleNamespace(
- 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,
- # iceberg is the equivalent of TeX's "height"
- iceberg = glyph.horiBearingY/64.0 + offset,
- slanted = slanted
- )
- result = self.glyphd[key] = types.SimpleNamespace(
- font = font,
- fontsize = fontsize,
- postscript_name = font.postscript_name,
- metrics = metrics,
- symbol_name = symbol_name,
- num = num,
- glyph = glyph,
- offset = offset
- )
- return result
- def get_xheight(self, fontname, fontsize, dpi):
- font = self._get_font(fontname)
- font.set_size(fontsize, dpi)
- pclt = font.get_sfnt_table('pclt')
- if pclt is None:
- # Some fonts don't store the xHeight, so we do a poor man's xHeight
- metrics = self.get_metrics(
- fontname, rcParams['mathtext.default'], 'x', fontsize, dpi)
- return metrics.iceberg
- xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
- return xHeight
- def get_underline_thickness(self, font, fontsize, dpi):
- # This function used to grab underline thickness from the font
- # metrics, but that information is just too un-reliable, so it
- # is now hardcoded.
- return ((0.75 / 12.0) * fontsize * dpi) / 72.0
- def get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi):
- if font1 == font2 and fontsize1 == fontsize2:
- info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
- info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
- font = info1.font
- return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64
- return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi)
- class BakomaFonts(TruetypeFonts):
- """
- Use the Bakoma TrueType fonts for rendering.
- Symbols are strewn about a number of font files, each of which has
- its own proprietary 8-bit encoding.
- """
- _fontmap = {
- 'cal': 'cmsy10',
- 'rm': 'cmr10',
- 'tt': 'cmtt10',
- 'it': 'cmmi10',
- 'bf': 'cmb10',
- 'sf': 'cmss10',
- 'ex': 'cmex10',
- }
- def __init__(self, *args, **kwargs):
- self._stix_fallback = StixFonts(*args, **kwargs)
- TruetypeFonts.__init__(self, *args, **kwargs)
- self.fontmap = {}
- for key, val in self._fontmap.items():
- fullpath = findfont(val)
- self.fontmap[key] = fullpath
- self.fontmap[val] = fullpath
- _slanted_symbols = set(r"\int \oint".split())
- def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
- symbol_name = None
- font = None
- if fontname in self.fontmap and sym in latex_to_bakoma:
- basename, num = latex_to_bakoma[sym]
- slanted = (basename == "cmmi10") or sym in self._slanted_symbols
- font = self._get_font(basename)
- elif len(sym) == 1:
- slanted = (fontname == "it")
- font = self._get_font(fontname)
- if font is not None:
- num = ord(sym)
- if font is not None:
- gid = font.get_char_index(num)
- if gid != 0:
- symbol_name = font.get_glyph_name(gid)
- if symbol_name is None:
- return self._stix_fallback._get_glyph(
- fontname, font_class, sym, fontsize, math)
- return font, num, symbol_name, fontsize, slanted
- # The Bakoma fonts contain many pre-sized alternatives for the
- # delimiters. The AutoSizedChar class will use these alternatives
- # and select the best (closest sized) glyph.
- _size_alternatives = {
- '(': [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
- ('ex', '\xb5'), ('ex', '\xc3')],
- ')': [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
- ('ex', '\xb6'), ('ex', '\x21')],
- '{': [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'),
- ('ex', '\xbd'), ('ex', '\x28')],
- '}': [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'),
- ('ex', '\xbe'), ('ex', '\x29')],
- # The fourth size of '[' is mysteriously missing from the BaKoMa
- # font, so I've omitted it for both '[' and ']'
- '[': [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'),
- ('ex', '\x22')],
- ']': [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'),
- ('ex', '\x23')],
- r'\lfloor': [('ex', '\xa5'), ('ex', '\x6a'),
- ('ex', '\xb9'), ('ex', '\x24')],
- r'\rfloor': [('ex', '\xa6'), ('ex', '\x6b'),
- ('ex', '\xba'), ('ex', '\x25')],
- r'\lceil': [('ex', '\xa7'), ('ex', '\x6c'),
- ('ex', '\xbb'), ('ex', '\x26')],
- r'\rceil': [('ex', '\xa8'), ('ex', '\x6d'),
- ('ex', '\xbc'), ('ex', '\x27')],
- r'\langle': [('ex', '\xad'), ('ex', '\x44'),
- ('ex', '\xbf'), ('ex', '\x2a')],
- r'\rangle': [('ex', '\xae'), ('ex', '\x45'),
- ('ex', '\xc0'), ('ex', '\x2b')],
- r'\__sqrt__': [('ex', '\x70'), ('ex', '\x71'),
- ('ex', '\x72'), ('ex', '\x73')],
- r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'),
- ('ex', '\xc2'), ('ex', '\x2d')],
- r'/': [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'),
- ('ex', '\xcb'), ('ex', '\x2c')],
- r'\widehat': [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'),
- ('ex', '\x64')],
- r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'),
- ('ex', '\x67')],
- r'<': [('cal', 'h'), ('ex', 'D')],
- r'>': [('cal', 'i'), ('ex', 'E')]
- }
- for alias, target in [(r'\leftparen', '('),
- (r'\rightparent', ')'),
- (r'\leftbrace', '{'),
- (r'\rightbrace', '}'),
- (r'\leftbracket', '['),
- (r'\rightbracket', ']'),
- (r'\{', '{'),
- (r'\}', '}'),
- (r'\[', '['),
- (r'\]', ']')]:
- _size_alternatives[alias] = _size_alternatives[target]
- def get_sized_alternatives_for_symbol(self, fontname, sym):
- return self._size_alternatives.get(sym, [(fontname, sym)])
- class UnicodeFonts(TruetypeFonts):
- """
- An abstract base class for handling Unicode fonts.
- While some reasonably complete Unicode fonts (such as DejaVu) may
- work in some situations, the only Unicode font I'm aware of with a
- complete set of math symbols is STIX.
- This class will "fallback" on the Bakoma fonts when a required
- symbol can not be found in the font.
- """
- use_cmex = True
- def __init__(self, *args, **kwargs):
- # This must come first so the backend's owner is set correctly
- if rcParams['mathtext.fallback_to_cm']:
- self.cm_fallback = BakomaFonts(*args, **kwargs)
- else:
- self.cm_fallback = None
- TruetypeFonts.__init__(self, *args, **kwargs)
- self.fontmap = {}
- for texfont in "cal rm tt it bf sf".split():
- prop = rcParams['mathtext.' + texfont]
- font = findfont(prop)
- self.fontmap[texfont] = font
- prop = FontProperties('cmex10')
- font = findfont(prop)
- self.fontmap['ex'] = font
- _slanted_symbols = set(r"\int \oint".split())
- def _map_virtual_font(self, fontname, font_class, uniindex):
- return fontname, uniindex
- def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
- found_symbol = False
- if self.use_cmex:
- uniindex = latex_to_cmex.get(sym)
- if uniindex is not None:
- fontname = 'ex'
- found_symbol = True
- if not found_symbol:
- try:
- uniindex = get_unicode_index(sym, math)
- found_symbol = True
- except ValueError:
- uniindex = ord('?')
- _log.warning(
- "No TeX to unicode mapping for {!a}.".format(sym))
- fontname, uniindex = self._map_virtual_font(
- fontname, font_class, uniindex)
- new_fontname = fontname
- # Only characters in the "Letter" class should be italicized in 'it'
- # mode. Greek capital letters should be Roman.
- if found_symbol:
- if fontname == 'it' and uniindex < 0x10000:
- char = chr(uniindex)
- if (unicodedata.category(char)[0] != "L"
- or unicodedata.name(char).startswith("GREEK CAPITAL")):
- new_fontname = 'rm'
- slanted = (new_fontname == 'it') or sym in self._slanted_symbols
- found_symbol = False
- font = self._get_font(new_fontname)
- if font is not None:
- glyphindex = font.get_char_index(uniindex)
- if glyphindex != 0:
- found_symbol = True
- if not found_symbol:
- if self.cm_fallback:
- if isinstance(self.cm_fallback, BakomaFonts):
- _log.warning(
- "Substituting with a symbol from Computer Modern.")
- if (fontname in ('it', 'regular') and
- isinstance(self.cm_fallback, StixFonts)):
- return self.cm_fallback._get_glyph(
- 'rm', font_class, sym, fontsize)
- else:
- return self.cm_fallback._get_glyph(
- fontname, font_class, sym, fontsize)
- else:
- if (fontname in ('it', 'regular')
- and isinstance(self, StixFonts)):
- return self._get_glyph('rm', font_class, sym, fontsize)
- _log.warning("Font {!r} does not have a glyph for {!a} "
- "[U+{:x}], substituting with a dummy "
- "symbol.".format(new_fontname, sym, uniindex))
- fontname = 'rm'
- font = self._get_font(fontname)
- uniindex = 0xA4 # currency char, for lack of anything better
- glyphindex = font.get_char_index(uniindex)
- slanted = False
- symbol_name = font.get_glyph_name(glyphindex)
- return font, uniindex, symbol_name, fontsize, slanted
- def get_sized_alternatives_for_symbol(self, fontname, sym):
- if self.cm_fallback:
- return self.cm_fallback.get_sized_alternatives_for_symbol(
- fontname, sym)
- return [(fontname, sym)]
- class DejaVuFonts(UnicodeFonts):
- use_cmex = False
- def __init__(self, *args, **kwargs):
- # This must come first so the backend's owner is set correctly
- if isinstance(self, DejaVuSerifFonts):
- self.cm_fallback = StixFonts(*args, **kwargs)
- else:
- self.cm_fallback = StixSansFonts(*args, **kwargs)
- self.bakoma = BakomaFonts(*args, **kwargs)
- TruetypeFonts.__init__(self, *args, **kwargs)
- self.fontmap = {}
- # Include Stix sized alternatives for glyphs
- self._fontmap.update({
- 1: 'STIXSizeOneSym',
- 2: 'STIXSizeTwoSym',
- 3: 'STIXSizeThreeSym',
- 4: 'STIXSizeFourSym',
- 5: 'STIXSizeFiveSym',
- })
- for key, name in self._fontmap.items():
- fullpath = findfont(name)
- self.fontmap[key] = fullpath
- self.fontmap[name] = fullpath
- def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
- # Override prime symbol to use Bakoma.
- if sym == r'\prime':
- return self.bakoma._get_glyph(
- fontname, font_class, sym, fontsize, math)
- else:
- # check whether the glyph is available in the display font
- uniindex = get_unicode_index(sym)
- font = self._get_font('ex')
- if font is not None:
- glyphindex = font.get_char_index(uniindex)
- if glyphindex != 0:
- return super()._get_glyph(
- 'ex', font_class, sym, fontsize, math)
- # otherwise return regular glyph
- return super()._get_glyph(
- fontname, font_class, sym, fontsize, math)
- class DejaVuSerifFonts(DejaVuFonts):
- """
- A font handling class for the DejaVu Serif fonts
- If a glyph is not found it will fallback to Stix Serif
- """
- _fontmap = {
- 'rm': 'DejaVu Serif',
- 'it': 'DejaVu Serif:italic',
- 'bf': 'DejaVu Serif:weight=bold',
- 'sf': 'DejaVu Sans',
- 'tt': 'DejaVu Sans Mono',
- 'ex': 'DejaVu Serif Display',
- 0: 'DejaVu Serif',
- }
- class DejaVuSansFonts(DejaVuFonts):
- """
- A font handling class for the DejaVu Sans fonts
- If a glyph is not found it will fallback to Stix Sans
- """
- _fontmap = {
- 'rm': 'DejaVu Sans',
- 'it': 'DejaVu Sans:italic',
- 'bf': 'DejaVu Sans:weight=bold',
- 'sf': 'DejaVu Sans',
- 'tt': 'DejaVu Sans Mono',
- 'ex': 'DejaVu Sans Display',
- 0: 'DejaVu Sans',
- }
- class StixFonts(UnicodeFonts):
- """
- A font handling class for the STIX fonts.
- In addition to what UnicodeFonts provides, this class:
- - supports "virtual fonts" which are complete alpha numeric
- character sets with different font styles at special Unicode
- code points, such as "Blackboard".
- - handles sized alternative characters for the STIXSizeX fonts.
- """
- _fontmap = {
- 'rm': 'STIXGeneral',
- 'it': 'STIXGeneral:italic',
- 'bf': 'STIXGeneral:weight=bold',
- 'nonunirm': 'STIXNonUnicode',
- 'nonuniit': 'STIXNonUnicode:italic',
- 'nonunibf': 'STIXNonUnicode:weight=bold',
- 0: 'STIXGeneral',
- 1: 'STIXSizeOneSym',
- 2: 'STIXSizeTwoSym',
- 3: 'STIXSizeThreeSym',
- 4: 'STIXSizeFourSym',
- 5: 'STIXSizeFiveSym',
- }
- use_cmex = False
- cm_fallback = False
- _sans = False
- def __init__(self, *args, **kwargs):
- TruetypeFonts.__init__(self, *args, **kwargs)
- self.fontmap = {}
- for key, name in self._fontmap.items():
- fullpath = findfont(name)
- self.fontmap[key] = fullpath
- self.fontmap[name] = fullpath
- def _map_virtual_font(self, fontname, font_class, uniindex):
- # Handle these "fonts" that are actually embedded in
- # other fonts.
- mapping = stix_virtual_fonts.get(fontname)
- if (self._sans and mapping is None
- and fontname not in ('regular', 'default')):
- mapping = stix_virtual_fonts['sf']
- doing_sans_conversion = True
- else:
- doing_sans_conversion = False
- if mapping is not None:
- if isinstance(mapping, dict):
- try:
- mapping = mapping[font_class]
- except KeyError:
- mapping = mapping['rm']
- # Binary search for the source glyph
- lo = 0
- hi = len(mapping)
- while lo < hi:
- mid = (lo+hi)//2
- range = mapping[mid]
- if uniindex < range[0]:
- hi = mid
- elif uniindex <= range[1]:
- break
- else:
- lo = mid + 1
- if range[0] <= uniindex <= range[1]:
- uniindex = uniindex - range[0] + range[3]
- fontname = range[2]
- elif not doing_sans_conversion:
- # This will generate a dummy character
- uniindex = 0x1
- fontname = rcParams['mathtext.default']
- # Handle private use area glyphs
- if fontname in ('it', 'rm', 'bf') and 0xe000 <= uniindex <= 0xf8ff:
- fontname = 'nonuni' + fontname
- return fontname, uniindex
- @functools.lru_cache()
- def get_sized_alternatives_for_symbol(self, fontname, sym):
- fixes = {
- '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']',
- '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}',
- '>': '\N{MATHEMATICAL RIGHT ANGLE BRACKET}',
- }
- sym = fixes.get(sym, sym)
- try:
- uniindex = get_unicode_index(sym)
- except ValueError:
- return [(fontname, sym)]
- alternatives = [(i, chr(uniindex)) for i in range(6)
- if self._get_font(i).get_char_index(uniindex) != 0]
- # The largest size of the radical symbol in STIX has incorrect
- # metrics that cause it to be disconnected from the stem.
- if sym == r'\__sqrt__':
- alternatives = alternatives[:-1]
- return alternatives
- class StixSansFonts(StixFonts):
- """
- A font handling class for the STIX fonts (that uses sans-serif
- characters by default).
- """
- _sans = True
- class StandardPsFonts(Fonts):
- """
- Use the standard postscript fonts for rendering to backend_ps
- Unlike the other font classes, BakomaFont and UnicodeFont, this
- one requires the Ps backend.
- """
- basepath = str(cbook._get_data_path('fonts/afm'))
- 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, default_font_prop):
- Fonts.__init__(self, default_font_prop, MathtextBackendPs())
- self.glyphd = {}
- self.fonts = {}
- filename = findfont(default_font_prop, fontext='afm',
- directory=self.basepath)
- if filename is None:
- filename = findfont('Helvetica', fontext='afm',
- directory=self.basepath)
- with open(filename, 'rb') as fd:
- default_font = AFM(fd)
- default_font.fname = filename
- self.fonts['default'] = default_font
- self.fonts['regular'] = default_font
- self.pswriter = StringIO()
- def _get_font(self, font):
- if font in self.fontmap:
- basename = self.fontmap[font]
- else:
- basename = font
- cached_font = self.fonts.get(basename)
- if cached_font is None:
- fname = os.path.join(self.basepath, basename + ".afm")
- with open(fname, 'rb') as fd:
- cached_font = AFM(fd)
- cached_font.fname = fname
- self.fonts[basename] = cached_font
- self.fonts[cached_font.get_fontname()] = cached_font
- return cached_font
- def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True):
- 'load the cmfont, metrics and glyph with caching'
- key = fontname, sym, fontsize, dpi
- tup = self.glyphd.get(key)
- if tup is not None:
- return tup
- # Only characters in the "Letter" class should really be italicized.
- # This class includes greek letters, so we're ok
- if (fontname == 'it' and
- (len(sym) > 1
- or not unicodedata.category(sym).startswith("L"))):
- fontname = 'rm'
- found_symbol = False
- if sym in latex_to_standard:
- fontname, num = latex_to_standard[sym]
- glyph = chr(num)
- found_symbol = True
- elif len(sym) == 1:
- glyph = sym
- num = ord(glyph)
- found_symbol = True
- else:
- _log.warning(
- "No TeX to built-in Postscript mapping for {!r}".format(sym))
- slanted = (fontname == 'it')
- font = self._get_font(fontname)
- if found_symbol:
- try:
- symbol_name = font.get_name_char(glyph)
- except KeyError:
- _log.warning(
- "No glyph in standard Postscript font {!r} for {!r}"
- .format(font.get_fontname(), sym))
- found_symbol = False
- if not found_symbol:
- glyph = '?'
- num = ord(glyph)
- symbol_name = font.get_name_char(glyph)
- offset = 0
- scale = 0.001 * fontsize
- xmin, ymin, xmax, ymax = [val * scale
- for val in font.get_bbox_char(glyph)]
- metrics = types.SimpleNamespace(
- advance = font.get_width_char(glyph) * 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,
- # iceberg is the equivalent of TeX's "height"
- iceberg = ymax + offset,
- slanted = slanted
- )
- self.glyphd[key] = types.SimpleNamespace(
- font = font,
- fontsize = fontsize,
- postscript_name = font.get_fontname(),
- metrics = metrics,
- symbol_name = symbol_name,
- num = num,
- glyph = glyph,
- offset = offset
- )
- return self.glyphd[key]
- def get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi):
- if font1 == font2 and fontsize1 == fontsize2:
- info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
- info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
- font = info1.font
- return (font.get_kern_dist(info1.glyph, info2.glyph)
- * 0.001 * fontsize1)
- return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi)
- def get_xheight(self, font, fontsize, dpi):
- font = self._get_font(font)
- return font.get_xheight() * 0.001 * fontsize
- def get_underline_thickness(self, font, fontsize, dpi):
- font = self._get_font(font)
- return font.get_underline_thickness() * 0.001 * fontsize
- ##############################################################################
- # TeX-LIKE BOX MODEL
- # 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., node123
- #
- # Note that (as TeX) y increases downward, unlike many other parts of
- # matplotlib.
- # How much text shrinks when going to the next-smallest level. GROW_FACTOR
- # must be the inverse of SHRINK_FACTOR.
- SHRINK_FACTOR = 0.7
- GROW_FACTOR = 1.0 / SHRINK_FACTOR
- # The number of different sizes of chars to use, beyond which they will not
- # get any smaller
- NUM_SIZE_LEVELS = 6
- class FontConstantsBase:
- """
- A set of constants that controls how certain things, such as sub-
- and superscripts are laid out. These are all metrics that can't
- be reliably retrieved from the font metrics in the font itself.
- """
- # Percentage of x-height of additional horiz. space after sub/superscripts
- script_space = 0.05
- # Percentage of x-height that sub/superscripts drop below the baseline
- subdrop = 0.4
- # Percentage of x-height that superscripts are raised from the baseline
- sup1 = 0.7
- # Percentage of x-height that subscripts drop below the baseline
- sub1 = 0.3
- # Percentage of x-height that subscripts drop below the baseline when a
- # superscript is present
- sub2 = 0.5
- # Percentage of x-height that sub/supercripts are offset relative to the
- # nucleus edge for non-slanted nuclei
- delta = 0.025
- # Additional percentage of last character height above 2/3 of the
- # x-height that supercripts are offset relative to the subscript
- # for slanted nuclei
- delta_slanted = 0.2
- # Percentage of x-height that supercripts and subscripts are offset for
- # integrals
- delta_integral = 0.1
- class ComputerModernFontConstants(FontConstantsBase):
- script_space = 0.075
- subdrop = 0.2
- sup1 = 0.45
- sub1 = 0.2
- sub2 = 0.3
- delta = 0.075
- delta_slanted = 0.3
- delta_integral = 0.3
- class STIXFontConstants(FontConstantsBase):
- script_space = 0.1
- sup1 = 0.8
- sub2 = 0.6
- delta = 0.05
- delta_slanted = 0.3
- delta_integral = 0.3
- class STIXSansFontConstants(FontConstantsBase):
- script_space = 0.05
- sup1 = 0.8
- delta_slanted = 0.6
- delta_integral = 0.3
- class DejaVuSerifFontConstants(FontConstantsBase):
- pass
- class DejaVuSansFontConstants(FontConstantsBase):
- pass
- # Maps font family names to the FontConstantBase subclass to use
- _font_constant_mapping = {
- 'DejaVu Sans': DejaVuSansFontConstants,
- 'DejaVu Sans Mono': DejaVuSansFontConstants,
- 'DejaVu Serif': DejaVuSerifFontConstants,
- 'cmb10': ComputerModernFontConstants,
- 'cmex10': ComputerModernFontConstants,
- 'cmmi10': ComputerModernFontConstants,
- 'cmr10': ComputerModernFontConstants,
- 'cmss10': ComputerModernFontConstants,
- 'cmsy10': ComputerModernFontConstants,
- 'cmtt10': ComputerModernFontConstants,
- 'STIXGeneral': STIXFontConstants,
- 'STIXNonUnicode': STIXFontConstants,
- 'STIXSizeFiveSym': STIXFontConstants,
- 'STIXSizeFourSym': STIXFontConstants,
- 'STIXSizeThreeSym': STIXFontConstants,
- 'STIXSizeTwoSym': STIXFontConstants,
- 'STIXSizeOneSym': STIXFontConstants,
- # Map the fonts we used to ship, just for good measure
- 'Bitstream Vera Sans': DejaVuSansFontConstants,
- 'Bitstream Vera': DejaVuSansFontConstants,
- }
- def _get_font_constant_set(state):
- constants = _font_constant_mapping.get(
- state.font_output._get_font(state.font).family_name,
- FontConstantsBase)
- # STIX sans isn't really its own fonts, just different code points
- # in the STIX fonts, so we have to detect this one separately.
- if (constants is STIXFontConstants and
- isinstance(state.font_output, StixSansFonts)):
- return STIXSansFontConstants
- return constants
- class MathTextWarning(Warning):
- pass
- class Node:
- """
- A node in the TeX box model
- """
- def __init__(self):
- self.size = 0
- def __repr__(self):
- return self.__class__.__name__
- def get_kerning(self, next):
- return 0.0
- def shrink(self):
- """
- Shrinks one level smaller. There are only three levels of
- sizes, after which things will no longer get smaller.
- """
- self.size += 1
- def grow(self):
- """
- Grows one level larger. There is no limit to how big
- something can get.
- """
- self.size -= 1
- def render(self, x, y):
- pass
- class Box(Node):
- """
- Represents any node with a physical location.
- """
- def __init__(self, width, height, depth):
- Node.__init__(self)
- self.width = width
- self.height = height
- self.depth = depth
- def shrink(self):
- Node.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- self.width *= SHRINK_FACTOR
- self.height *= SHRINK_FACTOR
- self.depth *= SHRINK_FACTOR
- def grow(self):
- Node.grow(self)
- self.width *= GROW_FACTOR
- self.height *= GROW_FACTOR
- self.depth *= GROW_FACTOR
- def render(self, x1, y1, x2, y2):
- pass
- class Vbox(Box):
- """
- A box with only height (zero width).
- """
- def __init__(self, height, depth):
- Box.__init__(self, 0., height, depth)
- class Hbox(Box):
- """
- A box with only width (zero height and depth).
- """
- 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 :class:`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 :class:`Kern` node when the
- :class:`Char` is added to its parent :class:`Hlist`.
- """
- def __init__(self, c, state, math=True):
- Node.__init__(self)
- self.c = c
- self.font_output = state.font_output
- self.font = state.font
- self.font_class = state.font_class
- self.fontsize = state.fontsize
- self.dpi = state.dpi
- self.math = math
- # The real width, height and depth will be set during the
- # pack phase, after we know the real fontsize
- self._update_metrics()
- def __repr__(self):
- return '`%s`' % self.c
- def _update_metrics(self):
- metrics = self._metrics = self.font_output.get_metrics(
- self.font, self.font_class, self.c, self.fontsize, self.dpi,
- self.math)
- if self.c == ' ':
- self.width = metrics.advance
- else:
- self.width = metrics.width
- self.height = metrics.iceberg
- self.depth = -(metrics.iceberg - metrics.height)
- def is_slanted(self):
- return self._metrics.slanted
- def get_kerning(self, next):
- """
- Return the amount of kerning between this and the given
- character. Called when characters are strung together into
- :class:`Hlist` to create :class:`Kern` nodes.
- """
- advance = self._metrics.advance - self.width
- kern = 0.
- if isinstance(next, Char):
- kern = self.font_output.get_kern(
- self.font, self.font_class, self.c, self.fontsize,
- next.font, next.font_class, next.c, next.fontsize,
- self.dpi)
- return advance + kern
- def render(self, x, y):
- """
- Render the character to the canvas
- """
- self.font_output.render_glyph(
- x, y,
- self.font, self.font_class, self.c, self.fontsize, self.dpi)
- def shrink(self):
- Node.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- self.fontsize *= SHRINK_FACTOR
- self.width *= SHRINK_FACTOR
- self.height *= SHRINK_FACTOR
- self.depth *= SHRINK_FACTOR
- def grow(self):
- Node.grow(self)
- self.fontsize *= GROW_FACTOR
- self.width *= GROW_FACTOR
- self.height *= GROW_FACTOR
- self.depth *= GROW_FACTOR
- 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.font_class, self.c, self.fontsize, self.dpi)
- self.width = metrics.xmax - metrics.xmin
- self.height = metrics.ymax - metrics.ymin
- self.depth = 0
- def shrink(self):
- Char.shrink(self)
- self._update_metrics()
- def grow(self):
- Char.grow(self)
- self._update_metrics()
- def render(self, x, y):
- """
- Render the character to the canvas.
- """
- self.font_output.render_glyph(
- x - self._metrics.xmin, y + self._metrics.ymin,
- self.font, self.font_class, self.c, self.fontsize, self.dpi)
- class List(Box):
- """
- A list of nodes (either horizontal or vertical).
- """
- def __init__(self, elements):
- Box.__init__(self, 0., 0., 0.)
- self.shift_amount = 0. # An arbitrary offset
- self.children = elements # The child nodes of this list
- # 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
- def __repr__(self):
- return '[%s <%.02f %.02f %.02f %.02f> %s]' % (
- super().__repr__(),
- self.width, self.height,
- self.depth, self.shift_amount,
- ' '.join([repr(x) for x in self.children]))
- @staticmethod
- def _determine_order(totals):
- """
- Determine the highest order of glue used by the members of this list.
- Helper function used by vpack and hpack.
- """
- for i in range(len(totals))[::-1]:
- if totals[i] != 0:
- return i
- return 0
- def _set_glue(self, x, sign, totals, error_type):
- o = self._determine_order(totals)
- self.glue_order = o
- self.glue_sign = sign
- if totals[o] != 0.:
- self.glue_set = x / totals[o]
- else:
- self.glue_sign = 0
- self.glue_ratio = 0.
- if o == 0:
- if len(self.children):
- _log.warning("%s %s: %r",
- error_type, self.__class__.__name__, self)
- def shrink(self):
- for child in self.children:
- child.shrink()
- Box.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- self.shift_amount *= SHRINK_FACTOR
- self.glue_set *= SHRINK_FACTOR
- def grow(self):
- for child in self.children:
- child.grow()
- Box.grow(self)
- self.shift_amount *= GROW_FACTOR
- self.glue_set *= GROW_FACTOR
- class Hlist(List):
- """
- A horizontal list of boxes.
- """
- def __init__(self, elements, w=0., m='additional', do_kern=True):
- List.__init__(self, elements)
- if do_kern:
- self.kern()
- self.hpack()
- def kern(self):
- """
- Insert :class:`Kern` nodes between :class:`Char` nodes to set
- kerning. The :class:`Char` nodes themselves determine the
- amount of kerning they need (in :meth:`~Char.get_kerning`),
- and this function just creates the linked list in the correct
- way.
- """
- new_children = []
- num_children = len(self.children)
- if num_children:
- for i in range(num_children):
- elem = self.children[i]
- if i < num_children - 1:
- next = self.children[i + 1]
- else:
- next = None
- new_children.append(elem)
- kerning_distance = elem.get_kerning(next)
- if kerning_distance != 0.:
- kern = Kern(kerning_distance)
- new_children.append(kern)
- self.children = new_children
- # This is a failed experiment to fake cross-font kerning.
- # def get_kerning(self, next):
- # if len(self.children) >= 2 and isinstance(self.children[-2], Char):
- # if isinstance(next, Char):
- # print "CASE A"
- # return self.children[-2].get_kerning(next)
- # elif (isinstance(next, Hlist) and len(next.children)
- # and isinstance(next.children[0], Char)):
- # print "CASE B"
- # result = self.children[-2].get_kerning(next.children[0])
- # print result
- # return result
- # return 0.0
- def hpack(self, w=0., m='additional'):
- r"""
- The main duty of :meth:`hpack` is to compute the dimensions of
- the resulting boxes, and to adjust the glue if one of those
- dimensions is pre-specified. The computed sizes normally
- enclose all of the material inside the new box; but some items
- may stick out if negative glue is used, if the box is
- overfull, or if a ``\vbox`` includes other boxes that have
- been shifted left.
- - *w*: specifies a width
- - *m*: is either 'exactly' or 'additional'.
- Thus, ``hpack(w, 'exactly')`` produces a box whose width is
- exactly *w*, while ``hpack(w, 'additional')`` yields a box
- whose width is the natural width plus *w*. The default values
- produce a box with the natural width.
- """
- # I don't know why these get reset in TeX. Shift_amount is pretty
- # much useless if we do.
- # self.shift_amount = 0.
- h = 0.
- d = 0.
- x = 0.
- total_stretch = [0.] * 4
- total_shrink = [0.] * 4
- for p in self.children:
- if isinstance(p, Char):
- x += p.width
- h = max(h, p.height)
- d = max(d, p.depth)
- elif isinstance(p, Box):
- x += p.width
- if not np.isinf(p.height) and not np.isinf(p.depth):
- s = getattr(p, 'shift_amount', 0.)
- h = max(h, p.height - s)
- d = max(d, p.depth + s)
- elif isinstance(p, Glue):
- glue_spec = p.glue_spec
- x += glue_spec.width
- total_stretch[glue_spec.stretch_order] += glue_spec.stretch
- total_shrink[glue_spec.shrink_order] += glue_spec.shrink
- elif isinstance(p, Kern):
- x += p.width
- self.height = h
- self.depth = d
- if m == 'additional':
- w += x
- self.width = w
- x = w - x
- if x == 0.:
- self.glue_sign = 0
- self.glue_order = 0
- self.glue_ratio = 0.
- return
- if x > 0.:
- self._set_glue(x, 1, total_stretch, "Overfull")
- else:
- self._set_glue(x, -1, total_shrink, "Underfull")
- class Vlist(List):
- """
- A vertical list of boxes.
- """
- def __init__(self, elements, h=0., m='additional'):
- List.__init__(self, elements)
- self.vpack()
- def vpack(self, h=0., m='additional', l=np.inf):
- """
- The main duty of :meth:`vpack` is to compute the dimensions of
- the resulting boxes, and to adjust the glue if one of those
- dimensions is pre-specified.
- - *h*: specifies a height
- - *m*: is either 'exactly' or 'additional'.
- - *l*: a maximum height
- Thus, ``vpack(h, 'exactly')`` produces a box whose height is
- exactly *h*, while ``vpack(h, 'additional')`` yields a box
- whose height is the natural height plus *h*. The default
- values produce a box with the natural width.
- """
- # I don't know why these get reset in TeX. Shift_amount is pretty
- # much useless if we do.
- # self.shift_amount = 0.
- w = 0.
- d = 0.
- x = 0.
- total_stretch = [0.] * 4
- total_shrink = [0.] * 4
- for p in self.children:
- if isinstance(p, Box):
- x += d + p.height
- d = p.depth
- if not np.isinf(p.width):
- s = getattr(p, 'shift_amount', 0.)
- w = max(w, p.width + s)
- elif isinstance(p, Glue):
- x += d
- d = 0.
- glue_spec = p.glue_spec
- x += glue_spec.width
- total_stretch[glue_spec.stretch_order] += glue_spec.stretch
- total_shrink[glue_spec.shrink_order] += glue_spec.shrink
- elif isinstance(p, Kern):
- x += d + p.width
- d = 0.
- elif isinstance(p, Char):
- raise RuntimeError(
- "Internal mathtext error: Char node found in Vlist")
- self.width = w
- if d > l:
- x += d - l
- self.depth = l
- else:
- self.depth = d
- if m == 'additional':
- h += x
- self.height = h
- x = h - x
- if x == 0:
- self.glue_sign = 0
- self.glue_order = 0
- self.glue_ratio = 0.
- return
- if x > 0.:
- self._set_glue(x, 1, total_stretch, "Overfull")
- else:
- self._set_glue(x, -1, total_shrink, "Underfull")
- class Rule(Box):
- """
- A :class:`Rule` node stands for a solid black rectangle; it has
- *width*, *depth*, and *height* fields just as in an
- :class:`Hlist`. However, if any of these dimensions is inf, the
- actual value will be determined by running the rule up to the
- boundary of the innermost enclosing box. This is called a "running
- dimension." The width is never running in an :class:`Hlist`; the
- height and depth are never running in a :class:`Vlist`.
- """
- def __init__(self, width, height, depth, state):
- Box.__init__(self, width, height, depth)
- self.font_output = state.font_output
- def render(self, x, y, w, h):
- self.font_output.render_rect_filled(x, y, x + w, y + h)
- class Hrule(Rule):
- """
- Convenience class to create a horizontal rule.
- """
- def __init__(self, state, thickness=None):
- if thickness is None:
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- height = depth = thickness * 0.5
- Rule.__init__(self, np.inf, height, depth, state)
- class Vrule(Rule):
- """
- Convenience class to create a vertical rule.
- """
- def __init__(self, state):
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- Rule.__init__(self, thickness, np.inf, np.inf, state)
- class Glue(Node):
- """
- Most of the information in this object is stored in the underlying
- :class:`GlueSpec` class, which is shared between multiple glue objects.
- (This is a memory optimization which probably doesn't matter anymore, but
- it's easier to stick to what TeX does.)
- """
- def __init__(self, glue_type, copy=False):
- Node.__init__(self)
- self.glue_subtype = 'normal'
- if isinstance(glue_type, str):
- glue_spec = GlueSpec.factory(glue_type)
- elif isinstance(glue_type, GlueSpec):
- glue_spec = glue_type
- else:
- raise ValueError("glue_type must be a glue spec name or instance")
- if copy:
- glue_spec = glue_spec.copy()
- self.glue_spec = glue_spec
- def shrink(self):
- Node.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- if self.glue_spec.width != 0.:
- self.glue_spec = self.glue_spec.copy()
- self.glue_spec.width *= SHRINK_FACTOR
- def grow(self):
- Node.grow(self)
- if self.glue_spec.width != 0.:
- self.glue_spec = self.glue_spec.copy()
- self.glue_spec.width *= GROW_FACTOR
- class GlueSpec:
- """
- See :class:`Glue`.
- """
- def __init__(self, width=0., stretch=0., stretch_order=0,
- shrink=0., shrink_order=0):
- self.width = width
- self.stretch = stretch
- self.stretch_order = stretch_order
- self.shrink = shrink
- self.shrink_order = shrink_order
- def copy(self):
- return GlueSpec(
- self.width,
- self.stretch,
- self.stretch_order,
- self.shrink,
- self.shrink_order)
- @classmethod
- def factory(cls, glue_type):
- return cls._types[glue_type]
- GlueSpec._types = {
- 'fil': GlueSpec(0., 1., 1, 0., 0),
- 'fill': GlueSpec(0., 1., 2, 0., 0),
- 'filll': GlueSpec(0., 1., 3, 0., 0),
- 'neg_fil': GlueSpec(0., 0., 0, 1., 1),
- 'neg_fill': GlueSpec(0., 0., 0, 1., 2),
- 'neg_filll': GlueSpec(0., 0., 0, 1., 3),
- 'empty': GlueSpec(0., 0., 0, 0., 0),
- 'ss': GlueSpec(0., 1., 1, -1., 1)
- }
- # Some convenient ways to get common kinds of glue
- class Fil(Glue):
- def __init__(self):
- Glue.__init__(self, 'fil')
- class Fill(Glue):
- def __init__(self):
- Glue.__init__(self, 'fill')
- class Filll(Glue):
- def __init__(self):
- Glue.__init__(self, 'filll')
- class NegFil(Glue):
- def __init__(self):
- Glue.__init__(self, 'neg_fil')
- class NegFill(Glue):
- def __init__(self):
- Glue.__init__(self, 'neg_fill')
- class NegFilll(Glue):
- def __init__(self):
- Glue.__init__(self, 'neg_filll')
- class SsGlue(Glue):
- def __init__(self):
- Glue.__init__(self, 'ss')
- class HCentered(Hlist):
- """
- A convenience class to create an :class:`Hlist` whose contents are
- centered within its enclosing box.
- """
- def __init__(self, elements):
- Hlist.__init__(self, [SsGlue()] + elements + [SsGlue()],
- do_kern=False)
- class VCentered(Hlist):
- """
- A convenience class to create a :class:`Vlist` whose contents are
- centered within its enclosing box.
- """
- def __init__(self, elements):
- Vlist.__init__(self, [SsGlue()] + elements + [SsGlue()])
- class Kern(Node):
- """
- A :class:`Kern` node has a width field to specify a (normally
- negative) amount of spacing. This spacing correction appears in
- horizontal lists between letters like A and V when the font
- designer said that it looks better to move them closer together or
- further apart. A kern node can also appear in a vertical list,
- when its *width* denotes additional spacing in the vertical
- direction.
- """
- height = 0
- depth = 0
- def __init__(self, width):
- Node.__init__(self)
- self.width = width
- def __repr__(self):
- return "k%.02f" % self.width
- def shrink(self):
- Node.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- self.width *= SHRINK_FACTOR
- def grow(self):
- Node.grow(self)
- self.width *= GROW_FACTOR
- class SubSuperCluster(Hlist):
- """
- :class:`SubSuperCluster` is a sort of hack to get around that fact
- that this code do a two-pass parse like TeX. This lets us store
- enough information in the hlist itself, namely the nucleus, sub-
- and super-script, such that if another script follows that needs
- to be attached, it can be reconfigured on the fly.
- """
- def __init__(self):
- self.nucleus = None
- self.sub = None
- self.super = None
- Hlist.__init__(self, [])
- class AutoHeightChar(Hlist):
- """
- :class:`AutoHeightChar` will create a character as close to the
- given height and depth as possible. When using a font with
- multiple height versions of some characters (such as the BaKoMa
- fonts), the correct glyph will be selected, otherwise this will
- always just return a scaled version of the glyph.
- """
- def __init__(self, c, height, depth, state, always=False, factor=None):
- alternatives = state.font_output.get_sized_alternatives_for_symbol(
- state.font, c)
- xHeight = state.font_output.get_xheight(
- state.font, state.fontsize, state.dpi)
- state = state.copy()
- target_total = height + depth
- for fontname, sym in alternatives:
- state.font = fontname
- char = Char(sym, state)
- # Ensure that size 0 is chosen when the text is regular sized but
- # with descender glyphs by subtracting 0.2 * xHeight
- if char.height + char.depth >= target_total - 0.2 * xHeight:
- break
- shift = 0
- if state.font != 0:
- if factor is None:
- factor = (target_total) / (char.height + char.depth)
- state.fontsize *= factor
- char = Char(sym, state)
- shift = (depth - char.depth)
- Hlist.__init__(self, [char])
- self.shift_amount = shift
- class AutoWidthChar(Hlist):
- """
- :class:`AutoWidthChar` will create a character as close to the
- given width as possible. When using a font with multiple width
- versions of some characters (such as the BaKoMa fonts), the
- correct glyph will be selected, otherwise this will always just
- return a scaled version of the glyph.
- """
- def __init__(self, c, width, state, always=False, char_class=Char):
- alternatives = state.font_output.get_sized_alternatives_for_symbol(
- state.font, c)
- state = state.copy()
- for fontname, sym in alternatives:
- state.font = fontname
- char = char_class(sym, state)
- if char.width >= width:
- break
- factor = width / char.width
- state.fontsize *= factor
- char = char_class(sym, state)
- Hlist.__init__(self, [char])
- self.width = char.width
- class Ship:
- """
- Once the boxes have been set up, this sends them to output. Since
- boxes can be inside of boxes inside of boxes, the main work of
- :class:`Ship` is done by two mutually recursive routines,
- :meth:`hlist_out` and :meth:`vlist_out`, which traverse the
- :class:`Hlist` nodes and :class:`Vlist` nodes inside of horizontal
- and vertical boxes. The global variables used in TeX to store
- state as it processes have become member variables here.
- """
- def __call__(self, ox, oy, box):
- self.max_push = 0 # Deepest nesting of push commands so far
- self.cur_s = 0
- self.cur_v = 0.
- self.cur_h = 0.
- self.off_h = ox
- self.off_v = oy + box.height
- self.hlist_out(box)
- @staticmethod
- def clamp(value):
- if value < -1000000000.:
- return -1000000000.
- if value > 1000000000.:
- return 1000000000.
- return value
- def hlist_out(self, box):
- cur_g = 0
- cur_glue = 0.
- glue_order = box.glue_order
- glue_sign = box.glue_sign
- base_line = self.cur_v
- left_edge = self.cur_h
- self.cur_s += 1
- self.max_push = max(self.cur_s, self.max_push)
- clamp = self.clamp
- for p in box.children:
- if isinstance(p, Char):
- p.render(self.cur_h + self.off_h, self.cur_v + self.off_v)
- self.cur_h += p.width
- elif isinstance(p, Kern):
- self.cur_h += p.width
- elif isinstance(p, List):
- # node623
- if len(p.children) == 0:
- self.cur_h += p.width
- else:
- edge = self.cur_h
- self.cur_v = base_line + p.shift_amount
- if isinstance(p, Hlist):
- self.hlist_out(p)
- else:
- # p.vpack(box.height + box.depth, 'exactly')
- self.vlist_out(p)
- self.cur_h = edge + p.width
- self.cur_v = base_line
- elif isinstance(p, Box):
- # node624
- rule_height = p.height
- rule_depth = p.depth
- rule_width = p.width
- if np.isinf(rule_height):
- rule_height = box.height
- if np.isinf(rule_depth):
- rule_depth = box.depth
- if rule_height > 0 and rule_width > 0:
- self.cur_v = base_line + rule_depth
- p.render(self.cur_h + self.off_h,
- self.cur_v + self.off_v,
- rule_width, rule_height)
- self.cur_v = base_line
- self.cur_h += rule_width
- elif isinstance(p, Glue):
- # node625
- glue_spec = p.glue_spec
- rule_width = glue_spec.width - cur_g
- if glue_sign != 0: # normal
- if glue_sign == 1: # stretching
- if glue_spec.stretch_order == glue_order:
- cur_glue += glue_spec.stretch
- cur_g = round(clamp(box.glue_set * cur_glue))
- elif glue_spec.shrink_order == glue_order:
- cur_glue += glue_spec.shrink
- cur_g = round(clamp(box.glue_set * cur_glue))
- rule_width += cur_g
- self.cur_h += rule_width
- self.cur_s -= 1
- def vlist_out(self, box):
- cur_g = 0
- cur_glue = 0.
- glue_order = box.glue_order
- glue_sign = box.glue_sign
- self.cur_s += 1
- self.max_push = max(self.max_push, self.cur_s)
- left_edge = self.cur_h
- self.cur_v -= box.height
- top_edge = self.cur_v
- clamp = self.clamp
- for p in box.children:
- if isinstance(p, Kern):
- self.cur_v += p.width
- elif isinstance(p, List):
- if len(p.children) == 0:
- self.cur_v += p.height + p.depth
- else:
- self.cur_v += p.height
- self.cur_h = left_edge + p.shift_amount
- save_v = self.cur_v
- p.width = box.width
- if isinstance(p, Hlist):
- self.hlist_out(p)
- else:
- self.vlist_out(p)
- self.cur_v = save_v + p.depth
- self.cur_h = left_edge
- elif isinstance(p, Box):
- rule_height = p.height
- rule_depth = p.depth
- rule_width = p.width
- if np.isinf(rule_width):
- rule_width = box.width
- rule_height += rule_depth
- if rule_height > 0 and rule_depth > 0:
- self.cur_v += rule_height
- p.render(self.cur_h + self.off_h,
- self.cur_v + self.off_v,
- rule_width, rule_height)
- elif isinstance(p, Glue):
- glue_spec = p.glue_spec
- rule_height = glue_spec.width - cur_g
- if glue_sign != 0: # normal
- if glue_sign == 1: # stretching
- if glue_spec.stretch_order == glue_order:
- cur_glue += glue_spec.stretch
- cur_g = round(clamp(box.glue_set * cur_glue))
- elif glue_spec.shrink_order == glue_order: # shrinking
- cur_glue += glue_spec.shrink
- cur_g = round(clamp(box.glue_set * cur_glue))
- rule_height += cur_g
- self.cur_v += rule_height
- elif isinstance(p, Char):
- raise RuntimeError(
- "Internal mathtext error: Char node found in vlist")
- self.cur_s -= 1
- ship = Ship()
- ##############################################################################
- # PARSER
- def Error(msg):
- """
- Helper class to raise parser errors.
- """
- def raise_error(s, loc, toks):
- raise ParseFatalException(s, loc, msg)
- empty = Empty()
- empty.setParseAction(raise_error)
- return empty
- class Parser:
- """
- This is the pyparsing-based parser for math expressions. It
- actually parses full strings *containing* math expressions, in
- that raw text may also appear outside of pairs of ``$``.
- The grammar is based directly on that in TeX, though it cuts a few
- corners.
- """
- _math_style_dict = dict(displaystyle=0, textstyle=1,
- scriptstyle=2, scriptscriptstyle=3)
- _binary_operators = set('''
- + * -
- \\pm \\sqcap \\rhd
- \\mp \\sqcup \\unlhd
- \\times \\vee \\unrhd
- \\div \\wedge \\oplus
- \\ast \\setminus \\ominus
- \\star \\wr \\otimes
- \\circ \\diamond \\oslash
- \\bullet \\bigtriangleup \\odot
- \\cdot \\bigtriangledown \\bigcirc
- \\cap \\triangleleft \\dagger
- \\cup \\triangleright \\ddagger
- \\uplus \\lhd \\amalg'''.split())
- _relation_symbols = set('''
- = < > :
- \\leq \\geq \\equiv \\models
- \\prec \\succ \\sim \\perp
- \\preceq \\succeq \\simeq \\mid
- \\ll \\gg \\asymp \\parallel
- \\subset \\supset \\approx \\bowtie
- \\subseteq \\supseteq \\cong \\Join
- \\sqsubset \\sqsupset \\neq \\smile
- \\sqsubseteq \\sqsupseteq \\doteq \\frown
- \\in \\ni \\propto \\vdash
- \\dashv \\dots \\dotplus \\doteqdot'''.split())
- _arrow_symbols = set('''
- \\leftarrow \\longleftarrow \\uparrow
- \\Leftarrow \\Longleftarrow \\Uparrow
- \\rightarrow \\longrightarrow \\downarrow
- \\Rightarrow \\Longrightarrow \\Downarrow
- \\leftrightarrow \\longleftrightarrow \\updownarrow
- \\Leftrightarrow \\Longleftrightarrow \\Updownarrow
- \\mapsto \\longmapsto \\nearrow
- \\hookleftarrow \\hookrightarrow \\searrow
- \\leftharpoonup \\rightharpoonup \\swarrow
- \\leftharpoondown \\rightharpoondown \\nwarrow
- \\rightleftharpoons \\leadsto'''.split())
- _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols
- _punctuation_symbols = set(r', ; . ! \ldotp \cdotp'.split())
- _overunder_symbols = set(r'''
- \sum \prod \coprod \bigcap \bigcup \bigsqcup \bigvee
- \bigwedge \bigodot \bigotimes \bigoplus \biguplus
- '''.split())
- _overunder_functions = set(
- "lim liminf limsup sup max min".split())
- _dropsub_symbols = set(r'''\int \oint'''.split())
- _fontnames = set(
- "rm cal it tt sf bf default bb frak circled scr regular".split())
- _function_names = set("""
- arccos csc ker min arcsin deg lg Pr arctan det lim sec arg dim
- liminf sin cos exp limsup sinh cosh gcd ln sup cot hom log tan
- coth inf max tanh""".split())
- _ambi_delim = set("""
- | \\| / \\backslash \\uparrow \\downarrow \\updownarrow \\Uparrow
- \\Downarrow \\Updownarrow . \\vert \\Vert \\\\|""".split())
- _left_delim = set(r"( [ \{ < \lfloor \langle \lceil".split())
- _right_delim = set(r") ] \} > \rfloor \rangle \rceil".split())
- def __init__(self):
- p = types.SimpleNamespace()
- # All forward declarations are here
- p.accent = Forward()
- p.ambi_delim = Forward()
- p.apostrophe = Forward()
- p.auto_delim = Forward()
- p.binom = Forward()
- p.bslash = Forward()
- p.c_over_c = Forward()
- p.customspace = Forward()
- p.end_group = Forward()
- p.float_literal = Forward()
- p.font = Forward()
- p.frac = Forward()
- p.dfrac = Forward()
- p.function = Forward()
- p.genfrac = Forward()
- p.group = Forward()
- p.int_literal = Forward()
- p.latexfont = Forward()
- p.lbracket = Forward()
- p.left_delim = Forward()
- p.lbrace = Forward()
- p.main = Forward()
- p.math = Forward()
- p.math_string = Forward()
- p.non_math = Forward()
- p.operatorname = Forward()
- p.overline = Forward()
- p.placeable = Forward()
- p.rbrace = Forward()
- p.rbracket = Forward()
- p.required_group = Forward()
- p.right_delim = Forward()
- p.right_delim_safe = Forward()
- p.simple = Forward()
- p.simple_group = Forward()
- p.single_symbol = Forward()
- p.snowflake = Forward()
- p.space = Forward()
- p.sqrt = Forward()
- p.stackrel = Forward()
- p.start_group = Forward()
- p.subsuper = Forward()
- p.subsuperop = Forward()
- p.symbol = Forward()
- p.symbol_name = Forward()
- p.token = Forward()
- p.unknown_symbol = Forward()
- # Set names on everything -- very useful for debugging
- for key, val in vars(p).items():
- if not key.startswith('_'):
- val.setName(key)
- p.float_literal <<= Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)")
- p.int_literal <<= Regex("[-+]?[0-9]+")
- p.lbrace <<= Literal('{').suppress()
- p.rbrace <<= Literal('}').suppress()
- p.lbracket <<= Literal('[').suppress()
- p.rbracket <<= Literal(']').suppress()
- p.bslash <<= Literal('\\')
- p.space <<= oneOf(list(self._space_widths))
- p.customspace <<= (
- Suppress(Literal(r'\hspace'))
- - ((p.lbrace + p.float_literal + p.rbrace)
- | Error(r"Expected \hspace{n}"))
- )
- unicode_range = "\U00000080-\U0001ffff"
- p.single_symbol <<= Regex(
- r"([a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|%s])|(\\[%%${}\[\]_|])" %
- unicode_range)
- p.snowflake <<= Suppress(p.bslash) + oneOf(self._snowflake)
- p.symbol_name <<= (
- Combine(p.bslash + oneOf(list(tex2uni)))
- + FollowedBy(Regex("[^A-Za-z]").leaveWhitespace() | StringEnd())
- )
- p.symbol <<= (p.single_symbol | p.symbol_name).leaveWhitespace()
- p.apostrophe <<= Regex("'+")
- p.c_over_c <<= (
- Suppress(p.bslash)
- + oneOf(list(self._char_over_chars))
- )
- p.accent <<= Group(
- Suppress(p.bslash)
- + oneOf([*self._accent_map, *self._wide_accents])
- - p.placeable
- )
- p.function <<= (
- Suppress(p.bslash)
- + oneOf(list(self._function_names))
- )
- p.start_group <<= Optional(p.latexfont) + p.lbrace
- p.end_group <<= p.rbrace.copy()
- p.simple_group <<= Group(p.lbrace + ZeroOrMore(p.token) + p.rbrace)
- p.required_group <<= Group(p.lbrace + OneOrMore(p.token) + p.rbrace)
- p.group <<= Group(
- p.start_group + ZeroOrMore(p.token) + p.end_group
- )
- p.font <<= Suppress(p.bslash) + oneOf(list(self._fontnames))
- p.latexfont <<= (
- Suppress(p.bslash)
- + oneOf(['math' + x for x in self._fontnames])
- )
- p.frac <<= Group(
- Suppress(Literal(r"\frac"))
- - ((p.required_group + p.required_group)
- | Error(r"Expected \frac{num}{den}"))
- )
- p.dfrac <<= Group(
- Suppress(Literal(r"\dfrac"))
- - ((p.required_group + p.required_group)
- | Error(r"Expected \dfrac{num}{den}"))
- )
- p.stackrel <<= Group(
- Suppress(Literal(r"\stackrel"))
- - ((p.required_group + p.required_group)
- | Error(r"Expected \stackrel{num}{den}"))
- )
- p.binom <<= Group(
- Suppress(Literal(r"\binom"))
- - ((p.required_group + p.required_group)
- | Error(r"Expected \binom{num}{den}"))
- )
- p.ambi_delim <<= oneOf(list(self._ambi_delim))
- p.left_delim <<= oneOf(list(self._left_delim))
- p.right_delim <<= oneOf(list(self._right_delim))
- p.right_delim_safe <<= oneOf([*(self._right_delim - {'}'}), r'\}'])
- p.genfrac <<= Group(
- Suppress(Literal(r"\genfrac"))
- - (((p.lbrace
- + Optional(p.ambi_delim | p.left_delim, default='')
- + p.rbrace)
- + (p.lbrace
- + Optional(p.ambi_delim | p.right_delim_safe, default='')
- + p.rbrace)
- + (p.lbrace + p.float_literal + p.rbrace)
- + p.simple_group + p.required_group + p.required_group)
- | Error("Expected "
- r"\genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}"))
- )
- p.sqrt <<= Group(
- Suppress(Literal(r"\sqrt"))
- - ((Optional(p.lbracket + p.int_literal + p.rbracket, default=None)
- + p.required_group)
- | Error("Expected \\sqrt{value}"))
- )
- p.overline <<= Group(
- Suppress(Literal(r"\overline"))
- - (p.required_group | Error("Expected \\overline{value}"))
- )
- p.unknown_symbol <<= Combine(p.bslash + Regex("[A-Za-z]*"))
- p.operatorname <<= Group(
- Suppress(Literal(r"\operatorname"))
- - ((p.lbrace + ZeroOrMore(p.simple | p.unknown_symbol) + p.rbrace)
- | Error("Expected \\operatorname{value}"))
- )
- p.placeable <<= (
- p.snowflake # Must be before accent so named symbols that are
- # prefixed with an accent name work
- | p.accent # Must be before symbol as all accents are symbols
- | p.symbol # Must be third to catch all named symbols and single
- # chars not in a group
- | p.c_over_c
- | p.function
- | p.group
- | p.frac
- | p.dfrac
- | p.stackrel
- | p.binom
- | p.genfrac
- | p.sqrt
- | p.overline
- | p.operatorname
- )
- p.simple <<= (
- p.space
- | p.customspace
- | p.font
- | p.subsuper
- )
- p.subsuperop <<= oneOf(["_", "^"])
- p.subsuper <<= Group(
- (Optional(p.placeable)
- + OneOrMore(p.subsuperop - p.placeable)
- + Optional(p.apostrophe))
- | (p.placeable + Optional(p.apostrophe))
- | p.apostrophe
- )
- p.token <<= (
- p.simple
- | p.auto_delim
- | p.unknown_symbol # Must be last
- )
- p.auto_delim <<= (
- Suppress(Literal(r"\left"))
- - ((p.left_delim | p.ambi_delim)
- | Error("Expected a delimiter"))
- + Group(ZeroOrMore(p.simple | p.auto_delim))
- + Suppress(Literal(r"\right"))
- - ((p.right_delim | p.ambi_delim)
- | Error("Expected a delimiter"))
- )
- p.math <<= OneOrMore(p.token)
- p.math_string <<= QuotedString('$', '\\', unquoteResults=False)
- p.non_math <<= Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace()
- p.main <<= (
- p.non_math + ZeroOrMore(p.math_string + p.non_math) + StringEnd()
- )
- # Set actions
- for key, val in vars(p).items():
- if not key.startswith('_'):
- if hasattr(self, key):
- val.setParseAction(getattr(self, key))
- self._expression = p.main
- self._math_expression = p.math
- def parse(self, s, fonts_object, fontsize, dpi):
- """
- Parse expression *s* using the given *fonts_object* for
- output, at the given *fontsize* and *dpi*.
- Returns the parse tree of :class:`Node` instances.
- """
- self._state_stack = [
- self.State(fonts_object, 'default', 'rm', fontsize, dpi)]
- self._em_width_cache = {}
- try:
- result = self._expression.parseString(s)
- except ParseBaseException as err:
- raise ValueError("\n".join(["",
- err.line,
- " " * (err.column - 1) + "^",
- str(err)]))
- self._state_stack = None
- self._em_width_cache = {}
- self._expression.resetCache()
- return result[0]
- # The state of the parser is maintained in a stack. Upon
- # entering and leaving a group { } or math/non-math, the stack
- # is pushed and popped accordingly. The current state always
- # exists in the top element of the stack.
- class State:
- """
- Stores the state of the parser.
- States are pushed and popped from a stack as necessary, and
- the "current" state is always at the top of the stack.
- """
- def __init__(self, font_output, font, font_class, fontsize, dpi):
- self.font_output = font_output
- self._font = font
- self.font_class = font_class
- self.fontsize = fontsize
- self.dpi = dpi
- def copy(self):
- return Parser.State(
- self.font_output,
- self.font,
- self.font_class,
- self.fontsize,
- self.dpi)
- @property
- def font(self):
- return self._font
- @font.setter
- def font(self, name):
- if name == "circled":
- cbook.warn_deprecated(
- "3.1", name="\\mathcircled", obj_type="mathtext command",
- alternative="unicode characters (e.g. '\\N{CIRCLED LATIN "
- "CAPITAL LETTER A}' or '\\u24b6')")
- if name in ('rm', 'it', 'bf'):
- self.font_class = name
- self._font = name
- def get_state(self):
- """
- Get the current :class:`State` of the parser.
- """
- return self._state_stack[-1]
- def pop_state(self):
- """
- Pop a :class:`State` off of the stack.
- """
- self._state_stack.pop()
- def push_state(self):
- """
- Push a new :class:`State` onto the stack which is just a copy
- of the current state.
- """
- self._state_stack.append(self.get_state().copy())
- def main(self, s, loc, toks):
- return [Hlist(toks)]
- def math_string(self, s, loc, toks):
- return self._math_expression.parseString(toks[0][1:-1])
- def math(self, s, loc, toks):
- hlist = Hlist(toks)
- self.pop_state()
- return [hlist]
- def non_math(self, s, loc, toks):
- s = toks[0].replace(r'\$', '$')
- symbols = [Char(c, self.get_state(), math=False) for c in s]
- hlist = Hlist(symbols)
- # We're going into math now, so set font to 'it'
- self.push_state()
- self.get_state().font = rcParams['mathtext.default']
- return [hlist]
- def _make_space(self, percentage):
- # All spaces are relative to em width
- state = self.get_state()
- key = (state.font, state.fontsize, state.dpi)
- width = self._em_width_cache.get(key)
- if width is None:
- metrics = state.font_output.get_metrics(
- state.font, rcParams['mathtext.default'], 'm', state.fontsize,
- state.dpi)
- width = metrics.advance
- self._em_width_cache[key] = width
- return Kern(width * percentage)
- _space_widths = {
- r'\,': 0.16667, # 3/18 em = 3 mu
- r'\thinspace': 0.16667, # 3/18 em = 3 mu
- r'\/': 0.16667, # 3/18 em = 3 mu
- r'\>': 0.22222, # 4/18 em = 4 mu
- r'\:': 0.22222, # 4/18 em = 4 mu
- r'\;': 0.27778, # 5/18 em = 5 mu
- r'\ ': 0.33333, # 6/18 em = 6 mu
- r'~': 0.33333, # 6/18 em = 6 mu, nonbreakable
- r'\enspace': 0.5, # 9/18 em = 9 mu
- r'\quad': 1, # 1 em = 18 mu
- r'\qquad': 2, # 2 em = 36 mu
- r'\!': -0.16667, # -3/18 em = -3 mu
- }
- def space(self, s, loc, toks):
- assert len(toks) == 1
- num = self._space_widths[toks[0]]
- box = self._make_space(num)
- return [box]
- def customspace(self, s, loc, toks):
- return [self._make_space(float(toks[0]))]
- def symbol(self, s, loc, toks):
- c = toks[0]
- try:
- char = Char(c, self.get_state())
- except ValueError:
- raise ParseFatalException(s, loc, "Unknown symbol: %s" % c)
- if c in self._spaced_symbols:
- # iterate until we find previous character, needed for cases
- # such as ${ -2}$, $ -2$, or $ -2$.
- prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
- # Binary operators at start of string should not be spaced
- if (c in self._binary_operators and
- (len(s[:loc].split()) == 0 or prev_char == '{' or
- prev_char in self._left_delim)):
- return [char]
- else:
- return [Hlist([self._make_space(0.2),
- char,
- self._make_space(0.2)],
- do_kern = True)]
- elif c in self._punctuation_symbols:
- # Do not space commas between brackets
- if c == ',':
- prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
- next_char = next((c for c in s[loc + 1:] if c != ' '), '')
- if prev_char == '{' and next_char == '}':
- return [char]
- # Do not space dots as decimal separators
- if c == '.' and s[loc - 1].isdigit() and s[loc + 1].isdigit():
- return [char]
- else:
- return [Hlist([char,
- self._make_space(0.2)],
- do_kern = True)]
- return [char]
- snowflake = symbol
- def unknown_symbol(self, s, loc, toks):
- c = toks[0]
- raise ParseFatalException(s, loc, "Unknown symbol: %s" % c)
- _char_over_chars = {
- # The first 2 entries in the tuple are (font, char, sizescale) for
- # the two symbols under and over. The third element is the space
- # (in multiples of underline height)
- r'AA': (('it', 'A', 1.0), (None, '\\circ', 0.5), 0.0),
- }
- def c_over_c(self, s, loc, toks):
- sym = toks[0]
- state = self.get_state()
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- under_desc, over_desc, space = \
- self._char_over_chars.get(sym, (None, None, 0.0))
- if under_desc is None:
- raise ParseFatalException("Error parsing symbol")
- over_state = state.copy()
- if over_desc[0] is not None:
- over_state.font = over_desc[0]
- over_state.fontsize *= over_desc[2]
- over = Accent(over_desc[1], over_state)
- under_state = state.copy()
- if under_desc[0] is not None:
- under_state.font = under_desc[0]
- under_state.fontsize *= under_desc[2]
- under = Char(under_desc[1], under_state)
- width = max(over.width, under.width)
- over_centered = HCentered([over])
- over_centered.hpack(width, 'exactly')
- under_centered = HCentered([under])
- under_centered.hpack(width, 'exactly')
- return Vlist([
- over_centered,
- Vbox(0., thickness * space),
- under_centered
- ])
- _accent_map = {
- r'hat': r'\circumflexaccent',
- r'breve': r'\combiningbreve',
- r'bar': r'\combiningoverline',
- r'grave': r'\combininggraveaccent',
- r'acute': r'\combiningacuteaccent',
- r'tilde': r'\combiningtilde',
- r'dot': r'\combiningdotabove',
- r'ddot': r'\combiningdiaeresis',
- r'vec': r'\combiningrightarrowabove',
- r'"': r'\combiningdiaeresis',
- r"`": r'\combininggraveaccent',
- r"'": r'\combiningacuteaccent',
- r'~': r'\combiningtilde',
- r'.': r'\combiningdotabove',
- r'^': r'\circumflexaccent',
- r'overrightarrow': r'\rightarrow',
- r'overleftarrow': r'\leftarrow',
- r'mathring': r'\circ',
- }
- _wide_accents = set(r"widehat widetilde widebar".split())
- # make a lambda and call it to get the namespace right
- _snowflake = (lambda am: [p for p in tex2uni if
- any(p.startswith(a) and a != p for a in am)])(
- set(_accent_map))
- def accent(self, s, loc, toks):
- assert len(toks) == 1
- state = self.get_state()
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- if len(toks[0]) != 2:
- raise ParseFatalException("Error parsing accent")
- accent, sym = toks[0]
- if accent in self._wide_accents:
- accent_box = AutoWidthChar(
- '\\' + accent, sym.width, state, char_class=Accent)
- else:
- accent_box = Accent(self._accent_map[accent], state)
- if accent == 'mathring':
- accent_box.shrink()
- accent_box.shrink()
- centered = HCentered([Hbox(sym.width / 4.0), accent_box])
- centered.hpack(sym.width, 'exactly')
- return Vlist([
- centered,
- Vbox(0., thickness * 2.0),
- Hlist([sym])
- ])
- def function(self, s, loc, toks):
- self.push_state()
- state = self.get_state()
- state.font = 'rm'
- hlist = Hlist([Char(c, state) for c in toks[0]])
- self.pop_state()
- hlist.function_name = toks[0]
- return hlist
- def operatorname(self, s, loc, toks):
- self.push_state()
- state = self.get_state()
- state.font = 'rm'
- # Change the font of Chars, but leave Kerns alone
- for c in toks[0]:
- if isinstance(c, Char):
- c.font = 'rm'
- c._update_metrics()
- self.pop_state()
- return Hlist(toks[0])
- def start_group(self, s, loc, toks):
- self.push_state()
- # Deal with LaTeX-style font tokens
- if len(toks):
- self.get_state().font = toks[0][4:]
- return []
- def group(self, s, loc, toks):
- grp = Hlist(toks[0])
- return [grp]
- required_group = simple_group = group
- def end_group(self, s, loc, toks):
- self.pop_state()
- return []
- def font(self, s, loc, toks):
- assert len(toks) == 1
- name = toks[0]
- self.get_state().font = name
- return []
- def is_overunder(self, nucleus):
- if isinstance(nucleus, Char):
- return nucleus.c in self._overunder_symbols
- elif isinstance(nucleus, Hlist) and hasattr(nucleus, 'function_name'):
- return nucleus.function_name in self._overunder_functions
- return False
- def is_dropsub(self, nucleus):
- if isinstance(nucleus, Char):
- return nucleus.c in self._dropsub_symbols
- return False
- def is_slanted(self, nucleus):
- if isinstance(nucleus, Char):
- return nucleus.is_slanted()
- return False
- def is_between_brackets(self, s, loc):
- return False
- def subsuper(self, s, loc, toks):
- assert len(toks) == 1
- nucleus = None
- sub = None
- super = None
- # Pick all of the apostrophes out, including first apostrophes that
- # have been parsed as characters
- napostrophes = 0
- new_toks = []
- for tok in toks[0]:
- if isinstance(tok, str) and tok not in ('^', '_'):
- napostrophes += len(tok)
- elif isinstance(tok, Char) and tok.c == "'":
- napostrophes += 1
- else:
- new_toks.append(tok)
- toks = new_toks
- if len(toks) == 0:
- assert napostrophes
- nucleus = Hbox(0.0)
- elif len(toks) == 1:
- if not napostrophes:
- return toks[0] # .asList()
- else:
- nucleus = toks[0]
- elif len(toks) in (2, 3):
- # single subscript or superscript
- nucleus = toks[0] if len(toks) == 3 else Hbox(0.0)
- op, next = toks[-2:]
- if op == '_':
- sub = next
- else:
- super = next
- elif len(toks) in (4, 5):
- # subscript and superscript
- nucleus = toks[0] if len(toks) == 5 else Hbox(0.0)
- op1, next1, op2, next2 = toks[-4:]
- if op1 == op2:
- if op1 == '_':
- raise ParseFatalException("Double subscript")
- else:
- raise ParseFatalException("Double superscript")
- if op1 == '_':
- sub = next1
- super = next2
- else:
- super = next1
- sub = next2
- else:
- raise ParseFatalException(
- "Subscript/superscript sequence is too long. "
- "Use braces { } to remove ambiguity.")
- state = self.get_state()
- rule_thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- xHeight = state.font_output.get_xheight(
- state.font, state.fontsize, state.dpi)
- if napostrophes:
- if super is None:
- super = Hlist([])
- for i in range(napostrophes):
- super.children.extend(self.symbol(s, loc, ['\\prime']))
- # kern() and hpack() needed to get the metrics right after
- # extending
- super.kern()
- super.hpack()
- # Handle over/under symbols, such as sum or integral
- if self.is_overunder(nucleus):
- vlist = []
- shift = 0.
- width = nucleus.width
- if super is not None:
- super.shrink()
- width = max(width, super.width)
- if sub is not None:
- sub.shrink()
- width = max(width, sub.width)
- if super is not None:
- hlist = HCentered([super])
- hlist.hpack(width, 'exactly')
- vlist.extend([hlist, Kern(rule_thickness * 3.0)])
- hlist = HCentered([nucleus])
- hlist.hpack(width, 'exactly')
- vlist.append(hlist)
- if sub is not None:
- hlist = HCentered([sub])
- hlist.hpack(width, 'exactly')
- vlist.extend([Kern(rule_thickness * 3.0), hlist])
- shift = hlist.height
- vlist = Vlist(vlist)
- vlist.shift_amount = shift + nucleus.depth
- result = Hlist([vlist])
- return [result]
- # We remove kerning on the last character for consistency (otherwise
- # it will compute kerning based on non-shrunk characters and may put
- # them too close together when superscripted)
- # We change the width of the last character to match the advance to
- # consider some fonts with weird metrics: e.g. stix's f has a width of
- # 7.75 and a kerning of -4.0 for an advance of 3.72, and we want to put
- # the superscript at the advance
- last_char = nucleus
- if isinstance(nucleus, Hlist):
- new_children = nucleus.children
- if len(new_children):
- # remove last kern
- if (isinstance(new_children[-1], Kern) and
- hasattr(new_children[-2], '_metrics')):
- new_children = new_children[:-1]
- last_char = new_children[-1]
- if hasattr(last_char, '_metrics'):
- last_char.width = last_char._metrics.advance
- # create new Hlist without kerning
- nucleus = Hlist(new_children, do_kern=False)
- else:
- if isinstance(nucleus, Char):
- last_char.width = last_char._metrics.advance
- nucleus = Hlist([nucleus])
- # Handle regular sub/superscripts
- constants = _get_font_constant_set(state)
- lc_height = last_char.height
- lc_baseline = 0
- if self.is_dropsub(last_char):
- lc_baseline = last_char.depth
- # Compute kerning for sub and super
- superkern = constants.delta * xHeight
- subkern = constants.delta * xHeight
- if self.is_slanted(last_char):
- superkern += constants.delta * xHeight
- superkern += (constants.delta_slanted *
- (lc_height - xHeight * 2. / 3.))
- if self.is_dropsub(last_char):
- subkern = (3 * constants.delta -
- constants.delta_integral) * lc_height
- superkern = (3 * constants.delta +
- constants.delta_integral) * lc_height
- else:
- subkern = 0
- if super is None:
- # node757
- x = Hlist([Kern(subkern), sub])
- x.shrink()
- if self.is_dropsub(last_char):
- shift_down = lc_baseline + constants.subdrop * xHeight
- else:
- shift_down = constants.sub1 * xHeight
- x.shift_amount = shift_down
- else:
- x = Hlist([Kern(superkern), super])
- x.shrink()
- if self.is_dropsub(last_char):
- shift_up = lc_height - constants.subdrop * xHeight
- else:
- shift_up = constants.sup1 * xHeight
- if sub is None:
- x.shift_amount = -shift_up
- else: # Both sub and superscript
- y = Hlist([Kern(subkern), sub])
- y.shrink()
- if self.is_dropsub(last_char):
- shift_down = lc_baseline + constants.subdrop * xHeight
- else:
- shift_down = constants.sub2 * xHeight
- # If sub and superscript collide, move super up
- clr = (2.0 * rule_thickness -
- ((shift_up - x.depth) - (y.height - shift_down)))
- if clr > 0.:
- shift_up += clr
- x = Vlist([
- x,
- Kern((shift_up - x.depth) - (y.height - shift_down)),
- y])
- x.shift_amount = shift_down
- if not self.is_dropsub(last_char):
- x.width += constants.script_space * xHeight
- result = Hlist([nucleus, x])
- return [result]
- def _genfrac(self, ldelim, rdelim, rule, style, num, den):
- state = self.get_state()
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- rule = float(rule)
- # If style != displaystyle == 0, shrink the num and den
- if style != self._math_style_dict['displaystyle']:
- num.shrink()
- den.shrink()
- cnum = HCentered([num])
- cden = HCentered([den])
- width = max(num.width, den.width)
- cnum.hpack(width, 'exactly')
- cden.hpack(width, 'exactly')
- vlist = Vlist([cnum, # numerator
- Vbox(0, thickness * 2.0), # space
- Hrule(state, rule), # rule
- Vbox(0, thickness * 2.0), # space
- cden # denominator
- ])
- # Shift so the fraction line sits in the middle of the
- # equals sign
- metrics = state.font_output.get_metrics(
- state.font, rcParams['mathtext.default'],
- '=', state.fontsize, state.dpi)
- shift = (cden.height -
- ((metrics.ymax + metrics.ymin) / 2 -
- thickness * 3.0))
- vlist.shift_amount = shift
- result = [Hlist([vlist, Hbox(thickness * 2.)])]
- if ldelim or rdelim:
- if ldelim == '':
- ldelim = '.'
- if rdelim == '':
- rdelim = '.'
- return self._auto_sized_delimiter(ldelim, result, rdelim)
- return result
- def genfrac(self, s, loc, toks):
- assert len(toks) == 1
- assert len(toks[0]) == 6
- return self._genfrac(*tuple(toks[0]))
- def frac(self, s, loc, toks):
- assert len(toks) == 1
- assert len(toks[0]) == 2
- state = self.get_state()
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- num, den = toks[0]
- return self._genfrac('', '', thickness,
- self._math_style_dict['textstyle'], num, den)
- def dfrac(self, s, loc, toks):
- assert len(toks) == 1
- assert len(toks[0]) == 2
- state = self.get_state()
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- num, den = toks[0]
- return self._genfrac('', '', thickness,
- self._math_style_dict['displaystyle'], num, den)
- @cbook.deprecated("3.1", obj_type="mathtext command",
- alternative=r"\genfrac")
- def stackrel(self, s, loc, toks):
- assert len(toks) == 1
- assert len(toks[0]) == 2
- num, den = toks[0]
- return self._genfrac('', '', 0.0,
- self._math_style_dict['textstyle'], num, den)
- def binom(self, s, loc, toks):
- assert len(toks) == 1
- assert len(toks[0]) == 2
- num, den = toks[0]
- return self._genfrac('(', ')', 0.0,
- self._math_style_dict['textstyle'], num, den)
- def sqrt(self, s, loc, toks):
- root, body = toks[0]
- state = self.get_state()
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- # Determine the height of the body, and add a little extra to
- # the height so it doesn't seem cramped
- height = body.height - body.shift_amount + thickness * 5.0
- depth = body.depth + body.shift_amount
- check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True)
- height = check.height - check.shift_amount
- depth = check.depth + check.shift_amount
- # Put a little extra space to the left and right of the body
- padded_body = Hlist([Hbox(thickness * 2.0),
- body,
- Hbox(thickness * 2.0)])
- rightside = Vlist([Hrule(state),
- Fill(),
- padded_body])
- # Stretch the glue between the hrule and the body
- rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
- 'exactly', depth)
- # Add the root and shift it upward so it is above the tick.
- # The value of 0.6 is a hard-coded hack ;)
- if root is None:
- root = Box(check.width * 0.5, 0., 0.)
- else:
- root = Hlist([Char(x, state) for x in root])
- root.shrink()
- root.shrink()
- root_vlist = Vlist([Hlist([root])])
- root_vlist.shift_amount = -height * 0.6
- hlist = Hlist([root_vlist, # Root
- # Negative kerning to put root over tick
- Kern(-check.width * 0.5),
- check, # Check
- rightside]) # Body
- return [hlist]
- def overline(self, s, loc, toks):
- assert len(toks) == 1
- assert len(toks[0]) == 1
- body = toks[0][0]
- state = self.get_state()
- thickness = state.font_output.get_underline_thickness(
- state.font, state.fontsize, state.dpi)
- height = body.height - body.shift_amount + thickness * 3.0
- depth = body.depth + body.shift_amount
- # Place overline above body
- rightside = Vlist([Hrule(state),
- Fill(),
- Hlist([body])])
- # Stretch the glue between the hrule and the body
- rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
- 'exactly', depth)
- hlist = Hlist([rightside])
- return [hlist]
- def _auto_sized_delimiter(self, front, middle, back):
- state = self.get_state()
- if len(middle):
- height = max(x.height for x in middle)
- depth = max(x.depth for x in middle)
- factor = None
- else:
- height = 0
- depth = 0
- factor = 1.0
- parts = []
- # \left. and \right. aren't supposed to produce any symbols
- if front != '.':
- parts.append(
- AutoHeightChar(front, height, depth, state, factor=factor))
- parts.extend(middle)
- if back != '.':
- parts.append(
- AutoHeightChar(back, height, depth, state, factor=factor))
- hlist = Hlist(parts)
- return hlist
- def auto_delim(self, s, loc, toks):
- front, middle, back = toks
- return self._auto_sized_delimiter(front, middle.asList(), back)
- ##############################################################################
- # MAIN
- class MathTextParser:
- _parser = None
- _backend_mapping = {
- 'bitmap': MathtextBackendBitmap,
- 'agg': MathtextBackendAgg,
- 'ps': MathtextBackendPs,
- 'pdf': MathtextBackendPdf,
- 'svg': MathtextBackendSvg,
- 'path': MathtextBackendPath,
- 'cairo': MathtextBackendCairo,
- 'macosx': MathtextBackendAgg,
- }
- _font_type_mapping = {
- 'cm': BakomaFonts,
- 'dejavuserif': DejaVuSerifFonts,
- 'dejavusans': DejaVuSansFonts,
- 'stix': StixFonts,
- 'stixsans': StixSansFonts,
- 'custom': UnicodeFonts,
- }
- def __init__(self, output):
- """
- Create a MathTextParser for the given backend *output*.
- """
- self._output = output.lower()
- @functools.lru_cache(50)
- def parse(self, s, dpi = 72, prop = None):
- """
- Parse the given math expression *s* at the given *dpi*. If
- *prop* is provided, it is a
- :class:`~matplotlib.font_manager.FontProperties` object
- specifying the "default" font to use in the math expression,
- used for all non-math text.
- The results are cached, so multiple calls to :meth:`parse`
- with the same expression should be fast.
- """
- if prop is None:
- prop = FontProperties()
- if self._output == 'ps' and rcParams['ps.useafm']:
- font_output = StandardPsFonts(prop)
- else:
- backend = self._backend_mapping[self._output]()
- fontset = rcParams['mathtext.fontset'].lower()
- fontset_class = cbook._check_getitem(
- self._font_type_mapping, fontset=fontset)
- font_output = fontset_class(prop, backend)
- fontsize = prop.get_size_in_points()
- # This is a class variable so we don't rebuild the parser
- # with each request.
- if self._parser is None:
- self.__class__._parser = Parser()
- box = self._parser.parse(s, font_output, fontsize, dpi)
- font_output.set_canvas_size(box.width, box.height, box.depth)
- return font_output.get_results(box)
- def to_mask(self, texstr, dpi=120, fontsize=14):
- r"""
- Parameters
- ----------
- texstr : str
- A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'.
- dpi : float
- The dots-per-inch setting used to render the text.
- fontsize : int
- The font size in points
- Returns
- -------
- array : 2D uint8 alpha
- Mask array of rasterized tex.
- depth : int
- Offset of the baseline from the bottom of the image, in pixels.
- """
- assert self._output == "bitmap"
- prop = FontProperties(size=fontsize)
- ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop)
- return np.asarray(ftimage), depth
- def to_rgba(self, texstr, color='black', dpi=120, fontsize=14):
- r"""
- Parameters
- ----------
- texstr : str
- A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'.
- color : color
- The text color.
- dpi : float
- The dots-per-inch setting used to render the text.
- fontsize : int
- The font size in points.
- Returns
- -------
- array : (M, N, 4) array
- RGBA color values of rasterized tex, colorized with *color*.
- depth : int
- Offset of the baseline from the bottom of the image, in pixels.
- """
- x, depth = self.to_mask(texstr, dpi=dpi, fontsize=fontsize)
- r, g, b, a = mcolors.to_rgba(color)
- RGBA = np.zeros((x.shape[0], x.shape[1], 4), dtype=np.uint8)
- RGBA[:, :, 0] = 255 * r
- RGBA[:, :, 1] = 255 * g
- RGBA[:, :, 2] = 255 * b
- RGBA[:, :, 3] = x
- return RGBA, depth
- def to_png(self, filename, texstr, color='black', dpi=120, fontsize=14):
- r"""
- Render a tex expression to a PNG file.
- Parameters
- ----------
- filename
- A writable filename or fileobject.
- texstr : str
- A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'.
- color : color
- The text color.
- dpi : float
- The dots-per-inch setting used to render the text.
- fontsize : int
- The font size in points.
- Returns
- -------
- depth : int
- Offset of the baseline from the bottom of the image, in pixels.
- """
- from matplotlib import _png
- rgba, depth = self.to_rgba(
- texstr, color=color, dpi=dpi, fontsize=fontsize)
- with cbook.open_file_cm(filename, "wb") as file:
- _png.write_png(rgba, file)
- return depth
- def get_depth(self, texstr, dpi=120, fontsize=14):
- r"""
- Parameters
- ----------
- texstr : str
- A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'.
- dpi : float
- The dots-per-inch setting used to render the text.
- Returns
- -------
- depth : int
- Offset of the baseline from the bottom of the image, in pixels.
- """
- assert self._output == "bitmap"
- prop = FontProperties(size=fontsize)
- ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop)
- return depth
- def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None):
- """
- Given a math expression, renders it in a closely-clipped bounding
- box to an image file.
- *s*
- A math expression. The math portion should be enclosed in
- dollar signs.
- *filename_or_obj*
- A filepath or writable file-like object to write the image data
- to.
- *prop*
- If provided, a FontProperties() object describing the size and
- style of the text.
- *dpi*
- Override the output dpi, otherwise use the default associated
- with the output format.
- *format*
- The output format, e.g., 'svg', 'pdf', 'ps' or 'png'. If not
- provided, will be deduced from the filename.
- """
- from matplotlib import figure
- # backend_agg supports all of the core output formats
- from matplotlib.backends import backend_agg
- if prop is None:
- prop = FontProperties()
- parser = MathTextParser('path')
- width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
- fig = figure.Figure(figsize=(width / 72.0, height / 72.0))
- fig.text(0, depth/height, s, fontproperties=prop)
- backend_agg.FigureCanvasAgg(fig)
- fig.savefig(filename_or_obj, dpi=dpi, format=format)
- return depth
|