backend_ps.py 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481
  1. """
  2. A PostScript backend, which can produce both PostScript .ps and .eps.
  3. """
  4. import bisect
  5. import codecs
  6. import datetime
  7. from enum import Enum
  8. import functools
  9. from io import StringIO
  10. import itertools
  11. import logging
  12. import math
  13. import os
  14. import pathlib
  15. import shutil
  16. import struct
  17. from tempfile import TemporaryDirectory
  18. import textwrap
  19. import time
  20. import fontTools
  21. import numpy as np
  22. import matplotlib as mpl
  23. from matplotlib import _api, cbook, _path, _text_helpers
  24. from matplotlib._afm import AFM
  25. from matplotlib.backend_bases import (
  26. _Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
  27. from matplotlib.cbook import is_writable_file_like, file_requires_unicode
  28. from matplotlib.font_manager import get_font
  29. from matplotlib.ft2font import LoadFlags
  30. from matplotlib._mathtext_data import uni2type1
  31. from matplotlib.path import Path
  32. from matplotlib.texmanager import TexManager
  33. from matplotlib.transforms import Affine2D
  34. from matplotlib.backends.backend_mixed import MixedModeRenderer
  35. from . import _backend_pdf_ps
  36. _log = logging.getLogger(__name__)
  37. debugPS = False
  38. papersize = {'letter': (8.5, 11),
  39. 'legal': (8.5, 14),
  40. 'ledger': (11, 17),
  41. 'a0': (33.11, 46.81),
  42. 'a1': (23.39, 33.11),
  43. 'a2': (16.54, 23.39),
  44. 'a3': (11.69, 16.54),
  45. 'a4': (8.27, 11.69),
  46. 'a5': (5.83, 8.27),
  47. 'a6': (4.13, 5.83),
  48. 'a7': (2.91, 4.13),
  49. 'a8': (2.05, 2.91),
  50. 'a9': (1.46, 2.05),
  51. 'a10': (1.02, 1.46),
  52. 'b0': (40.55, 57.32),
  53. 'b1': (28.66, 40.55),
  54. 'b2': (20.27, 28.66),
  55. 'b3': (14.33, 20.27),
  56. 'b4': (10.11, 14.33),
  57. 'b5': (7.16, 10.11),
  58. 'b6': (5.04, 7.16),
  59. 'b7': (3.58, 5.04),
  60. 'b8': (2.51, 3.58),
  61. 'b9': (1.76, 2.51),
  62. 'b10': (1.26, 1.76)}
  63. def _nums_to_str(*args, sep=" "):
  64. return sep.join(f"{arg:1.3f}".rstrip("0").rstrip(".") for arg in args)
  65. def _move_path_to_path_or_stream(src, dst):
  66. """
  67. Move the contents of file at *src* to path-or-filelike *dst*.
  68. If *dst* is a path, the metadata of *src* are *not* copied.
  69. """
  70. if is_writable_file_like(dst):
  71. fh = (open(src, encoding='latin-1')
  72. if file_requires_unicode(dst)
  73. else open(src, 'rb'))
  74. with fh:
  75. shutil.copyfileobj(fh, dst)
  76. else:
  77. shutil.move(src, dst, copy_function=shutil.copyfile)
  78. def _font_to_ps_type3(font_path, chars):
  79. """
  80. Subset *chars* from the font at *font_path* into a Type 3 font.
  81. Parameters
  82. ----------
  83. font_path : path-like
  84. Path to the font to be subsetted.
  85. chars : str
  86. The characters to include in the subsetted font.
  87. Returns
  88. -------
  89. str
  90. The string representation of a Type 3 font, which can be included
  91. verbatim into a PostScript file.
  92. """
  93. font = get_font(font_path, hinting_factor=1)
  94. glyph_ids = [font.get_char_index(c) for c in chars]
  95. preamble = """\
  96. %!PS-Adobe-3.0 Resource-Font
  97. %%Creator: Converted from TrueType to Type 3 by Matplotlib.
  98. 10 dict begin
  99. /FontName /{font_name} def
  100. /PaintType 0 def
  101. /FontMatrix [{inv_units_per_em} 0 0 {inv_units_per_em} 0 0] def
  102. /FontBBox [{bbox}] def
  103. /FontType 3 def
  104. /Encoding [{encoding}] def
  105. /CharStrings {num_glyphs} dict dup begin
  106. /.notdef 0 def
  107. """.format(font_name=font.postscript_name,
  108. inv_units_per_em=1 / font.units_per_EM,
  109. bbox=" ".join(map(str, font.bbox)),
  110. encoding=" ".join(f"/{font.get_glyph_name(glyph_id)}"
  111. for glyph_id in glyph_ids),
  112. num_glyphs=len(glyph_ids) + 1)
  113. postamble = """
  114. end readonly def
  115. /BuildGlyph {
  116. exch begin
  117. CharStrings exch
  118. 2 copy known not {pop /.notdef} if
  119. true 3 1 roll get exec
  120. end
  121. } _d
  122. /BuildChar {
  123. 1 index /Encoding get exch get
  124. 1 index /BuildGlyph get exec
  125. } _d
  126. FontName currentdict end definefont pop
  127. """
  128. entries = []
  129. for glyph_id in glyph_ids:
  130. g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE)
  131. v, c = font.get_path()
  132. entries.append(
  133. "/%(name)s{%(bbox)s sc\n" % {
  134. "name": font.get_glyph_name(glyph_id),
  135. "bbox": " ".join(map(str, [g.horiAdvance, 0, *g.bbox])),
  136. }
  137. + _path.convert_to_string(
  138. # Convert back to TrueType's internal units (1/64's).
  139. # (Other dimensions are already in these units.)
  140. Path(v * 64, c), None, None, False, None, 0,
  141. # No code for quad Beziers triggers auto-conversion to cubics.
  142. # Drop intermediate closepolys (relying on the outline
  143. # decomposer always explicitly moving to the closing point
  144. # first).
  145. [b"m", b"l", b"", b"c", b""], True).decode("ascii")
  146. + "ce} _d"
  147. )
  148. return preamble + "\n".join(entries) + postamble
  149. def _font_to_ps_type42(font_path, chars, fh):
  150. """
  151. Subset *chars* from the font at *font_path* into a Type 42 font at *fh*.
  152. Parameters
  153. ----------
  154. font_path : path-like
  155. Path to the font to be subsetted.
  156. chars : str
  157. The characters to include in the subsetted font.
  158. fh : file-like
  159. Where to write the font.
  160. """
  161. subset_str = ''.join(chr(c) for c in chars)
  162. _log.debug("SUBSET %s characters: %s", font_path, subset_str)
  163. try:
  164. kw = {}
  165. # fix this once we support loading more fonts from a collection
  166. # https://github.com/matplotlib/matplotlib/issues/3135#issuecomment-571085541
  167. if font_path.endswith('.ttc'):
  168. kw['fontNumber'] = 0
  169. with (fontTools.ttLib.TTFont(font_path, **kw) as font,
  170. _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) as subset):
  171. fontdata = _backend_pdf_ps.font_as_file(subset).getvalue()
  172. _log.debug(
  173. "SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size,
  174. len(fontdata)
  175. )
  176. fh.write(_serialize_type42(font, subset, fontdata))
  177. except RuntimeError:
  178. _log.warning(
  179. "The PostScript backend does not currently support the selected font (%s).",
  180. font_path)
  181. raise
  182. def _serialize_type42(font, subset, fontdata):
  183. """
  184. Output a PostScript Type-42 format representation of font
  185. Parameters
  186. ----------
  187. font : fontTools.ttLib.ttFont.TTFont
  188. The original font object
  189. subset : fontTools.ttLib.ttFont.TTFont
  190. The subset font object
  191. fontdata : bytes
  192. The raw font data in TTF format
  193. Returns
  194. -------
  195. str
  196. The Type-42 formatted font
  197. """
  198. version, breakpoints = _version_and_breakpoints(font.get('loca'), fontdata)
  199. post = font['post']
  200. name = font['name']
  201. chars = _generate_charstrings(subset)
  202. sfnts = _generate_sfnts(fontdata, subset, breakpoints)
  203. return textwrap.dedent(f"""
  204. %%!PS-TrueTypeFont-{version[0]}.{version[1]}-{font['head'].fontRevision:.7f}
  205. 10 dict begin
  206. /FontType 42 def
  207. /FontMatrix [1 0 0 1 0 0] def
  208. /FontName /{name.getDebugName(6)} def
  209. /FontInfo 7 dict dup begin
  210. /FullName ({name.getDebugName(4)}) def
  211. /FamilyName ({name.getDebugName(1)}) def
  212. /Version ({name.getDebugName(5)}) def
  213. /ItalicAngle {post.italicAngle} def
  214. /isFixedPitch {'true' if post.isFixedPitch else 'false'} def
  215. /UnderlinePosition {post.underlinePosition} def
  216. /UnderlineThickness {post.underlineThickness} def
  217. end readonly def
  218. /Encoding StandardEncoding def
  219. /FontBBox [{_nums_to_str(*_bounds(font))}] def
  220. /PaintType 0 def
  221. /CIDMap 0 def
  222. {chars}
  223. {sfnts}
  224. FontName currentdict end definefont pop
  225. """)
  226. def _version_and_breakpoints(loca, fontdata):
  227. """
  228. Read the version number of the font and determine sfnts breakpoints.
  229. When a TrueType font file is written as a Type 42 font, it has to be
  230. broken into substrings of at most 65535 bytes. These substrings must
  231. begin at font table boundaries or glyph boundaries in the glyf table.
  232. This function determines all possible breakpoints and it is the caller's
  233. responsibility to do the splitting.
  234. Helper function for _font_to_ps_type42.
  235. Parameters
  236. ----------
  237. loca : fontTools.ttLib._l_o_c_a.table__l_o_c_a or None
  238. The loca table of the font if available
  239. fontdata : bytes
  240. The raw data of the font
  241. Returns
  242. -------
  243. version : tuple[int, int]
  244. A 2-tuple of the major version number and minor version number.
  245. breakpoints : list[int]
  246. The breakpoints is a sorted list of offsets into fontdata; if loca is not
  247. available, just the table boundaries.
  248. """
  249. v1, v2, numTables = struct.unpack('>3h', fontdata[:6])
  250. version = (v1, v2)
  251. tables = {}
  252. for i in range(numTables):
  253. tag, _, offset, _ = struct.unpack('>4sIII', fontdata[12 + i*16:12 + (i+1)*16])
  254. tables[tag.decode('ascii')] = offset
  255. if loca is not None:
  256. glyf_breakpoints = {tables['glyf'] + offset for offset in loca.locations[:-1]}
  257. else:
  258. glyf_breakpoints = set()
  259. breakpoints = sorted({*tables.values(), *glyf_breakpoints, len(fontdata)})
  260. return version, breakpoints
  261. def _bounds(font):
  262. """
  263. Compute the font bounding box, as if all glyphs were written
  264. at the same start position.
  265. Helper function for _font_to_ps_type42.
  266. Parameters
  267. ----------
  268. font : fontTools.ttLib.ttFont.TTFont
  269. The font
  270. Returns
  271. -------
  272. tuple
  273. (xMin, yMin, xMax, yMax) of the combined bounding box
  274. of all the glyphs in the font
  275. """
  276. gs = font.getGlyphSet(False)
  277. pen = fontTools.pens.boundsPen.BoundsPen(gs)
  278. for name in gs.keys():
  279. gs[name].draw(pen)
  280. return pen.bounds or (0, 0, 0, 0)
  281. def _generate_charstrings(font):
  282. """
  283. Transform font glyphs into CharStrings
  284. Helper function for _font_to_ps_type42.
  285. Parameters
  286. ----------
  287. font : fontTools.ttLib.ttFont.TTFont
  288. The font
  289. Returns
  290. -------
  291. str
  292. A definition of the CharStrings dictionary in PostScript
  293. """
  294. go = font.getGlyphOrder()
  295. s = f'/CharStrings {len(go)} dict dup begin\n'
  296. for i, name in enumerate(go):
  297. s += f'/{name} {i} def\n'
  298. s += 'end readonly def'
  299. return s
  300. def _generate_sfnts(fontdata, font, breakpoints):
  301. """
  302. Transform font data into PostScript sfnts format.
  303. Helper function for _font_to_ps_type42.
  304. Parameters
  305. ----------
  306. fontdata : bytes
  307. The raw data of the font
  308. font : fontTools.ttLib.ttFont.TTFont
  309. The fontTools font object
  310. breakpoints : list
  311. Sorted offsets of possible breakpoints
  312. Returns
  313. -------
  314. str
  315. The sfnts array for the font definition, consisting
  316. of hex-encoded strings in PostScript format
  317. """
  318. s = '/sfnts['
  319. pos = 0
  320. while pos < len(fontdata):
  321. i = bisect.bisect_left(breakpoints, pos + 65534)
  322. newpos = breakpoints[i-1]
  323. if newpos <= pos:
  324. # have to accept a larger string
  325. newpos = breakpoints[-1]
  326. s += f'<{fontdata[pos:newpos].hex()}00>' # Always NUL terminate.
  327. pos = newpos
  328. s += ']def'
  329. return '\n'.join(s[i:i+100] for i in range(0, len(s), 100))
  330. def _log_if_debug_on(meth):
  331. """
  332. Wrap `RendererPS` method *meth* to emit a PS comment with the method name,
  333. if the global flag `debugPS` is set.
  334. """
  335. @functools.wraps(meth)
  336. def wrapper(self, *args, **kwargs):
  337. if debugPS:
  338. self._pswriter.write(f"% {meth.__name__}\n")
  339. return meth(self, *args, **kwargs)
  340. return wrapper
  341. class RendererPS(_backend_pdf_ps.RendererPDFPSBase):
  342. """
  343. The renderer handles all the drawing primitives using a graphics
  344. context instance that controls the colors/styles.
  345. """
  346. _afm_font_dir = cbook._get_data_path("fonts/afm")
  347. _use_afm_rc_name = "ps.useafm"
  348. def __init__(self, width, height, pswriter, imagedpi=72):
  349. # Although postscript itself is dpi independent, we need to inform the
  350. # image code about a requested dpi to generate high resolution images
  351. # and them scale them before embedding them.
  352. super().__init__(width, height)
  353. self._pswriter = pswriter
  354. if mpl.rcParams['text.usetex']:
  355. self.textcnt = 0
  356. self.psfrag = []
  357. self.imagedpi = imagedpi
  358. # current renderer state (None=uninitialised)
  359. self.color = None
  360. self.linewidth = None
  361. self.linejoin = None
  362. self.linecap = None
  363. self.linedash = None
  364. self.fontname = None
  365. self.fontsize = None
  366. self._hatches = {}
  367. self.image_magnification = imagedpi / 72
  368. self._clip_paths = {}
  369. self._path_collection_id = 0
  370. self._character_tracker = _backend_pdf_ps.CharacterTracker()
  371. self._logwarn_once = functools.cache(_log.warning)
  372. def _is_transparent(self, rgb_or_rgba):
  373. if rgb_or_rgba is None:
  374. return True # Consistent with rgbFace semantics.
  375. elif len(rgb_or_rgba) == 4:
  376. if rgb_or_rgba[3] == 0:
  377. return True
  378. if rgb_or_rgba[3] != 1:
  379. self._logwarn_once(
  380. "The PostScript backend does not support transparency; "
  381. "partially transparent artists will be rendered opaque.")
  382. return False
  383. else: # len() == 3.
  384. return False
  385. def set_color(self, r, g, b, store=True):
  386. if (r, g, b) != self.color:
  387. self._pswriter.write(f"{_nums_to_str(r)} setgray\n"
  388. if r == g == b else
  389. f"{_nums_to_str(r, g, b)} setrgbcolor\n")
  390. if store:
  391. self.color = (r, g, b)
  392. def set_linewidth(self, linewidth, store=True):
  393. linewidth = float(linewidth)
  394. if linewidth != self.linewidth:
  395. self._pswriter.write(f"{_nums_to_str(linewidth)} setlinewidth\n")
  396. if store:
  397. self.linewidth = linewidth
  398. @staticmethod
  399. def _linejoin_cmd(linejoin):
  400. # Support for directly passing integer values is for backcompat.
  401. linejoin = {'miter': 0, 'round': 1, 'bevel': 2, 0: 0, 1: 1, 2: 2}[
  402. linejoin]
  403. return f"{linejoin:d} setlinejoin\n"
  404. def set_linejoin(self, linejoin, store=True):
  405. if linejoin != self.linejoin:
  406. self._pswriter.write(self._linejoin_cmd(linejoin))
  407. if store:
  408. self.linejoin = linejoin
  409. @staticmethod
  410. def _linecap_cmd(linecap):
  411. # Support for directly passing integer values is for backcompat.
  412. linecap = {'butt': 0, 'round': 1, 'projecting': 2, 0: 0, 1: 1, 2: 2}[
  413. linecap]
  414. return f"{linecap:d} setlinecap\n"
  415. def set_linecap(self, linecap, store=True):
  416. if linecap != self.linecap:
  417. self._pswriter.write(self._linecap_cmd(linecap))
  418. if store:
  419. self.linecap = linecap
  420. def set_linedash(self, offset, seq, store=True):
  421. if self.linedash is not None:
  422. oldo, oldseq = self.linedash
  423. if np.array_equal(seq, oldseq) and oldo == offset:
  424. return
  425. self._pswriter.write(f"[{_nums_to_str(*seq)}] {_nums_to_str(offset)} setdash\n"
  426. if seq is not None and len(seq) else
  427. "[] 0 setdash\n")
  428. if store:
  429. self.linedash = (offset, seq)
  430. def set_font(self, fontname, fontsize, store=True):
  431. if (fontname, fontsize) != (self.fontname, self.fontsize):
  432. self._pswriter.write(f"/{fontname} {fontsize:1.3f} selectfont\n")
  433. if store:
  434. self.fontname = fontname
  435. self.fontsize = fontsize
  436. def create_hatch(self, hatch, linewidth):
  437. sidelen = 72
  438. if hatch in self._hatches:
  439. return self._hatches[hatch]
  440. name = 'H%d' % len(self._hatches)
  441. pageheight = self.height * 72
  442. self._pswriter.write(f"""\
  443. << /PatternType 1
  444. /PaintType 2
  445. /TilingType 2
  446. /BBox[0 0 {sidelen:d} {sidelen:d}]
  447. /XStep {sidelen:d}
  448. /YStep {sidelen:d}
  449. /PaintProc {{
  450. pop
  451. {linewidth:g} setlinewidth
  452. {self._convert_path(Path.hatch(hatch), Affine2D().scale(sidelen), simplify=False)}
  453. gsave
  454. fill
  455. grestore
  456. stroke
  457. }} bind
  458. >>
  459. matrix
  460. 0 {pageheight:g} translate
  461. makepattern
  462. /{name} exch def
  463. """)
  464. self._hatches[hatch] = name
  465. return name
  466. def get_image_magnification(self):
  467. """
  468. Get the factor by which to magnify images passed to draw_image.
  469. Allows a backend to have images at a different resolution to other
  470. artists.
  471. """
  472. return self.image_magnification
  473. def _convert_path(self, path, transform, clip=False, simplify=None):
  474. if clip:
  475. clip = (0.0, 0.0, self.width * 72.0, self.height * 72.0)
  476. else:
  477. clip = None
  478. return _path.convert_to_string(
  479. path, transform, clip, simplify, None,
  480. 6, [b"m", b"l", b"", b"c", b"cl"], True).decode("ascii")
  481. def _get_clip_cmd(self, gc):
  482. clip = []
  483. rect = gc.get_clip_rectangle()
  484. if rect is not None:
  485. clip.append(f"{_nums_to_str(*rect.p0, *rect.size)} rectclip\n")
  486. path, trf = gc.get_clip_path()
  487. if path is not None:
  488. key = (path, id(trf))
  489. custom_clip_cmd = self._clip_paths.get(key)
  490. if custom_clip_cmd is None:
  491. custom_clip_cmd = "c%d" % len(self._clip_paths)
  492. self._pswriter.write(f"""\
  493. /{custom_clip_cmd} {{
  494. {self._convert_path(path, trf, simplify=False)}
  495. clip
  496. newpath
  497. }} bind def
  498. """)
  499. self._clip_paths[key] = custom_clip_cmd
  500. clip.append(f"{custom_clip_cmd}\n")
  501. return "".join(clip)
  502. @_log_if_debug_on
  503. def draw_image(self, gc, x, y, im, transform=None):
  504. # docstring inherited
  505. h, w = im.shape[:2]
  506. imagecmd = "false 3 colorimage"
  507. data = im[::-1, :, :3] # Vertically flipped rgb values.
  508. hexdata = data.tobytes().hex("\n", -64) # Linewrap to 128 chars.
  509. if transform is None:
  510. matrix = "1 0 0 1 0 0"
  511. xscale = w / self.image_magnification
  512. yscale = h / self.image_magnification
  513. else:
  514. matrix = " ".join(map(str, transform.frozen().to_values()))
  515. xscale = 1.0
  516. yscale = 1.0
  517. self._pswriter.write(f"""\
  518. gsave
  519. {self._get_clip_cmd(gc)}
  520. {x:g} {y:g} translate
  521. [{matrix}] concat
  522. {xscale:g} {yscale:g} scale
  523. /DataString {w:d} string def
  524. {w:d} {h:d} 8 [ {w:d} 0 0 -{h:d} 0 {h:d} ]
  525. {{
  526. currentfile DataString readhexstring pop
  527. }} bind {imagecmd}
  528. {hexdata}
  529. grestore
  530. """)
  531. @_log_if_debug_on
  532. def draw_path(self, gc, path, transform, rgbFace=None):
  533. # docstring inherited
  534. clip = rgbFace is None and gc.get_hatch_path() is None
  535. simplify = path.should_simplify and clip
  536. ps = self._convert_path(path, transform, clip=clip, simplify=simplify)
  537. self._draw_ps(ps, gc, rgbFace)
  538. @_log_if_debug_on
  539. def draw_markers(
  540. self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
  541. # docstring inherited
  542. ps_color = (
  543. None
  544. if self._is_transparent(rgbFace)
  545. else f'{_nums_to_str(rgbFace[0])} setgray'
  546. if rgbFace[0] == rgbFace[1] == rgbFace[2]
  547. else f'{_nums_to_str(*rgbFace[:3])} setrgbcolor')
  548. # construct the generic marker command:
  549. # don't want the translate to be global
  550. ps_cmd = ['/o {', 'gsave', 'newpath', 'translate']
  551. lw = gc.get_linewidth()
  552. alpha = (gc.get_alpha()
  553. if gc.get_forced_alpha() or len(gc.get_rgb()) == 3
  554. else gc.get_rgb()[3])
  555. stroke = lw > 0 and alpha > 0
  556. if stroke:
  557. ps_cmd.append('%.1f setlinewidth' % lw)
  558. ps_cmd.append(self._linejoin_cmd(gc.get_joinstyle()))
  559. ps_cmd.append(self._linecap_cmd(gc.get_capstyle()))
  560. ps_cmd.append(self._convert_path(marker_path, marker_trans,
  561. simplify=False))
  562. if rgbFace:
  563. if stroke:
  564. ps_cmd.append('gsave')
  565. if ps_color:
  566. ps_cmd.extend([ps_color, 'fill'])
  567. if stroke:
  568. ps_cmd.append('grestore')
  569. if stroke:
  570. ps_cmd.append('stroke')
  571. ps_cmd.extend(['grestore', '} bind def'])
  572. for vertices, code in path.iter_segments(
  573. trans,
  574. clip=(0, 0, self.width*72, self.height*72),
  575. simplify=False):
  576. if len(vertices):
  577. x, y = vertices[-2:]
  578. ps_cmd.append(f"{x:g} {y:g} o")
  579. ps = '\n'.join(ps_cmd)
  580. self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False)
  581. @_log_if_debug_on
  582. def draw_path_collection(self, gc, master_transform, paths, all_transforms,
  583. offsets, offset_trans, facecolors, edgecolors,
  584. linewidths, linestyles, antialiaseds, urls,
  585. offset_position):
  586. # Is the optimization worth it? Rough calculation:
  587. # cost of emitting a path in-line is
  588. # (len_path + 2) * uses_per_path
  589. # cost of definition+use is
  590. # (len_path + 3) + 3 * uses_per_path
  591. len_path = len(paths[0].vertices) if len(paths) > 0 else 0
  592. uses_per_path = self._iter_collection_uses_per_path(
  593. paths, all_transforms, offsets, facecolors, edgecolors)
  594. should_do_optimization = \
  595. len_path + 3 * uses_per_path + 3 < (len_path + 2) * uses_per_path
  596. if not should_do_optimization:
  597. return RendererBase.draw_path_collection(
  598. self, gc, master_transform, paths, all_transforms,
  599. offsets, offset_trans, facecolors, edgecolors,
  600. linewidths, linestyles, antialiaseds, urls,
  601. offset_position)
  602. path_codes = []
  603. for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
  604. master_transform, paths, all_transforms)):
  605. name = 'p%d_%d' % (self._path_collection_id, i)
  606. path_bytes = self._convert_path(path, transform, simplify=False)
  607. self._pswriter.write(f"""\
  608. /{name} {{
  609. newpath
  610. translate
  611. {path_bytes}
  612. }} bind def
  613. """)
  614. path_codes.append(name)
  615. for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
  616. gc, path_codes, offsets, offset_trans,
  617. facecolors, edgecolors, linewidths, linestyles,
  618. antialiaseds, urls, offset_position):
  619. ps = f"{xo:g} {yo:g} {path_id}"
  620. self._draw_ps(ps, gc0, rgbFace)
  621. self._path_collection_id += 1
  622. @_log_if_debug_on
  623. def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
  624. # docstring inherited
  625. if self._is_transparent(gc.get_rgb()):
  626. return # Special handling for fully transparent.
  627. if not hasattr(self, "psfrag"):
  628. self._logwarn_once(
  629. "The PS backend determines usetex status solely based on "
  630. "rcParams['text.usetex'] and does not support having "
  631. "usetex=True only for some elements; this element will thus "
  632. "be rendered as if usetex=False.")
  633. self.draw_text(gc, x, y, s, prop, angle, False, mtext)
  634. return
  635. w, h, bl = self.get_text_width_height_descent(s, prop, ismath="TeX")
  636. fontsize = prop.get_size_in_points()
  637. thetext = 'psmarker%d' % self.textcnt
  638. color = _nums_to_str(*gc.get_rgb()[:3], sep=',')
  639. fontcmd = {'sans-serif': r'{\sffamily %s}',
  640. 'monospace': r'{\ttfamily %s}'}.get(
  641. mpl.rcParams['font.family'][0], r'{\rmfamily %s}')
  642. s = fontcmd % s
  643. tex = r'\color[rgb]{%s} %s' % (color, s)
  644. # Stick to bottom-left alignment, so subtract descent from the text-normal
  645. # direction since text is normally positioned by its baseline.
  646. rangle = np.radians(angle + 90)
  647. pos = _nums_to_str(x - bl * np.cos(rangle), y - bl * np.sin(rangle))
  648. self.psfrag.append(
  649. r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' % (
  650. thetext, angle, fontsize, fontsize*1.25, tex))
  651. self._pswriter.write(f"""\
  652. gsave
  653. {pos} moveto
  654. ({thetext})
  655. show
  656. grestore
  657. """)
  658. self.textcnt += 1
  659. @_log_if_debug_on
  660. def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
  661. # docstring inherited
  662. if self._is_transparent(gc.get_rgb()):
  663. return # Special handling for fully transparent.
  664. if ismath == 'TeX':
  665. return self.draw_tex(gc, x, y, s, prop, angle)
  666. if ismath:
  667. return self.draw_mathtext(gc, x, y, s, prop, angle)
  668. stream = [] # list of (ps_name, x, char_name)
  669. if mpl.rcParams['ps.useafm']:
  670. font = self._get_font_afm(prop)
  671. ps_name = (font.postscript_name.encode("ascii", "replace")
  672. .decode("ascii"))
  673. scale = 0.001 * prop.get_size_in_points()
  674. thisx = 0
  675. last_name = None # kerns returns 0 for None.
  676. for c in s:
  677. name = uni2type1.get(ord(c), f"uni{ord(c):04X}")
  678. try:
  679. width = font.get_width_from_char_name(name)
  680. except KeyError:
  681. name = 'question'
  682. width = font.get_width_char('?')
  683. kern = font.get_kern_dist_from_name(last_name, name)
  684. last_name = name
  685. thisx += kern * scale
  686. stream.append((ps_name, thisx, name))
  687. thisx += width * scale
  688. else:
  689. font = self._get_font_ttf(prop)
  690. self._character_tracker.track(font, s)
  691. for item in _text_helpers.layout(s, font):
  692. ps_name = (item.ft_object.postscript_name
  693. .encode("ascii", "replace").decode("ascii"))
  694. glyph_name = item.ft_object.get_glyph_name(item.glyph_idx)
  695. stream.append((ps_name, item.x, glyph_name))
  696. self.set_color(*gc.get_rgb())
  697. for ps_name, group in itertools. \
  698. groupby(stream, lambda entry: entry[0]):
  699. self.set_font(ps_name, prop.get_size_in_points(), False)
  700. thetext = "\n".join(f"{x:g} 0 m /{name:s} glyphshow"
  701. for _, x, name in group)
  702. self._pswriter.write(f"""\
  703. gsave
  704. {self._get_clip_cmd(gc)}
  705. {x:g} {y:g} translate
  706. {angle:g} rotate
  707. {thetext}
  708. grestore
  709. """)
  710. @_log_if_debug_on
  711. def draw_mathtext(self, gc, x, y, s, prop, angle):
  712. """Draw the math text using matplotlib.mathtext."""
  713. width, height, descent, glyphs, rects = \
  714. self._text2path.mathtext_parser.parse(s, 72, prop)
  715. self.set_color(*gc.get_rgb())
  716. self._pswriter.write(
  717. f"gsave\n"
  718. f"{x:g} {y:g} translate\n"
  719. f"{angle:g} rotate\n")
  720. lastfont = None
  721. for font, fontsize, num, ox, oy in glyphs:
  722. self._character_tracker.track_glyph(font, num)
  723. if (font.postscript_name, fontsize) != lastfont:
  724. lastfont = font.postscript_name, fontsize
  725. self._pswriter.write(
  726. f"/{font.postscript_name} {fontsize} selectfont\n")
  727. glyph_name = (
  728. font.get_name_char(chr(num)) if isinstance(font, AFM) else
  729. font.get_glyph_name(font.get_char_index(num)))
  730. self._pswriter.write(
  731. f"{ox:g} {oy:g} moveto\n"
  732. f"/{glyph_name} glyphshow\n")
  733. for ox, oy, w, h in rects:
  734. self._pswriter.write(f"{ox} {oy} {w} {h} rectfill\n")
  735. self._pswriter.write("grestore\n")
  736. @_log_if_debug_on
  737. def draw_gouraud_triangles(self, gc, points, colors, trans):
  738. assert len(points) == len(colors)
  739. if len(points) == 0:
  740. return
  741. assert points.ndim == 3
  742. assert points.shape[1] == 3
  743. assert points.shape[2] == 2
  744. assert colors.ndim == 3
  745. assert colors.shape[1] == 3
  746. assert colors.shape[2] == 4
  747. shape = points.shape
  748. flat_points = points.reshape((shape[0] * shape[1], 2))
  749. flat_points = trans.transform(flat_points)
  750. flat_colors = colors.reshape((shape[0] * shape[1], 4))
  751. points_min = np.min(flat_points, axis=0) - (1 << 12)
  752. points_max = np.max(flat_points, axis=0) + (1 << 12)
  753. factor = np.ceil((2 ** 32 - 1) / (points_max - points_min))
  754. xmin, ymin = points_min
  755. xmax, ymax = points_max
  756. data = np.empty(
  757. shape[0] * shape[1],
  758. dtype=[('flags', 'u1'), ('points', '2>u4'), ('colors', '3u1')])
  759. data['flags'] = 0
  760. data['points'] = (flat_points - points_min) * factor
  761. data['colors'] = flat_colors[:, :3] * 255.0
  762. hexdata = data.tobytes().hex("\n", -64) # Linewrap to 128 chars.
  763. self._pswriter.write(f"""\
  764. gsave
  765. << /ShadingType 4
  766. /ColorSpace [/DeviceRGB]
  767. /BitsPerCoordinate 32
  768. /BitsPerComponent 8
  769. /BitsPerFlag 8
  770. /AntiAlias true
  771. /Decode [ {xmin:g} {xmax:g} {ymin:g} {ymax:g} 0 1 0 1 0 1 ]
  772. /DataSource <
  773. {hexdata}
  774. >
  775. >>
  776. shfill
  777. grestore
  778. """)
  779. def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True):
  780. """
  781. Emit the PostScript snippet *ps* with all the attributes from *gc*
  782. applied. *ps* must consist of PostScript commands to construct a path.
  783. The *fill* and/or *stroke* kwargs can be set to False if the *ps*
  784. string already includes filling and/or stroking, in which case
  785. `_draw_ps` is just supplying properties and clipping.
  786. """
  787. write = self._pswriter.write
  788. mightstroke = (gc.get_linewidth() > 0
  789. and not self._is_transparent(gc.get_rgb()))
  790. if not mightstroke:
  791. stroke = False
  792. if self._is_transparent(rgbFace):
  793. fill = False
  794. hatch = gc.get_hatch()
  795. if mightstroke:
  796. self.set_linewidth(gc.get_linewidth())
  797. self.set_linejoin(gc.get_joinstyle())
  798. self.set_linecap(gc.get_capstyle())
  799. self.set_linedash(*gc.get_dashes())
  800. if mightstroke or hatch:
  801. self.set_color(*gc.get_rgb()[:3])
  802. write('gsave\n')
  803. write(self._get_clip_cmd(gc))
  804. write(ps.strip())
  805. write("\n")
  806. if fill:
  807. if stroke or hatch:
  808. write("gsave\n")
  809. self.set_color(*rgbFace[:3], store=False)
  810. write("fill\n")
  811. if stroke or hatch:
  812. write("grestore\n")
  813. if hatch:
  814. hatch_name = self.create_hatch(hatch, gc.get_hatch_linewidth())
  815. write("gsave\n")
  816. write(_nums_to_str(*gc.get_hatch_color()[:3]))
  817. write(f" {hatch_name} setpattern fill grestore\n")
  818. if stroke:
  819. write("stroke\n")
  820. write("grestore\n")
  821. class _Orientation(Enum):
  822. portrait, landscape = range(2)
  823. def swap_if_landscape(self, shape):
  824. return shape[::-1] if self.name == "landscape" else shape
  825. class FigureCanvasPS(FigureCanvasBase):
  826. fixed_dpi = 72
  827. filetypes = {'ps': 'Postscript',
  828. 'eps': 'Encapsulated Postscript'}
  829. def get_default_filetype(self):
  830. return 'ps'
  831. def _print_ps(
  832. self, fmt, outfile, *,
  833. metadata=None, papertype=None, orientation='portrait',
  834. bbox_inches_restore=None, **kwargs):
  835. dpi = self.figure.dpi
  836. self.figure.dpi = 72 # Override the dpi kwarg
  837. dsc_comments = {}
  838. if isinstance(outfile, (str, os.PathLike)):
  839. filename = pathlib.Path(outfile).name
  840. dsc_comments["Title"] = \
  841. filename.encode("ascii", "replace").decode("ascii")
  842. dsc_comments["Creator"] = (metadata or {}).get(
  843. "Creator",
  844. f"Matplotlib v{mpl.__version__}, https://matplotlib.org/")
  845. # See https://reproducible-builds.org/specs/source-date-epoch/
  846. source_date_epoch = os.getenv("SOURCE_DATE_EPOCH")
  847. dsc_comments["CreationDate"] = (
  848. datetime.datetime.fromtimestamp(
  849. int(source_date_epoch),
  850. datetime.timezone.utc).strftime("%a %b %d %H:%M:%S %Y")
  851. if source_date_epoch
  852. else time.ctime())
  853. dsc_comments = "\n".join(
  854. f"%%{k}: {v}" for k, v in dsc_comments.items())
  855. if papertype is None:
  856. papertype = mpl.rcParams['ps.papersize']
  857. papertype = papertype.lower()
  858. _api.check_in_list(['figure', *papersize], papertype=papertype)
  859. orientation = _api.check_getitem(
  860. _Orientation, orientation=orientation.lower())
  861. printer = (self._print_figure_tex
  862. if mpl.rcParams['text.usetex'] else
  863. self._print_figure)
  864. printer(fmt, outfile, dpi=dpi, dsc_comments=dsc_comments,
  865. orientation=orientation, papertype=papertype,
  866. bbox_inches_restore=bbox_inches_restore, **kwargs)
  867. def _print_figure(
  868. self, fmt, outfile, *,
  869. dpi, dsc_comments, orientation, papertype,
  870. bbox_inches_restore=None):
  871. """
  872. Render the figure to a filesystem path or a file-like object.
  873. Parameters are as for `.print_figure`, except that *dsc_comments* is a
  874. string containing Document Structuring Convention comments,
  875. generated from the *metadata* parameter to `.print_figure`.
  876. """
  877. is_eps = fmt == 'eps'
  878. if not (isinstance(outfile, (str, os.PathLike))
  879. or is_writable_file_like(outfile)):
  880. raise ValueError("outfile must be a path or a file-like object")
  881. # find the appropriate papertype
  882. width, height = self.figure.get_size_inches()
  883. if is_eps or papertype == 'figure':
  884. paper_width, paper_height = width, height
  885. else:
  886. paper_width, paper_height = orientation.swap_if_landscape(
  887. papersize[papertype])
  888. # center the figure on the paper
  889. xo = 72 * 0.5 * (paper_width - width)
  890. yo = 72 * 0.5 * (paper_height - height)
  891. llx = xo
  892. lly = yo
  893. urx = llx + self.figure.bbox.width
  894. ury = lly + self.figure.bbox.height
  895. rotation = 0
  896. if orientation is _Orientation.landscape:
  897. llx, lly, urx, ury = lly, llx, ury, urx
  898. xo, yo = 72 * paper_height - yo, xo
  899. rotation = 90
  900. bbox = (llx, lly, urx, ury)
  901. self._pswriter = StringIO()
  902. # mixed mode rendering
  903. ps_renderer = RendererPS(width, height, self._pswriter, imagedpi=dpi)
  904. renderer = MixedModeRenderer(
  905. self.figure, width, height, dpi, ps_renderer,
  906. bbox_inches_restore=bbox_inches_restore)
  907. self.figure.draw(renderer)
  908. def print_figure_impl(fh):
  909. # write the PostScript headers
  910. if is_eps:
  911. print("%!PS-Adobe-3.0 EPSF-3.0", file=fh)
  912. else:
  913. print("%!PS-Adobe-3.0", file=fh)
  914. if papertype != 'figure':
  915. print(f"%%DocumentPaperSizes: {papertype}", file=fh)
  916. print("%%Pages: 1", file=fh)
  917. print(f"%%LanguageLevel: 3\n"
  918. f"{dsc_comments}\n"
  919. f"%%Orientation: {orientation.name}\n"
  920. f"{_get_bbox_header(bbox)}\n"
  921. f"%%EndComments\n",
  922. end="", file=fh)
  923. Ndict = len(_psDefs)
  924. print("%%BeginProlog", file=fh)
  925. if not mpl.rcParams['ps.useafm']:
  926. Ndict += len(ps_renderer._character_tracker.used)
  927. print("/mpldict %d dict def" % Ndict, file=fh)
  928. print("mpldict begin", file=fh)
  929. print("\n".join(_psDefs), file=fh)
  930. if not mpl.rcParams['ps.useafm']:
  931. for font_path, chars \
  932. in ps_renderer._character_tracker.used.items():
  933. if not chars:
  934. continue
  935. fonttype = mpl.rcParams['ps.fonttype']
  936. # Can't use more than 255 chars from a single Type 3 font.
  937. if len(chars) > 255:
  938. fonttype = 42
  939. fh.flush()
  940. if fonttype == 3:
  941. fh.write(_font_to_ps_type3(font_path, chars))
  942. else: # Type 42 only.
  943. _font_to_ps_type42(font_path, chars, fh)
  944. print("end", file=fh)
  945. print("%%EndProlog", file=fh)
  946. if not is_eps:
  947. print("%%Page: 1 1", file=fh)
  948. print("mpldict begin", file=fh)
  949. print("%s translate" % _nums_to_str(xo, yo), file=fh)
  950. if rotation:
  951. print("%d rotate" % rotation, file=fh)
  952. print(f"0 0 {_nums_to_str(width*72, height*72)} rectclip", file=fh)
  953. # write the figure
  954. print(self._pswriter.getvalue(), file=fh)
  955. # write the trailer
  956. print("end", file=fh)
  957. print("showpage", file=fh)
  958. if not is_eps:
  959. print("%%EOF", file=fh)
  960. fh.flush()
  961. if mpl.rcParams['ps.usedistiller']:
  962. # We are going to use an external program to process the output.
  963. # Write to a temporary file.
  964. with TemporaryDirectory() as tmpdir:
  965. tmpfile = os.path.join(tmpdir, "tmp.ps")
  966. with open(tmpfile, 'w', encoding='latin-1') as fh:
  967. print_figure_impl(fh)
  968. if mpl.rcParams['ps.usedistiller'] == 'ghostscript':
  969. _try_distill(gs_distill,
  970. tmpfile, is_eps, ptype=papertype, bbox=bbox)
  971. elif mpl.rcParams['ps.usedistiller'] == 'xpdf':
  972. _try_distill(xpdf_distill,
  973. tmpfile, is_eps, ptype=papertype, bbox=bbox)
  974. _move_path_to_path_or_stream(tmpfile, outfile)
  975. else: # Write directly to outfile.
  976. with cbook.open_file_cm(outfile, "w", encoding="latin-1") as file:
  977. if not file_requires_unicode(file):
  978. file = codecs.getwriter("latin-1")(file)
  979. print_figure_impl(file)
  980. def _print_figure_tex(
  981. self, fmt, outfile, *,
  982. dpi, dsc_comments, orientation, papertype,
  983. bbox_inches_restore=None):
  984. """
  985. If :rc:`text.usetex` is True, a temporary pair of tex/eps files
  986. are created to allow tex to manage the text layout via the PSFrags
  987. package. These files are processed to yield the final ps or eps file.
  988. The rest of the behavior is as for `._print_figure`.
  989. """
  990. is_eps = fmt == 'eps'
  991. width, height = self.figure.get_size_inches()
  992. xo = 0
  993. yo = 0
  994. llx = xo
  995. lly = yo
  996. urx = llx + self.figure.bbox.width
  997. ury = lly + self.figure.bbox.height
  998. bbox = (llx, lly, urx, ury)
  999. self._pswriter = StringIO()
  1000. # mixed mode rendering
  1001. ps_renderer = RendererPS(width, height, self._pswriter, imagedpi=dpi)
  1002. renderer = MixedModeRenderer(self.figure,
  1003. width, height, dpi, ps_renderer,
  1004. bbox_inches_restore=bbox_inches_restore)
  1005. self.figure.draw(renderer)
  1006. # write to a temp file, we'll move it to outfile when done
  1007. with TemporaryDirectory() as tmpdir:
  1008. tmppath = pathlib.Path(tmpdir, "tmp.ps")
  1009. tmppath.write_text(
  1010. f"""\
  1011. %!PS-Adobe-3.0 EPSF-3.0
  1012. %%LanguageLevel: 3
  1013. {dsc_comments}
  1014. {_get_bbox_header(bbox)}
  1015. %%EndComments
  1016. %%BeginProlog
  1017. /mpldict {len(_psDefs)} dict def
  1018. mpldict begin
  1019. {"".join(_psDefs)}
  1020. end
  1021. %%EndProlog
  1022. mpldict begin
  1023. {_nums_to_str(xo, yo)} translate
  1024. 0 0 {_nums_to_str(width*72, height*72)} rectclip
  1025. {self._pswriter.getvalue()}
  1026. end
  1027. showpage
  1028. """,
  1029. encoding="latin-1")
  1030. if orientation is _Orientation.landscape: # now, ready to rotate
  1031. width, height = height, width
  1032. bbox = (lly, llx, ury, urx)
  1033. # set the paper size to the figure size if is_eps. The
  1034. # resulting ps file has the given size with correct bounding
  1035. # box so that there is no need to call 'pstoeps'
  1036. if is_eps or papertype == 'figure':
  1037. paper_width, paper_height = orientation.swap_if_landscape(
  1038. self.figure.get_size_inches())
  1039. else:
  1040. paper_width, paper_height = papersize[papertype]
  1041. psfrag_rotated = _convert_psfrags(
  1042. tmppath, ps_renderer.psfrag, paper_width, paper_height,
  1043. orientation.name)
  1044. if (mpl.rcParams['ps.usedistiller'] == 'ghostscript'
  1045. or mpl.rcParams['text.usetex']):
  1046. _try_distill(gs_distill,
  1047. tmppath, is_eps, ptype=papertype, bbox=bbox,
  1048. rotated=psfrag_rotated)
  1049. elif mpl.rcParams['ps.usedistiller'] == 'xpdf':
  1050. _try_distill(xpdf_distill,
  1051. tmppath, is_eps, ptype=papertype, bbox=bbox,
  1052. rotated=psfrag_rotated)
  1053. _move_path_to_path_or_stream(tmppath, outfile)
  1054. print_ps = functools.partialmethod(_print_ps, "ps")
  1055. print_eps = functools.partialmethod(_print_ps, "eps")
  1056. def draw(self):
  1057. self.figure.draw_without_rendering()
  1058. return super().draw()
  1059. def _convert_psfrags(tmppath, psfrags, paper_width, paper_height, orientation):
  1060. """
  1061. When we want to use the LaTeX backend with postscript, we write PSFrag tags
  1062. to a temporary postscript file, each one marking a position for LaTeX to
  1063. render some text. convert_psfrags generates a LaTeX document containing the
  1064. commands to convert those tags to text. LaTeX/dvips produces the postscript
  1065. file that includes the actual text.
  1066. """
  1067. with mpl.rc_context({
  1068. "text.latex.preamble":
  1069. mpl.rcParams["text.latex.preamble"] +
  1070. mpl.texmanager._usepackage_if_not_loaded("color") +
  1071. mpl.texmanager._usepackage_if_not_loaded("graphicx") +
  1072. mpl.texmanager._usepackage_if_not_loaded("psfrag") +
  1073. r"\geometry{papersize={%(width)sin,%(height)sin},margin=0in}"
  1074. % {"width": paper_width, "height": paper_height}
  1075. }):
  1076. dvifile = TexManager().make_dvi(
  1077. "\n"
  1078. r"\begin{figure}""\n"
  1079. r" \centering\leavevmode""\n"
  1080. r" %(psfrags)s""\n"
  1081. r" \includegraphics*[angle=%(angle)s]{%(epsfile)s}""\n"
  1082. r"\end{figure}"
  1083. % {
  1084. "psfrags": "\n".join(psfrags),
  1085. "angle": 90 if orientation == 'landscape' else 0,
  1086. "epsfile": tmppath.resolve().as_posix(),
  1087. },
  1088. fontsize=10) # tex's default fontsize.
  1089. with TemporaryDirectory() as tmpdir:
  1090. psfile = os.path.join(tmpdir, "tmp.ps")
  1091. cbook._check_and_log_subprocess(
  1092. ['dvips', '-q', '-R0', '-o', psfile, dvifile], _log)
  1093. shutil.move(psfile, tmppath)
  1094. # check if the dvips created a ps in landscape paper. Somehow,
  1095. # above latex+dvips results in a ps file in a landscape mode for a
  1096. # certain figure sizes (e.g., 8.3in, 5.8in which is a5). And the
  1097. # bounding box of the final output got messed up. We check see if
  1098. # the generated ps file is in landscape and return this
  1099. # information. The return value is used in pstoeps step to recover
  1100. # the correct bounding box. 2010-06-05 JJL
  1101. with open(tmppath) as fh:
  1102. psfrag_rotated = "Landscape" in fh.read(1000)
  1103. return psfrag_rotated
  1104. def _try_distill(func, tmppath, *args, **kwargs):
  1105. try:
  1106. func(str(tmppath), *args, **kwargs)
  1107. except mpl.ExecutableNotFoundError as exc:
  1108. _log.warning("%s. Distillation step skipped.", exc)
  1109. def gs_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False):
  1110. """
  1111. Use ghostscript's pswrite or epswrite device to distill a file.
  1112. This yields smaller files without illegal encapsulated postscript
  1113. operators. The output is low-level, converting text to outlines.
  1114. """
  1115. if eps:
  1116. paper_option = ["-dEPSCrop"]
  1117. elif ptype == "figure":
  1118. # The bbox will have its lower-left corner at (0, 0), so upper-right
  1119. # corner corresponds with paper size.
  1120. paper_option = [f"-dDEVICEWIDTHPOINTS={bbox[2]}",
  1121. f"-dDEVICEHEIGHTPOINTS={bbox[3]}"]
  1122. else:
  1123. paper_option = [f"-sPAPERSIZE={ptype}"]
  1124. psfile = tmpfile + '.ps'
  1125. dpi = mpl.rcParams['ps.distiller.res']
  1126. cbook._check_and_log_subprocess(
  1127. [mpl._get_executable_info("gs").executable,
  1128. "-dBATCH", "-dNOPAUSE", "-r%d" % dpi, "-sDEVICE=ps2write",
  1129. *paper_option, f"-sOutputFile={psfile}", tmpfile],
  1130. _log)
  1131. os.remove(tmpfile)
  1132. shutil.move(psfile, tmpfile)
  1133. # While it is best if above steps preserve the original bounding
  1134. # box, there seem to be cases when it is not. For those cases,
  1135. # the original bbox can be restored during the pstoeps step.
  1136. if eps:
  1137. # For some versions of gs, above steps result in a ps file where the
  1138. # original bbox is no more correct. Do not adjust bbox for now.
  1139. pstoeps(tmpfile, bbox, rotated=rotated)
  1140. def xpdf_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False):
  1141. """
  1142. Use ghostscript's ps2pdf and xpdf's/poppler's pdftops to distill a file.
  1143. This yields smaller files without illegal encapsulated postscript
  1144. operators. This distiller is preferred, generating high-level postscript
  1145. output that treats text as text.
  1146. """
  1147. mpl._get_executable_info("gs") # Effectively checks for ps2pdf.
  1148. mpl._get_executable_info("pdftops")
  1149. if eps:
  1150. paper_option = ["-dEPSCrop"]
  1151. elif ptype == "figure":
  1152. # The bbox will have its lower-left corner at (0, 0), so upper-right
  1153. # corner corresponds with paper size.
  1154. paper_option = [f"-dDEVICEWIDTHPOINTS#{bbox[2]}",
  1155. f"-dDEVICEHEIGHTPOINTS#{bbox[3]}"]
  1156. else:
  1157. paper_option = [f"-sPAPERSIZE#{ptype}"]
  1158. with TemporaryDirectory() as tmpdir:
  1159. tmppdf = pathlib.Path(tmpdir, "tmp.pdf")
  1160. tmpps = pathlib.Path(tmpdir, "tmp.ps")
  1161. # Pass options as `-foo#bar` instead of `-foo=bar` to keep Windows
  1162. # happy (https://ghostscript.com/doc/9.56.1/Use.htm#MS_Windows).
  1163. cbook._check_and_log_subprocess(
  1164. ["ps2pdf",
  1165. "-dAutoFilterColorImages#false",
  1166. "-dAutoFilterGrayImages#false",
  1167. "-sAutoRotatePages#None",
  1168. "-sGrayImageFilter#FlateEncode",
  1169. "-sColorImageFilter#FlateEncode",
  1170. *paper_option,
  1171. tmpfile, tmppdf], _log)
  1172. cbook._check_and_log_subprocess(
  1173. ["pdftops", "-paper", "match", "-level3", tmppdf, tmpps], _log)
  1174. shutil.move(tmpps, tmpfile)
  1175. if eps:
  1176. pstoeps(tmpfile)
  1177. @_api.deprecated("3.9")
  1178. def get_bbox_header(lbrt, rotated=False):
  1179. """
  1180. Return a postscript header string for the given bbox lbrt=(l, b, r, t).
  1181. Optionally, return rotate command.
  1182. """
  1183. return _get_bbox_header(lbrt), (_get_rotate_command(lbrt) if rotated else "")
  1184. def _get_bbox_header(lbrt):
  1185. """Return a PostScript header string for bounding box *lbrt*=(l, b, r, t)."""
  1186. l, b, r, t = lbrt
  1187. return (f"%%BoundingBox: {int(l)} {int(b)} {math.ceil(r)} {math.ceil(t)}\n"
  1188. f"%%HiResBoundingBox: {l:.6f} {b:.6f} {r:.6f} {t:.6f}")
  1189. def _get_rotate_command(lbrt):
  1190. """Return a PostScript 90° rotation command for bounding box *lbrt*=(l, b, r, t)."""
  1191. l, b, r, t = lbrt
  1192. return f"{l+r:.2f} {0:.2f} translate\n90 rotate"
  1193. def pstoeps(tmpfile, bbox=None, rotated=False):
  1194. """
  1195. Convert the postscript to encapsulated postscript. The bbox of
  1196. the eps file will be replaced with the given *bbox* argument. If
  1197. None, original bbox will be used.
  1198. """
  1199. epsfile = tmpfile + '.eps'
  1200. with open(epsfile, 'wb') as epsh, open(tmpfile, 'rb') as tmph:
  1201. write = epsh.write
  1202. # Modify the header:
  1203. for line in tmph:
  1204. if line.startswith(b'%!PS'):
  1205. write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
  1206. if bbox:
  1207. write(_get_bbox_header(bbox).encode('ascii') + b'\n')
  1208. elif line.startswith(b'%%EndComments'):
  1209. write(line)
  1210. write(b'%%BeginProlog\n'
  1211. b'save\n'
  1212. b'countdictstack\n'
  1213. b'mark\n'
  1214. b'newpath\n'
  1215. b'/showpage {} def\n'
  1216. b'/setpagedevice {pop} def\n'
  1217. b'%%EndProlog\n'
  1218. b'%%Page 1 1\n')
  1219. if rotated: # The output eps file need to be rotated.
  1220. write(_get_rotate_command(bbox).encode('ascii') + b'\n')
  1221. break
  1222. elif bbox and line.startswith((b'%%Bound', b'%%HiResBound',
  1223. b'%%DocumentMedia', b'%%Pages')):
  1224. pass
  1225. else:
  1226. write(line)
  1227. # Now rewrite the rest of the file, and modify the trailer.
  1228. # This is done in a second loop such that the header of the embedded
  1229. # eps file is not modified.
  1230. for line in tmph:
  1231. if line.startswith(b'%%EOF'):
  1232. write(b'cleartomark\n'
  1233. b'countdictstack\n'
  1234. b'exch sub { end } repeat\n'
  1235. b'restore\n'
  1236. b'showpage\n'
  1237. b'%%EOF\n')
  1238. elif line.startswith(b'%%PageBoundingBox'):
  1239. pass
  1240. else:
  1241. write(line)
  1242. os.remove(tmpfile)
  1243. shutil.move(epsfile, tmpfile)
  1244. FigureManagerPS = FigureManagerBase
  1245. # The following Python dictionary psDefs contains the entries for the
  1246. # PostScript dictionary mpldict. This dictionary implements most of
  1247. # the matplotlib primitives and some abbreviations.
  1248. #
  1249. # References:
  1250. # https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf
  1251. # http://preserve.mactech.com/articles/mactech/Vol.09/09.04/PostscriptTutorial
  1252. # http://www.math.ubc.ca/people/faculty/cass/graphics/text/www/
  1253. #
  1254. # The usage comments use the notation of the operator summary
  1255. # in the PostScript Language reference manual.
  1256. _psDefs = [
  1257. # name proc *_d* -
  1258. # Note that this cannot be bound to /d, because when embedding a Type3 font
  1259. # we may want to define a "d" glyph using "/d{...} d" which would locally
  1260. # overwrite the definition.
  1261. "/_d { bind def } bind def",
  1262. # x y *m* -
  1263. "/m { moveto } _d",
  1264. # x y *l* -
  1265. "/l { lineto } _d",
  1266. # x y *r* -
  1267. "/r { rlineto } _d",
  1268. # x1 y1 x2 y2 x y *c* -
  1269. "/c { curveto } _d",
  1270. # *cl* -
  1271. "/cl { closepath } _d",
  1272. # *ce* -
  1273. "/ce { closepath eofill } _d",
  1274. # wx wy llx lly urx ury *setcachedevice* -
  1275. "/sc { setcachedevice } _d",
  1276. ]
  1277. @_Backend.export
  1278. class _BackendPS(_Backend):
  1279. backend_version = 'Level II'
  1280. FigureCanvas = FigureCanvasPS