mathtext.py 118 KB

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