mathtext.py 117 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495
  1. r"""
  2. A module for parsing a subset of the TeX math syntax and rendering it to a
  3. Matplotlib backend.
  4. For a tutorial of its usage, see :doc:`/tutorials/text/mathtext`. This
  5. document is primarily concerned with implementation details.
  6. The module uses pyparsing_ to parse the TeX expression.
  7. .. _pyparsing: https://pypi.org/project/pyparsing/
  8. The Bakoma distribution of the TeX Computer Modern fonts, and STIX
  9. fonts are supported. There is experimental support for using
  10. arbitrary fonts, but results may vary without proper tweaking and
  11. metrics for those fonts.
  12. """
  13. from collections import namedtuple
  14. import functools
  15. from io import StringIO
  16. import logging
  17. import os
  18. import types
  19. import unicodedata
  20. import numpy as np
  21. from PIL import Image
  22. from pyparsing import (
  23. Combine, Empty, FollowedBy, Forward, Group, Literal, oneOf, OneOrMore,
  24. Optional, ParseBaseException, ParseFatalException, ParserElement,
  25. QuotedString, Regex, StringEnd, Suppress, ZeroOrMore)
  26. from matplotlib import cbook, colors as mcolors, rcParams
  27. from matplotlib.afm import AFM
  28. from matplotlib.ft2font import FT2Image, KERNING_DEFAULT, LOAD_NO_HINTING
  29. from matplotlib.font_manager import findfont, FontProperties, get_font
  30. from matplotlib._mathtext_data import (latex_to_bakoma, latex_to_standard,
  31. tex2uni, latex_to_cmex,
  32. stix_virtual_fonts)
  33. ParserElement.enablePackrat()
  34. _log = logging.getLogger(__name__)
  35. ##############################################################################
  36. # FONTS
  37. def get_unicode_index(symbol, math=True):
  38. r"""
  39. Return the integer index (from the Unicode table) of *symbol*.
  40. Parameters
  41. ----------
  42. symbol : str
  43. A single unicode character, a TeX command (e.g. r'\pi') or a Type1
  44. symbol name (e.g. 'phi').
  45. math : bool, default: True
  46. If False, always treat as a single unicode character.
  47. """
  48. # for a non-math symbol, simply return its unicode index
  49. if not math:
  50. return ord(symbol)
  51. # From UTF #25: U+2212 minus sign is the preferred
  52. # representation of the unary and binary minus sign rather than
  53. # the ASCII-derived U+002D hyphen-minus, because minus sign is
  54. # unambiguous and because it is rendered with a more desirable
  55. # length, usually longer than a hyphen.
  56. if symbol == '-':
  57. return 0x2212
  58. try: # This will succeed if symbol is a single unicode char
  59. return ord(symbol)
  60. except TypeError:
  61. pass
  62. try: # Is symbol a TeX symbol (i.e. \alpha)
  63. return tex2uni[symbol.strip("\\")]
  64. except KeyError as err:
  65. raise ValueError(
  66. "'{}' is not a valid Unicode character or TeX/Type1 symbol"
  67. .format(symbol)) from err
  68. class MathtextBackend:
  69. """
  70. The base class for the mathtext backend-specific code. `MathtextBackend`
  71. subclasses interface between mathtext and specific Matplotlib graphics
  72. backends.
  73. Subclasses need to override the following:
  74. - :meth:`render_glyph`
  75. - :meth:`render_rect_filled`
  76. - :meth:`get_results`
  77. And optionally, if you need to use a FreeType hinting style:
  78. - :meth:`get_hinting_type`
  79. """
  80. def __init__(self):
  81. self.width = 0
  82. self.height = 0
  83. self.depth = 0
  84. def set_canvas_size(self, w, h, d):
  85. """Set the dimension of the drawing canvas."""
  86. self.width = w
  87. self.height = h
  88. self.depth = d
  89. def render_glyph(self, ox, oy, info):
  90. """
  91. Draw a glyph described by *info* to the reference point (*ox*,
  92. *oy*).
  93. """
  94. raise NotImplementedError()
  95. def render_rect_filled(self, x1, y1, x2, y2):
  96. """
  97. Draw a filled black rectangle from (*x1*, *y1*) to (*x2*, *y2*).
  98. """
  99. raise NotImplementedError()
  100. def get_results(self, box):
  101. """
  102. Return a backend-specific tuple to return to the backend after
  103. all processing is done.
  104. """
  105. raise NotImplementedError()
  106. def get_hinting_type(self):
  107. """
  108. Get the FreeType hinting type to use with this particular
  109. backend.
  110. """
  111. return LOAD_NO_HINTING
  112. class MathtextBackendAgg(MathtextBackend):
  113. """
  114. Render glyphs and rectangles to an FTImage buffer, which is later
  115. transferred to the Agg image by the Agg backend.
  116. """
  117. def __init__(self):
  118. self.ox = 0
  119. self.oy = 0
  120. self.image = None
  121. self.mode = 'bbox'
  122. self.bbox = [0, 0, 0, 0]
  123. MathtextBackend.__init__(self)
  124. def _update_bbox(self, x1, y1, x2, y2):
  125. self.bbox = [min(self.bbox[0], x1),
  126. min(self.bbox[1], y1),
  127. max(self.bbox[2], x2),
  128. max(self.bbox[3], y2)]
  129. def set_canvas_size(self, w, h, d):
  130. MathtextBackend.set_canvas_size(self, w, h, d)
  131. if self.mode != 'bbox':
  132. self.image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0)))
  133. def render_glyph(self, ox, oy, info):
  134. if self.mode == 'bbox':
  135. self._update_bbox(ox + info.metrics.xmin,
  136. oy - info.metrics.ymax,
  137. ox + info.metrics.xmax,
  138. oy - info.metrics.ymin)
  139. else:
  140. info.font.draw_glyph_to_bitmap(
  141. self.image, ox, oy - info.metrics.iceberg, info.glyph,
  142. antialiased=rcParams['text.antialiased'])
  143. def render_rect_filled(self, x1, y1, x2, y2):
  144. if self.mode == 'bbox':
  145. self._update_bbox(x1, y1, x2, y2)
  146. else:
  147. height = max(int(y2 - y1) - 1, 0)
  148. if height == 0:
  149. center = (y2 + y1) / 2.0
  150. y = int(center - (height + 1) / 2.0)
  151. else:
  152. y = int(y1)
  153. self.image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height)
  154. def get_results(self, box, used_characters):
  155. self.mode = 'bbox'
  156. orig_height = box.height
  157. orig_depth = box.depth
  158. ship(0, 0, box)
  159. bbox = self.bbox
  160. bbox = [bbox[0] - 1, bbox[1] - 1, bbox[2] + 1, bbox[3] + 1]
  161. self.mode = 'render'
  162. self.set_canvas_size(
  163. bbox[2] - bbox[0],
  164. (bbox[3] - bbox[1]) - orig_depth,
  165. (bbox[3] - bbox[1]) - orig_height)
  166. ship(-bbox[0], -bbox[1], box)
  167. result = (self.ox,
  168. self.oy,
  169. self.width,
  170. self.height + self.depth,
  171. self.depth,
  172. self.image,
  173. used_characters)
  174. self.image = None
  175. return result
  176. def get_hinting_type(self):
  177. from matplotlib.backends import backend_agg
  178. return backend_agg.get_hinting_flag()
  179. class MathtextBackendBitmap(MathtextBackendAgg):
  180. def get_results(self, box, used_characters):
  181. ox, oy, width, height, depth, image, characters = \
  182. MathtextBackendAgg.get_results(self, box, used_characters)
  183. return image, depth
  184. class MathtextBackendPs(MathtextBackend):
  185. """
  186. Store information to write a mathtext rendering to the PostScript backend.
  187. """
  188. _PSResult = namedtuple(
  189. "_PSResult", "width height depth pswriter used_characters")
  190. def __init__(self):
  191. self.pswriter = StringIO()
  192. self.lastfont = None
  193. def render_glyph(self, ox, oy, info):
  194. oy = self.height - oy + info.offset
  195. postscript_name = info.postscript_name
  196. fontsize = info.fontsize
  197. if (postscript_name, fontsize) != self.lastfont:
  198. self.lastfont = postscript_name, fontsize
  199. self.pswriter.write(
  200. f"/{postscript_name} findfont\n"
  201. f"{fontsize} scalefont\n"
  202. f"setfont\n")
  203. self.pswriter.write(
  204. f"{ox:f} {oy:f} moveto\n"
  205. f"/{info.symbol_name} glyphshow\n")
  206. def render_rect_filled(self, x1, y1, x2, y2):
  207. ps = "%f %f %f %f rectfill\n" % (
  208. x1, self.height - y2, x2 - x1, y2 - y1)
  209. self.pswriter.write(ps)
  210. def get_results(self, box, used_characters):
  211. ship(0, 0, box)
  212. return self._PSResult(self.width,
  213. self.height + self.depth,
  214. self.depth,
  215. self.pswriter,
  216. used_characters)
  217. class MathtextBackendPdf(MathtextBackend):
  218. """Store information to write a mathtext rendering to the PDF backend."""
  219. _PDFResult = namedtuple(
  220. "_PDFResult", "width height depth glyphs rects used_characters")
  221. def __init__(self):
  222. self.glyphs = []
  223. self.rects = []
  224. def render_glyph(self, ox, oy, info):
  225. filename = info.font.fname
  226. oy = self.height - oy + info.offset
  227. self.glyphs.append(
  228. (ox, oy, filename, info.fontsize,
  229. info.num, info.symbol_name))
  230. def render_rect_filled(self, x1, y1, x2, y2):
  231. self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1))
  232. def get_results(self, box, used_characters):
  233. ship(0, 0, box)
  234. return self._PDFResult(self.width,
  235. self.height + self.depth,
  236. self.depth,
  237. self.glyphs,
  238. self.rects,
  239. used_characters)
  240. class MathtextBackendSvg(MathtextBackend):
  241. """
  242. Store information to write a mathtext rendering to the SVG
  243. backend.
  244. """
  245. def __init__(self):
  246. self.svg_glyphs = []
  247. self.svg_rects = []
  248. def render_glyph(self, ox, oy, info):
  249. oy = self.height - oy + info.offset
  250. self.svg_glyphs.append(
  251. (info.font, info.fontsize, info.num, ox, oy, info.metrics))
  252. def render_rect_filled(self, x1, y1, x2, y2):
  253. self.svg_rects.append(
  254. (x1, self.height - y1 + 1, x2 - x1, y2 - y1))
  255. def get_results(self, box, used_characters):
  256. ship(0, 0, box)
  257. svg_elements = types.SimpleNamespace(svg_glyphs=self.svg_glyphs,
  258. svg_rects=self.svg_rects)
  259. return (self.width,
  260. self.height + self.depth,
  261. self.depth,
  262. svg_elements,
  263. used_characters)
  264. class MathtextBackendPath(MathtextBackend):
  265. """
  266. Store information to write a mathtext rendering to the text path
  267. machinery.
  268. """
  269. def __init__(self):
  270. self.glyphs = []
  271. self.rects = []
  272. def render_glyph(self, ox, oy, info):
  273. oy = self.height - oy + info.offset
  274. thetext = info.num
  275. self.glyphs.append(
  276. (info.font, info.fontsize, thetext, ox, oy))
  277. def render_rect_filled(self, x1, y1, x2, y2):
  278. self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1))
  279. def get_results(self, box, used_characters):
  280. ship(0, 0, box)
  281. return (self.width,
  282. self.height + self.depth,
  283. self.depth,
  284. self.glyphs,
  285. self.rects)
  286. class MathtextBackendCairo(MathtextBackend):
  287. """
  288. Store information to write a mathtext rendering to the Cairo
  289. backend.
  290. """
  291. def __init__(self):
  292. self.glyphs = []
  293. self.rects = []
  294. def render_glyph(self, ox, oy, info):
  295. oy = oy - info.offset - self.height
  296. thetext = chr(info.num)
  297. self.glyphs.append(
  298. (info.font, info.fontsize, thetext, ox, oy))
  299. def render_rect_filled(self, x1, y1, x2, y2):
  300. self.rects.append(
  301. (x1, y1 - self.height, x2 - x1, y2 - y1))
  302. def get_results(self, box, used_characters):
  303. ship(0, 0, box)
  304. return (self.width,
  305. self.height + self.depth,
  306. self.depth,
  307. self.glyphs,
  308. self.rects)
  309. class Fonts:
  310. """
  311. An abstract base class for a system of fonts to use for mathtext.
  312. The class must be able to take symbol keys and font file names and
  313. return the character metrics. It also delegates to a backend class
  314. to do the actual drawing.
  315. """
  316. def __init__(self, default_font_prop, mathtext_backend):
  317. """
  318. Parameters
  319. ----------
  320. default_font_prop: `~.font_manager.FontProperties`
  321. The default non-math font, or the base font for Unicode (generic)
  322. font rendering.
  323. mathtext_backend: `MathtextBackend` subclass
  324. Backend to which rendering is actually delegated.
  325. """
  326. self.default_font_prop = default_font_prop
  327. self.mathtext_backend = mathtext_backend
  328. self.used_characters = {}
  329. def destroy(self):
  330. """
  331. Fix any cyclical references before the object is about
  332. to be destroyed.
  333. """
  334. self.used_characters = None
  335. def get_kern(self, font1, fontclass1, sym1, fontsize1,
  336. font2, fontclass2, sym2, fontsize2, dpi):
  337. r"""
  338. Get the kerning distance for font between *sym1* and *sym2*.
  339. *fontX*: one of the TeX font names::
  340. tt, it, rm, cal, sf, bf or default/regular (non-math)
  341. *fontclassX*: TODO
  342. *symX*: a symbol in raw TeX form. e.g., '1', 'x' or '\sigma'
  343. *fontsizeX*: the fontsize in points
  344. *dpi*: the current dots-per-inch
  345. """
  346. return 0.
  347. def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True):
  348. r"""
  349. *font*: one of the TeX font names::
  350. tt, it, rm, cal, sf, bf or default/regular (non-math)
  351. *font_class*: TODO
  352. *sym*: a symbol in raw TeX form. e.g., '1', 'x' or '\sigma'
  353. *fontsize*: font size in points
  354. *dpi*: current dots-per-inch
  355. *math*: whether sym is a math character
  356. Returns an object with the following attributes:
  357. - *advance*: The advance distance (in points) of the glyph.
  358. - *height*: The height of the glyph in points.
  359. - *width*: The width of the glyph in points.
  360. - *xmin*, *xmax*, *ymin*, *ymax* - the ink rectangle of the glyph
  361. - *iceberg* - the distance from the baseline to the top of
  362. the glyph. This corresponds to TeX's definition of "height".
  363. """
  364. info = self._get_info(font, font_class, sym, fontsize, dpi, math)
  365. return info.metrics
  366. def set_canvas_size(self, w, h, d):
  367. """
  368. Set the size of the buffer used to render the math expression.
  369. Only really necessary for the bitmap backends.
  370. """
  371. self.width, self.height, self.depth = np.ceil([w, h, d])
  372. self.mathtext_backend.set_canvas_size(
  373. self.width, self.height, self.depth)
  374. def render_glyph(self, ox, oy, facename, font_class, sym, fontsize, dpi):
  375. """
  376. Draw a glyph at
  377. - *ox*, *oy*: position
  378. - *facename*: One of the TeX face names
  379. - *font_class*:
  380. - *sym*: TeX symbol name or single character
  381. - *fontsize*: fontsize in points
  382. - *dpi*: The dpi to draw at.
  383. """
  384. info = self._get_info(facename, font_class, sym, fontsize, dpi)
  385. self.used_characters.setdefault(info.font.fname, set()).add(info.num)
  386. self.mathtext_backend.render_glyph(ox, oy, info)
  387. def render_rect_filled(self, x1, y1, x2, y2):
  388. """
  389. Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*).
  390. """
  391. self.mathtext_backend.render_rect_filled(x1, y1, x2, y2)
  392. def get_xheight(self, font, fontsize, dpi):
  393. """
  394. Get the xheight for the given *font* and *fontsize*.
  395. """
  396. raise NotImplementedError()
  397. def get_underline_thickness(self, font, fontsize, dpi):
  398. """
  399. Get the line thickness that matches the given font. Used as a
  400. base unit for drawing lines such as in a fraction or radical.
  401. """
  402. raise NotImplementedError()
  403. def get_used_characters(self):
  404. """
  405. Get the set of characters that were used in the math
  406. expression. Used by backends that need to subset fonts so
  407. they know which glyphs to include.
  408. """
  409. return self.used_characters
  410. def get_results(self, box):
  411. """
  412. Get the data needed by the backend to render the math
  413. expression. The return value is backend-specific.
  414. """
  415. result = self.mathtext_backend.get_results(
  416. box, self.get_used_characters())
  417. self.destroy()
  418. return result
  419. def get_sized_alternatives_for_symbol(self, fontname, sym):
  420. """
  421. Override if your font provides multiple sizes of the same
  422. symbol. Should return a list of symbols matching *sym* in
  423. various sizes. The expression renderer will select the most
  424. appropriate size for a given situation from this list.
  425. """
  426. return [(fontname, sym)]
  427. class TruetypeFonts(Fonts):
  428. """
  429. A generic base class for all font setups that use Truetype fonts
  430. (through FT2Font).
  431. """
  432. def __init__(self, default_font_prop, mathtext_backend):
  433. Fonts.__init__(self, default_font_prop, mathtext_backend)
  434. self.glyphd = {}
  435. self._fonts = {}
  436. filename = findfont(default_font_prop)
  437. default_font = get_font(filename)
  438. self._fonts['default'] = default_font
  439. self._fonts['regular'] = default_font
  440. def destroy(self):
  441. self.glyphd = None
  442. Fonts.destroy(self)
  443. def _get_font(self, font):
  444. if font in self.fontmap:
  445. basename = self.fontmap[font]
  446. else:
  447. basename = font
  448. cached_font = self._fonts.get(basename)
  449. if cached_font is None and os.path.exists(basename):
  450. cached_font = get_font(basename)
  451. self._fonts[basename] = cached_font
  452. self._fonts[cached_font.postscript_name] = cached_font
  453. self._fonts[cached_font.postscript_name.lower()] = cached_font
  454. return cached_font
  455. def _get_offset(self, font, glyph, fontsize, dpi):
  456. if font.postscript_name == 'Cmex10':
  457. return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72)
  458. return 0.
  459. def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True):
  460. key = fontname, font_class, sym, fontsize, dpi
  461. bunch = self.glyphd.get(key)
  462. if bunch is not None:
  463. return bunch
  464. font, num, symbol_name, fontsize, slanted = \
  465. self._get_glyph(fontname, font_class, sym, fontsize, math)
  466. font.set_size(fontsize, dpi)
  467. glyph = font.load_char(
  468. num,
  469. flags=self.mathtext_backend.get_hinting_type())
  470. xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox]
  471. offset = self._get_offset(font, glyph, fontsize, dpi)
  472. metrics = types.SimpleNamespace(
  473. advance = glyph.linearHoriAdvance/65536.0,
  474. height = glyph.height/64.0,
  475. width = glyph.width/64.0,
  476. xmin = xmin,
  477. xmax = xmax,
  478. ymin = ymin+offset,
  479. ymax = ymax+offset,
  480. # iceberg is the equivalent of TeX's "height"
  481. iceberg = glyph.horiBearingY/64.0 + offset,
  482. slanted = slanted
  483. )
  484. result = self.glyphd[key] = types.SimpleNamespace(
  485. font = font,
  486. fontsize = fontsize,
  487. postscript_name = font.postscript_name,
  488. metrics = metrics,
  489. symbol_name = symbol_name,
  490. num = num,
  491. glyph = glyph,
  492. offset = offset
  493. )
  494. return result
  495. def get_xheight(self, fontname, fontsize, dpi):
  496. font = self._get_font(fontname)
  497. font.set_size(fontsize, dpi)
  498. pclt = font.get_sfnt_table('pclt')
  499. if pclt is None:
  500. # Some fonts don't store the xHeight, so we do a poor man's xHeight
  501. metrics = self.get_metrics(
  502. fontname, rcParams['mathtext.default'], 'x', fontsize, dpi)
  503. return metrics.iceberg
  504. xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
  505. return xHeight
  506. def get_underline_thickness(self, font, fontsize, dpi):
  507. # This function used to grab underline thickness from the font
  508. # metrics, but that information is just too un-reliable, so it
  509. # is now hardcoded.
  510. return ((0.75 / 12.0) * fontsize * dpi) / 72.0
  511. def get_kern(self, font1, fontclass1, sym1, fontsize1,
  512. font2, fontclass2, sym2, fontsize2, dpi):
  513. if font1 == font2 and fontsize1 == fontsize2:
  514. info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
  515. info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
  516. font = info1.font
  517. return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64
  518. return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1,
  519. font2, fontclass2, sym2, fontsize2, dpi)
  520. class BakomaFonts(TruetypeFonts):
  521. """
  522. Use the Bakoma TrueType fonts for rendering.
  523. Symbols are strewn about a number of font files, each of which has
  524. its own proprietary 8-bit encoding.
  525. """
  526. _fontmap = {
  527. 'cal': 'cmsy10',
  528. 'rm': 'cmr10',
  529. 'tt': 'cmtt10',
  530. 'it': 'cmmi10',
  531. 'bf': 'cmb10',
  532. 'sf': 'cmss10',
  533. 'ex': 'cmex10',
  534. }
  535. def __init__(self, *args, **kwargs):
  536. self._stix_fallback = StixFonts(*args, **kwargs)
  537. TruetypeFonts.__init__(self, *args, **kwargs)
  538. self.fontmap = {}
  539. for key, val in self._fontmap.items():
  540. fullpath = findfont(val)
  541. self.fontmap[key] = fullpath
  542. self.fontmap[val] = fullpath
  543. _slanted_symbols = set(r"\int \oint".split())
  544. def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
  545. symbol_name = None
  546. font = None
  547. if fontname in self.fontmap and sym in latex_to_bakoma:
  548. basename, num = latex_to_bakoma[sym]
  549. slanted = (basename == "cmmi10") or sym in self._slanted_symbols
  550. font = self._get_font(basename)
  551. elif len(sym) == 1:
  552. slanted = (fontname == "it")
  553. font = self._get_font(fontname)
  554. if font is not None:
  555. num = ord(sym)
  556. if font is not None:
  557. gid = font.get_char_index(num)
  558. if gid != 0:
  559. symbol_name = font.get_glyph_name(gid)
  560. if symbol_name is None:
  561. return self._stix_fallback._get_glyph(
  562. fontname, font_class, sym, fontsize, math)
  563. return font, num, symbol_name, fontsize, slanted
  564. # The Bakoma fonts contain many pre-sized alternatives for the
  565. # delimiters. The AutoSizedChar class will use these alternatives
  566. # and select the best (closest sized) glyph.
  567. _size_alternatives = {
  568. '(': [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
  569. ('ex', '\xb5'), ('ex', '\xc3')],
  570. ')': [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
  571. ('ex', '\xb6'), ('ex', '\x21')],
  572. '{': [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'),
  573. ('ex', '\xbd'), ('ex', '\x28')],
  574. '}': [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'),
  575. ('ex', '\xbe'), ('ex', '\x29')],
  576. # The fourth size of '[' is mysteriously missing from the BaKoMa
  577. # font, so I've omitted it for both '[' and ']'
  578. '[': [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'),
  579. ('ex', '\x22')],
  580. ']': [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'),
  581. ('ex', '\x23')],
  582. r'\lfloor': [('ex', '\xa5'), ('ex', '\x6a'),
  583. ('ex', '\xb9'), ('ex', '\x24')],
  584. r'\rfloor': [('ex', '\xa6'), ('ex', '\x6b'),
  585. ('ex', '\xba'), ('ex', '\x25')],
  586. r'\lceil': [('ex', '\xa7'), ('ex', '\x6c'),
  587. ('ex', '\xbb'), ('ex', '\x26')],
  588. r'\rceil': [('ex', '\xa8'), ('ex', '\x6d'),
  589. ('ex', '\xbc'), ('ex', '\x27')],
  590. r'\langle': [('ex', '\xad'), ('ex', '\x44'),
  591. ('ex', '\xbf'), ('ex', '\x2a')],
  592. r'\rangle': [('ex', '\xae'), ('ex', '\x45'),
  593. ('ex', '\xc0'), ('ex', '\x2b')],
  594. r'\__sqrt__': [('ex', '\x70'), ('ex', '\x71'),
  595. ('ex', '\x72'), ('ex', '\x73')],
  596. r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'),
  597. ('ex', '\xc2'), ('ex', '\x2d')],
  598. r'/': [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'),
  599. ('ex', '\xcb'), ('ex', '\x2c')],
  600. r'\widehat': [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'),
  601. ('ex', '\x64')],
  602. r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'),
  603. ('ex', '\x67')],
  604. r'<': [('cal', 'h'), ('ex', 'D')],
  605. r'>': [('cal', 'i'), ('ex', 'E')]
  606. }
  607. for alias, target in [(r'\leftparen', '('),
  608. (r'\rightparent', ')'),
  609. (r'\leftbrace', '{'),
  610. (r'\rightbrace', '}'),
  611. (r'\leftbracket', '['),
  612. (r'\rightbracket', ']'),
  613. (r'\{', '{'),
  614. (r'\}', '}'),
  615. (r'\[', '['),
  616. (r'\]', ']')]:
  617. _size_alternatives[alias] = _size_alternatives[target]
  618. def get_sized_alternatives_for_symbol(self, fontname, sym):
  619. return self._size_alternatives.get(sym, [(fontname, sym)])
  620. class UnicodeFonts(TruetypeFonts):
  621. """
  622. An abstract base class for handling Unicode fonts.
  623. While some reasonably complete Unicode fonts (such as DejaVu) may
  624. work in some situations, the only Unicode font I'm aware of with a
  625. complete set of math symbols is STIX.
  626. This class will "fallback" on the Bakoma fonts when a required
  627. symbol can not be found in the font.
  628. """
  629. use_cmex = True
  630. def __init__(self, *args, **kwargs):
  631. # This must come first so the backend's owner is set correctly
  632. fallback_rc = rcParams['mathtext.fallback']
  633. if rcParams['mathtext.fallback_to_cm'] is not None:
  634. fallback_rc = ('cm' if rcParams['mathtext.fallback_to_cm']
  635. else None)
  636. font_class = {'stix': StixFonts,
  637. 'stixsans': StixSansFonts,
  638. 'cm': BakomaFonts
  639. }.get(fallback_rc)
  640. self.cm_fallback = font_class(*args, **kwargs) if font_class else None
  641. TruetypeFonts.__init__(self, *args, **kwargs)
  642. self.fontmap = {}
  643. for texfont in "cal rm tt it bf sf".split():
  644. prop = rcParams['mathtext.' + texfont]
  645. font = findfont(prop)
  646. self.fontmap[texfont] = font
  647. prop = FontProperties('cmex10')
  648. font = findfont(prop)
  649. self.fontmap['ex'] = font
  650. # include STIX sized alternatives for glyphs if fallback is STIX
  651. if isinstance(self.cm_fallback, StixFonts):
  652. stixsizedaltfonts = {
  653. 0: 'STIXGeneral',
  654. 1: 'STIXSizeOneSym',
  655. 2: 'STIXSizeTwoSym',
  656. 3: 'STIXSizeThreeSym',
  657. 4: 'STIXSizeFourSym',
  658. 5: 'STIXSizeFiveSym'}
  659. for size, name in stixsizedaltfonts.items():
  660. fullpath = findfont(name)
  661. self.fontmap[size] = fullpath
  662. self.fontmap[name] = fullpath
  663. _slanted_symbols = set(r"\int \oint".split())
  664. def _map_virtual_font(self, fontname, font_class, uniindex):
  665. return fontname, uniindex
  666. def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
  667. found_symbol = False
  668. if self.use_cmex:
  669. uniindex = latex_to_cmex.get(sym)
  670. if uniindex is not None:
  671. fontname = 'ex'
  672. found_symbol = True
  673. if not found_symbol:
  674. try:
  675. uniindex = get_unicode_index(sym, math)
  676. found_symbol = True
  677. except ValueError:
  678. uniindex = ord('?')
  679. _log.warning(
  680. "No TeX to unicode mapping for {!a}.".format(sym))
  681. fontname, uniindex = self._map_virtual_font(
  682. fontname, font_class, uniindex)
  683. new_fontname = fontname
  684. # Only characters in the "Letter" class should be italicized in 'it'
  685. # mode. Greek capital letters should be Roman.
  686. if found_symbol:
  687. if fontname == 'it' and uniindex < 0x10000:
  688. char = chr(uniindex)
  689. if (unicodedata.category(char)[0] != "L"
  690. or unicodedata.name(char).startswith("GREEK CAPITAL")):
  691. new_fontname = 'rm'
  692. slanted = (new_fontname == 'it') or sym in self._slanted_symbols
  693. found_symbol = False
  694. font = self._get_font(new_fontname)
  695. if font is not None:
  696. glyphindex = font.get_char_index(uniindex)
  697. if glyphindex != 0:
  698. found_symbol = True
  699. if not found_symbol:
  700. if self.cm_fallback:
  701. if (fontname in ('it', 'regular')
  702. and isinstance(self.cm_fallback, StixFonts)):
  703. fontname = 'rm'
  704. g = self.cm_fallback._get_glyph(fontname, font_class,
  705. sym, fontsize)
  706. fname = g[0].family_name
  707. if fname in list(BakomaFonts._fontmap.values()):
  708. fname = "Computer Modern"
  709. _log.info("Substituting symbol %s from %s", sym, fname)
  710. return g
  711. else:
  712. if (fontname in ('it', 'regular')
  713. and isinstance(self, StixFonts)):
  714. return self._get_glyph('rm', font_class, sym, fontsize)
  715. _log.warning("Font {!r} does not have a glyph for {!a} "
  716. "[U+{:x}], substituting with a dummy "
  717. "symbol.".format(new_fontname, sym, uniindex))
  718. fontname = 'rm'
  719. font = self._get_font(fontname)
  720. uniindex = 0xA4 # currency char, for lack of anything better
  721. glyphindex = font.get_char_index(uniindex)
  722. slanted = False
  723. symbol_name = font.get_glyph_name(glyphindex)
  724. return font, uniindex, symbol_name, fontsize, slanted
  725. def get_sized_alternatives_for_symbol(self, fontname, sym):
  726. if self.cm_fallback:
  727. return self.cm_fallback.get_sized_alternatives_for_symbol(
  728. fontname, sym)
  729. return [(fontname, sym)]
  730. class DejaVuFonts(UnicodeFonts):
  731. use_cmex = False
  732. def __init__(self, *args, **kwargs):
  733. # This must come first so the backend's owner is set correctly
  734. if isinstance(self, DejaVuSerifFonts):
  735. self.cm_fallback = StixFonts(*args, **kwargs)
  736. else:
  737. self.cm_fallback = StixSansFonts(*args, **kwargs)
  738. self.bakoma = BakomaFonts(*args, **kwargs)
  739. TruetypeFonts.__init__(self, *args, **kwargs)
  740. self.fontmap = {}
  741. # Include Stix sized alternatives for glyphs
  742. self._fontmap.update({
  743. 1: 'STIXSizeOneSym',
  744. 2: 'STIXSizeTwoSym',
  745. 3: 'STIXSizeThreeSym',
  746. 4: 'STIXSizeFourSym',
  747. 5: 'STIXSizeFiveSym',
  748. })
  749. for key, name in self._fontmap.items():
  750. fullpath = findfont(name)
  751. self.fontmap[key] = fullpath
  752. self.fontmap[name] = fullpath
  753. def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
  754. # Override prime symbol to use Bakoma.
  755. if sym == r'\prime':
  756. return self.bakoma._get_glyph(
  757. fontname, font_class, sym, fontsize, math)
  758. else:
  759. # check whether the glyph is available in the display font
  760. uniindex = get_unicode_index(sym)
  761. font = self._get_font('ex')
  762. if font is not None:
  763. glyphindex = font.get_char_index(uniindex)
  764. if glyphindex != 0:
  765. return super()._get_glyph(
  766. 'ex', font_class, sym, fontsize, math)
  767. # otherwise return regular glyph
  768. return super()._get_glyph(
  769. fontname, font_class, sym, fontsize, math)
  770. class DejaVuSerifFonts(DejaVuFonts):
  771. """
  772. A font handling class for the DejaVu Serif fonts
  773. If a glyph is not found it will fallback to Stix Serif
  774. """
  775. _fontmap = {
  776. 'rm': 'DejaVu Serif',
  777. 'it': 'DejaVu Serif:italic',
  778. 'bf': 'DejaVu Serif:weight=bold',
  779. 'sf': 'DejaVu Sans',
  780. 'tt': 'DejaVu Sans Mono',
  781. 'ex': 'DejaVu Serif Display',
  782. 0: 'DejaVu Serif',
  783. }
  784. class DejaVuSansFonts(DejaVuFonts):
  785. """
  786. A font handling class for the DejaVu Sans fonts
  787. If a glyph is not found it will fallback to Stix Sans
  788. """
  789. _fontmap = {
  790. 'rm': 'DejaVu Sans',
  791. 'it': 'DejaVu Sans:italic',
  792. 'bf': 'DejaVu Sans:weight=bold',
  793. 'sf': 'DejaVu Sans',
  794. 'tt': 'DejaVu Sans Mono',
  795. 'ex': 'DejaVu Sans Display',
  796. 0: 'DejaVu Sans',
  797. }
  798. class StixFonts(UnicodeFonts):
  799. """
  800. A font handling class for the STIX fonts.
  801. In addition to what UnicodeFonts provides, this class:
  802. - supports "virtual fonts" which are complete alpha numeric
  803. character sets with different font styles at special Unicode
  804. code points, such as "Blackboard".
  805. - handles sized alternative characters for the STIXSizeX fonts.
  806. """
  807. _fontmap = {
  808. 'rm': 'STIXGeneral',
  809. 'it': 'STIXGeneral:italic',
  810. 'bf': 'STIXGeneral:weight=bold',
  811. 'nonunirm': 'STIXNonUnicode',
  812. 'nonuniit': 'STIXNonUnicode:italic',
  813. 'nonunibf': 'STIXNonUnicode:weight=bold',
  814. 0: 'STIXGeneral',
  815. 1: 'STIXSizeOneSym',
  816. 2: 'STIXSizeTwoSym',
  817. 3: 'STIXSizeThreeSym',
  818. 4: 'STIXSizeFourSym',
  819. 5: 'STIXSizeFiveSym',
  820. }
  821. use_cmex = False
  822. cm_fallback = False
  823. _sans = False
  824. def __init__(self, *args, **kwargs):
  825. TruetypeFonts.__init__(self, *args, **kwargs)
  826. self.fontmap = {}
  827. for key, name in self._fontmap.items():
  828. fullpath = findfont(name)
  829. self.fontmap[key] = fullpath
  830. self.fontmap[name] = fullpath
  831. def _map_virtual_font(self, fontname, font_class, uniindex):
  832. # Handle these "fonts" that are actually embedded in
  833. # other fonts.
  834. mapping = stix_virtual_fonts.get(fontname)
  835. if (self._sans and mapping is None
  836. and fontname not in ('regular', 'default')):
  837. mapping = stix_virtual_fonts['sf']
  838. doing_sans_conversion = True
  839. else:
  840. doing_sans_conversion = False
  841. if mapping is not None:
  842. if isinstance(mapping, dict):
  843. try:
  844. mapping = mapping[font_class]
  845. except KeyError:
  846. mapping = mapping['rm']
  847. # Binary search for the source glyph
  848. lo = 0
  849. hi = len(mapping)
  850. while lo < hi:
  851. mid = (lo+hi)//2
  852. range = mapping[mid]
  853. if uniindex < range[0]:
  854. hi = mid
  855. elif uniindex <= range[1]:
  856. break
  857. else:
  858. lo = mid + 1
  859. if range[0] <= uniindex <= range[1]:
  860. uniindex = uniindex - range[0] + range[3]
  861. fontname = range[2]
  862. elif not doing_sans_conversion:
  863. # This will generate a dummy character
  864. uniindex = 0x1
  865. fontname = rcParams['mathtext.default']
  866. # Handle private use area glyphs
  867. if fontname in ('it', 'rm', 'bf') and 0xe000 <= uniindex <= 0xf8ff:
  868. fontname = 'nonuni' + fontname
  869. return fontname, uniindex
  870. @functools.lru_cache()
  871. def get_sized_alternatives_for_symbol(self, fontname, sym):
  872. fixes = {
  873. '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']',
  874. '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}',
  875. '>': '\N{MATHEMATICAL RIGHT ANGLE BRACKET}',
  876. }
  877. sym = fixes.get(sym, sym)
  878. try:
  879. uniindex = get_unicode_index(sym)
  880. except ValueError:
  881. return [(fontname, sym)]
  882. alternatives = [(i, chr(uniindex)) for i in range(6)
  883. if self._get_font(i).get_char_index(uniindex) != 0]
  884. # The largest size of the radical symbol in STIX has incorrect
  885. # metrics that cause it to be disconnected from the stem.
  886. if sym == r'\__sqrt__':
  887. alternatives = alternatives[:-1]
  888. return alternatives
  889. class StixSansFonts(StixFonts):
  890. """
  891. A font handling class for the STIX fonts (that uses sans-serif
  892. characters by default).
  893. """
  894. _sans = True
  895. class StandardPsFonts(Fonts):
  896. """
  897. Use the standard postscript fonts for rendering to backend_ps
  898. Unlike the other font classes, BakomaFont and UnicodeFont, this
  899. one requires the Ps backend.
  900. """
  901. basepath = str(cbook._get_data_path('fonts/afm'))
  902. fontmap = {
  903. 'cal': 'pzcmi8a', # Zapf Chancery
  904. 'rm': 'pncr8a', # New Century Schoolbook
  905. 'tt': 'pcrr8a', # Courier
  906. 'it': 'pncri8a', # New Century Schoolbook Italic
  907. 'sf': 'phvr8a', # Helvetica
  908. 'bf': 'pncb8a', # New Century Schoolbook Bold
  909. None: 'psyr', # Symbol
  910. }
  911. def __init__(self, default_font_prop):
  912. Fonts.__init__(self, default_font_prop, MathtextBackendPs())
  913. self.glyphd = {}
  914. self.fonts = {}
  915. filename = findfont(default_font_prop, fontext='afm',
  916. directory=self.basepath)
  917. if filename is None:
  918. filename = findfont('Helvetica', fontext='afm',
  919. directory=self.basepath)
  920. with open(filename, 'rb') as fd:
  921. default_font = AFM(fd)
  922. default_font.fname = filename
  923. self.fonts['default'] = default_font
  924. self.fonts['regular'] = default_font
  925. self.pswriter = StringIO()
  926. def _get_font(self, font):
  927. if font in self.fontmap:
  928. basename = self.fontmap[font]
  929. else:
  930. basename = font
  931. cached_font = self.fonts.get(basename)
  932. if cached_font is None:
  933. fname = os.path.join(self.basepath, basename + ".afm")
  934. with open(fname, 'rb') as fd:
  935. cached_font = AFM(fd)
  936. cached_font.fname = fname
  937. self.fonts[basename] = cached_font
  938. self.fonts[cached_font.get_fontname()] = cached_font
  939. return cached_font
  940. def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True):
  941. """Load the cmfont, metrics and glyph with caching."""
  942. key = fontname, sym, fontsize, dpi
  943. tup = self.glyphd.get(key)
  944. if tup is not None:
  945. return tup
  946. # Only characters in the "Letter" class should really be italicized.
  947. # This class includes greek letters, so we're ok
  948. if (fontname == 'it' and
  949. (len(sym) > 1
  950. or not unicodedata.category(sym).startswith("L"))):
  951. fontname = 'rm'
  952. found_symbol = False
  953. if sym in latex_to_standard:
  954. fontname, num = latex_to_standard[sym]
  955. glyph = chr(num)
  956. found_symbol = True
  957. elif len(sym) == 1:
  958. glyph = sym
  959. num = ord(glyph)
  960. found_symbol = True
  961. else:
  962. _log.warning(
  963. "No TeX to built-in Postscript mapping for {!r}".format(sym))
  964. slanted = (fontname == 'it')
  965. font = self._get_font(fontname)
  966. if found_symbol:
  967. try:
  968. symbol_name = font.get_name_char(glyph)
  969. except KeyError:
  970. _log.warning(
  971. "No glyph in standard Postscript font {!r} for {!r}"
  972. .format(font.get_fontname(), sym))
  973. found_symbol = False
  974. if not found_symbol:
  975. glyph = '?'
  976. num = ord(glyph)
  977. symbol_name = font.get_name_char(glyph)
  978. offset = 0
  979. scale = 0.001 * fontsize
  980. xmin, ymin, xmax, ymax = [val * scale
  981. for val in font.get_bbox_char(glyph)]
  982. metrics = types.SimpleNamespace(
  983. advance = font.get_width_char(glyph) * scale,
  984. width = font.get_width_char(glyph) * scale,
  985. height = font.get_height_char(glyph) * scale,
  986. xmin = xmin,
  987. xmax = xmax,
  988. ymin = ymin+offset,
  989. ymax = ymax+offset,
  990. # iceberg is the equivalent of TeX's "height"
  991. iceberg = ymax + offset,
  992. slanted = slanted
  993. )
  994. self.glyphd[key] = types.SimpleNamespace(
  995. font = font,
  996. fontsize = fontsize,
  997. postscript_name = font.get_fontname(),
  998. metrics = metrics,
  999. symbol_name = symbol_name,
  1000. num = num,
  1001. glyph = glyph,
  1002. offset = offset
  1003. )
  1004. return self.glyphd[key]
  1005. def get_kern(self, font1, fontclass1, sym1, fontsize1,
  1006. font2, fontclass2, sym2, fontsize2, dpi):
  1007. if font1 == font2 and fontsize1 == fontsize2:
  1008. info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
  1009. info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
  1010. font = info1.font
  1011. return (font.get_kern_dist(info1.glyph, info2.glyph)
  1012. * 0.001 * fontsize1)
  1013. return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1,
  1014. font2, fontclass2, sym2, fontsize2, dpi)
  1015. def get_xheight(self, font, fontsize, dpi):
  1016. font = self._get_font(font)
  1017. return font.get_xheight() * 0.001 * fontsize
  1018. def get_underline_thickness(self, font, fontsize, dpi):
  1019. font = self._get_font(font)
  1020. return font.get_underline_thickness() * 0.001 * fontsize
  1021. ##############################################################################
  1022. # TeX-LIKE BOX MODEL
  1023. # The following is based directly on the document 'woven' from the
  1024. # TeX82 source code. This information is also available in printed
  1025. # form:
  1026. #
  1027. # Knuth, Donald E.. 1986. Computers and Typesetting, Volume B:
  1028. # TeX: The Program. Addison-Wesley Professional.
  1029. #
  1030. # The most relevant "chapters" are:
  1031. # Data structures for boxes and their friends
  1032. # Shipping pages out (Ship class)
  1033. # Packaging (hpack and vpack)
  1034. # Data structures for math mode
  1035. # Subroutines for math mode
  1036. # Typesetting math formulas
  1037. #
  1038. # Many of the docstrings below refer to a numbered "node" in that
  1039. # book, e.g., node123
  1040. #
  1041. # Note that (as TeX) y increases downward, unlike many other parts of
  1042. # matplotlib.
  1043. # How much text shrinks when going to the next-smallest level. GROW_FACTOR
  1044. # must be the inverse of SHRINK_FACTOR.
  1045. SHRINK_FACTOR = 0.7
  1046. GROW_FACTOR = 1.0 / SHRINK_FACTOR
  1047. # The number of different sizes of chars to use, beyond which they will not
  1048. # get any smaller
  1049. NUM_SIZE_LEVELS = 6
  1050. class FontConstantsBase:
  1051. """
  1052. A set of constants that controls how certain things, such as sub-
  1053. and superscripts are laid out. These are all metrics that can't
  1054. be reliably retrieved from the font metrics in the font itself.
  1055. """
  1056. # Percentage of x-height of additional horiz. space after sub/superscripts
  1057. script_space = 0.05
  1058. # Percentage of x-height that sub/superscripts drop below the baseline
  1059. subdrop = 0.4
  1060. # Percentage of x-height that superscripts are raised from the baseline
  1061. sup1 = 0.7
  1062. # Percentage of x-height that subscripts drop below the baseline
  1063. sub1 = 0.3
  1064. # Percentage of x-height that subscripts drop below the baseline when a
  1065. # superscript is present
  1066. sub2 = 0.5
  1067. # Percentage of x-height that sub/supercripts are offset relative to the
  1068. # nucleus edge for non-slanted nuclei
  1069. delta = 0.025
  1070. # Additional percentage of last character height above 2/3 of the
  1071. # x-height that supercripts are offset relative to the subscript
  1072. # for slanted nuclei
  1073. delta_slanted = 0.2
  1074. # Percentage of x-height that supercripts and subscripts are offset for
  1075. # integrals
  1076. delta_integral = 0.1
  1077. class ComputerModernFontConstants(FontConstantsBase):
  1078. script_space = 0.075
  1079. subdrop = 0.2
  1080. sup1 = 0.45
  1081. sub1 = 0.2
  1082. sub2 = 0.3
  1083. delta = 0.075
  1084. delta_slanted = 0.3
  1085. delta_integral = 0.3
  1086. class STIXFontConstants(FontConstantsBase):
  1087. script_space = 0.1
  1088. sup1 = 0.8
  1089. sub2 = 0.6
  1090. delta = 0.05
  1091. delta_slanted = 0.3
  1092. delta_integral = 0.3
  1093. class STIXSansFontConstants(FontConstantsBase):
  1094. script_space = 0.05
  1095. sup1 = 0.8
  1096. delta_slanted = 0.6
  1097. delta_integral = 0.3
  1098. class DejaVuSerifFontConstants(FontConstantsBase):
  1099. pass
  1100. class DejaVuSansFontConstants(FontConstantsBase):
  1101. pass
  1102. # Maps font family names to the FontConstantBase subclass to use
  1103. _font_constant_mapping = {
  1104. 'DejaVu Sans': DejaVuSansFontConstants,
  1105. 'DejaVu Sans Mono': DejaVuSansFontConstants,
  1106. 'DejaVu Serif': DejaVuSerifFontConstants,
  1107. 'cmb10': ComputerModernFontConstants,
  1108. 'cmex10': ComputerModernFontConstants,
  1109. 'cmmi10': ComputerModernFontConstants,
  1110. 'cmr10': ComputerModernFontConstants,
  1111. 'cmss10': ComputerModernFontConstants,
  1112. 'cmsy10': ComputerModernFontConstants,
  1113. 'cmtt10': ComputerModernFontConstants,
  1114. 'STIXGeneral': STIXFontConstants,
  1115. 'STIXNonUnicode': STIXFontConstants,
  1116. 'STIXSizeFiveSym': STIXFontConstants,
  1117. 'STIXSizeFourSym': STIXFontConstants,
  1118. 'STIXSizeThreeSym': STIXFontConstants,
  1119. 'STIXSizeTwoSym': STIXFontConstants,
  1120. 'STIXSizeOneSym': STIXFontConstants,
  1121. # Map the fonts we used to ship, just for good measure
  1122. 'Bitstream Vera Sans': DejaVuSansFontConstants,
  1123. 'Bitstream Vera': DejaVuSansFontConstants,
  1124. }
  1125. def _get_font_constant_set(state):
  1126. constants = _font_constant_mapping.get(
  1127. state.font_output._get_font(state.font).family_name,
  1128. FontConstantsBase)
  1129. # STIX sans isn't really its own fonts, just different code points
  1130. # in the STIX fonts, so we have to detect this one separately.
  1131. if (constants is STIXFontConstants and
  1132. isinstance(state.font_output, StixSansFonts)):
  1133. return STIXSansFontConstants
  1134. return constants
  1135. class MathTextWarning(Warning):
  1136. pass
  1137. class Node:
  1138. """A node in the TeX box model."""
  1139. def __init__(self):
  1140. self.size = 0
  1141. def __repr__(self):
  1142. return self.__class__.__name__
  1143. def get_kerning(self, next):
  1144. return 0.0
  1145. def shrink(self):
  1146. """
  1147. Shrinks one level smaller. There are only three levels of
  1148. sizes, after which things will no longer get smaller.
  1149. """
  1150. self.size += 1
  1151. def grow(self):
  1152. """
  1153. Grows one level larger. There is no limit to how big
  1154. something can get.
  1155. """
  1156. self.size -= 1
  1157. def render(self, x, y):
  1158. pass
  1159. class Box(Node):
  1160. """A node with a physical location."""
  1161. def __init__(self, width, height, depth):
  1162. Node.__init__(self)
  1163. self.width = width
  1164. self.height = height
  1165. self.depth = depth
  1166. def shrink(self):
  1167. Node.shrink(self)
  1168. if self.size < NUM_SIZE_LEVELS:
  1169. self.width *= SHRINK_FACTOR
  1170. self.height *= SHRINK_FACTOR
  1171. self.depth *= SHRINK_FACTOR
  1172. def grow(self):
  1173. Node.grow(self)
  1174. self.width *= GROW_FACTOR
  1175. self.height *= GROW_FACTOR
  1176. self.depth *= GROW_FACTOR
  1177. def render(self, x1, y1, x2, y2):
  1178. pass
  1179. class Vbox(Box):
  1180. """A box with only height (zero width)."""
  1181. def __init__(self, height, depth):
  1182. Box.__init__(self, 0., height, depth)
  1183. class Hbox(Box):
  1184. """A box with only width (zero height and depth)."""
  1185. def __init__(self, width):
  1186. Box.__init__(self, width, 0., 0.)
  1187. class Char(Node):
  1188. """
  1189. A single character.
  1190. Unlike TeX, the font information and metrics are stored with each `Char`
  1191. to make it easier to lookup the font metrics when needed. Note that TeX
  1192. boxes have a width, height, and depth, unlike Type1 and TrueType which use
  1193. a full bounding box and an advance in the x-direction. The metrics must
  1194. be converted to the TeX model, and the advance (if different from width)
  1195. must be converted into a `Kern` node when the `Char` is added to its parent
  1196. `Hlist`.
  1197. """
  1198. def __init__(self, c, state, math=True):
  1199. Node.__init__(self)
  1200. self.c = c
  1201. self.font_output = state.font_output
  1202. self.font = state.font
  1203. self.font_class = state.font_class
  1204. self.fontsize = state.fontsize
  1205. self.dpi = state.dpi
  1206. self.math = math
  1207. # The real width, height and depth will be set during the
  1208. # pack phase, after we know the real fontsize
  1209. self._update_metrics()
  1210. def __repr__(self):
  1211. return '`%s`' % self.c
  1212. def _update_metrics(self):
  1213. metrics = self._metrics = self.font_output.get_metrics(
  1214. self.font, self.font_class, self.c, self.fontsize, self.dpi,
  1215. self.math)
  1216. if self.c == ' ':
  1217. self.width = metrics.advance
  1218. else:
  1219. self.width = metrics.width
  1220. self.height = metrics.iceberg
  1221. self.depth = -(metrics.iceberg - metrics.height)
  1222. def is_slanted(self):
  1223. return self._metrics.slanted
  1224. def get_kerning(self, next):
  1225. """
  1226. Return the amount of kerning between this and the given character.
  1227. This method is called when characters are strung together into `Hlist`
  1228. to create `Kern` nodes.
  1229. """
  1230. advance = self._metrics.advance - self.width
  1231. kern = 0.
  1232. if isinstance(next, Char):
  1233. kern = self.font_output.get_kern(
  1234. self.font, self.font_class, self.c, self.fontsize,
  1235. next.font, next.font_class, next.c, next.fontsize,
  1236. self.dpi)
  1237. return advance + kern
  1238. def render(self, x, y):
  1239. """
  1240. Render the character to the canvas
  1241. """
  1242. self.font_output.render_glyph(
  1243. x, y,
  1244. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  1245. def shrink(self):
  1246. Node.shrink(self)
  1247. if self.size < NUM_SIZE_LEVELS:
  1248. self.fontsize *= SHRINK_FACTOR
  1249. self.width *= SHRINK_FACTOR
  1250. self.height *= SHRINK_FACTOR
  1251. self.depth *= SHRINK_FACTOR
  1252. def grow(self):
  1253. Node.grow(self)
  1254. self.fontsize *= GROW_FACTOR
  1255. self.width *= GROW_FACTOR
  1256. self.height *= GROW_FACTOR
  1257. self.depth *= GROW_FACTOR
  1258. class Accent(Char):
  1259. """
  1260. The font metrics need to be dealt with differently for accents,
  1261. since they are already offset correctly from the baseline in
  1262. TrueType fonts.
  1263. """
  1264. def _update_metrics(self):
  1265. metrics = self._metrics = self.font_output.get_metrics(
  1266. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  1267. self.width = metrics.xmax - metrics.xmin
  1268. self.height = metrics.ymax - metrics.ymin
  1269. self.depth = 0
  1270. def shrink(self):
  1271. Char.shrink(self)
  1272. self._update_metrics()
  1273. def grow(self):
  1274. Char.grow(self)
  1275. self._update_metrics()
  1276. def render(self, x, y):
  1277. """
  1278. Render the character to the canvas.
  1279. """
  1280. self.font_output.render_glyph(
  1281. x - self._metrics.xmin, y + self._metrics.ymin,
  1282. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  1283. class List(Box):
  1284. """A list of nodes (either horizontal or vertical)."""
  1285. def __init__(self, elements):
  1286. Box.__init__(self, 0., 0., 0.)
  1287. self.shift_amount = 0. # An arbitrary offset
  1288. self.children = elements # The child nodes of this list
  1289. # The following parameters are set in the vpack and hpack functions
  1290. self.glue_set = 0. # The glue setting of this list
  1291. self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching
  1292. self.glue_order = 0 # The order of infinity (0 - 3) for the glue
  1293. def __repr__(self):
  1294. return '[%s <%.02f %.02f %.02f %.02f> %s]' % (
  1295. super().__repr__(),
  1296. self.width, self.height,
  1297. self.depth, self.shift_amount,
  1298. ' '.join([repr(x) for x in self.children]))
  1299. @staticmethod
  1300. def _determine_order(totals):
  1301. """
  1302. Determine the highest order of glue used by the members of this list.
  1303. Helper function used by vpack and hpack.
  1304. """
  1305. for i in range(len(totals))[::-1]:
  1306. if totals[i] != 0:
  1307. return i
  1308. return 0
  1309. def _set_glue(self, x, sign, totals, error_type):
  1310. o = self._determine_order(totals)
  1311. self.glue_order = o
  1312. self.glue_sign = sign
  1313. if totals[o] != 0.:
  1314. self.glue_set = x / totals[o]
  1315. else:
  1316. self.glue_sign = 0
  1317. self.glue_ratio = 0.
  1318. if o == 0:
  1319. if len(self.children):
  1320. _log.warning("%s %s: %r",
  1321. error_type, self.__class__.__name__, self)
  1322. def shrink(self):
  1323. for child in self.children:
  1324. child.shrink()
  1325. Box.shrink(self)
  1326. if self.size < NUM_SIZE_LEVELS:
  1327. self.shift_amount *= SHRINK_FACTOR
  1328. self.glue_set *= SHRINK_FACTOR
  1329. def grow(self):
  1330. for child in self.children:
  1331. child.grow()
  1332. Box.grow(self)
  1333. self.shift_amount *= GROW_FACTOR
  1334. self.glue_set *= GROW_FACTOR
  1335. class Hlist(List):
  1336. """A horizontal list of boxes."""
  1337. def __init__(self, elements, w=0., m='additional', do_kern=True):
  1338. List.__init__(self, elements)
  1339. if do_kern:
  1340. self.kern()
  1341. self.hpack()
  1342. def kern(self):
  1343. """
  1344. Insert `Kern` nodes between `Char` nodes to set kerning.
  1345. The `Char` nodes themselves determine the amount of kerning they need
  1346. (in `~Char.get_kerning`), and this function just creates the correct
  1347. linked list.
  1348. """
  1349. new_children = []
  1350. num_children = len(self.children)
  1351. if num_children:
  1352. for i in range(num_children):
  1353. elem = self.children[i]
  1354. if i < num_children - 1:
  1355. next = self.children[i + 1]
  1356. else:
  1357. next = None
  1358. new_children.append(elem)
  1359. kerning_distance = elem.get_kerning(next)
  1360. if kerning_distance != 0.:
  1361. kern = Kern(kerning_distance)
  1362. new_children.append(kern)
  1363. self.children = new_children
  1364. # This is a failed experiment to fake cross-font kerning.
  1365. # def get_kerning(self, next):
  1366. # if len(self.children) >= 2 and isinstance(self.children[-2], Char):
  1367. # if isinstance(next, Char):
  1368. # print "CASE A"
  1369. # return self.children[-2].get_kerning(next)
  1370. # elif (isinstance(next, Hlist) and len(next.children)
  1371. # and isinstance(next.children[0], Char)):
  1372. # print "CASE B"
  1373. # result = self.children[-2].get_kerning(next.children[0])
  1374. # print result
  1375. # return result
  1376. # return 0.0
  1377. def hpack(self, w=0., m='additional'):
  1378. r"""
  1379. Compute the dimensions of the resulting boxes, and adjust the glue if
  1380. one of those dimensions is pre-specified. The computed sizes normally
  1381. enclose all of the material inside the new box; but some items may
  1382. stick out if negative glue is used, if the box is overfull, or if a
  1383. ``\vbox`` includes other boxes that have been shifted left.
  1384. Parameters
  1385. ----------
  1386. w : float, default: 0
  1387. A width.
  1388. m : {'exactly', 'additional'}, default: 'additional'
  1389. Whether to produce a box whose width is 'exactly' *w*; or a box
  1390. with the natural width of the contents, plus *w* ('additional').
  1391. Notes
  1392. -----
  1393. The defaults produce a box with the natural width of the contents.
  1394. """
  1395. # I don't know why these get reset in TeX. Shift_amount is pretty
  1396. # much useless if we do.
  1397. # self.shift_amount = 0.
  1398. h = 0.
  1399. d = 0.
  1400. x = 0.
  1401. total_stretch = [0.] * 4
  1402. total_shrink = [0.] * 4
  1403. for p in self.children:
  1404. if isinstance(p, Char):
  1405. x += p.width
  1406. h = max(h, p.height)
  1407. d = max(d, p.depth)
  1408. elif isinstance(p, Box):
  1409. x += p.width
  1410. if not np.isinf(p.height) and not np.isinf(p.depth):
  1411. s = getattr(p, 'shift_amount', 0.)
  1412. h = max(h, p.height - s)
  1413. d = max(d, p.depth + s)
  1414. elif isinstance(p, Glue):
  1415. glue_spec = p.glue_spec
  1416. x += glue_spec.width
  1417. total_stretch[glue_spec.stretch_order] += glue_spec.stretch
  1418. total_shrink[glue_spec.shrink_order] += glue_spec.shrink
  1419. elif isinstance(p, Kern):
  1420. x += p.width
  1421. self.height = h
  1422. self.depth = d
  1423. if m == 'additional':
  1424. w += x
  1425. self.width = w
  1426. x = w - x
  1427. if x == 0.:
  1428. self.glue_sign = 0
  1429. self.glue_order = 0
  1430. self.glue_ratio = 0.
  1431. return
  1432. if x > 0.:
  1433. self._set_glue(x, 1, total_stretch, "Overfull")
  1434. else:
  1435. self._set_glue(x, -1, total_shrink, "Underfull")
  1436. class Vlist(List):
  1437. """A vertical list of boxes."""
  1438. def __init__(self, elements, h=0., m='additional'):
  1439. List.__init__(self, elements)
  1440. self.vpack()
  1441. def vpack(self, h=0., m='additional', l=np.inf):
  1442. """
  1443. Compute the dimensions of the resulting boxes, and to adjust the glue
  1444. if one of those dimensions is pre-specified.
  1445. Parameters
  1446. ----------
  1447. h : float, default: 0
  1448. A height.
  1449. m : {'exactly', 'additional'}, default: 'additional'
  1450. Whether to produce a box whose height is 'exactly' *w*; or a box
  1451. with the natural height of the contents, plus *w* ('additional').
  1452. l : float, default: np.inf
  1453. The maximum height.
  1454. Notes
  1455. -----
  1456. The defaults produce a box with the natural height of the contents.
  1457. """
  1458. # I don't know why these get reset in TeX. Shift_amount is pretty
  1459. # much useless if we do.
  1460. # self.shift_amount = 0.
  1461. w = 0.
  1462. d = 0.
  1463. x = 0.
  1464. total_stretch = [0.] * 4
  1465. total_shrink = [0.] * 4
  1466. for p in self.children:
  1467. if isinstance(p, Box):
  1468. x += d + p.height
  1469. d = p.depth
  1470. if not np.isinf(p.width):
  1471. s = getattr(p, 'shift_amount', 0.)
  1472. w = max(w, p.width + s)
  1473. elif isinstance(p, Glue):
  1474. x += d
  1475. d = 0.
  1476. glue_spec = p.glue_spec
  1477. x += glue_spec.width
  1478. total_stretch[glue_spec.stretch_order] += glue_spec.stretch
  1479. total_shrink[glue_spec.shrink_order] += glue_spec.shrink
  1480. elif isinstance(p, Kern):
  1481. x += d + p.width
  1482. d = 0.
  1483. elif isinstance(p, Char):
  1484. raise RuntimeError(
  1485. "Internal mathtext error: Char node found in Vlist")
  1486. self.width = w
  1487. if d > l:
  1488. x += d - l
  1489. self.depth = l
  1490. else:
  1491. self.depth = d
  1492. if m == 'additional':
  1493. h += x
  1494. self.height = h
  1495. x = h - x
  1496. if x == 0:
  1497. self.glue_sign = 0
  1498. self.glue_order = 0
  1499. self.glue_ratio = 0.
  1500. return
  1501. if x > 0.:
  1502. self._set_glue(x, 1, total_stretch, "Overfull")
  1503. else:
  1504. self._set_glue(x, -1, total_shrink, "Underfull")
  1505. class Rule(Box):
  1506. """
  1507. A solid black rectangle.
  1508. It has *width*, *depth*, and *height* fields just as in an `Hlist`.
  1509. However, if any of these dimensions is inf, the actual value will be
  1510. determined by running the rule up to the boundary of the innermost
  1511. enclosing box. This is called a "running dimension". The width is never
  1512. running in an `Hlist`; the height and depth are never running in a `Vlist`.
  1513. """
  1514. def __init__(self, width, height, depth, state):
  1515. Box.__init__(self, width, height, depth)
  1516. self.font_output = state.font_output
  1517. def render(self, x, y, w, h):
  1518. self.font_output.render_rect_filled(x, y, x + w, y + h)
  1519. class Hrule(Rule):
  1520. """Convenience class to create a horizontal rule."""
  1521. def __init__(self, state, thickness=None):
  1522. if thickness is None:
  1523. thickness = state.font_output.get_underline_thickness(
  1524. state.font, state.fontsize, state.dpi)
  1525. height = depth = thickness * 0.5
  1526. Rule.__init__(self, np.inf, height, depth, state)
  1527. class Vrule(Rule):
  1528. """Convenience class to create a vertical rule."""
  1529. def __init__(self, state):
  1530. thickness = state.font_output.get_underline_thickness(
  1531. state.font, state.fontsize, state.dpi)
  1532. Rule.__init__(self, thickness, np.inf, np.inf, state)
  1533. _GlueSpec = namedtuple(
  1534. "_GlueSpec", "width stretch stretch_order shrink shrink_order")
  1535. _GlueSpec._named = {
  1536. 'fil': _GlueSpec(0., 1., 1, 0., 0),
  1537. 'fill': _GlueSpec(0., 1., 2, 0., 0),
  1538. 'filll': _GlueSpec(0., 1., 3, 0., 0),
  1539. 'neg_fil': _GlueSpec(0., 0., 0, 1., 1),
  1540. 'neg_fill': _GlueSpec(0., 0., 0, 1., 2),
  1541. 'neg_filll': _GlueSpec(0., 0., 0, 1., 3),
  1542. 'empty': _GlueSpec(0., 0., 0, 0., 0),
  1543. 'ss': _GlueSpec(0., 1., 1, -1., 1),
  1544. }
  1545. class Glue(Node):
  1546. """
  1547. Most of the information in this object is stored in the underlying
  1548. ``_GlueSpec`` class, which is shared between multiple glue objects.
  1549. (This is a memory optimization which probably doesn't matter anymore, but
  1550. it's easier to stick to what TeX does.)
  1551. """
  1552. @cbook.deprecated("3.3")
  1553. @property
  1554. def glue_subtype(self):
  1555. return "normal"
  1556. @cbook._delete_parameter("3.3", "copy")
  1557. def __init__(self, glue_type, copy=False):
  1558. Node.__init__(self)
  1559. if isinstance(glue_type, str):
  1560. glue_spec = _GlueSpec._named[glue_type]
  1561. elif isinstance(glue_type, _GlueSpec):
  1562. glue_spec = glue_type
  1563. else:
  1564. raise ValueError("glue_type must be a glue spec name or instance")
  1565. self.glue_spec = glue_spec
  1566. def shrink(self):
  1567. Node.shrink(self)
  1568. if self.size < NUM_SIZE_LEVELS:
  1569. g = self.glue_spec
  1570. self.glue_spec = g._replace(width=g.width * SHRINK_FACTOR)
  1571. def grow(self):
  1572. Node.grow(self)
  1573. g = self.glue_spec
  1574. self.glue_spec = g._replace(width=g.width * GROW_FACTOR)
  1575. @cbook.deprecated("3.3")
  1576. class GlueSpec:
  1577. """See `Glue`."""
  1578. def __init__(self, width=0., stretch=0., stretch_order=0,
  1579. shrink=0., shrink_order=0):
  1580. self.width = width
  1581. self.stretch = stretch
  1582. self.stretch_order = stretch_order
  1583. self.shrink = shrink
  1584. self.shrink_order = shrink_order
  1585. def copy(self):
  1586. return GlueSpec(
  1587. self.width,
  1588. self.stretch,
  1589. self.stretch_order,
  1590. self.shrink,
  1591. self.shrink_order)
  1592. @classmethod
  1593. def factory(cls, glue_type):
  1594. return cls._types[glue_type]
  1595. with cbook._suppress_matplotlib_deprecation_warning():
  1596. GlueSpec._types = {k: GlueSpec(**v._asdict())
  1597. for k, v in _GlueSpec._named.items()}
  1598. # Some convenient ways to get common kinds of glue
  1599. @cbook.deprecated("3.3", alternative="Glue('fil')")
  1600. class Fil(Glue):
  1601. def __init__(self):
  1602. Glue.__init__(self, 'fil')
  1603. @cbook.deprecated("3.3", alternative="Glue('fill')")
  1604. class Fill(Glue):
  1605. def __init__(self):
  1606. Glue.__init__(self, 'fill')
  1607. @cbook.deprecated("3.3", alternative="Glue('filll')")
  1608. class Filll(Glue):
  1609. def __init__(self):
  1610. Glue.__init__(self, 'filll')
  1611. @cbook.deprecated("3.3", alternative="Glue('neg_fil')")
  1612. class NegFil(Glue):
  1613. def __init__(self):
  1614. Glue.__init__(self, 'neg_fil')
  1615. @cbook.deprecated("3.3", alternative="Glue('neg_fill')")
  1616. class NegFill(Glue):
  1617. def __init__(self):
  1618. Glue.__init__(self, 'neg_fill')
  1619. @cbook.deprecated("3.3", alternative="Glue('neg_filll')")
  1620. class NegFilll(Glue):
  1621. def __init__(self):
  1622. Glue.__init__(self, 'neg_filll')
  1623. @cbook.deprecated("3.3", alternative="Glue('ss')")
  1624. class SsGlue(Glue):
  1625. def __init__(self):
  1626. Glue.__init__(self, 'ss')
  1627. class HCentered(Hlist):
  1628. """
  1629. A convenience class to create an `Hlist` whose contents are
  1630. centered within its enclosing box.
  1631. """
  1632. def __init__(self, elements):
  1633. super().__init__([Glue('ss'), *elements, Glue('ss')], do_kern=False)
  1634. class VCentered(Vlist):
  1635. """
  1636. A convenience class to create a `Vlist` whose contents are
  1637. centered within its enclosing box.
  1638. """
  1639. def __init__(self, elements):
  1640. super().__init__([Glue('ss'), *elements, Glue('ss')])
  1641. class Kern(Node):
  1642. """
  1643. A `Kern` node has a width field to specify a (normally
  1644. negative) amount of spacing. This spacing correction appears in
  1645. horizontal lists between letters like A and V when the font
  1646. designer said that it looks better to move them closer together or
  1647. further apart. A kern node can also appear in a vertical list,
  1648. when its *width* denotes additional spacing in the vertical
  1649. direction.
  1650. """
  1651. height = 0
  1652. depth = 0
  1653. def __init__(self, width):
  1654. Node.__init__(self)
  1655. self.width = width
  1656. def __repr__(self):
  1657. return "k%.02f" % self.width
  1658. def shrink(self):
  1659. Node.shrink(self)
  1660. if self.size < NUM_SIZE_LEVELS:
  1661. self.width *= SHRINK_FACTOR
  1662. def grow(self):
  1663. Node.grow(self)
  1664. self.width *= GROW_FACTOR
  1665. class SubSuperCluster(Hlist):
  1666. """
  1667. A hack to get around that fact that this code does a two-pass parse like
  1668. TeX. This lets us store enough information in the hlist itself, namely the
  1669. nucleus, sub- and super-script, such that if another script follows that
  1670. needs to be attached, it can be reconfigured on the fly.
  1671. """
  1672. def __init__(self):
  1673. self.nucleus = None
  1674. self.sub = None
  1675. self.super = None
  1676. Hlist.__init__(self, [])
  1677. class AutoHeightChar(Hlist):
  1678. """
  1679. A character as close to the given height and depth as possible.
  1680. When using a font with multiple height versions of some characters (such as
  1681. the BaKoMa fonts), the correct glyph will be selected, otherwise this will
  1682. always just return a scaled version of the glyph.
  1683. """
  1684. def __init__(self, c, height, depth, state, always=False, factor=None):
  1685. alternatives = state.font_output.get_sized_alternatives_for_symbol(
  1686. state.font, c)
  1687. xHeight = state.font_output.get_xheight(
  1688. state.font, state.fontsize, state.dpi)
  1689. state = state.copy()
  1690. target_total = height + depth
  1691. for fontname, sym in alternatives:
  1692. state.font = fontname
  1693. char = Char(sym, state)
  1694. # Ensure that size 0 is chosen when the text is regular sized but
  1695. # with descender glyphs by subtracting 0.2 * xHeight
  1696. if char.height + char.depth >= target_total - 0.2 * xHeight:
  1697. break
  1698. shift = 0
  1699. if state.font != 0:
  1700. if factor is None:
  1701. factor = target_total / (char.height + char.depth)
  1702. state.fontsize *= factor
  1703. char = Char(sym, state)
  1704. shift = (depth - char.depth)
  1705. Hlist.__init__(self, [char])
  1706. self.shift_amount = shift
  1707. class AutoWidthChar(Hlist):
  1708. """
  1709. A character as close to the given width as possible.
  1710. When using a font with multiple width versions of some characters (such as
  1711. the BaKoMa fonts), the correct glyph will be selected, otherwise this will
  1712. always just return a scaled version of the glyph.
  1713. """
  1714. def __init__(self, c, width, state, always=False, char_class=Char):
  1715. alternatives = state.font_output.get_sized_alternatives_for_symbol(
  1716. state.font, c)
  1717. state = state.copy()
  1718. for fontname, sym in alternatives:
  1719. state.font = fontname
  1720. char = char_class(sym, state)
  1721. if char.width >= width:
  1722. break
  1723. factor = width / char.width
  1724. state.fontsize *= factor
  1725. char = char_class(sym, state)
  1726. Hlist.__init__(self, [char])
  1727. self.width = char.width
  1728. class Ship:
  1729. """
  1730. Ship boxes to output once they have been set up, this sends them to output.
  1731. Since boxes can be inside of boxes inside of boxes, the main work of `Ship`
  1732. is done by two mutually recursive routines, `hlist_out` and `vlist_out`,
  1733. which traverse the `Hlist` nodes and `Vlist` nodes inside of horizontal
  1734. and vertical boxes. The global variables used in TeX to store state as it
  1735. processes have become member variables here.
  1736. """
  1737. def __call__(self, ox, oy, box):
  1738. self.max_push = 0 # Deepest nesting of push commands so far
  1739. self.cur_s = 0
  1740. self.cur_v = 0.
  1741. self.cur_h = 0.
  1742. self.off_h = ox
  1743. self.off_v = oy + box.height
  1744. self.hlist_out(box)
  1745. @staticmethod
  1746. def clamp(value):
  1747. if value < -1000000000.:
  1748. return -1000000000.
  1749. if value > 1000000000.:
  1750. return 1000000000.
  1751. return value
  1752. def hlist_out(self, box):
  1753. cur_g = 0
  1754. cur_glue = 0.
  1755. glue_order = box.glue_order
  1756. glue_sign = box.glue_sign
  1757. base_line = self.cur_v
  1758. left_edge = self.cur_h
  1759. self.cur_s += 1
  1760. self.max_push = max(self.cur_s, self.max_push)
  1761. clamp = self.clamp
  1762. for p in box.children:
  1763. if isinstance(p, Char):
  1764. p.render(self.cur_h + self.off_h, self.cur_v + self.off_v)
  1765. self.cur_h += p.width
  1766. elif isinstance(p, Kern):
  1767. self.cur_h += p.width
  1768. elif isinstance(p, List):
  1769. # node623
  1770. if len(p.children) == 0:
  1771. self.cur_h += p.width
  1772. else:
  1773. edge = self.cur_h
  1774. self.cur_v = base_line + p.shift_amount
  1775. if isinstance(p, Hlist):
  1776. self.hlist_out(p)
  1777. else:
  1778. # p.vpack(box.height + box.depth, 'exactly')
  1779. self.vlist_out(p)
  1780. self.cur_h = edge + p.width
  1781. self.cur_v = base_line
  1782. elif isinstance(p, Box):
  1783. # node624
  1784. rule_height = p.height
  1785. rule_depth = p.depth
  1786. rule_width = p.width
  1787. if np.isinf(rule_height):
  1788. rule_height = box.height
  1789. if np.isinf(rule_depth):
  1790. rule_depth = box.depth
  1791. if rule_height > 0 and rule_width > 0:
  1792. self.cur_v = base_line + rule_depth
  1793. p.render(self.cur_h + self.off_h,
  1794. self.cur_v + self.off_v,
  1795. rule_width, rule_height)
  1796. self.cur_v = base_line
  1797. self.cur_h += rule_width
  1798. elif isinstance(p, Glue):
  1799. # node625
  1800. glue_spec = p.glue_spec
  1801. rule_width = glue_spec.width - cur_g
  1802. if glue_sign != 0: # normal
  1803. if glue_sign == 1: # stretching
  1804. if glue_spec.stretch_order == glue_order:
  1805. cur_glue += glue_spec.stretch
  1806. cur_g = round(clamp(box.glue_set * cur_glue))
  1807. elif glue_spec.shrink_order == glue_order:
  1808. cur_glue += glue_spec.shrink
  1809. cur_g = round(clamp(box.glue_set * cur_glue))
  1810. rule_width += cur_g
  1811. self.cur_h += rule_width
  1812. self.cur_s -= 1
  1813. def vlist_out(self, box):
  1814. cur_g = 0
  1815. cur_glue = 0.
  1816. glue_order = box.glue_order
  1817. glue_sign = box.glue_sign
  1818. self.cur_s += 1
  1819. self.max_push = max(self.max_push, self.cur_s)
  1820. left_edge = self.cur_h
  1821. self.cur_v -= box.height
  1822. top_edge = self.cur_v
  1823. clamp = self.clamp
  1824. for p in box.children:
  1825. if isinstance(p, Kern):
  1826. self.cur_v += p.width
  1827. elif isinstance(p, List):
  1828. if len(p.children) == 0:
  1829. self.cur_v += p.height + p.depth
  1830. else:
  1831. self.cur_v += p.height
  1832. self.cur_h = left_edge + p.shift_amount
  1833. save_v = self.cur_v
  1834. p.width = box.width
  1835. if isinstance(p, Hlist):
  1836. self.hlist_out(p)
  1837. else:
  1838. self.vlist_out(p)
  1839. self.cur_v = save_v + p.depth
  1840. self.cur_h = left_edge
  1841. elif isinstance(p, Box):
  1842. rule_height = p.height
  1843. rule_depth = p.depth
  1844. rule_width = p.width
  1845. if np.isinf(rule_width):
  1846. rule_width = box.width
  1847. rule_height += rule_depth
  1848. if rule_height > 0 and rule_depth > 0:
  1849. self.cur_v += rule_height
  1850. p.render(self.cur_h + self.off_h,
  1851. self.cur_v + self.off_v,
  1852. rule_width, rule_height)
  1853. elif isinstance(p, Glue):
  1854. glue_spec = p.glue_spec
  1855. rule_height = glue_spec.width - cur_g
  1856. if glue_sign != 0: # normal
  1857. if glue_sign == 1: # stretching
  1858. if glue_spec.stretch_order == glue_order:
  1859. cur_glue += glue_spec.stretch
  1860. cur_g = round(clamp(box.glue_set * cur_glue))
  1861. elif glue_spec.shrink_order == glue_order: # shrinking
  1862. cur_glue += glue_spec.shrink
  1863. cur_g = round(clamp(box.glue_set * cur_glue))
  1864. rule_height += cur_g
  1865. self.cur_v += rule_height
  1866. elif isinstance(p, Char):
  1867. raise RuntimeError(
  1868. "Internal mathtext error: Char node found in vlist")
  1869. self.cur_s -= 1
  1870. ship = Ship()
  1871. ##############################################################################
  1872. # PARSER
  1873. def Error(msg):
  1874. """Helper class to raise parser errors."""
  1875. def raise_error(s, loc, toks):
  1876. raise ParseFatalException(s, loc, msg)
  1877. empty = Empty()
  1878. empty.setParseAction(raise_error)
  1879. return empty
  1880. class Parser:
  1881. """
  1882. A pyparsing-based parser for strings containing math expressions.
  1883. Raw text may also appear outside of pairs of ``$``.
  1884. The grammar is based directly on that in TeX, though it cuts a few corners.
  1885. """
  1886. _math_style_dict = dict(displaystyle=0, textstyle=1,
  1887. scriptstyle=2, scriptscriptstyle=3)
  1888. _binary_operators = set('''
  1889. + * -
  1890. \\pm \\sqcap \\rhd
  1891. \\mp \\sqcup \\unlhd
  1892. \\times \\vee \\unrhd
  1893. \\div \\wedge \\oplus
  1894. \\ast \\setminus \\ominus
  1895. \\star \\wr \\otimes
  1896. \\circ \\diamond \\oslash
  1897. \\bullet \\bigtriangleup \\odot
  1898. \\cdot \\bigtriangledown \\bigcirc
  1899. \\cap \\triangleleft \\dagger
  1900. \\cup \\triangleright \\ddagger
  1901. \\uplus \\lhd \\amalg'''.split())
  1902. _relation_symbols = set('''
  1903. = < > :
  1904. \\leq \\geq \\equiv \\models
  1905. \\prec \\succ \\sim \\perp
  1906. \\preceq \\succeq \\simeq \\mid
  1907. \\ll \\gg \\asymp \\parallel
  1908. \\subset \\supset \\approx \\bowtie
  1909. \\subseteq \\supseteq \\cong \\Join
  1910. \\sqsubset \\sqsupset \\neq \\smile
  1911. \\sqsubseteq \\sqsupseteq \\doteq \\frown
  1912. \\in \\ni \\propto \\vdash
  1913. \\dashv \\dots \\dotplus \\doteqdot'''.split())
  1914. _arrow_symbols = set('''
  1915. \\leftarrow \\longleftarrow \\uparrow
  1916. \\Leftarrow \\Longleftarrow \\Uparrow
  1917. \\rightarrow \\longrightarrow \\downarrow
  1918. \\Rightarrow \\Longrightarrow \\Downarrow
  1919. \\leftrightarrow \\longleftrightarrow \\updownarrow
  1920. \\Leftrightarrow \\Longleftrightarrow \\Updownarrow
  1921. \\mapsto \\longmapsto \\nearrow
  1922. \\hookleftarrow \\hookrightarrow \\searrow
  1923. \\leftharpoonup \\rightharpoonup \\swarrow
  1924. \\leftharpoondown \\rightharpoondown \\nwarrow
  1925. \\rightleftharpoons \\leadsto'''.split())
  1926. _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols
  1927. _punctuation_symbols = set(r', ; . ! \ldotp \cdotp'.split())
  1928. _overunder_symbols = set(r'''
  1929. \sum \prod \coprod \bigcap \bigcup \bigsqcup \bigvee
  1930. \bigwedge \bigodot \bigotimes \bigoplus \biguplus
  1931. '''.split())
  1932. _overunder_functions = set(
  1933. "lim liminf limsup sup max min".split())
  1934. _dropsub_symbols = set(r'''\int \oint'''.split())
  1935. _fontnames = set("rm cal it tt sf bf default bb frak scr regular".split())
  1936. _function_names = set("""
  1937. arccos csc ker min arcsin deg lg Pr arctan det lim sec arg dim
  1938. liminf sin cos exp limsup sinh cosh gcd ln sup cot hom log tan
  1939. coth inf max tanh""".split())
  1940. _ambi_delim = set("""
  1941. | \\| / \\backslash \\uparrow \\downarrow \\updownarrow \\Uparrow
  1942. \\Downarrow \\Updownarrow . \\vert \\Vert \\\\|""".split())
  1943. _left_delim = set(r"( [ \{ < \lfloor \langle \lceil".split())
  1944. _right_delim = set(r") ] \} > \rfloor \rangle \rceil".split())
  1945. def __init__(self):
  1946. p = types.SimpleNamespace()
  1947. # All forward declarations are here
  1948. p.accent = Forward()
  1949. p.ambi_delim = Forward()
  1950. p.apostrophe = Forward()
  1951. p.auto_delim = Forward()
  1952. p.binom = Forward()
  1953. p.bslash = Forward()
  1954. p.c_over_c = Forward()
  1955. p.customspace = Forward()
  1956. p.end_group = Forward()
  1957. p.float_literal = Forward()
  1958. p.font = Forward()
  1959. p.frac = Forward()
  1960. p.dfrac = Forward()
  1961. p.function = Forward()
  1962. p.genfrac = Forward()
  1963. p.group = Forward()
  1964. p.int_literal = Forward()
  1965. p.latexfont = Forward()
  1966. p.lbracket = Forward()
  1967. p.left_delim = Forward()
  1968. p.lbrace = Forward()
  1969. p.main = Forward()
  1970. p.math = Forward()
  1971. p.math_string = Forward()
  1972. p.non_math = Forward()
  1973. p.operatorname = Forward()
  1974. p.overline = Forward()
  1975. p.placeable = Forward()
  1976. p.rbrace = Forward()
  1977. p.rbracket = Forward()
  1978. p.required_group = Forward()
  1979. p.right_delim = Forward()
  1980. p.right_delim_safe = Forward()
  1981. p.simple = Forward()
  1982. p.simple_group = Forward()
  1983. p.single_symbol = Forward()
  1984. p.accentprefixed = Forward()
  1985. p.space = Forward()
  1986. p.sqrt = Forward()
  1987. p.start_group = Forward()
  1988. p.subsuper = Forward()
  1989. p.subsuperop = Forward()
  1990. p.symbol = Forward()
  1991. p.symbol_name = Forward()
  1992. p.token = Forward()
  1993. p.unknown_symbol = Forward()
  1994. # Set names on everything -- very useful for debugging
  1995. for key, val in vars(p).items():
  1996. if not key.startswith('_'):
  1997. val.setName(key)
  1998. p.float_literal <<= Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)")
  1999. p.int_literal <<= Regex("[-+]?[0-9]+")
  2000. p.lbrace <<= Literal('{').suppress()
  2001. p.rbrace <<= Literal('}').suppress()
  2002. p.lbracket <<= Literal('[').suppress()
  2003. p.rbracket <<= Literal(']').suppress()
  2004. p.bslash <<= Literal('\\')
  2005. p.space <<= oneOf(list(self._space_widths))
  2006. p.customspace <<= (
  2007. Suppress(Literal(r'\hspace'))
  2008. - ((p.lbrace + p.float_literal + p.rbrace)
  2009. | Error(r"Expected \hspace{n}"))
  2010. )
  2011. unicode_range = "\U00000080-\U0001ffff"
  2012. p.single_symbol <<= Regex(
  2013. r"([a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|%s])|(\\[%%${}\[\]_|])" %
  2014. unicode_range)
  2015. p.accentprefixed <<= Suppress(p.bslash) + oneOf(self._accentprefixed)
  2016. p.symbol_name <<= (
  2017. Combine(p.bslash + oneOf(list(tex2uni)))
  2018. + FollowedBy(Regex("[^A-Za-z]").leaveWhitespace() | StringEnd())
  2019. )
  2020. p.symbol <<= (p.single_symbol | p.symbol_name).leaveWhitespace()
  2021. p.apostrophe <<= Regex("'+")
  2022. p.c_over_c <<= (
  2023. Suppress(p.bslash)
  2024. + oneOf(list(self._char_over_chars))
  2025. )
  2026. p.accent <<= Group(
  2027. Suppress(p.bslash)
  2028. + oneOf([*self._accent_map, *self._wide_accents])
  2029. - p.placeable
  2030. )
  2031. p.function <<= (
  2032. Suppress(p.bslash)
  2033. + oneOf(list(self._function_names))
  2034. )
  2035. p.start_group <<= Optional(p.latexfont) + p.lbrace
  2036. p.end_group <<= p.rbrace.copy()
  2037. p.simple_group <<= Group(p.lbrace + ZeroOrMore(p.token) + p.rbrace)
  2038. p.required_group <<= Group(p.lbrace + OneOrMore(p.token) + p.rbrace)
  2039. p.group <<= Group(
  2040. p.start_group + ZeroOrMore(p.token) + p.end_group
  2041. )
  2042. p.font <<= Suppress(p.bslash) + oneOf(list(self._fontnames))
  2043. p.latexfont <<= (
  2044. Suppress(p.bslash)
  2045. + oneOf(['math' + x for x in self._fontnames])
  2046. )
  2047. p.frac <<= Group(
  2048. Suppress(Literal(r"\frac"))
  2049. - ((p.required_group + p.required_group)
  2050. | Error(r"Expected \frac{num}{den}"))
  2051. )
  2052. p.dfrac <<= Group(
  2053. Suppress(Literal(r"\dfrac"))
  2054. - ((p.required_group + p.required_group)
  2055. | Error(r"Expected \dfrac{num}{den}"))
  2056. )
  2057. p.binom <<= Group(
  2058. Suppress(Literal(r"\binom"))
  2059. - ((p.required_group + p.required_group)
  2060. | Error(r"Expected \binom{num}{den}"))
  2061. )
  2062. p.ambi_delim <<= oneOf(list(self._ambi_delim))
  2063. p.left_delim <<= oneOf(list(self._left_delim))
  2064. p.right_delim <<= oneOf(list(self._right_delim))
  2065. p.right_delim_safe <<= oneOf([*(self._right_delim - {'}'}), r'\}'])
  2066. p.genfrac <<= Group(
  2067. Suppress(Literal(r"\genfrac"))
  2068. - (((p.lbrace
  2069. + Optional(p.ambi_delim | p.left_delim, default='')
  2070. + p.rbrace)
  2071. + (p.lbrace
  2072. + Optional(p.ambi_delim | p.right_delim_safe, default='')
  2073. + p.rbrace)
  2074. + (p.lbrace + p.float_literal + p.rbrace)
  2075. + p.simple_group + p.required_group + p.required_group)
  2076. | Error("Expected "
  2077. r"\genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}"))
  2078. )
  2079. p.sqrt <<= Group(
  2080. Suppress(Literal(r"\sqrt"))
  2081. - ((Optional(p.lbracket + p.int_literal + p.rbracket, default=None)
  2082. + p.required_group)
  2083. | Error("Expected \\sqrt{value}"))
  2084. )
  2085. p.overline <<= Group(
  2086. Suppress(Literal(r"\overline"))
  2087. - (p.required_group | Error("Expected \\overline{value}"))
  2088. )
  2089. p.unknown_symbol <<= Combine(p.bslash + Regex("[A-Za-z]*"))
  2090. p.operatorname <<= Group(
  2091. Suppress(Literal(r"\operatorname"))
  2092. - ((p.lbrace + ZeroOrMore(p.simple | p.unknown_symbol) + p.rbrace)
  2093. | Error("Expected \\operatorname{value}"))
  2094. )
  2095. p.placeable <<= (
  2096. p.accentprefixed # Must be before accent so named symbols that are
  2097. # prefixed with an accent name work
  2098. | p.accent # Must be before symbol as all accents are symbols
  2099. | p.symbol # Must be third to catch all named symbols and single
  2100. # chars not in a group
  2101. | p.c_over_c
  2102. | p.function
  2103. | p.group
  2104. | p.frac
  2105. | p.dfrac
  2106. | p.binom
  2107. | p.genfrac
  2108. | p.sqrt
  2109. | p.overline
  2110. | p.operatorname
  2111. )
  2112. p.simple <<= (
  2113. p.space
  2114. | p.customspace
  2115. | p.font
  2116. | p.subsuper
  2117. )
  2118. p.subsuperop <<= oneOf(["_", "^"])
  2119. p.subsuper <<= Group(
  2120. (Optional(p.placeable)
  2121. + OneOrMore(p.subsuperop - p.placeable)
  2122. + Optional(p.apostrophe))
  2123. | (p.placeable + Optional(p.apostrophe))
  2124. | p.apostrophe
  2125. )
  2126. p.token <<= (
  2127. p.simple
  2128. | p.auto_delim
  2129. | p.unknown_symbol # Must be last
  2130. )
  2131. p.auto_delim <<= (
  2132. Suppress(Literal(r"\left"))
  2133. - ((p.left_delim | p.ambi_delim)
  2134. | Error("Expected a delimiter"))
  2135. + Group(ZeroOrMore(p.simple | p.auto_delim))
  2136. + Suppress(Literal(r"\right"))
  2137. - ((p.right_delim | p.ambi_delim)
  2138. | Error("Expected a delimiter"))
  2139. )
  2140. p.math <<= OneOrMore(p.token)
  2141. p.math_string <<= QuotedString('$', '\\', unquoteResults=False)
  2142. p.non_math <<= Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace()
  2143. p.main <<= (
  2144. p.non_math + ZeroOrMore(p.math_string + p.non_math) + StringEnd()
  2145. )
  2146. # Set actions
  2147. for key, val in vars(p).items():
  2148. if not key.startswith('_'):
  2149. if hasattr(self, key):
  2150. val.setParseAction(getattr(self, key))
  2151. self._expression = p.main
  2152. self._math_expression = p.math
  2153. def parse(self, s, fonts_object, fontsize, dpi):
  2154. """
  2155. Parse expression *s* using the given *fonts_object* for
  2156. output, at the given *fontsize* and *dpi*.
  2157. Returns the parse tree of `Node` instances.
  2158. """
  2159. self._state_stack = [
  2160. self.State(fonts_object, 'default', 'rm', fontsize, dpi)]
  2161. self._em_width_cache = {}
  2162. try:
  2163. result = self._expression.parseString(s)
  2164. except ParseBaseException as err:
  2165. raise ValueError("\n".join(["",
  2166. err.line,
  2167. " " * (err.column - 1) + "^",
  2168. str(err)])) from err
  2169. self._state_stack = None
  2170. self._em_width_cache = {}
  2171. self._expression.resetCache()
  2172. return result[0]
  2173. # The state of the parser is maintained in a stack. Upon
  2174. # entering and leaving a group { } or math/non-math, the stack
  2175. # is pushed and popped accordingly. The current state always
  2176. # exists in the top element of the stack.
  2177. class State:
  2178. """
  2179. Stores the state of the parser.
  2180. States are pushed and popped from a stack as necessary, and
  2181. the "current" state is always at the top of the stack.
  2182. """
  2183. def __init__(self, font_output, font, font_class, fontsize, dpi):
  2184. self.font_output = font_output
  2185. self._font = font
  2186. self.font_class = font_class
  2187. self.fontsize = fontsize
  2188. self.dpi = dpi
  2189. def copy(self):
  2190. return Parser.State(
  2191. self.font_output,
  2192. self.font,
  2193. self.font_class,
  2194. self.fontsize,
  2195. self.dpi)
  2196. @property
  2197. def font(self):
  2198. return self._font
  2199. @font.setter
  2200. def font(self, name):
  2201. if name in ('rm', 'it', 'bf'):
  2202. self.font_class = name
  2203. self._font = name
  2204. def get_state(self):
  2205. """Get the current `State` of the parser."""
  2206. return self._state_stack[-1]
  2207. def pop_state(self):
  2208. """Pop a `State` off of the stack."""
  2209. self._state_stack.pop()
  2210. def push_state(self):
  2211. """Push a new `State` onto the stack, copying the current state."""
  2212. self._state_stack.append(self.get_state().copy())
  2213. def main(self, s, loc, toks):
  2214. return [Hlist(toks)]
  2215. def math_string(self, s, loc, toks):
  2216. return self._math_expression.parseString(toks[0][1:-1])
  2217. def math(self, s, loc, toks):
  2218. hlist = Hlist(toks)
  2219. self.pop_state()
  2220. return [hlist]
  2221. def non_math(self, s, loc, toks):
  2222. s = toks[0].replace(r'\$', '$')
  2223. symbols = [Char(c, self.get_state(), math=False) for c in s]
  2224. hlist = Hlist(symbols)
  2225. # We're going into math now, so set font to 'it'
  2226. self.push_state()
  2227. self.get_state().font = rcParams['mathtext.default']
  2228. return [hlist]
  2229. def _make_space(self, percentage):
  2230. # All spaces are relative to em width
  2231. state = self.get_state()
  2232. key = (state.font, state.fontsize, state.dpi)
  2233. width = self._em_width_cache.get(key)
  2234. if width is None:
  2235. metrics = state.font_output.get_metrics(
  2236. state.font, rcParams['mathtext.default'], 'm', state.fontsize,
  2237. state.dpi)
  2238. width = metrics.advance
  2239. self._em_width_cache[key] = width
  2240. return Kern(width * percentage)
  2241. _space_widths = {
  2242. r'\,': 0.16667, # 3/18 em = 3 mu
  2243. r'\thinspace': 0.16667, # 3/18 em = 3 mu
  2244. r'\/': 0.16667, # 3/18 em = 3 mu
  2245. r'\>': 0.22222, # 4/18 em = 4 mu
  2246. r'\:': 0.22222, # 4/18 em = 4 mu
  2247. r'\;': 0.27778, # 5/18 em = 5 mu
  2248. r'\ ': 0.33333, # 6/18 em = 6 mu
  2249. r'~': 0.33333, # 6/18 em = 6 mu, nonbreakable
  2250. r'\enspace': 0.5, # 9/18 em = 9 mu
  2251. r'\quad': 1, # 1 em = 18 mu
  2252. r'\qquad': 2, # 2 em = 36 mu
  2253. r'\!': -0.16667, # -3/18 em = -3 mu
  2254. }
  2255. def space(self, s, loc, toks):
  2256. assert len(toks) == 1
  2257. num = self._space_widths[toks[0]]
  2258. box = self._make_space(num)
  2259. return [box]
  2260. def customspace(self, s, loc, toks):
  2261. return [self._make_space(float(toks[0]))]
  2262. def symbol(self, s, loc, toks):
  2263. c = toks[0]
  2264. try:
  2265. char = Char(c, self.get_state())
  2266. except ValueError as err:
  2267. raise ParseFatalException(s, loc,
  2268. "Unknown symbol: %s" % c) from err
  2269. if c in self._spaced_symbols:
  2270. # iterate until we find previous character, needed for cases
  2271. # such as ${ -2}$, $ -2$, or $ -2$.
  2272. prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
  2273. # Binary operators at start of string should not be spaced
  2274. if (c in self._binary_operators and
  2275. (len(s[:loc].split()) == 0 or prev_char == '{' or
  2276. prev_char in self._left_delim)):
  2277. return [char]
  2278. else:
  2279. return [Hlist([self._make_space(0.2),
  2280. char,
  2281. self._make_space(0.2)],
  2282. do_kern=True)]
  2283. elif c in self._punctuation_symbols:
  2284. # Do not space commas between brackets
  2285. if c == ',':
  2286. prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
  2287. next_char = next((c for c in s[loc + 1:] if c != ' '), '')
  2288. if prev_char == '{' and next_char == '}':
  2289. return [char]
  2290. # Do not space dots as decimal separators
  2291. if c == '.' and s[loc - 1].isdigit() and s[loc + 1].isdigit():
  2292. return [char]
  2293. else:
  2294. return [Hlist([char, self._make_space(0.2)], do_kern=True)]
  2295. return [char]
  2296. accentprefixed = symbol
  2297. def unknown_symbol(self, s, loc, toks):
  2298. c = toks[0]
  2299. raise ParseFatalException(s, loc, "Unknown symbol: %s" % c)
  2300. _char_over_chars = {
  2301. # The first 2 entries in the tuple are (font, char, sizescale) for
  2302. # the two symbols under and over. The third element is the space
  2303. # (in multiples of underline height)
  2304. r'AA': (('it', 'A', 1.0), (None, '\\circ', 0.5), 0.0),
  2305. }
  2306. def c_over_c(self, s, loc, toks):
  2307. sym = toks[0]
  2308. state = self.get_state()
  2309. thickness = state.font_output.get_underline_thickness(
  2310. state.font, state.fontsize, state.dpi)
  2311. under_desc, over_desc, space = \
  2312. self._char_over_chars.get(sym, (None, None, 0.0))
  2313. if under_desc is None:
  2314. raise ParseFatalException("Error parsing symbol")
  2315. over_state = state.copy()
  2316. if over_desc[0] is not None:
  2317. over_state.font = over_desc[0]
  2318. over_state.fontsize *= over_desc[2]
  2319. over = Accent(over_desc[1], over_state)
  2320. under_state = state.copy()
  2321. if under_desc[0] is not None:
  2322. under_state.font = under_desc[0]
  2323. under_state.fontsize *= under_desc[2]
  2324. under = Char(under_desc[1], under_state)
  2325. width = max(over.width, under.width)
  2326. over_centered = HCentered([over])
  2327. over_centered.hpack(width, 'exactly')
  2328. under_centered = HCentered([under])
  2329. under_centered.hpack(width, 'exactly')
  2330. return Vlist([
  2331. over_centered,
  2332. Vbox(0., thickness * space),
  2333. under_centered
  2334. ])
  2335. _accent_map = {
  2336. r'hat': r'\circumflexaccent',
  2337. r'breve': r'\combiningbreve',
  2338. r'bar': r'\combiningoverline',
  2339. r'grave': r'\combininggraveaccent',
  2340. r'acute': r'\combiningacuteaccent',
  2341. r'tilde': r'\combiningtilde',
  2342. r'dot': r'\combiningdotabove',
  2343. r'ddot': r'\combiningdiaeresis',
  2344. r'vec': r'\combiningrightarrowabove',
  2345. r'"': r'\combiningdiaeresis',
  2346. r"`": r'\combininggraveaccent',
  2347. r"'": r'\combiningacuteaccent',
  2348. r'~': r'\combiningtilde',
  2349. r'.': r'\combiningdotabove',
  2350. r'^': r'\circumflexaccent',
  2351. r'overrightarrow': r'\rightarrow',
  2352. r'overleftarrow': r'\leftarrow',
  2353. r'mathring': r'\circ',
  2354. }
  2355. _wide_accents = set(r"widehat widetilde widebar".split())
  2356. # make a lambda and call it to get the namespace right
  2357. _accentprefixed = (lambda am: [
  2358. p for p in tex2uni
  2359. if any(p.startswith(a) and a != p for a in am)
  2360. ])(set(_accent_map))
  2361. def accent(self, s, loc, toks):
  2362. assert len(toks) == 1
  2363. state = self.get_state()
  2364. thickness = state.font_output.get_underline_thickness(
  2365. state.font, state.fontsize, state.dpi)
  2366. if len(toks[0]) != 2:
  2367. raise ParseFatalException("Error parsing accent")
  2368. accent, sym = toks[0]
  2369. if accent in self._wide_accents:
  2370. accent_box = AutoWidthChar(
  2371. '\\' + accent, sym.width, state, char_class=Accent)
  2372. else:
  2373. accent_box = Accent(self._accent_map[accent], state)
  2374. if accent == 'mathring':
  2375. accent_box.shrink()
  2376. accent_box.shrink()
  2377. centered = HCentered([Hbox(sym.width / 4.0), accent_box])
  2378. centered.hpack(sym.width, 'exactly')
  2379. return Vlist([
  2380. centered,
  2381. Vbox(0., thickness * 2.0),
  2382. Hlist([sym])
  2383. ])
  2384. def function(self, s, loc, toks):
  2385. self.push_state()
  2386. state = self.get_state()
  2387. state.font = 'rm'
  2388. hlist = Hlist([Char(c, state) for c in toks[0]])
  2389. self.pop_state()
  2390. hlist.function_name = toks[0]
  2391. return hlist
  2392. def operatorname(self, s, loc, toks):
  2393. self.push_state()
  2394. state = self.get_state()
  2395. state.font = 'rm'
  2396. # Change the font of Chars, but leave Kerns alone
  2397. for c in toks[0]:
  2398. if isinstance(c, Char):
  2399. c.font = 'rm'
  2400. c._update_metrics()
  2401. self.pop_state()
  2402. return Hlist(toks[0])
  2403. def start_group(self, s, loc, toks):
  2404. self.push_state()
  2405. # Deal with LaTeX-style font tokens
  2406. if len(toks):
  2407. self.get_state().font = toks[0][4:]
  2408. return []
  2409. def group(self, s, loc, toks):
  2410. grp = Hlist(toks[0])
  2411. return [grp]
  2412. required_group = simple_group = group
  2413. def end_group(self, s, loc, toks):
  2414. self.pop_state()
  2415. return []
  2416. def font(self, s, loc, toks):
  2417. assert len(toks) == 1
  2418. name = toks[0]
  2419. self.get_state().font = name
  2420. return []
  2421. def is_overunder(self, nucleus):
  2422. if isinstance(nucleus, Char):
  2423. return nucleus.c in self._overunder_symbols
  2424. elif isinstance(nucleus, Hlist) and hasattr(nucleus, 'function_name'):
  2425. return nucleus.function_name in self._overunder_functions
  2426. return False
  2427. def is_dropsub(self, nucleus):
  2428. if isinstance(nucleus, Char):
  2429. return nucleus.c in self._dropsub_symbols
  2430. return False
  2431. def is_slanted(self, nucleus):
  2432. if isinstance(nucleus, Char):
  2433. return nucleus.is_slanted()
  2434. return False
  2435. def is_between_brackets(self, s, loc):
  2436. return False
  2437. def subsuper(self, s, loc, toks):
  2438. assert len(toks) == 1
  2439. nucleus = None
  2440. sub = None
  2441. super = None
  2442. # Pick all of the apostrophes out, including first apostrophes that
  2443. # have been parsed as characters
  2444. napostrophes = 0
  2445. new_toks = []
  2446. for tok in toks[0]:
  2447. if isinstance(tok, str) and tok not in ('^', '_'):
  2448. napostrophes += len(tok)
  2449. elif isinstance(tok, Char) and tok.c == "'":
  2450. napostrophes += 1
  2451. else:
  2452. new_toks.append(tok)
  2453. toks = new_toks
  2454. if len(toks) == 0:
  2455. assert napostrophes
  2456. nucleus = Hbox(0.0)
  2457. elif len(toks) == 1:
  2458. if not napostrophes:
  2459. return toks[0] # .asList()
  2460. else:
  2461. nucleus = toks[0]
  2462. elif len(toks) in (2, 3):
  2463. # single subscript or superscript
  2464. nucleus = toks[0] if len(toks) == 3 else Hbox(0.0)
  2465. op, next = toks[-2:]
  2466. if op == '_':
  2467. sub = next
  2468. else:
  2469. super = next
  2470. elif len(toks) in (4, 5):
  2471. # subscript and superscript
  2472. nucleus = toks[0] if len(toks) == 5 else Hbox(0.0)
  2473. op1, next1, op2, next2 = toks[-4:]
  2474. if op1 == op2:
  2475. if op1 == '_':
  2476. raise ParseFatalException("Double subscript")
  2477. else:
  2478. raise ParseFatalException("Double superscript")
  2479. if op1 == '_':
  2480. sub = next1
  2481. super = next2
  2482. else:
  2483. super = next1
  2484. sub = next2
  2485. else:
  2486. raise ParseFatalException(
  2487. "Subscript/superscript sequence is too long. "
  2488. "Use braces { } to remove ambiguity.")
  2489. state = self.get_state()
  2490. rule_thickness = state.font_output.get_underline_thickness(
  2491. state.font, state.fontsize, state.dpi)
  2492. xHeight = state.font_output.get_xheight(
  2493. state.font, state.fontsize, state.dpi)
  2494. if napostrophes:
  2495. if super is None:
  2496. super = Hlist([])
  2497. for i in range(napostrophes):
  2498. super.children.extend(self.symbol(s, loc, ['\\prime']))
  2499. # kern() and hpack() needed to get the metrics right after
  2500. # extending
  2501. super.kern()
  2502. super.hpack()
  2503. # Handle over/under symbols, such as sum or integral
  2504. if self.is_overunder(nucleus):
  2505. vlist = []
  2506. shift = 0.
  2507. width = nucleus.width
  2508. if super is not None:
  2509. super.shrink()
  2510. width = max(width, super.width)
  2511. if sub is not None:
  2512. sub.shrink()
  2513. width = max(width, sub.width)
  2514. if super is not None:
  2515. hlist = HCentered([super])
  2516. hlist.hpack(width, 'exactly')
  2517. vlist.extend([hlist, Kern(rule_thickness * 3.0)])
  2518. hlist = HCentered([nucleus])
  2519. hlist.hpack(width, 'exactly')
  2520. vlist.append(hlist)
  2521. if sub is not None:
  2522. hlist = HCentered([sub])
  2523. hlist.hpack(width, 'exactly')
  2524. vlist.extend([Kern(rule_thickness * 3.0), hlist])
  2525. shift = hlist.height
  2526. vlist = Vlist(vlist)
  2527. vlist.shift_amount = shift + nucleus.depth
  2528. result = Hlist([vlist])
  2529. return [result]
  2530. # We remove kerning on the last character for consistency (otherwise
  2531. # it will compute kerning based on non-shrunk characters and may put
  2532. # them too close together when superscripted)
  2533. # We change the width of the last character to match the advance to
  2534. # consider some fonts with weird metrics: e.g. stix's f has a width of
  2535. # 7.75 and a kerning of -4.0 for an advance of 3.72, and we want to put
  2536. # the superscript at the advance
  2537. last_char = nucleus
  2538. if isinstance(nucleus, Hlist):
  2539. new_children = nucleus.children
  2540. if len(new_children):
  2541. # remove last kern
  2542. if (isinstance(new_children[-1], Kern) and
  2543. hasattr(new_children[-2], '_metrics')):
  2544. new_children = new_children[:-1]
  2545. last_char = new_children[-1]
  2546. if hasattr(last_char, '_metrics'):
  2547. last_char.width = last_char._metrics.advance
  2548. # create new Hlist without kerning
  2549. nucleus = Hlist(new_children, do_kern=False)
  2550. else:
  2551. if isinstance(nucleus, Char):
  2552. last_char.width = last_char._metrics.advance
  2553. nucleus = Hlist([nucleus])
  2554. # Handle regular sub/superscripts
  2555. constants = _get_font_constant_set(state)
  2556. lc_height = last_char.height
  2557. lc_baseline = 0
  2558. if self.is_dropsub(last_char):
  2559. lc_baseline = last_char.depth
  2560. # Compute kerning for sub and super
  2561. superkern = constants.delta * xHeight
  2562. subkern = constants.delta * xHeight
  2563. if self.is_slanted(last_char):
  2564. superkern += constants.delta * xHeight
  2565. superkern += (constants.delta_slanted *
  2566. (lc_height - xHeight * 2. / 3.))
  2567. if self.is_dropsub(last_char):
  2568. subkern = (3 * constants.delta -
  2569. constants.delta_integral) * lc_height
  2570. superkern = (3 * constants.delta +
  2571. constants.delta_integral) * lc_height
  2572. else:
  2573. subkern = 0
  2574. if super is None:
  2575. # node757
  2576. x = Hlist([Kern(subkern), sub])
  2577. x.shrink()
  2578. if self.is_dropsub(last_char):
  2579. shift_down = lc_baseline + constants.subdrop * xHeight
  2580. else:
  2581. shift_down = constants.sub1 * xHeight
  2582. x.shift_amount = shift_down
  2583. else:
  2584. x = Hlist([Kern(superkern), super])
  2585. x.shrink()
  2586. if self.is_dropsub(last_char):
  2587. shift_up = lc_height - constants.subdrop * xHeight
  2588. else:
  2589. shift_up = constants.sup1 * xHeight
  2590. if sub is None:
  2591. x.shift_amount = -shift_up
  2592. else: # Both sub and superscript
  2593. y = Hlist([Kern(subkern), sub])
  2594. y.shrink()
  2595. if self.is_dropsub(last_char):
  2596. shift_down = lc_baseline + constants.subdrop * xHeight
  2597. else:
  2598. shift_down = constants.sub2 * xHeight
  2599. # If sub and superscript collide, move super up
  2600. clr = (2.0 * rule_thickness -
  2601. ((shift_up - x.depth) - (y.height - shift_down)))
  2602. if clr > 0.:
  2603. shift_up += clr
  2604. x = Vlist([
  2605. x,
  2606. Kern((shift_up - x.depth) - (y.height - shift_down)),
  2607. y])
  2608. x.shift_amount = shift_down
  2609. if not self.is_dropsub(last_char):
  2610. x.width += constants.script_space * xHeight
  2611. result = Hlist([nucleus, x])
  2612. return [result]
  2613. def _genfrac(self, ldelim, rdelim, rule, style, num, den):
  2614. state = self.get_state()
  2615. thickness = state.font_output.get_underline_thickness(
  2616. state.font, state.fontsize, state.dpi)
  2617. rule = float(rule)
  2618. # If style != displaystyle == 0, shrink the num and den
  2619. if style != self._math_style_dict['displaystyle']:
  2620. num.shrink()
  2621. den.shrink()
  2622. cnum = HCentered([num])
  2623. cden = HCentered([den])
  2624. width = max(num.width, den.width)
  2625. cnum.hpack(width, 'exactly')
  2626. cden.hpack(width, 'exactly')
  2627. vlist = Vlist([cnum, # numerator
  2628. Vbox(0, thickness * 2.0), # space
  2629. Hrule(state, rule), # rule
  2630. Vbox(0, thickness * 2.0), # space
  2631. cden # denominator
  2632. ])
  2633. # Shift so the fraction line sits in the middle of the
  2634. # equals sign
  2635. metrics = state.font_output.get_metrics(
  2636. state.font, rcParams['mathtext.default'],
  2637. '=', state.fontsize, state.dpi)
  2638. shift = (cden.height -
  2639. ((metrics.ymax + metrics.ymin) / 2 -
  2640. thickness * 3.0))
  2641. vlist.shift_amount = shift
  2642. result = [Hlist([vlist, Hbox(thickness * 2.)])]
  2643. if ldelim or rdelim:
  2644. if ldelim == '':
  2645. ldelim = '.'
  2646. if rdelim == '':
  2647. rdelim = '.'
  2648. return self._auto_sized_delimiter(ldelim, result, rdelim)
  2649. return result
  2650. def genfrac(self, s, loc, toks):
  2651. assert len(toks) == 1
  2652. assert len(toks[0]) == 6
  2653. return self._genfrac(*tuple(toks[0]))
  2654. def frac(self, s, loc, toks):
  2655. assert len(toks) == 1
  2656. assert len(toks[0]) == 2
  2657. state = self.get_state()
  2658. thickness = state.font_output.get_underline_thickness(
  2659. state.font, state.fontsize, state.dpi)
  2660. num, den = toks[0]
  2661. return self._genfrac('', '', thickness,
  2662. self._math_style_dict['textstyle'], num, den)
  2663. def dfrac(self, s, loc, toks):
  2664. assert len(toks) == 1
  2665. assert len(toks[0]) == 2
  2666. state = self.get_state()
  2667. thickness = state.font_output.get_underline_thickness(
  2668. state.font, state.fontsize, state.dpi)
  2669. num, den = toks[0]
  2670. return self._genfrac('', '', thickness,
  2671. self._math_style_dict['displaystyle'], num, den)
  2672. def binom(self, s, loc, toks):
  2673. assert len(toks) == 1
  2674. assert len(toks[0]) == 2
  2675. num, den = toks[0]
  2676. return self._genfrac('(', ')', 0.0,
  2677. self._math_style_dict['textstyle'], num, den)
  2678. def sqrt(self, s, loc, toks):
  2679. root, body = toks[0]
  2680. state = self.get_state()
  2681. thickness = state.font_output.get_underline_thickness(
  2682. state.font, state.fontsize, state.dpi)
  2683. # Determine the height of the body, and add a little extra to
  2684. # the height so it doesn't seem cramped
  2685. height = body.height - body.shift_amount + thickness * 5.0
  2686. depth = body.depth + body.shift_amount
  2687. check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True)
  2688. height = check.height - check.shift_amount
  2689. depth = check.depth + check.shift_amount
  2690. # Put a little extra space to the left and right of the body
  2691. padded_body = Hlist([Hbox(2 * thickness), body, Hbox(2 * thickness)])
  2692. rightside = Vlist([Hrule(state), Glue('fill'), padded_body])
  2693. # Stretch the glue between the hrule and the body
  2694. rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
  2695. 'exactly', depth)
  2696. # Add the root and shift it upward so it is above the tick.
  2697. # The value of 0.6 is a hard-coded hack ;)
  2698. if root is None:
  2699. root = Box(check.width * 0.5, 0., 0.)
  2700. else:
  2701. root = Hlist([Char(x, state) for x in root])
  2702. root.shrink()
  2703. root.shrink()
  2704. root_vlist = Vlist([Hlist([root])])
  2705. root_vlist.shift_amount = -height * 0.6
  2706. hlist = Hlist([root_vlist, # Root
  2707. # Negative kerning to put root over tick
  2708. Kern(-check.width * 0.5),
  2709. check, # Check
  2710. rightside]) # Body
  2711. return [hlist]
  2712. def overline(self, s, loc, toks):
  2713. assert len(toks) == 1
  2714. assert len(toks[0]) == 1
  2715. body = toks[0][0]
  2716. state = self.get_state()
  2717. thickness = state.font_output.get_underline_thickness(
  2718. state.font, state.fontsize, state.dpi)
  2719. height = body.height - body.shift_amount + thickness * 3.0
  2720. depth = body.depth + body.shift_amount
  2721. # Place overline above body
  2722. rightside = Vlist([Hrule(state), Glue('fill'), Hlist([body])])
  2723. # Stretch the glue between the hrule and the body
  2724. rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
  2725. 'exactly', depth)
  2726. hlist = Hlist([rightside])
  2727. return [hlist]
  2728. def _auto_sized_delimiter(self, front, middle, back):
  2729. state = self.get_state()
  2730. if len(middle):
  2731. height = max(x.height for x in middle)
  2732. depth = max(x.depth for x in middle)
  2733. factor = None
  2734. else:
  2735. height = 0
  2736. depth = 0
  2737. factor = 1.0
  2738. parts = []
  2739. # \left. and \right. aren't supposed to produce any symbols
  2740. if front != '.':
  2741. parts.append(
  2742. AutoHeightChar(front, height, depth, state, factor=factor))
  2743. parts.extend(middle)
  2744. if back != '.':
  2745. parts.append(
  2746. AutoHeightChar(back, height, depth, state, factor=factor))
  2747. hlist = Hlist(parts)
  2748. return hlist
  2749. def auto_delim(self, s, loc, toks):
  2750. front, middle, back = toks
  2751. return self._auto_sized_delimiter(front, middle.asList(), back)
  2752. ##############################################################################
  2753. # MAIN
  2754. class MathTextParser:
  2755. _parser = None
  2756. _backend_mapping = {
  2757. 'bitmap': MathtextBackendBitmap,
  2758. 'agg': MathtextBackendAgg,
  2759. 'ps': MathtextBackendPs,
  2760. 'pdf': MathtextBackendPdf,
  2761. 'svg': MathtextBackendSvg,
  2762. 'path': MathtextBackendPath,
  2763. 'cairo': MathtextBackendCairo,
  2764. 'macosx': MathtextBackendAgg,
  2765. }
  2766. _font_type_mapping = {
  2767. 'cm': BakomaFonts,
  2768. 'dejavuserif': DejaVuSerifFonts,
  2769. 'dejavusans': DejaVuSansFonts,
  2770. 'stix': StixFonts,
  2771. 'stixsans': StixSansFonts,
  2772. 'custom': UnicodeFonts,
  2773. }
  2774. def __init__(self, output):
  2775. """Create a MathTextParser for the given backend *output*."""
  2776. self._output = output.lower()
  2777. def parse(self, s, dpi=72, prop=None):
  2778. """
  2779. Parse the given math expression *s* at the given *dpi*. If *prop* is
  2780. provided, it is a `.FontProperties` object specifying the "default"
  2781. font to use in the math expression, used for all non-math text.
  2782. The results are cached, so multiple calls to `parse`
  2783. with the same expression should be fast.
  2784. """
  2785. # lru_cache can't decorate parse() directly because the ps.useafm and
  2786. # mathtext.fontset rcParams also affect the parse (e.g. by affecting
  2787. # the glyph metrics).
  2788. return self._parse_cached(
  2789. s, dpi, prop, rcParams['ps.useafm'], rcParams['mathtext.fontset'])
  2790. @functools.lru_cache(50)
  2791. def _parse_cached(self, s, dpi, prop, ps_useafm, fontset):
  2792. if prop is None:
  2793. prop = FontProperties()
  2794. if self._output == 'ps' and ps_useafm:
  2795. font_output = StandardPsFonts(prop)
  2796. else:
  2797. backend = self._backend_mapping[self._output]()
  2798. fontset_class = cbook._check_getitem(
  2799. self._font_type_mapping, fontset=fontset)
  2800. font_output = fontset_class(prop, backend)
  2801. fontsize = prop.get_size_in_points()
  2802. # This is a class variable so we don't rebuild the parser
  2803. # with each request.
  2804. if self._parser is None:
  2805. self.__class__._parser = Parser()
  2806. box = self._parser.parse(s, font_output, fontsize, dpi)
  2807. font_output.set_canvas_size(box.width, box.height, box.depth)
  2808. return font_output.get_results(box)
  2809. def to_mask(self, texstr, dpi=120, fontsize=14):
  2810. r"""
  2811. Parameters
  2812. ----------
  2813. texstr : str
  2814. A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'.
  2815. dpi : float
  2816. The dots-per-inch setting used to render the text.
  2817. fontsize : int
  2818. The font size in points
  2819. Returns
  2820. -------
  2821. array : 2D uint8 alpha
  2822. Mask array of rasterized tex.
  2823. depth : int
  2824. Offset of the baseline from the bottom of the image, in pixels.
  2825. """
  2826. assert self._output == "bitmap"
  2827. prop = FontProperties(size=fontsize)
  2828. ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop)
  2829. return np.asarray(ftimage), depth
  2830. def to_rgba(self, texstr, color='black', dpi=120, fontsize=14):
  2831. r"""
  2832. Parameters
  2833. ----------
  2834. texstr : str
  2835. A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'.
  2836. color : color
  2837. The text color.
  2838. dpi : float
  2839. The dots-per-inch setting used to render the text.
  2840. fontsize : int
  2841. The font size in points.
  2842. Returns
  2843. -------
  2844. array : (M, N, 4) array
  2845. RGBA color values of rasterized tex, colorized with *color*.
  2846. depth : int
  2847. Offset of the baseline from the bottom of the image, in pixels.
  2848. """
  2849. x, depth = self.to_mask(texstr, dpi=dpi, fontsize=fontsize)
  2850. r, g, b, a = mcolors.to_rgba(color)
  2851. RGBA = np.zeros((x.shape[0], x.shape[1], 4), dtype=np.uint8)
  2852. RGBA[:, :, 0] = 255 * r
  2853. RGBA[:, :, 1] = 255 * g
  2854. RGBA[:, :, 2] = 255 * b
  2855. RGBA[:, :, 3] = x
  2856. return RGBA, depth
  2857. def to_png(self, filename, texstr, color='black', dpi=120, fontsize=14):
  2858. r"""
  2859. Render a tex expression to a PNG file.
  2860. Parameters
  2861. ----------
  2862. filename
  2863. A writable filename or fileobject.
  2864. texstr : str
  2865. A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'.
  2866. color : color
  2867. The text color.
  2868. dpi : float
  2869. The dots-per-inch setting used to render the text.
  2870. fontsize : int
  2871. The font size in points.
  2872. Returns
  2873. -------
  2874. int
  2875. Offset of the baseline from the bottom of the image, in pixels.
  2876. """
  2877. rgba, depth = self.to_rgba(
  2878. texstr, color=color, dpi=dpi, fontsize=fontsize)
  2879. Image.fromarray(rgba).save(filename, format="png")
  2880. return depth
  2881. def get_depth(self, texstr, dpi=120, fontsize=14):
  2882. r"""
  2883. Parameters
  2884. ----------
  2885. texstr : str
  2886. A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'.
  2887. dpi : float
  2888. The dots-per-inch setting used to render the text.
  2889. Returns
  2890. -------
  2891. int
  2892. Offset of the baseline from the bottom of the image, in pixels.
  2893. """
  2894. assert self._output == "bitmap"
  2895. prop = FontProperties(size=fontsize)
  2896. ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop)
  2897. return depth
  2898. def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None):
  2899. """
  2900. Given a math expression, renders it in a closely-clipped bounding
  2901. box to an image file.
  2902. Parameters
  2903. ----------
  2904. s : str
  2905. A math expression. The math portion must be enclosed in dollar signs.
  2906. filename_or_obj : str or path-like or file-like
  2907. Where to write the image data.
  2908. prop : `.FontProperties`, optional
  2909. The size and style of the text.
  2910. dpi : float, optional
  2911. The output dpi. If not set, the dpi is determined as for
  2912. `.Figure.savefig`.
  2913. format : str, optional
  2914. The output format, e.g., 'svg', 'pdf', 'ps' or 'png'. If not set, the
  2915. format is determined as for `.Figure.savefig`.
  2916. """
  2917. from matplotlib import figure
  2918. # backend_agg supports all of the core output formats
  2919. from matplotlib.backends import backend_agg
  2920. if prop is None:
  2921. prop = FontProperties()
  2922. parser = MathTextParser('path')
  2923. width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
  2924. fig = figure.Figure(figsize=(width / 72.0, height / 72.0))
  2925. fig.text(0, depth/height, s, fontproperties=prop)
  2926. backend_agg.FigureCanvasAgg(fig)
  2927. fig.savefig(filename_or_obj, dpi=dpi, format=format)
  2928. return depth