123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495 |
- r"""
- A module for parsing a subset of the TeX math syntax and rendering it 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: https://pypi.org/project/pyparsing/
- 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 PIL import Image
- 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.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: 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 as err:
- raise ValueError(
- "'{}' is not a valid Unicode character or TeX/Type1 symbol"
- .format(symbol)) from err
- class MathtextBackend:
- """
- The base class for the mathtext backend-specific code. `MathtextBackend`
- subclasses interface between mathtext and specific Matplotlib graphics
- backends.
- 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):
- """Set the dimension of 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
- 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"/{info.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):
- """
- Parameters
- ----------
- default_font_prop: `~.font_manager.FontProperties`
- The default non-math font, or the base font for Unicode (generic)
- font rendering.
- mathtext_backend: `MathtextBackend` subclass
- Backend to which rendering is actually delegated.
- """
- 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)
- self.used_characters.setdefault(info.font.fname, set()).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 / 2) + (fontsize/3 * dpi/72)
- 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
- fallback_rc = rcParams['mathtext.fallback']
- if rcParams['mathtext.fallback_to_cm'] is not None:
- fallback_rc = ('cm' if rcParams['mathtext.fallback_to_cm']
- else None)
- font_class = {'stix': StixFonts,
- 'stixsans': StixSansFonts,
- 'cm': BakomaFonts
- }.get(fallback_rc)
- self.cm_fallback = font_class(*args, **kwargs) if font_class else 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
- # include STIX sized alternatives for glyphs if fallback is STIX
- if isinstance(self.cm_fallback, StixFonts):
- stixsizedaltfonts = {
- 0: 'STIXGeneral',
- 1: 'STIXSizeOneSym',
- 2: 'STIXSizeTwoSym',
- 3: 'STIXSizeThreeSym',
- 4: 'STIXSizeFourSym',
- 5: 'STIXSizeFiveSym'}
- for size, name in stixsizedaltfonts.items():
- fullpath = findfont(name)
- self.fontmap[size] = fullpath
- self.fontmap[name] = fullpath
- _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 (fontname in ('it', 'regular')
- and isinstance(self.cm_fallback, StixFonts)):
- fontname = 'rm'
- g = self.cm_fallback._get_glyph(fontname, font_class,
- sym, fontsize)
- fname = g[0].family_name
- if fname in list(BakomaFonts._fontmap.values()):
- fname = "Computer Modern"
- _log.info("Substituting symbol %s from %s", sym, fname)
- return g
- 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):
- """A 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):
- """
- A single character.
- Unlike TeX, the font information and metrics are stored with each `Char`
- to make it easier to lookup the font metrics when needed. Note that TeX
- boxes have a width, height, and depth, unlike Type1 and TrueType which use
- a full bounding box and an advance in the x-direction. The metrics must
- be converted to the TeX model, and the advance (if different from width)
- must be converted into a `Kern` node when the `Char` is added to its parent
- `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.
- This method is called when characters are strung together into `Hlist`
- to create `Kern` nodes.
- """
- advance = self._metrics.advance - self.width
- kern = 0.
- if isinstance(next, Char):
- kern = self.font_output.get_kern(
- self.font, self.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 `Kern` nodes between `Char` nodes to set kerning.
- The `Char` nodes themselves determine the amount of kerning they need
- (in `~Char.get_kerning`), and this function just creates the correct
- linked list.
- """
- 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"""
- Compute the dimensions of the resulting boxes, and 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.
- Parameters
- ----------
- w : float, default: 0
- A width.
- m : {'exactly', 'additional'}, default: 'additional'
- Whether to produce a box whose width is 'exactly' *w*; or a box
- with the natural width of the contents, plus *w* ('additional').
- Notes
- -----
- The defaults produce a box with the natural width of the contents.
- """
- # 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):
- """
- Compute the dimensions of the resulting boxes, and to adjust the glue
- if one of those dimensions is pre-specified.
- Parameters
- ----------
- h : float, default: 0
- A height.
- m : {'exactly', 'additional'}, default: 'additional'
- Whether to produce a box whose height is 'exactly' *w*; or a box
- with the natural height of the contents, plus *w* ('additional').
- l : float, default: np.inf
- The maximum height.
- Notes
- -----
- The defaults produce a box with the natural height of the contents.
- """
- # 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 solid black rectangle.
- It has *width*, *depth*, and *height* fields just as in an `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 `Hlist`; the height and depth are never running in a `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)
- _GlueSpec = namedtuple(
- "_GlueSpec", "width stretch stretch_order shrink shrink_order")
- _GlueSpec._named = {
- '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),
- }
- class Glue(Node):
- """
- Most of the information in this object is stored in the underlying
- ``_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.)
- """
- @cbook.deprecated("3.3")
- @property
- def glue_subtype(self):
- return "normal"
- @cbook._delete_parameter("3.3", "copy")
- def __init__(self, glue_type, copy=False):
- Node.__init__(self)
- if isinstance(glue_type, str):
- glue_spec = _GlueSpec._named[glue_type]
- elif isinstance(glue_type, _GlueSpec):
- glue_spec = glue_type
- else:
- raise ValueError("glue_type must be a glue spec name or instance")
- self.glue_spec = glue_spec
- def shrink(self):
- Node.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- g = self.glue_spec
- self.glue_spec = g._replace(width=g.width * SHRINK_FACTOR)
- def grow(self):
- Node.grow(self)
- g = self.glue_spec
- self.glue_spec = g._replace(width=g.width * GROW_FACTOR)
- @cbook.deprecated("3.3")
- class GlueSpec:
- """See `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]
- with cbook._suppress_matplotlib_deprecation_warning():
- GlueSpec._types = {k: GlueSpec(**v._asdict())
- for k, v in _GlueSpec._named.items()}
- # Some convenient ways to get common kinds of glue
- @cbook.deprecated("3.3", alternative="Glue('fil')")
- class Fil(Glue):
- def __init__(self):
- Glue.__init__(self, 'fil')
- @cbook.deprecated("3.3", alternative="Glue('fill')")
- class Fill(Glue):
- def __init__(self):
- Glue.__init__(self, 'fill')
- @cbook.deprecated("3.3", alternative="Glue('filll')")
- class Filll(Glue):
- def __init__(self):
- Glue.__init__(self, 'filll')
- @cbook.deprecated("3.3", alternative="Glue('neg_fil')")
- class NegFil(Glue):
- def __init__(self):
- Glue.__init__(self, 'neg_fil')
- @cbook.deprecated("3.3", alternative="Glue('neg_fill')")
- class NegFill(Glue):
- def __init__(self):
- Glue.__init__(self, 'neg_fill')
- @cbook.deprecated("3.3", alternative="Glue('neg_filll')")
- class NegFilll(Glue):
- def __init__(self):
- Glue.__init__(self, 'neg_filll')
- @cbook.deprecated("3.3", alternative="Glue('ss')")
- class SsGlue(Glue):
- def __init__(self):
- Glue.__init__(self, 'ss')
- class HCentered(Hlist):
- """
- A convenience class to create an `Hlist` whose contents are
- centered within its enclosing box.
- """
- def __init__(self, elements):
- super().__init__([Glue('ss'), *elements, Glue('ss')], do_kern=False)
- class VCentered(Vlist):
- """
- A convenience class to create a `Vlist` whose contents are
- centered within its enclosing box.
- """
- def __init__(self, elements):
- super().__init__([Glue('ss'), *elements, Glue('ss')])
- class Kern(Node):
- """
- A `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):
- """
- A hack to get around that fact that this code does 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):
- """
- 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):
- """
- 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:
- """
- Ship boxes to output once they have been set up, this sends them to output.
- Since boxes can be inside of boxes inside of boxes, the main work of `Ship`
- is done by two mutually recursive routines, `hlist_out` and `vlist_out`,
- which traverse the `Hlist` nodes and `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:
- """
- A pyparsing-based parser for strings containing math expressions.
- 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 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.accentprefixed = Forward()
- p.space = Forward()
- p.sqrt = 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.accentprefixed <<= Suppress(p.bslash) + oneOf(self._accentprefixed)
- 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.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.accentprefixed # 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.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 `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)])) from 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 in ('rm', 'it', 'bf'):
- self.font_class = name
- self._font = name
- def get_state(self):
- """Get the current `State` of the parser."""
- return self._state_stack[-1]
- def pop_state(self):
- """Pop a `State` off of the stack."""
- self._state_stack.pop()
- def push_state(self):
- """Push a new `State` onto the stack, copying 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 as err:
- raise ParseFatalException(s, loc,
- "Unknown symbol: %s" % c) from err
- 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]
- accentprefixed = 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
- _accentprefixed = (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)
- 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(2 * thickness), body, Hbox(2 * thickness)])
- rightside = Vlist([Hrule(state), Glue('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), Glue('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()
- 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 `.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 `parse`
- with the same expression should be fast.
- """
- # lru_cache can't decorate parse() directly because the ps.useafm and
- # mathtext.fontset rcParams also affect the parse (e.g. by affecting
- # the glyph metrics).
- return self._parse_cached(
- s, dpi, prop, rcParams['ps.useafm'], rcParams['mathtext.fontset'])
- @functools.lru_cache(50)
- def _parse_cached(self, s, dpi, prop, ps_useafm, fontset):
- if prop is None:
- prop = FontProperties()
- if self._output == 'ps' and ps_useafm:
- font_output = StandardPsFonts(prop)
- else:
- backend = self._backend_mapping[self._output]()
- 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
- -------
- int
- Offset of the baseline from the bottom of the image, in pixels.
- """
- rgba, depth = self.to_rgba(
- texstr, color=color, dpi=dpi, fontsize=fontsize)
- Image.fromarray(rgba).save(filename, format="png")
- 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
- -------
- 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.
- Parameters
- ----------
- s : str
- A math expression. The math portion must be enclosed in dollar signs.
- filename_or_obj : str or path-like or file-like
- Where to write the image data.
- prop : `.FontProperties`, optional
- The size and style of the text.
- dpi : float, optional
- The output dpi. If not set, the dpi is determined as for
- `.Figure.savefig`.
- format : str, optional
- The output format, e.g., 'svg', 'pdf', 'ps' or 'png'. If not set, the
- format is determined as for `.Figure.savefig`.
- """
- 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
|