backend_pdf.py 95 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609
  1. """
  2. A PDF matplotlib backend
  3. Author: Jouni K Seppänen <jks@iki.fi>
  4. """
  5. import codecs
  6. import collections
  7. from datetime import datetime
  8. from functools import total_ordering
  9. from io import BytesIO
  10. import itertools
  11. import logging
  12. import math
  13. import os
  14. import re
  15. import struct
  16. import time
  17. import types
  18. import warnings
  19. import zlib
  20. import numpy as np
  21. from PIL import Image
  22. import matplotlib as mpl
  23. from matplotlib import _text_layout, cbook
  24. from matplotlib._pylab_helpers import Gcf
  25. from matplotlib.backend_bases import (
  26. _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
  27. GraphicsContextBase, RendererBase)
  28. from matplotlib.backends.backend_mixed import MixedModeRenderer
  29. from matplotlib.figure import Figure
  30. from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font
  31. from matplotlib.afm import AFM
  32. import matplotlib.type1font as type1font
  33. import matplotlib.dviread as dviread
  34. from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE,
  35. LOAD_NO_HINTING, KERNING_UNFITTED)
  36. from matplotlib.mathtext import MathTextParser
  37. from matplotlib.transforms import Affine2D, BboxBase
  38. from matplotlib.path import Path
  39. from matplotlib.dates import UTC
  40. from matplotlib import _path
  41. from matplotlib import _ttconv
  42. from . import _backend_pdf_ps
  43. _log = logging.getLogger(__name__)
  44. # Overview
  45. #
  46. # The low-level knowledge about pdf syntax lies mainly in the pdfRepr
  47. # function and the classes Reference, Name, Operator, and Stream. The
  48. # PdfFile class knows about the overall structure of pdf documents.
  49. # It provides a "write" method for writing arbitrary strings in the
  50. # file, and an "output" method that passes objects through the pdfRepr
  51. # function before writing them in the file. The output method is
  52. # called by the RendererPdf class, which contains the various draw_foo
  53. # methods. RendererPdf contains a GraphicsContextPdf instance, and
  54. # each draw_foo calls self.check_gc before outputting commands. This
  55. # method checks whether the pdf graphics state needs to be modified
  56. # and outputs the necessary commands. GraphicsContextPdf represents
  57. # the graphics state, and its "delta" method returns the commands that
  58. # modify the state.
  59. # Add "pdf.use14corefonts: True" in your configuration file to use only
  60. # the 14 PDF core fonts. These fonts do not need to be embedded; every
  61. # PDF viewing application is required to have them. This results in very
  62. # light PDF files you can use directly in LaTeX or ConTeXt documents
  63. # generated with pdfTeX, without any conversion.
  64. # These fonts are: Helvetica, Helvetica-Bold, Helvetica-Oblique,
  65. # Helvetica-BoldOblique, Courier, Courier-Bold, Courier-Oblique,
  66. # Courier-BoldOblique, Times-Roman, Times-Bold, Times-Italic,
  67. # Times-BoldItalic, Symbol, ZapfDingbats.
  68. #
  69. # Some tricky points:
  70. #
  71. # 1. The clip path can only be widened by popping from the state
  72. # stack. Thus the state must be pushed onto the stack before narrowing
  73. # the clip path. This is taken care of by GraphicsContextPdf.
  74. #
  75. # 2. Sometimes it is necessary to refer to something (e.g., font,
  76. # image, or extended graphics state, which contains the alpha value)
  77. # in the page stream by a name that needs to be defined outside the
  78. # stream. PdfFile provides the methods fontName, imageObject, and
  79. # alphaState for this purpose. The implementations of these methods
  80. # should perhaps be generalized.
  81. # TODOs:
  82. #
  83. # * encoding of fonts, including mathtext fonts and unicode support
  84. # * TTF support has lots of small TODOs, e.g., how do you know if a font
  85. # is serif/sans-serif, or symbolic/non-symbolic?
  86. # * draw_quad_mesh
  87. def fill(strings, linelen=75):
  88. """
  89. Make one string from sequence of strings, with whitespace in between.
  90. The whitespace is chosen to form lines of at most *linelen* characters,
  91. if possible.
  92. """
  93. currpos = 0
  94. lasti = 0
  95. result = []
  96. for i, s in enumerate(strings):
  97. length = len(s)
  98. if currpos + length < linelen:
  99. currpos += length + 1
  100. else:
  101. result.append(b' '.join(strings[lasti:i]))
  102. lasti = i
  103. currpos = length
  104. result.append(b' '.join(strings[lasti:]))
  105. return b'\n'.join(result)
  106. # PDF strings are supposed to be able to include any eight-bit data,
  107. # except that unbalanced parens and backslashes must be escaped by a
  108. # backslash. However, sf bug #2708559 shows that the carriage return
  109. # character may get read as a newline; these characters correspond to
  110. # \gamma and \Omega in TeX's math font encoding. Escaping them fixes
  111. # the bug.
  112. _string_escape_regex = re.compile(br'([\\()\r\n])')
  113. def _string_escape(match):
  114. m = match.group(0)
  115. if m in br'\()':
  116. return b'\\' + m
  117. elif m == b'\n':
  118. return br'\n'
  119. elif m == b'\r':
  120. return br'\r'
  121. assert False
  122. def _create_pdf_info_dict(backend, metadata):
  123. """
  124. Create a PDF infoDict based on user-supplied metadata.
  125. A default ``Creator``, ``Producer``, and ``CreationDate`` are added, though
  126. the user metadata may override it. The date may be the current time, or a
  127. time set by the ``SOURCE_DATE_EPOCH`` environment variable.
  128. Metadata is verified to have the correct keys and their expected types. Any
  129. unknown keys/types will raise a warning.
  130. Parameters
  131. ----------
  132. backend : str
  133. The name of the backend to use in the Producer value.
  134. metadata : Dict[str, Union[str, datetime, Name]]
  135. A dictionary of metadata supplied by the user with information
  136. following the PDF specification, also defined in
  137. `~.backend_pdf.PdfPages` below.
  138. If any value is *None*, then the key will be removed. This can be used
  139. to remove any pre-defined values.
  140. Returns
  141. -------
  142. Dict[str, Union[str, datetime, Name]]
  143. A validated dictionary of metadata.
  144. """
  145. # get source date from SOURCE_DATE_EPOCH, if set
  146. # See https://reproducible-builds.org/specs/source-date-epoch/
  147. source_date_epoch = os.getenv("SOURCE_DATE_EPOCH")
  148. if source_date_epoch:
  149. source_date = datetime.utcfromtimestamp(int(source_date_epoch))
  150. source_date = source_date.replace(tzinfo=UTC)
  151. else:
  152. source_date = datetime.today()
  153. info = {
  154. 'Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org',
  155. 'Producer': f'Matplotlib {backend} backend v{mpl.__version__}',
  156. 'CreationDate': source_date,
  157. **metadata
  158. }
  159. info = {k: v for (k, v) in info.items() if v is not None}
  160. def is_string_like(x):
  161. return isinstance(x, str)
  162. def is_date(x):
  163. return isinstance(x, datetime)
  164. def check_trapped(x):
  165. if isinstance(x, Name):
  166. return x.name in (b'True', b'False', b'Unknown')
  167. else:
  168. return x in ('True', 'False', 'Unknown')
  169. keywords = {
  170. 'Title': is_string_like,
  171. 'Author': is_string_like,
  172. 'Subject': is_string_like,
  173. 'Keywords': is_string_like,
  174. 'Creator': is_string_like,
  175. 'Producer': is_string_like,
  176. 'CreationDate': is_date,
  177. 'ModDate': is_date,
  178. 'Trapped': check_trapped,
  179. }
  180. for k in info:
  181. if k not in keywords:
  182. cbook._warn_external(f'Unknown infodict keyword: {k}')
  183. elif not keywords[k](info[k]):
  184. cbook._warn_external(f'Bad value for infodict keyword {k}')
  185. if 'Trapped' in info:
  186. info['Trapped'] = Name(info['Trapped'])
  187. return info
  188. def _datetime_to_pdf(d):
  189. """
  190. Convert a datetime to a PDF string representing it.
  191. Used for PDF and PGF.
  192. """
  193. r = d.strftime('D:%Y%m%d%H%M%S')
  194. z = d.utcoffset()
  195. if z is not None:
  196. z = z.seconds
  197. else:
  198. if time.daylight:
  199. z = time.altzone
  200. else:
  201. z = time.timezone
  202. if z == 0:
  203. r += 'Z'
  204. elif z < 0:
  205. r += "+%02d'%02d'" % ((-z) // 3600, (-z) % 3600)
  206. else:
  207. r += "-%02d'%02d'" % (z // 3600, z % 3600)
  208. return r
  209. def pdfRepr(obj):
  210. """Map Python objects to PDF syntax."""
  211. # Some objects defined later have their own pdfRepr method.
  212. if hasattr(obj, 'pdfRepr'):
  213. return obj.pdfRepr()
  214. # Floats. PDF does not have exponential notation (1.0e-10) so we
  215. # need to use %f with some precision. Perhaps the precision
  216. # should adapt to the magnitude of the number?
  217. elif isinstance(obj, (float, np.floating)):
  218. if not np.isfinite(obj):
  219. raise ValueError("Can only output finite numbers in PDF")
  220. r = b"%.10f" % obj
  221. return r.rstrip(b'0').rstrip(b'.')
  222. # Booleans. Needs to be tested before integers since
  223. # isinstance(True, int) is true.
  224. elif isinstance(obj, bool):
  225. return [b'false', b'true'][obj]
  226. # Integers are written as such.
  227. elif isinstance(obj, (int, np.integer)):
  228. return b"%d" % obj
  229. # Unicode strings are encoded in UTF-16BE with byte-order mark.
  230. elif isinstance(obj, str):
  231. try:
  232. # But maybe it's really ASCII?
  233. s = obj.encode('ASCII')
  234. return pdfRepr(s)
  235. except UnicodeEncodeError:
  236. s = codecs.BOM_UTF16_BE + obj.encode('UTF-16BE')
  237. return pdfRepr(s)
  238. # Strings are written in parentheses, with backslashes and parens
  239. # escaped. Actually balanced parens are allowed, but it is
  240. # simpler to escape them all. TODO: cut long strings into lines;
  241. # I believe there is some maximum line length in PDF.
  242. elif isinstance(obj, bytes):
  243. return b'(' + _string_escape_regex.sub(_string_escape, obj) + b')'
  244. # Dictionaries. The keys must be PDF names, so if we find strings
  245. # there, we make Name objects from them. The values may be
  246. # anything, so the caller must ensure that PDF names are
  247. # represented as Name objects.
  248. elif isinstance(obj, dict):
  249. return fill([
  250. b"<<",
  251. *[Name(key).pdfRepr() + b" " + pdfRepr(obj[key])
  252. for key in sorted(obj)],
  253. b">>",
  254. ])
  255. # Lists.
  256. elif isinstance(obj, (list, tuple)):
  257. return fill([b"[", *[pdfRepr(val) for val in obj], b"]"])
  258. # The null keyword.
  259. elif obj is None:
  260. return b'null'
  261. # A date.
  262. elif isinstance(obj, datetime):
  263. return pdfRepr(_datetime_to_pdf(obj))
  264. # A bounding box
  265. elif isinstance(obj, BboxBase):
  266. return fill([pdfRepr(val) for val in obj.bounds])
  267. else:
  268. raise TypeError("Don't know a PDF representation for {} objects"
  269. .format(type(obj)))
  270. class Reference:
  271. """
  272. PDF reference object.
  273. Use PdfFile.reserveObject() to create References.
  274. """
  275. def __init__(self, id):
  276. self.id = id
  277. def __repr__(self):
  278. return "<Reference %d>" % self.id
  279. def pdfRepr(self):
  280. return b"%d 0 R" % self.id
  281. def write(self, contents, file):
  282. write = file.write
  283. write(b"%d 0 obj\n" % self.id)
  284. write(pdfRepr(contents))
  285. write(b"\nendobj\n")
  286. @total_ordering
  287. class Name:
  288. """PDF name object."""
  289. __slots__ = ('name',)
  290. _regex = re.compile(r'[^!-~]')
  291. def __init__(self, name):
  292. if isinstance(name, Name):
  293. self.name = name.name
  294. else:
  295. if isinstance(name, bytes):
  296. name = name.decode('ascii')
  297. self.name = self._regex.sub(Name.hexify, name).encode('ascii')
  298. def __repr__(self):
  299. return "<Name %s>" % self.name
  300. def __str__(self):
  301. return '/' + str(self.name)
  302. def __eq__(self, other):
  303. return isinstance(other, Name) and self.name == other.name
  304. def __lt__(self, other):
  305. return isinstance(other, Name) and self.name < other.name
  306. def __hash__(self):
  307. return hash(self.name)
  308. @staticmethod
  309. def hexify(match):
  310. return '#%02x' % ord(match.group())
  311. def pdfRepr(self):
  312. return b'/' + self.name
  313. class Operator:
  314. """PDF operator object."""
  315. __slots__ = ('op',)
  316. def __init__(self, op):
  317. self.op = op
  318. def __repr__(self):
  319. return '<Operator %s>' % self.op
  320. def pdfRepr(self):
  321. return self.op
  322. class Verbatim:
  323. """Store verbatim PDF command content for later inclusion in the stream."""
  324. def __init__(self, x):
  325. self._x = x
  326. def pdfRepr(self):
  327. return self._x
  328. # PDF operators (not an exhaustive list)
  329. _pdfops = dict(
  330. close_fill_stroke=b'b', fill_stroke=b'B', fill=b'f', closepath=b'h',
  331. close_stroke=b's', stroke=b'S', endpath=b'n', begin_text=b'BT',
  332. end_text=b'ET', curveto=b'c', rectangle=b're', lineto=b'l', moveto=b'm',
  333. concat_matrix=b'cm', use_xobject=b'Do', setgray_stroke=b'G',
  334. setgray_nonstroke=b'g', setrgb_stroke=b'RG', setrgb_nonstroke=b'rg',
  335. setcolorspace_stroke=b'CS', setcolorspace_nonstroke=b'cs',
  336. setcolor_stroke=b'SCN', setcolor_nonstroke=b'scn', setdash=b'd',
  337. setlinejoin=b'j', setlinecap=b'J', setgstate=b'gs', gsave=b'q',
  338. grestore=b'Q', textpos=b'Td', selectfont=b'Tf', textmatrix=b'Tm',
  339. show=b'Tj', showkern=b'TJ', setlinewidth=b'w', clip=b'W', shading=b'sh')
  340. Op = types.SimpleNamespace(**{name: Operator(value)
  341. for name, value in _pdfops.items()})
  342. def _paint_path(fill, stroke):
  343. """
  344. Return the PDF operator to paint a path.
  345. Parameters
  346. ----------
  347. fill: bool
  348. Fill the path with the fill color.
  349. stroke: bool
  350. Stroke the outline of the path with the line color.
  351. """
  352. if stroke:
  353. if fill:
  354. return Op.fill_stroke
  355. else:
  356. return Op.stroke
  357. else:
  358. if fill:
  359. return Op.fill
  360. else:
  361. return Op.endpath
  362. Op.paint_path = _paint_path
  363. class Stream:
  364. """
  365. PDF stream object.
  366. This has no pdfRepr method. Instead, call begin(), then output the
  367. contents of the stream by calling write(), and finally call end().
  368. """
  369. __slots__ = ('id', 'len', 'pdfFile', 'file', 'compressobj', 'extra', 'pos')
  370. def __init__(self, id, len, file, extra=None, png=None):
  371. """
  372. Parameters
  373. ----------
  374. id : int
  375. Object id of the stream.
  376. len : Reference or None
  377. An unused Reference object for the length of the stream;
  378. None means to use a memory buffer so the length can be inlined.
  379. file : PdfFile
  380. The underlying object to write the stream to.
  381. extra : dict from Name to anything, or None
  382. Extra key-value pairs to include in the stream header.
  383. png : dict or None
  384. If the data is already png encoded, the decode parameters.
  385. """
  386. self.id = id # object id
  387. self.len = len # id of length object
  388. self.pdfFile = file
  389. self.file = file.fh # file to which the stream is written
  390. self.compressobj = None # compression object
  391. if extra is None:
  392. self.extra = dict()
  393. else:
  394. self.extra = extra.copy()
  395. if png is not None:
  396. self.extra.update({'Filter': Name('FlateDecode'),
  397. 'DecodeParms': png})
  398. self.pdfFile.recordXref(self.id)
  399. if mpl.rcParams['pdf.compression'] and not png:
  400. self.compressobj = zlib.compressobj(
  401. mpl.rcParams['pdf.compression'])
  402. if self.len is None:
  403. self.file = BytesIO()
  404. else:
  405. self._writeHeader()
  406. self.pos = self.file.tell()
  407. def _writeHeader(self):
  408. write = self.file.write
  409. write(b"%d 0 obj\n" % self.id)
  410. dict = self.extra
  411. dict['Length'] = self.len
  412. if mpl.rcParams['pdf.compression']:
  413. dict['Filter'] = Name('FlateDecode')
  414. write(pdfRepr(dict))
  415. write(b"\nstream\n")
  416. def end(self):
  417. """Finalize stream."""
  418. self._flush()
  419. if self.len is None:
  420. contents = self.file.getvalue()
  421. self.len = len(contents)
  422. self.file = self.pdfFile.fh
  423. self._writeHeader()
  424. self.file.write(contents)
  425. self.file.write(b"\nendstream\nendobj\n")
  426. else:
  427. length = self.file.tell() - self.pos
  428. self.file.write(b"\nendstream\nendobj\n")
  429. self.pdfFile.writeObject(self.len, length)
  430. def write(self, data):
  431. """Write some data on the stream."""
  432. if self.compressobj is None:
  433. self.file.write(data)
  434. else:
  435. compressed = self.compressobj.compress(data)
  436. self.file.write(compressed)
  437. def _flush(self):
  438. """Flush the compression object."""
  439. if self.compressobj is not None:
  440. compressed = self.compressobj.flush()
  441. self.file.write(compressed)
  442. self.compressobj = None
  443. class PdfFile:
  444. """PDF file object."""
  445. def __init__(self, filename, metadata=None):
  446. """
  447. Parameters
  448. ----------
  449. filename : str or path-like or file-like
  450. Output target; if a string, a file will be opened for writing.
  451. metadata : dict from strings to strings and dates
  452. Information dictionary object (see PDF reference section 10.2.1
  453. 'Document Information Dictionary'), e.g.:
  454. ``{'Creator': 'My software', 'Author': 'Me', 'Title': 'Awesome'}``.
  455. The standard keys are 'Title', 'Author', 'Subject', 'Keywords',
  456. 'Creator', 'Producer', 'CreationDate', 'ModDate', and
  457. 'Trapped'. Values have been predefined for 'Creator', 'Producer'
  458. and 'CreationDate'. They can be removed by setting them to `None`.
  459. """
  460. super().__init__()
  461. self._object_seq = itertools.count(1) # consumed by reserveObject
  462. self.xrefTable = [[0, 65535, 'the zero object']]
  463. self.passed_in_file_object = False
  464. self.original_file_like = None
  465. self.tell_base = 0
  466. fh, opened = cbook.to_filehandle(filename, "wb", return_opened=True)
  467. if not opened:
  468. try:
  469. self.tell_base = filename.tell()
  470. except IOError:
  471. fh = BytesIO()
  472. self.original_file_like = filename
  473. else:
  474. fh = filename
  475. self.passed_in_file_object = True
  476. self.fh = fh
  477. self.currentstream = None # stream object to write to, if any
  478. fh.write(b"%PDF-1.4\n") # 1.4 is the first version to have alpha
  479. # Output some eight-bit chars as a comment so various utilities
  480. # recognize the file as binary by looking at the first few
  481. # lines (see note in section 3.4.1 of the PDF reference).
  482. fh.write(b"%\254\334 \253\272\n")
  483. self.rootObject = self.reserveObject('root')
  484. self.pagesObject = self.reserveObject('pages')
  485. self.pageList = []
  486. self.fontObject = self.reserveObject('fonts')
  487. self._extGStateObject = self.reserveObject('extended graphics states')
  488. self.hatchObject = self.reserveObject('tiling patterns')
  489. self.gouraudObject = self.reserveObject('Gouraud triangles')
  490. self.XObjectObject = self.reserveObject('external objects')
  491. self.resourceObject = self.reserveObject('resources')
  492. root = {'Type': Name('Catalog'),
  493. 'Pages': self.pagesObject}
  494. self.writeObject(self.rootObject, root)
  495. self.infoDict = _create_pdf_info_dict('pdf', metadata or {})
  496. self.fontNames = {} # maps filenames to internal font names
  497. self._internal_font_seq = (Name(f'F{i}') for i in itertools.count(1))
  498. self.dviFontInfo = {} # maps dvi font names to embedding information
  499. # differently encoded Type-1 fonts may share the same descriptor
  500. self.type1Descriptors = {}
  501. self._character_tracker = _backend_pdf_ps.CharacterTracker()
  502. self.alphaStates = {} # maps alpha values to graphics state objects
  503. self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1))
  504. self._soft_mask_states = {}
  505. self._soft_mask_seq = (Name(f'SM{i}') for i in itertools.count(1))
  506. self._soft_mask_groups = []
  507. # reproducible writeHatches needs an ordered dict:
  508. self.hatchPatterns = collections.OrderedDict()
  509. self._hatch_pattern_seq = (Name(f'H{i}') for i in itertools.count(1))
  510. self.gouraudTriangles = []
  511. self._images = collections.OrderedDict() # reproducible writeImages
  512. self._image_seq = (Name(f'I{i}') for i in itertools.count(1))
  513. self.markers = collections.OrderedDict() # reproducible writeMarkers
  514. self.multi_byte_charprocs = {}
  515. self.paths = []
  516. self.pageAnnotations = [] # A list of annotations for the current page
  517. # The PDF spec recommends to include every procset
  518. procsets = [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()]
  519. # Write resource dictionary.
  520. # Possibly TODO: more general ExtGState (graphics state dictionaries)
  521. # ColorSpace Pattern Shading Properties
  522. resources = {'Font': self.fontObject,
  523. 'XObject': self.XObjectObject,
  524. 'ExtGState': self._extGStateObject,
  525. 'Pattern': self.hatchObject,
  526. 'Shading': self.gouraudObject,
  527. 'ProcSet': procsets}
  528. self.writeObject(self.resourceObject, resources)
  529. @cbook.deprecated("3.3")
  530. @property
  531. def used_characters(self):
  532. return self.file._character_tracker.used_characters
  533. def newPage(self, width, height):
  534. self.endStream()
  535. self.width, self.height = width, height
  536. contentObject = self.reserveObject('page contents')
  537. thePage = {'Type': Name('Page'),
  538. 'Parent': self.pagesObject,
  539. 'Resources': self.resourceObject,
  540. 'MediaBox': [0, 0, 72 * width, 72 * height],
  541. 'Contents': contentObject,
  542. 'Group': {'Type': Name('Group'),
  543. 'S': Name('Transparency'),
  544. 'CS': Name('DeviceRGB')},
  545. 'Annots': self.pageAnnotations,
  546. }
  547. pageObject = self.reserveObject('page')
  548. self.writeObject(pageObject, thePage)
  549. self.pageList.append(pageObject)
  550. self.beginStream(contentObject.id,
  551. self.reserveObject('length of content stream'))
  552. # Initialize the pdf graphics state to match the default mpl
  553. # graphics context: currently only the join style needs to be set
  554. self.output(GraphicsContextPdf.joinstyles['round'], Op.setlinejoin)
  555. # Clear the list of annotations for the next page
  556. self.pageAnnotations = []
  557. def newTextnote(self, text, positionRect=[-100, -100, 0, 0]):
  558. # Create a new annotation of type text
  559. theNote = {'Type': Name('Annot'),
  560. 'Subtype': Name('Text'),
  561. 'Contents': text,
  562. 'Rect': positionRect,
  563. }
  564. annotObject = self.reserveObject('annotation')
  565. self.writeObject(annotObject, theNote)
  566. self.pageAnnotations.append(annotObject)
  567. def finalize(self):
  568. """Write out the various deferred objects and the pdf end matter."""
  569. self.endStream()
  570. self.writeFonts()
  571. self.writeExtGSTates()
  572. self._write_soft_mask_groups()
  573. self.writeHatches()
  574. self.writeGouraudTriangles()
  575. xobjects = {
  576. name: ob for image, name, ob in self._images.values()}
  577. for tup in self.markers.values():
  578. xobjects[tup[0]] = tup[1]
  579. for name, value in self.multi_byte_charprocs.items():
  580. xobjects[name] = value
  581. for name, path, trans, ob, join, cap, padding, filled, stroked \
  582. in self.paths:
  583. xobjects[name] = ob
  584. self.writeObject(self.XObjectObject, xobjects)
  585. self.writeImages()
  586. self.writeMarkers()
  587. self.writePathCollectionTemplates()
  588. self.writeObject(self.pagesObject,
  589. {'Type': Name('Pages'),
  590. 'Kids': self.pageList,
  591. 'Count': len(self.pageList)})
  592. self.writeInfoDict()
  593. # Finalize the file
  594. self.writeXref()
  595. self.writeTrailer()
  596. def close(self):
  597. """Flush all buffers and free all resources."""
  598. self.endStream()
  599. if self.passed_in_file_object:
  600. self.fh.flush()
  601. else:
  602. if self.original_file_like is not None:
  603. self.original_file_like.write(self.fh.getvalue())
  604. self.fh.close()
  605. def write(self, data):
  606. if self.currentstream is None:
  607. self.fh.write(data)
  608. else:
  609. self.currentstream.write(data)
  610. def output(self, *data):
  611. self.write(fill([pdfRepr(x) for x in data]))
  612. self.write(b'\n')
  613. def beginStream(self, id, len, extra=None, png=None):
  614. assert self.currentstream is None
  615. self.currentstream = Stream(id, len, self, extra, png)
  616. def endStream(self):
  617. if self.currentstream is not None:
  618. self.currentstream.end()
  619. self.currentstream = None
  620. def fontName(self, fontprop):
  621. """
  622. Select a font based on fontprop and return a name suitable for
  623. Op.selectfont. If fontprop is a string, it will be interpreted
  624. as the filename of the font.
  625. """
  626. if isinstance(fontprop, str):
  627. filename = fontprop
  628. elif mpl.rcParams['pdf.use14corefonts']:
  629. filename = findfont(
  630. fontprop, fontext='afm', directory=RendererPdf._afm_font_dir)
  631. else:
  632. filename = findfont(fontprop)
  633. Fx = self.fontNames.get(filename)
  634. if Fx is None:
  635. Fx = next(self._internal_font_seq)
  636. self.fontNames[filename] = Fx
  637. _log.debug('Assigning font %s = %r', Fx, filename)
  638. return Fx
  639. def dviFontName(self, dvifont):
  640. """
  641. Given a dvi font object, return a name suitable for Op.selectfont.
  642. This registers the font information in ``self.dviFontInfo`` if not yet
  643. registered.
  644. """
  645. dvi_info = self.dviFontInfo.get(dvifont.texname)
  646. if dvi_info is not None:
  647. return dvi_info.pdfname
  648. tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
  649. psfont = tex_font_map[dvifont.texname]
  650. if psfont.filename is None:
  651. raise ValueError(
  652. "No usable font file found for {} (TeX: {}); "
  653. "the font may lack a Type-1 version"
  654. .format(psfont.psname, dvifont.texname))
  655. pdfname = next(self._internal_font_seq)
  656. _log.debug('Assigning font %s = %s (dvi)', pdfname, dvifont.texname)
  657. self.dviFontInfo[dvifont.texname] = types.SimpleNamespace(
  658. dvifont=dvifont,
  659. pdfname=pdfname,
  660. fontfile=psfont.filename,
  661. basefont=psfont.psname,
  662. encodingfile=psfont.encoding,
  663. effects=psfont.effects)
  664. return pdfname
  665. def writeFonts(self):
  666. fonts = {}
  667. for dviname, info in sorted(self.dviFontInfo.items()):
  668. Fx = info.pdfname
  669. _log.debug('Embedding Type-1 font %s from dvi.', dviname)
  670. fonts[Fx] = self._embedTeXFont(info)
  671. for filename in sorted(self.fontNames):
  672. Fx = self.fontNames[filename]
  673. _log.debug('Embedding font %s.', filename)
  674. if filename.endswith('.afm'):
  675. # from pdf.use14corefonts
  676. _log.debug('Writing AFM font.')
  677. fonts[Fx] = self._write_afm_font(filename)
  678. else:
  679. # a normal TrueType font
  680. _log.debug('Writing TrueType font.')
  681. chars = self._character_tracker.used.get(filename)
  682. if chars:
  683. fonts[Fx] = self.embedTTF(filename, chars)
  684. self.writeObject(self.fontObject, fonts)
  685. def _write_afm_font(self, filename):
  686. with open(filename, 'rb') as fh:
  687. font = AFM(fh)
  688. fontname = font.get_fontname()
  689. fontdict = {'Type': Name('Font'),
  690. 'Subtype': Name('Type1'),
  691. 'BaseFont': Name(fontname),
  692. 'Encoding': Name('WinAnsiEncoding')}
  693. fontdictObject = self.reserveObject('font dictionary')
  694. self.writeObject(fontdictObject, fontdict)
  695. return fontdictObject
  696. def _embedTeXFont(self, fontinfo):
  697. _log.debug('Embedding TeX font %s - fontinfo=%s',
  698. fontinfo.dvifont.texname, fontinfo.__dict__)
  699. # Widths
  700. widthsObject = self.reserveObject('font widths')
  701. self.writeObject(widthsObject, fontinfo.dvifont.widths)
  702. # Font dictionary
  703. fontdictObject = self.reserveObject('font dictionary')
  704. fontdict = {
  705. 'Type': Name('Font'),
  706. 'Subtype': Name('Type1'),
  707. 'FirstChar': 0,
  708. 'LastChar': len(fontinfo.dvifont.widths) - 1,
  709. 'Widths': widthsObject,
  710. }
  711. # Encoding (if needed)
  712. if fontinfo.encodingfile is not None:
  713. fontdict['Encoding'] = {
  714. 'Type': Name('Encoding'),
  715. 'Differences': [
  716. 0, *map(Name, dviread._parse_enc(fontinfo.encodingfile))],
  717. }
  718. # If no file is specified, stop short
  719. if fontinfo.fontfile is None:
  720. _log.warning(
  721. "Because of TeX configuration (pdftex.map, see updmap option "
  722. "pdftexDownloadBase14) the font %s is not embedded. This is "
  723. "deprecated as of PDF 1.5 and it may cause the consumer "
  724. "application to show something that was not intended.",
  725. fontinfo.basefont)
  726. fontdict['BaseFont'] = Name(fontinfo.basefont)
  727. self.writeObject(fontdictObject, fontdict)
  728. return fontdictObject
  729. # We have a font file to embed - read it in and apply any effects
  730. t1font = type1font.Type1Font(fontinfo.fontfile)
  731. if fontinfo.effects:
  732. t1font = t1font.transform(fontinfo.effects)
  733. fontdict['BaseFont'] = Name(t1font.prop['FontName'])
  734. # Font descriptors may be shared between differently encoded
  735. # Type-1 fonts, so only create a new descriptor if there is no
  736. # existing descriptor for this font.
  737. effects = (fontinfo.effects.get('slant', 0.0),
  738. fontinfo.effects.get('extend', 1.0))
  739. fontdesc = self.type1Descriptors.get((fontinfo.fontfile, effects))
  740. if fontdesc is None:
  741. fontdesc = self.createType1Descriptor(t1font, fontinfo.fontfile)
  742. self.type1Descriptors[(fontinfo.fontfile, effects)] = fontdesc
  743. fontdict['FontDescriptor'] = fontdesc
  744. self.writeObject(fontdictObject, fontdict)
  745. return fontdictObject
  746. def createType1Descriptor(self, t1font, fontfile):
  747. # Create and write the font descriptor and the font file
  748. # of a Type-1 font
  749. fontdescObject = self.reserveObject('font descriptor')
  750. fontfileObject = self.reserveObject('font file')
  751. italic_angle = t1font.prop['ItalicAngle']
  752. fixed_pitch = t1font.prop['isFixedPitch']
  753. flags = 0
  754. # fixed width
  755. if fixed_pitch:
  756. flags |= 1 << 0
  757. # TODO: serif
  758. if 0:
  759. flags |= 1 << 1
  760. # TODO: symbolic (most TeX fonts are)
  761. if 1:
  762. flags |= 1 << 2
  763. # non-symbolic
  764. else:
  765. flags |= 1 << 5
  766. # italic
  767. if italic_angle:
  768. flags |= 1 << 6
  769. # TODO: all caps
  770. if 0:
  771. flags |= 1 << 16
  772. # TODO: small caps
  773. if 0:
  774. flags |= 1 << 17
  775. # TODO: force bold
  776. if 0:
  777. flags |= 1 << 18
  778. ft2font = get_font(fontfile)
  779. descriptor = {
  780. 'Type': Name('FontDescriptor'),
  781. 'FontName': Name(t1font.prop['FontName']),
  782. 'Flags': flags,
  783. 'FontBBox': ft2font.bbox,
  784. 'ItalicAngle': italic_angle,
  785. 'Ascent': ft2font.ascender,
  786. 'Descent': ft2font.descender,
  787. 'CapHeight': 1000, # TODO: find this out
  788. 'XHeight': 500, # TODO: this one too
  789. 'FontFile': fontfileObject,
  790. 'FontFamily': t1font.prop['FamilyName'],
  791. 'StemV': 50, # TODO
  792. # (see also revision 3874; but not all TeX distros have AFM files!)
  793. # 'FontWeight': a number where 400 = Regular, 700 = Bold
  794. }
  795. self.writeObject(fontdescObject, descriptor)
  796. self.beginStream(fontfileObject.id, None,
  797. {'Length1': len(t1font.parts[0]),
  798. 'Length2': len(t1font.parts[1]),
  799. 'Length3': 0})
  800. self.currentstream.write(t1font.parts[0])
  801. self.currentstream.write(t1font.parts[1])
  802. self.endStream()
  803. return fontdescObject
  804. def _get_xobject_symbol_name(self, filename, symbol_name):
  805. Fx = self.fontName(filename)
  806. return "-".join([
  807. Fx.name.decode(),
  808. os.path.splitext(os.path.basename(filename))[0],
  809. symbol_name])
  810. _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin
  811. 12 dict begin
  812. begincmap
  813. /CIDSystemInfo
  814. << /Registry (Adobe)
  815. /Ordering (UCS)
  816. /Supplement 0
  817. >> def
  818. /CMapName /Adobe-Identity-UCS def
  819. /CMapType 2 def
  820. 1 begincodespacerange
  821. <0000> <ffff>
  822. endcodespacerange
  823. %d beginbfrange
  824. %s
  825. endbfrange
  826. endcmap
  827. CMapName currentdict /CMap defineresource pop
  828. end
  829. end"""
  830. def embedTTF(self, filename, characters):
  831. """Embed the TTF font from the named file into the document."""
  832. font = get_font(filename)
  833. fonttype = mpl.rcParams['pdf.fonttype']
  834. def cvt(length, upe=font.units_per_EM, nearest=True):
  835. """Convert font coordinates to PDF glyph coordinates."""
  836. value = length / upe * 1000
  837. if nearest:
  838. return round(value)
  839. # Best(?) to round away from zero for bounding boxes and the like.
  840. if value < 0:
  841. return math.floor(value)
  842. else:
  843. return math.ceil(value)
  844. def embedTTFType3(font, characters, descriptor):
  845. """The Type 3-specific part of embedding a Truetype font"""
  846. widthsObject = self.reserveObject('font widths')
  847. fontdescObject = self.reserveObject('font descriptor')
  848. fontdictObject = self.reserveObject('font dictionary')
  849. charprocsObject = self.reserveObject('character procs')
  850. differencesArray = []
  851. firstchar, lastchar = 0, 255
  852. bbox = [cvt(x, nearest=False) for x in font.bbox]
  853. fontdict = {
  854. 'Type': Name('Font'),
  855. 'BaseFont': ps_name,
  856. 'FirstChar': firstchar,
  857. 'LastChar': lastchar,
  858. 'FontDescriptor': fontdescObject,
  859. 'Subtype': Name('Type3'),
  860. 'Name': descriptor['FontName'],
  861. 'FontBBox': bbox,
  862. 'FontMatrix': [.001, 0, 0, .001, 0, 0],
  863. 'CharProcs': charprocsObject,
  864. 'Encoding': {
  865. 'Type': Name('Encoding'),
  866. 'Differences': differencesArray},
  867. 'Widths': widthsObject
  868. }
  869. from encodings import cp1252
  870. # Make the "Widths" array
  871. def get_char_width(charcode):
  872. s = ord(cp1252.decoding_table[charcode])
  873. width = font.load_char(
  874. s, flags=LOAD_NO_SCALE | LOAD_NO_HINTING).horiAdvance
  875. return cvt(width)
  876. with warnings.catch_warnings():
  877. # Ignore 'Required glyph missing from current font' warning
  878. # from ft2font: here we're just building the widths table, but
  879. # the missing glyphs may not even be used in the actual string.
  880. warnings.filterwarnings("ignore")
  881. widths = [get_char_width(charcode)
  882. for charcode in range(firstchar, lastchar+1)]
  883. descriptor['MaxWidth'] = max(widths)
  884. # Make the "Differences" array, sort the ccodes < 255 from
  885. # the multi-byte ccodes, and build the whole set of glyph ids
  886. # that we need from this font.
  887. glyph_ids = []
  888. differences = []
  889. multi_byte_chars = set()
  890. for c in characters:
  891. ccode = c
  892. gind = font.get_char_index(ccode)
  893. glyph_ids.append(gind)
  894. glyph_name = font.get_glyph_name(gind)
  895. if ccode <= 255:
  896. differences.append((ccode, glyph_name))
  897. else:
  898. multi_byte_chars.add(glyph_name)
  899. differences.sort()
  900. last_c = -2
  901. for c, name in differences:
  902. if c != last_c + 1:
  903. differencesArray.append(c)
  904. differencesArray.append(Name(name))
  905. last_c = c
  906. # Make the charprocs array (using ttconv to generate the
  907. # actual outlines)
  908. try:
  909. rawcharprocs = _ttconv.get_pdf_charprocs(
  910. os.fsencode(filename), glyph_ids)
  911. except RuntimeError:
  912. _log.warning("The PDF backend does not currently support the "
  913. "selected font.")
  914. raise
  915. charprocs = {}
  916. for charname in sorted(rawcharprocs):
  917. stream = rawcharprocs[charname]
  918. charprocDict = {'Length': len(stream)}
  919. # The 2-byte characters are used as XObjects, so they
  920. # need extra info in their dictionary
  921. if charname in multi_byte_chars:
  922. charprocDict['Type'] = Name('XObject')
  923. charprocDict['Subtype'] = Name('Form')
  924. charprocDict['BBox'] = bbox
  925. # Each glyph includes bounding box information,
  926. # but xpdf and ghostscript can't handle it in a
  927. # Form XObject (they segfault!!!), so we remove it
  928. # from the stream here. It's not needed anyway,
  929. # since the Form XObject includes it in its BBox
  930. # value.
  931. stream = stream[stream.find(b"d1") + 2:]
  932. charprocObject = self.reserveObject('charProc')
  933. self.beginStream(charprocObject.id, None, charprocDict)
  934. self.currentstream.write(stream)
  935. self.endStream()
  936. # Send the glyphs with ccode > 255 to the XObject dictionary,
  937. # and the others to the font itself
  938. if charname in multi_byte_chars:
  939. name = self._get_xobject_symbol_name(filename, charname)
  940. self.multi_byte_charprocs[name] = charprocObject
  941. else:
  942. charprocs[charname] = charprocObject
  943. # Write everything out
  944. self.writeObject(fontdictObject, fontdict)
  945. self.writeObject(fontdescObject, descriptor)
  946. self.writeObject(widthsObject, widths)
  947. self.writeObject(charprocsObject, charprocs)
  948. return fontdictObject
  949. def embedTTFType42(font, characters, descriptor):
  950. """The Type 42-specific part of embedding a Truetype font"""
  951. fontdescObject = self.reserveObject('font descriptor')
  952. cidFontDictObject = self.reserveObject('CID font dictionary')
  953. type0FontDictObject = self.reserveObject('Type 0 font dictionary')
  954. cidToGidMapObject = self.reserveObject('CIDToGIDMap stream')
  955. fontfileObject = self.reserveObject('font file stream')
  956. wObject = self.reserveObject('Type 0 widths')
  957. toUnicodeMapObject = self.reserveObject('ToUnicode map')
  958. cidFontDict = {
  959. 'Type': Name('Font'),
  960. 'Subtype': Name('CIDFontType2'),
  961. 'BaseFont': ps_name,
  962. 'CIDSystemInfo': {
  963. 'Registry': 'Adobe',
  964. 'Ordering': 'Identity',
  965. 'Supplement': 0},
  966. 'FontDescriptor': fontdescObject,
  967. 'W': wObject,
  968. 'CIDToGIDMap': cidToGidMapObject
  969. }
  970. type0FontDict = {
  971. 'Type': Name('Font'),
  972. 'Subtype': Name('Type0'),
  973. 'BaseFont': ps_name,
  974. 'Encoding': Name('Identity-H'),
  975. 'DescendantFonts': [cidFontDictObject],
  976. 'ToUnicode': toUnicodeMapObject
  977. }
  978. # Make fontfile stream
  979. descriptor['FontFile2'] = fontfileObject
  980. length1Object = self.reserveObject('decoded length of a font')
  981. self.beginStream(
  982. fontfileObject.id,
  983. self.reserveObject('length of font stream'),
  984. {'Length1': length1Object})
  985. with open(filename, 'rb') as fontfile:
  986. length1 = 0
  987. while True:
  988. data = fontfile.read(4096)
  989. if not data:
  990. break
  991. length1 += len(data)
  992. self.currentstream.write(data)
  993. self.endStream()
  994. self.writeObject(length1Object, length1)
  995. # Make the 'W' (Widths) array, CidToGidMap and ToUnicode CMap
  996. # at the same time
  997. cid_to_gid_map = ['\0'] * 65536
  998. widths = []
  999. max_ccode = 0
  1000. for c in characters:
  1001. ccode = c
  1002. gind = font.get_char_index(ccode)
  1003. glyph = font.load_char(ccode,
  1004. flags=LOAD_NO_SCALE | LOAD_NO_HINTING)
  1005. widths.append((ccode, cvt(glyph.horiAdvance)))
  1006. if ccode < 65536:
  1007. cid_to_gid_map[ccode] = chr(gind)
  1008. max_ccode = max(ccode, max_ccode)
  1009. widths.sort()
  1010. cid_to_gid_map = cid_to_gid_map[:max_ccode + 1]
  1011. last_ccode = -2
  1012. w = []
  1013. max_width = 0
  1014. unicode_groups = []
  1015. for ccode, width in widths:
  1016. if ccode != last_ccode + 1:
  1017. w.append(ccode)
  1018. w.append([width])
  1019. unicode_groups.append([ccode, ccode])
  1020. else:
  1021. w[-1].append(width)
  1022. unicode_groups[-1][1] = ccode
  1023. max_width = max(max_width, width)
  1024. last_ccode = ccode
  1025. unicode_bfrange = []
  1026. for start, end in unicode_groups:
  1027. unicode_bfrange.append(
  1028. b"<%04x> <%04x> [%s]" %
  1029. (start, end,
  1030. b" ".join(b"<%04x>" % x for x in range(start, end+1))))
  1031. unicode_cmap = (self._identityToUnicodeCMap %
  1032. (len(unicode_groups), b"\n".join(unicode_bfrange)))
  1033. # CIDToGIDMap stream
  1034. cid_to_gid_map = "".join(cid_to_gid_map).encode("utf-16be")
  1035. self.beginStream(cidToGidMapObject.id,
  1036. None,
  1037. {'Length': len(cid_to_gid_map)})
  1038. self.currentstream.write(cid_to_gid_map)
  1039. self.endStream()
  1040. # ToUnicode CMap
  1041. self.beginStream(toUnicodeMapObject.id,
  1042. None,
  1043. {'Length': unicode_cmap})
  1044. self.currentstream.write(unicode_cmap)
  1045. self.endStream()
  1046. descriptor['MaxWidth'] = max_width
  1047. # Write everything out
  1048. self.writeObject(cidFontDictObject, cidFontDict)
  1049. self.writeObject(type0FontDictObject, type0FontDict)
  1050. self.writeObject(fontdescObject, descriptor)
  1051. self.writeObject(wObject, w)
  1052. return type0FontDictObject
  1053. # Beginning of main embedTTF function...
  1054. ps_name = font.postscript_name.encode('ascii', 'replace')
  1055. ps_name = Name(ps_name)
  1056. pclt = font.get_sfnt_table('pclt') or {'capHeight': 0, 'xHeight': 0}
  1057. post = font.get_sfnt_table('post') or {'italicAngle': (0, 0)}
  1058. ff = font.face_flags
  1059. sf = font.style_flags
  1060. flags = 0
  1061. symbolic = False # ps_name.name in ('Cmsy10', 'Cmmi10', 'Cmex10')
  1062. if ff & FIXED_WIDTH:
  1063. flags |= 1 << 0
  1064. if 0: # TODO: serif
  1065. flags |= 1 << 1
  1066. if symbolic:
  1067. flags |= 1 << 2
  1068. else:
  1069. flags |= 1 << 5
  1070. if sf & ITALIC:
  1071. flags |= 1 << 6
  1072. if 0: # TODO: all caps
  1073. flags |= 1 << 16
  1074. if 0: # TODO: small caps
  1075. flags |= 1 << 17
  1076. if 0: # TODO: force bold
  1077. flags |= 1 << 18
  1078. descriptor = {
  1079. 'Type': Name('FontDescriptor'),
  1080. 'FontName': ps_name,
  1081. 'Flags': flags,
  1082. 'FontBBox': [cvt(x, nearest=False) for x in font.bbox],
  1083. 'Ascent': cvt(font.ascender, nearest=False),
  1084. 'Descent': cvt(font.descender, nearest=False),
  1085. 'CapHeight': cvt(pclt['capHeight'], nearest=False),
  1086. 'XHeight': cvt(pclt['xHeight']),
  1087. 'ItalicAngle': post['italicAngle'][1], # ???
  1088. 'StemV': 0 # ???
  1089. }
  1090. # The font subsetting to a Type 3 font does not work for
  1091. # OpenType (.otf) that embed a Postscript CFF font, so avoid that --
  1092. # save as a (non-subsetted) Type 42 font instead.
  1093. if is_opentype_cff_font(filename):
  1094. fonttype = 42
  1095. _log.warning("%r can not be subsetted into a Type 3 font. The "
  1096. "entire font will be embedded in the output.",
  1097. os.path.basename(filename))
  1098. if fonttype == 3:
  1099. return embedTTFType3(font, characters, descriptor)
  1100. elif fonttype == 42:
  1101. return embedTTFType42(font, characters, descriptor)
  1102. def alphaState(self, alpha):
  1103. """Return name of an ExtGState that sets alpha to the given value."""
  1104. state = self.alphaStates.get(alpha, None)
  1105. if state is not None:
  1106. return state[0]
  1107. name = next(self._alpha_state_seq)
  1108. self.alphaStates[alpha] = \
  1109. (name, {'Type': Name('ExtGState'),
  1110. 'CA': alpha[0], 'ca': alpha[1]})
  1111. return name
  1112. def _soft_mask_state(self, smask):
  1113. """
  1114. Return an ExtGState that sets the soft mask to the given shading.
  1115. Parameters
  1116. ----------
  1117. smask : Reference
  1118. Reference to a shading in DeviceGray color space, whose luminosity
  1119. is to be used as the alpha channel.
  1120. Returns
  1121. -------
  1122. Name
  1123. """
  1124. state = self._soft_mask_states.get(smask, None)
  1125. if state is not None:
  1126. return state[0]
  1127. name = next(self._soft_mask_seq)
  1128. groupOb = self.reserveObject('transparency group for soft mask')
  1129. self._soft_mask_states[smask] = (
  1130. name,
  1131. {
  1132. 'Type': Name('ExtGState'),
  1133. 'AIS': False,
  1134. 'SMask': {
  1135. 'Type': Name('Mask'),
  1136. 'S': Name('Luminosity'),
  1137. 'BC': [1],
  1138. 'G': groupOb
  1139. }
  1140. }
  1141. )
  1142. self._soft_mask_groups.append((
  1143. groupOb,
  1144. {
  1145. 'Type': Name('XObject'),
  1146. 'Subtype': Name('Form'),
  1147. 'FormType': 1,
  1148. 'Group': {
  1149. 'S': Name('Transparency'),
  1150. 'CS': Name('DeviceGray')
  1151. },
  1152. 'Matrix': [1, 0, 0, 1, 0, 0],
  1153. 'Resources': {'Shading': {'S': smask}},
  1154. 'BBox': [0, 0, 1, 1]
  1155. },
  1156. [Name('S'), Op.shading]
  1157. ))
  1158. return name
  1159. def writeExtGSTates(self):
  1160. self.writeObject(
  1161. self._extGStateObject,
  1162. dict([
  1163. *self.alphaStates.values(),
  1164. *self._soft_mask_states.values()
  1165. ])
  1166. )
  1167. def _write_soft_mask_groups(self):
  1168. for ob, attributes, content in self._soft_mask_groups:
  1169. self.beginStream(ob.id, None, attributes)
  1170. self.output(*content)
  1171. self.endStream()
  1172. def hatchPattern(self, hatch_style):
  1173. # The colors may come in as numpy arrays, which aren't hashable
  1174. if hatch_style is not None:
  1175. edge, face, hatch = hatch_style
  1176. if edge is not None:
  1177. edge = tuple(edge)
  1178. if face is not None:
  1179. face = tuple(face)
  1180. hatch_style = (edge, face, hatch)
  1181. pattern = self.hatchPatterns.get(hatch_style, None)
  1182. if pattern is not None:
  1183. return pattern
  1184. name = next(self._hatch_pattern_seq)
  1185. self.hatchPatterns[hatch_style] = name
  1186. return name
  1187. def writeHatches(self):
  1188. hatchDict = dict()
  1189. sidelen = 72.0
  1190. for hatch_style, name in self.hatchPatterns.items():
  1191. ob = self.reserveObject('hatch pattern')
  1192. hatchDict[name] = ob
  1193. res = {'Procsets':
  1194. [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()]}
  1195. self.beginStream(
  1196. ob.id, None,
  1197. {'Type': Name('Pattern'),
  1198. 'PatternType': 1, 'PaintType': 1, 'TilingType': 1,
  1199. 'BBox': [0, 0, sidelen, sidelen],
  1200. 'XStep': sidelen, 'YStep': sidelen,
  1201. 'Resources': res,
  1202. # Change origin to match Agg at top-left.
  1203. 'Matrix': [1, 0, 0, 1, 0, self.height * 72]})
  1204. stroke_rgb, fill_rgb, path = hatch_style
  1205. self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2],
  1206. Op.setrgb_stroke)
  1207. if fill_rgb is not None:
  1208. self.output(fill_rgb[0], fill_rgb[1], fill_rgb[2],
  1209. Op.setrgb_nonstroke,
  1210. 0, 0, sidelen, sidelen, Op.rectangle,
  1211. Op.fill)
  1212. self.output(mpl.rcParams['hatch.linewidth'], Op.setlinewidth)
  1213. self.output(*self.pathOperations(
  1214. Path.hatch(path),
  1215. Affine2D().scale(sidelen),
  1216. simplify=False))
  1217. self.output(Op.fill_stroke)
  1218. self.endStream()
  1219. self.writeObject(self.hatchObject, hatchDict)
  1220. def addGouraudTriangles(self, points, colors):
  1221. """
  1222. Add a Gouraud triangle shading.
  1223. Parameters
  1224. ----------
  1225. points : np.ndarray
  1226. Triangle vertices, shape (n, 3, 2)
  1227. where n = number of triangles, 3 = vertices, 2 = x, y.
  1228. colors : np.ndarray
  1229. Vertex colors, shape (n, 3, 1) or (n, 3, 4)
  1230. as with points, but last dimension is either (gray,)
  1231. or (r, g, b, alpha).
  1232. Returns
  1233. -------
  1234. Name, Reference
  1235. """
  1236. name = Name('GT%d' % len(self.gouraudTriangles))
  1237. ob = self.reserveObject(f'Gouraud triangle {name}')
  1238. self.gouraudTriangles.append((name, ob, points, colors))
  1239. return name, ob
  1240. def writeGouraudTriangles(self):
  1241. gouraudDict = dict()
  1242. for name, ob, points, colors in self.gouraudTriangles:
  1243. gouraudDict[name] = ob
  1244. shape = points.shape
  1245. flat_points = points.reshape((shape[0] * shape[1], 2))
  1246. colordim = colors.shape[2]
  1247. assert colordim in (1, 4)
  1248. flat_colors = colors.reshape((shape[0] * shape[1], colordim))
  1249. if colordim == 4:
  1250. # strip the alpha channel
  1251. colordim = 3
  1252. points_min = np.min(flat_points, axis=0) - (1 << 8)
  1253. points_max = np.max(flat_points, axis=0) + (1 << 8)
  1254. factor = 0xffffffff / (points_max - points_min)
  1255. self.beginStream(
  1256. ob.id, None,
  1257. {'ShadingType': 4,
  1258. 'BitsPerCoordinate': 32,
  1259. 'BitsPerComponent': 8,
  1260. 'BitsPerFlag': 8,
  1261. 'ColorSpace': Name(
  1262. 'DeviceRGB' if colordim == 3 else 'DeviceGray'
  1263. ),
  1264. 'AntiAlias': False,
  1265. 'Decode': ([points_min[0], points_max[0],
  1266. points_min[1], points_max[1]]
  1267. + [0, 1] * colordim),
  1268. })
  1269. streamarr = np.empty(
  1270. (shape[0] * shape[1],),
  1271. dtype=[('flags', 'u1'),
  1272. ('points', '>u4', (2,)),
  1273. ('colors', 'u1', (colordim,))])
  1274. streamarr['flags'] = 0
  1275. streamarr['points'] = (flat_points - points_min) * factor
  1276. streamarr['colors'] = flat_colors[:, :colordim] * 255.0
  1277. self.write(streamarr.tobytes())
  1278. self.endStream()
  1279. self.writeObject(self.gouraudObject, gouraudDict)
  1280. def imageObject(self, image):
  1281. """Return name of an image XObject representing the given image."""
  1282. entry = self._images.get(id(image), None)
  1283. if entry is not None:
  1284. return entry[1]
  1285. name = next(self._image_seq)
  1286. ob = self.reserveObject(f'image {name}')
  1287. self._images[id(image)] = (image, name, ob)
  1288. return name
  1289. def _unpack(self, im):
  1290. """
  1291. Unpack image array *im* into ``(data, alpha)``, which have shape
  1292. ``(height, width, 3)`` (RGB) or ``(height, width, 1)`` (grayscale or
  1293. alpha), except that alpha is None if the image is fully opaque.
  1294. """
  1295. im = im[::-1]
  1296. if im.ndim == 2:
  1297. return im, None
  1298. else:
  1299. rgb = im[:, :, :3]
  1300. rgb = np.array(rgb, order='C')
  1301. # PDF needs a separate alpha image
  1302. if im.shape[2] == 4:
  1303. alpha = im[:, :, 3][..., None]
  1304. if np.all(alpha == 255):
  1305. alpha = None
  1306. else:
  1307. alpha = np.array(alpha, order='C')
  1308. else:
  1309. alpha = None
  1310. return rgb, alpha
  1311. def _writePng(self, data):
  1312. """
  1313. Write the image *data* into the pdf file using png
  1314. predictors with Flate compression.
  1315. """
  1316. buffer = BytesIO()
  1317. if data.shape[-1] == 1:
  1318. data = data.squeeze(axis=-1)
  1319. Image.fromarray(data).save(buffer, format="png")
  1320. buffer.seek(8)
  1321. while True:
  1322. length, type = struct.unpack(b'!L4s', buffer.read(8))
  1323. if type == b'IDAT':
  1324. data = buffer.read(length)
  1325. if len(data) != length:
  1326. raise RuntimeError("truncated data")
  1327. self.currentstream.write(data)
  1328. elif type == b'IEND':
  1329. break
  1330. else:
  1331. buffer.seek(length, 1)
  1332. buffer.seek(4, 1) # skip CRC
  1333. def _writeImg(self, data, id, smask=None):
  1334. """
  1335. Write the image *data*, of shape ``(height, width, 1)`` (grayscale) or
  1336. ``(height, width, 3)`` (RGB), as pdf object *id* and with the soft mask
  1337. (alpha channel) *smask*, which should be either None or a ``(height,
  1338. width, 1)`` array.
  1339. """
  1340. height, width, colors = data.shape
  1341. obj = {'Type': Name('XObject'),
  1342. 'Subtype': Name('Image'),
  1343. 'Width': width,
  1344. 'Height': height,
  1345. 'ColorSpace': Name({1: 'DeviceGray', 3: 'DeviceRGB'}[colors]),
  1346. 'BitsPerComponent': 8}
  1347. if smask:
  1348. obj['SMask'] = smask
  1349. if mpl.rcParams['pdf.compression']:
  1350. png = {'Predictor': 10, 'Colors': colors, 'Columns': width}
  1351. else:
  1352. png = None
  1353. self.beginStream(
  1354. id,
  1355. self.reserveObject('length of image stream'),
  1356. obj,
  1357. png=png
  1358. )
  1359. if png:
  1360. self._writePng(data)
  1361. else:
  1362. self.currentstream.write(data.tobytes())
  1363. self.endStream()
  1364. def writeImages(self):
  1365. for img, name, ob in self._images.values():
  1366. data, adata = self._unpack(img)
  1367. if adata is not None:
  1368. smaskObject = self.reserveObject("smask")
  1369. self._writeImg(adata, smaskObject.id)
  1370. else:
  1371. smaskObject = None
  1372. self._writeImg(data, ob.id, smaskObject)
  1373. def markerObject(self, path, trans, fill, stroke, lw, joinstyle,
  1374. capstyle):
  1375. """Return name of a marker XObject representing the given path."""
  1376. # self.markers used by markerObject, writeMarkers, close:
  1377. # mapping from (path operations, fill?, stroke?) to
  1378. # [name, object reference, bounding box, linewidth]
  1379. # This enables different draw_markers calls to share the XObject
  1380. # if the gc is sufficiently similar: colors etc can vary, but
  1381. # the choices of whether to fill and whether to stroke cannot.
  1382. # We need a bounding box enclosing all of the XObject path,
  1383. # but since line width may vary, we store the maximum of all
  1384. # occurring line widths in self.markers.
  1385. # close() is somewhat tightly coupled in that it expects the
  1386. # first two components of each value in self.markers to be the
  1387. # name and object reference.
  1388. pathops = self.pathOperations(path, trans, simplify=False)
  1389. key = (tuple(pathops), bool(fill), bool(stroke), joinstyle, capstyle)
  1390. result = self.markers.get(key)
  1391. if result is None:
  1392. name = Name('M%d' % len(self.markers))
  1393. ob = self.reserveObject('marker %d' % len(self.markers))
  1394. bbox = path.get_extents(trans)
  1395. self.markers[key] = [name, ob, bbox, lw]
  1396. else:
  1397. if result[-1] < lw:
  1398. result[-1] = lw
  1399. name = result[0]
  1400. return name
  1401. def writeMarkers(self):
  1402. for ((pathops, fill, stroke, joinstyle, capstyle),
  1403. (name, ob, bbox, lw)) in self.markers.items():
  1404. # bbox wraps the exact limits of the control points, so half a line
  1405. # will appear outside it. If the join style is miter and the line
  1406. # is not parallel to the edge, then the line will extend even
  1407. # further. From the PDF specification, Section 8.4.3.5, the miter
  1408. # limit is miterLength / lineWidth and from Table 52, the default
  1409. # is 10. With half the miter length outside, that works out to the
  1410. # following padding:
  1411. bbox = bbox.padded(lw * 5)
  1412. self.beginStream(
  1413. ob.id, None,
  1414. {'Type': Name('XObject'), 'Subtype': Name('Form'),
  1415. 'BBox': list(bbox.extents)})
  1416. self.output(GraphicsContextPdf.joinstyles[joinstyle],
  1417. Op.setlinejoin)
  1418. self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap)
  1419. self.output(*pathops)
  1420. self.output(Op.paint_path(fill, stroke))
  1421. self.endStream()
  1422. def pathCollectionObject(self, gc, path, trans, padding, filled, stroked):
  1423. name = Name('P%d' % len(self.paths))
  1424. ob = self.reserveObject('path %d' % len(self.paths))
  1425. self.paths.append(
  1426. (name, path, trans, ob, gc.get_joinstyle(), gc.get_capstyle(),
  1427. padding, filled, stroked))
  1428. return name
  1429. def writePathCollectionTemplates(self):
  1430. for (name, path, trans, ob, joinstyle, capstyle, padding, filled,
  1431. stroked) in self.paths:
  1432. pathops = self.pathOperations(path, trans, simplify=False)
  1433. bbox = path.get_extents(trans)
  1434. if not np.all(np.isfinite(bbox.extents)):
  1435. extents = [0, 0, 0, 0]
  1436. else:
  1437. bbox = bbox.padded(padding)
  1438. extents = list(bbox.extents)
  1439. self.beginStream(
  1440. ob.id, None,
  1441. {'Type': Name('XObject'), 'Subtype': Name('Form'),
  1442. 'BBox': extents})
  1443. self.output(GraphicsContextPdf.joinstyles[joinstyle],
  1444. Op.setlinejoin)
  1445. self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap)
  1446. self.output(*pathops)
  1447. self.output(Op.paint_path(filled, stroked))
  1448. self.endStream()
  1449. @staticmethod
  1450. def pathOperations(path, transform, clip=None, simplify=None, sketch=None):
  1451. return [Verbatim(_path.convert_to_string(
  1452. path, transform, clip, simplify, sketch,
  1453. 6,
  1454. [Op.moveto.op, Op.lineto.op, b'', Op.curveto.op, Op.closepath.op],
  1455. True))]
  1456. def writePath(self, path, transform, clip=False, sketch=None):
  1457. if clip:
  1458. clip = (0.0, 0.0, self.width * 72, self.height * 72)
  1459. simplify = path.should_simplify
  1460. else:
  1461. clip = None
  1462. simplify = False
  1463. cmds = self.pathOperations(path, transform, clip, simplify=simplify,
  1464. sketch=sketch)
  1465. self.output(*cmds)
  1466. def reserveObject(self, name=''):
  1467. """
  1468. Reserve an ID for an indirect object.
  1469. The name is used for debugging in case we forget to print out
  1470. the object with writeObject.
  1471. """
  1472. id = next(self._object_seq)
  1473. self.xrefTable.append([None, 0, name])
  1474. return Reference(id)
  1475. def recordXref(self, id):
  1476. self.xrefTable[id][0] = self.fh.tell() - self.tell_base
  1477. def writeObject(self, object, contents):
  1478. self.recordXref(object.id)
  1479. object.write(contents, self)
  1480. def writeXref(self):
  1481. """Write out the xref table."""
  1482. self.startxref = self.fh.tell() - self.tell_base
  1483. self.write(b"xref\n0 %d\n" % len(self.xrefTable))
  1484. for i, (offset, generation, name) in enumerate(self.xrefTable):
  1485. if offset is None:
  1486. raise AssertionError(
  1487. 'No offset for object %d (%s)' % (i, name))
  1488. else:
  1489. key = b"f" if name == 'the zero object' else b"n"
  1490. text = b"%010d %05d %b \n" % (offset, generation, key)
  1491. self.write(text)
  1492. def writeInfoDict(self):
  1493. """Write out the info dictionary, checking it for good form"""
  1494. self.infoObject = self.reserveObject('info')
  1495. self.writeObject(self.infoObject, self.infoDict)
  1496. def writeTrailer(self):
  1497. """Write out the PDF trailer."""
  1498. self.write(b"trailer\n")
  1499. self.write(pdfRepr(
  1500. {'Size': len(self.xrefTable),
  1501. 'Root': self.rootObject,
  1502. 'Info': self.infoObject}))
  1503. # Could add 'ID'
  1504. self.write(b"\nstartxref\n%d\n%%%%EOF\n" % self.startxref)
  1505. class RendererPdf(_backend_pdf_ps.RendererPDFPSBase):
  1506. _afm_font_dir = cbook._get_data_path("fonts/pdfcorefonts")
  1507. _use_afm_rc_name = "pdf.use14corefonts"
  1508. def __init__(self, file, image_dpi, height, width):
  1509. super().__init__(width, height)
  1510. self.file = file
  1511. self.gc = self.new_gc()
  1512. self.mathtext_parser = MathTextParser("Pdf")
  1513. self.image_dpi = image_dpi
  1514. def finalize(self):
  1515. self.file.output(*self.gc.finalize())
  1516. def check_gc(self, gc, fillcolor=None):
  1517. orig_fill = getattr(gc, '_fillcolor', (0., 0., 0.))
  1518. gc._fillcolor = fillcolor
  1519. orig_alphas = getattr(gc, '_effective_alphas', (1.0, 1.0))
  1520. if gc.get_rgb() is None:
  1521. # It should not matter what color here since linewidth should be
  1522. # 0 unless affected by global settings in rcParams, hence setting
  1523. # zero alpha just in case.
  1524. gc.set_foreground((0, 0, 0, 0), isRGBA=True)
  1525. if gc._forced_alpha:
  1526. gc._effective_alphas = (gc._alpha, gc._alpha)
  1527. elif fillcolor is None or len(fillcolor) < 4:
  1528. gc._effective_alphas = (gc._rgb[3], 1.0)
  1529. else:
  1530. gc._effective_alphas = (gc._rgb[3], fillcolor[3])
  1531. delta = self.gc.delta(gc)
  1532. if delta:
  1533. self.file.output(*delta)
  1534. # Restore gc to avoid unwanted side effects
  1535. gc._fillcolor = orig_fill
  1536. gc._effective_alphas = orig_alphas
  1537. @cbook.deprecated("3.3")
  1538. def track_characters(self, *args, **kwargs):
  1539. """Keep track of which characters are required from each font."""
  1540. self.file._character_tracker.track(*args, **kwargs)
  1541. @cbook.deprecated("3.3")
  1542. def merge_used_characters(self, *args, **kwargs):
  1543. self.file._character_tracker.merge(*args, **kwargs)
  1544. def get_image_magnification(self):
  1545. return self.image_dpi/72.0
  1546. def draw_image(self, gc, x, y, im, transform=None):
  1547. # docstring inherited
  1548. h, w = im.shape[:2]
  1549. if w == 0 or h == 0:
  1550. return
  1551. if transform is None:
  1552. # If there's no transform, alpha has already been applied
  1553. gc.set_alpha(1.0)
  1554. self.check_gc(gc)
  1555. w = 72.0 * w / self.image_dpi
  1556. h = 72.0 * h / self.image_dpi
  1557. imob = self.file.imageObject(im)
  1558. if transform is None:
  1559. self.file.output(Op.gsave,
  1560. w, 0, 0, h, x, y, Op.concat_matrix,
  1561. imob, Op.use_xobject, Op.grestore)
  1562. else:
  1563. tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values()
  1564. self.file.output(Op.gsave,
  1565. 1, 0, 0, 1, x, y, Op.concat_matrix,
  1566. tr1, tr2, tr3, tr4, tr5, tr6, Op.concat_matrix,
  1567. imob, Op.use_xobject, Op.grestore)
  1568. def draw_path(self, gc, path, transform, rgbFace=None):
  1569. # docstring inherited
  1570. self.check_gc(gc, rgbFace)
  1571. self.file.writePath(
  1572. path, transform,
  1573. rgbFace is None and gc.get_hatch_path() is None,
  1574. gc.get_sketch_params())
  1575. self.file.output(self.gc.paint())
  1576. def draw_path_collection(self, gc, master_transform, paths, all_transforms,
  1577. offsets, offsetTrans, facecolors, edgecolors,
  1578. linewidths, linestyles, antialiaseds, urls,
  1579. offset_position):
  1580. # We can only reuse the objects if the presence of fill and
  1581. # stroke (and the amount of alpha for each) is the same for
  1582. # all of them
  1583. can_do_optimization = True
  1584. facecolors = np.asarray(facecolors)
  1585. edgecolors = np.asarray(edgecolors)
  1586. if not len(facecolors):
  1587. filled = False
  1588. can_do_optimization = not gc.get_hatch()
  1589. else:
  1590. if np.all(facecolors[:, 3] == facecolors[0, 3]):
  1591. filled = facecolors[0, 3] != 0.0
  1592. else:
  1593. can_do_optimization = False
  1594. if not len(edgecolors):
  1595. stroked = False
  1596. else:
  1597. if np.all(np.asarray(linewidths) == 0.0):
  1598. stroked = False
  1599. elif np.all(edgecolors[:, 3] == edgecolors[0, 3]):
  1600. stroked = edgecolors[0, 3] != 0.0
  1601. else:
  1602. can_do_optimization = False
  1603. # Is the optimization worth it? Rough calculation:
  1604. # cost of emitting a path in-line is len_path * uses_per_path
  1605. # cost of XObject is len_path + 5 for the definition,
  1606. # uses_per_path for the uses
  1607. len_path = len(paths[0].vertices) if len(paths) > 0 else 0
  1608. uses_per_path = self._iter_collection_uses_per_path(
  1609. paths, all_transforms, offsets, facecolors, edgecolors)
  1610. should_do_optimization = \
  1611. len_path + uses_per_path + 5 < len_path * uses_per_path
  1612. if (not can_do_optimization) or (not should_do_optimization):
  1613. return RendererBase.draw_path_collection(
  1614. self, gc, master_transform, paths, all_transforms,
  1615. offsets, offsetTrans, facecolors, edgecolors,
  1616. linewidths, linestyles, antialiaseds, urls,
  1617. offset_position)
  1618. padding = np.max(linewidths)
  1619. path_codes = []
  1620. for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
  1621. master_transform, paths, all_transforms)):
  1622. name = self.file.pathCollectionObject(
  1623. gc, path, transform, padding, filled, stroked)
  1624. path_codes.append(name)
  1625. output = self.file.output
  1626. output(*self.gc.push())
  1627. lastx, lasty = 0, 0
  1628. for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
  1629. gc, master_transform, all_transforms, path_codes, offsets,
  1630. offsetTrans, facecolors, edgecolors, linewidths, linestyles,
  1631. antialiaseds, urls, offset_position):
  1632. self.check_gc(gc0, rgbFace)
  1633. dx, dy = xo - lastx, yo - lasty
  1634. output(1, 0, 0, 1, dx, dy, Op.concat_matrix, path_id,
  1635. Op.use_xobject)
  1636. lastx, lasty = xo, yo
  1637. output(*self.gc.pop())
  1638. def draw_markers(self, gc, marker_path, marker_trans, path, trans,
  1639. rgbFace=None):
  1640. # docstring inherited
  1641. # Same logic as in draw_path_collection
  1642. len_marker_path = len(marker_path)
  1643. uses = len(path)
  1644. if len_marker_path * uses < len_marker_path + uses + 5:
  1645. RendererBase.draw_markers(self, gc, marker_path, marker_trans,
  1646. path, trans, rgbFace)
  1647. return
  1648. self.check_gc(gc, rgbFace)
  1649. fill = gc.fill(rgbFace)
  1650. stroke = gc.stroke()
  1651. output = self.file.output
  1652. marker = self.file.markerObject(
  1653. marker_path, marker_trans, fill, stroke, self.gc._linewidth,
  1654. gc.get_joinstyle(), gc.get_capstyle())
  1655. output(Op.gsave)
  1656. lastx, lasty = 0, 0
  1657. for vertices, code in path.iter_segments(
  1658. trans,
  1659. clip=(0, 0, self.file.width*72, self.file.height*72),
  1660. simplify=False):
  1661. if len(vertices):
  1662. x, y = vertices[-2:]
  1663. if not (0 <= x <= self.file.width * 72
  1664. and 0 <= y <= self.file.height * 72):
  1665. continue
  1666. dx, dy = x - lastx, y - lasty
  1667. output(1, 0, 0, 1, dx, dy, Op.concat_matrix,
  1668. marker, Op.use_xobject)
  1669. lastx, lasty = x, y
  1670. output(Op.grestore)
  1671. def draw_gouraud_triangle(self, gc, points, colors, trans):
  1672. self.draw_gouraud_triangles(gc, points.reshape((1, 3, 2)),
  1673. colors.reshape((1, 3, 4)), trans)
  1674. def draw_gouraud_triangles(self, gc, points, colors, trans):
  1675. assert len(points) == len(colors)
  1676. if len(points) == 0:
  1677. return
  1678. assert points.ndim == 3
  1679. assert points.shape[1] == 3
  1680. assert points.shape[2] == 2
  1681. assert colors.ndim == 3
  1682. assert colors.shape[1] == 3
  1683. assert colors.shape[2] in (1, 4)
  1684. shape = points.shape
  1685. points = points.reshape((shape[0] * shape[1], 2))
  1686. tpoints = trans.transform(points)
  1687. tpoints = tpoints.reshape(shape)
  1688. name, _ = self.file.addGouraudTriangles(tpoints, colors)
  1689. output = self.file.output
  1690. if colors.shape[2] == 1:
  1691. # grayscale
  1692. gc.set_alpha(1.0)
  1693. self.check_gc(gc)
  1694. output(name, Op.shading)
  1695. return
  1696. alpha = colors[0, 0, 3]
  1697. if np.allclose(alpha, colors[:, :, 3]):
  1698. # single alpha value
  1699. gc.set_alpha(alpha)
  1700. self.check_gc(gc)
  1701. output(name, Op.shading)
  1702. else:
  1703. # varying alpha: use a soft mask
  1704. alpha = colors[:, :, 3][:, :, None]
  1705. _, smask_ob = self.file.addGouraudTriangles(tpoints, alpha)
  1706. gstate = self.file._soft_mask_state(smask_ob)
  1707. output(Op.gsave, gstate, Op.setgstate,
  1708. name, Op.shading,
  1709. Op.grestore)
  1710. def _setup_textpos(self, x, y, angle, oldx=0, oldy=0, oldangle=0):
  1711. if angle == oldangle == 0:
  1712. self.file.output(x - oldx, y - oldy, Op.textpos)
  1713. else:
  1714. angle = math.radians(angle)
  1715. self.file.output(math.cos(angle), math.sin(angle),
  1716. -math.sin(angle), math.cos(angle),
  1717. x, y, Op.textmatrix)
  1718. self.file.output(0, 0, Op.textpos)
  1719. def draw_mathtext(self, gc, x, y, s, prop, angle):
  1720. # TODO: fix positioning and encoding
  1721. width, height, descent, glyphs, rects, used_characters = \
  1722. self.mathtext_parser.parse(s, 72, prop)
  1723. self.file._character_tracker.merge(used_characters)
  1724. # When using Type 3 fonts, we can't use character codes higher
  1725. # than 255, so we use the "Do" command to render those
  1726. # instead.
  1727. global_fonttype = mpl.rcParams['pdf.fonttype']
  1728. # Set up a global transformation matrix for the whole math expression
  1729. a = math.radians(angle)
  1730. self.file.output(Op.gsave)
  1731. self.file.output(math.cos(a), math.sin(a),
  1732. -math.sin(a), math.cos(a),
  1733. x, y, Op.concat_matrix)
  1734. self.check_gc(gc, gc._rgb)
  1735. self.file.output(Op.begin_text)
  1736. prev_font = None, None
  1737. oldx, oldy = 0, 0
  1738. for ox, oy, fontname, fontsize, num, symbol_name in glyphs:
  1739. if is_opentype_cff_font(fontname):
  1740. fonttype = 42
  1741. else:
  1742. fonttype = global_fonttype
  1743. if fonttype == 42 or num <= 255:
  1744. self._setup_textpos(ox, oy, 0, oldx, oldy)
  1745. oldx, oldy = ox, oy
  1746. if (fontname, fontsize) != prev_font:
  1747. self.file.output(self.file.fontName(fontname), fontsize,
  1748. Op.selectfont)
  1749. prev_font = fontname, fontsize
  1750. self.file.output(self.encode_string(chr(num), fonttype),
  1751. Op.show)
  1752. self.file.output(Op.end_text)
  1753. # If using Type 3 fonts, render all of the multi-byte characters
  1754. # as XObjects using the 'Do' command.
  1755. if global_fonttype == 3:
  1756. for ox, oy, fontname, fontsize, num, symbol_name in glyphs:
  1757. if is_opentype_cff_font(fontname):
  1758. fonttype = 42
  1759. else:
  1760. fonttype = global_fonttype
  1761. if fonttype == 3 and num > 255:
  1762. self.file.fontName(fontname)
  1763. self.file.output(Op.gsave,
  1764. 0.001 * fontsize, 0,
  1765. 0, 0.001 * fontsize,
  1766. ox, oy, Op.concat_matrix)
  1767. name = self.file._get_xobject_symbol_name(
  1768. fontname, symbol_name)
  1769. self.file.output(Name(name), Op.use_xobject)
  1770. self.file.output(Op.grestore)
  1771. # Draw any horizontal lines in the math layout
  1772. for ox, oy, width, height in rects:
  1773. self.file.output(Op.gsave, ox, oy, width, height,
  1774. Op.rectangle, Op.fill, Op.grestore)
  1775. # Pop off the global transformation
  1776. self.file.output(Op.grestore)
  1777. @cbook._delete_parameter("3.3", "ismath")
  1778. def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
  1779. # docstring inherited
  1780. texmanager = self.get_texmanager()
  1781. fontsize = prop.get_size_in_points()
  1782. dvifile = texmanager.make_dvi(s, fontsize)
  1783. with dviread.Dvi(dvifile, 72) as dvi:
  1784. page, = dvi
  1785. # Gather font information and do some setup for combining
  1786. # characters into strings. The variable seq will contain a
  1787. # sequence of font and text entries. A font entry is a list
  1788. # ['font', name, size] where name is a Name object for the
  1789. # font. A text entry is ['text', x, y, glyphs, x+w] where x
  1790. # and y are the starting coordinates, w is the width, and
  1791. # glyphs is a list; in this phase it will always contain just
  1792. # one one-character string, but later it may have longer
  1793. # strings interspersed with kern amounts.
  1794. oldfont, seq = None, []
  1795. for x1, y1, dvifont, glyph, width in page.text:
  1796. if dvifont != oldfont:
  1797. pdfname = self.file.dviFontName(dvifont)
  1798. seq += [['font', pdfname, dvifont.size]]
  1799. oldfont = dvifont
  1800. seq += [['text', x1, y1, [bytes([glyph])], x1+width]]
  1801. # Find consecutive text strings with constant y coordinate and
  1802. # combine into a sequence of strings and kerns, or just one
  1803. # string (if any kerns would be less than 0.1 points).
  1804. i, curx, fontsize = 0, 0, None
  1805. while i < len(seq)-1:
  1806. elt, nxt = seq[i:i+2]
  1807. if elt[0] == 'font':
  1808. fontsize = elt[2]
  1809. elif elt[0] == nxt[0] == 'text' and elt[2] == nxt[2]:
  1810. offset = elt[4] - nxt[1]
  1811. if abs(offset) < 0.1:
  1812. elt[3][-1] += nxt[3][0]
  1813. elt[4] += nxt[4]-nxt[1]
  1814. else:
  1815. elt[3] += [offset*1000.0/fontsize, nxt[3][0]]
  1816. elt[4] = nxt[4]
  1817. del seq[i+1]
  1818. continue
  1819. i += 1
  1820. # Create a transform to map the dvi contents to the canvas.
  1821. mytrans = Affine2D().rotate_deg(angle).translate(x, y)
  1822. # Output the text.
  1823. self.check_gc(gc, gc._rgb)
  1824. self.file.output(Op.begin_text)
  1825. curx, cury, oldx, oldy = 0, 0, 0, 0
  1826. for elt in seq:
  1827. if elt[0] == 'font':
  1828. self.file.output(elt[1], elt[2], Op.selectfont)
  1829. elif elt[0] == 'text':
  1830. curx, cury = mytrans.transform((elt[1], elt[2]))
  1831. self._setup_textpos(curx, cury, angle, oldx, oldy)
  1832. oldx, oldy = curx, cury
  1833. if len(elt[3]) == 1:
  1834. self.file.output(elt[3][0], Op.show)
  1835. else:
  1836. self.file.output(elt[3], Op.showkern)
  1837. else:
  1838. assert False
  1839. self.file.output(Op.end_text)
  1840. # Then output the boxes (e.g., variable-length lines of square
  1841. # roots).
  1842. boxgc = self.new_gc()
  1843. boxgc.copy_properties(gc)
  1844. boxgc.set_linewidth(0)
  1845. pathops = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
  1846. Path.CLOSEPOLY]
  1847. for x1, y1, h, w in page.boxes:
  1848. path = Path([[x1, y1], [x1+w, y1], [x1+w, y1+h], [x1, y1+h],
  1849. [0, 0]], pathops)
  1850. self.draw_path(boxgc, path, mytrans, gc._rgb)
  1851. def encode_string(self, s, fonttype):
  1852. if fonttype in (1, 3):
  1853. return s.encode('cp1252', 'replace')
  1854. return s.encode('utf-16be', 'replace')
  1855. def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
  1856. # docstring inherited
  1857. # TODO: combine consecutive texts into one BT/ET delimited section
  1858. self.check_gc(gc, gc._rgb)
  1859. if ismath:
  1860. return self.draw_mathtext(gc, x, y, s, prop, angle)
  1861. fontsize = prop.get_size_in_points()
  1862. if mpl.rcParams['pdf.use14corefonts']:
  1863. font = self._get_font_afm(prop)
  1864. fonttype = 1
  1865. else:
  1866. font = self._get_font_ttf(prop)
  1867. self.file._character_tracker.track(font, s)
  1868. fonttype = mpl.rcParams['pdf.fonttype']
  1869. # We can't subset all OpenType fonts, so switch to Type 42
  1870. # in that case.
  1871. if is_opentype_cff_font(font.fname):
  1872. fonttype = 42
  1873. # If fonttype != 3 or there are no multibyte characters, emit the whole
  1874. # string at once.
  1875. if fonttype != 3 or all(ord(char) <= 255 for char in s):
  1876. self.file.output(Op.begin_text,
  1877. self.file.fontName(prop), fontsize, Op.selectfont)
  1878. self._setup_textpos(x, y, angle)
  1879. self.file.output(self.encode_string(s, fonttype), Op.show,
  1880. Op.end_text)
  1881. # There is no way to access multibyte characters of Type 3 fonts, as
  1882. # they cannot have a CIDMap. Therefore, in this case we break the
  1883. # string into chunks, where each chunk contains either a string of
  1884. # consecutive 1-byte characters or a single multibyte character. Each
  1885. # chunk is emitted with a separate command: 1-byte characters use the
  1886. # regular text show command (Tj), whereas multibyte characters use
  1887. # the XObject command (Do). (If using Type 42 fonts, all of this
  1888. # complication is avoided, but of course, those fonts can not be
  1889. # subsetted.)
  1890. else:
  1891. singlebyte_chunks = [] # List of (start_x, list-of-1-byte-chars).
  1892. multibyte_glyphs = [] # List of (start_x, glyph_index).
  1893. prev_was_singlebyte = False
  1894. for char, (glyph_idx, glyph_x) in zip(
  1895. s,
  1896. _text_layout.layout(s, font, kern_mode=KERNING_UNFITTED)):
  1897. if ord(char) <= 255:
  1898. if prev_was_singlebyte:
  1899. singlebyte_chunks[-1][1].append(char)
  1900. else:
  1901. singlebyte_chunks.append((glyph_x, [char]))
  1902. prev_was_singlebyte = True
  1903. else:
  1904. multibyte_glyphs.append((glyph_x, glyph_idx))
  1905. prev_was_singlebyte = False
  1906. # Do the rotation and global translation as a single matrix
  1907. # concatenation up front
  1908. self.file.output(Op.gsave)
  1909. a = math.radians(angle)
  1910. self.file.output(math.cos(a), math.sin(a),
  1911. -math.sin(a), math.cos(a),
  1912. x, y, Op.concat_matrix)
  1913. # Emit all the 1-byte characters in a BT/ET group.
  1914. self.file.output(Op.begin_text,
  1915. self.file.fontName(prop), fontsize, Op.selectfont)
  1916. prev_start_x = 0
  1917. for start_x, chars in singlebyte_chunks:
  1918. self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0)
  1919. self.file.output(self.encode_string(''.join(chars), fonttype),
  1920. Op.show)
  1921. prev_start_x = start_x
  1922. self.file.output(Op.end_text)
  1923. # Then emit all the multibyte characters, one at a time.
  1924. for start_x, glyph_idx in multibyte_glyphs:
  1925. glyph_name = font.get_glyph_name(glyph_idx)
  1926. self.file.output(Op.gsave)
  1927. self.file.output(0.001 * fontsize, 0,
  1928. 0, 0.001 * fontsize,
  1929. start_x, 0, Op.concat_matrix)
  1930. name = self.file._get_xobject_symbol_name(
  1931. font.fname, glyph_name)
  1932. self.file.output(Name(name), Op.use_xobject)
  1933. self.file.output(Op.grestore)
  1934. self.file.output(Op.grestore)
  1935. def new_gc(self):
  1936. # docstring inherited
  1937. return GraphicsContextPdf(self.file)
  1938. class GraphicsContextPdf(GraphicsContextBase):
  1939. def __init__(self, file):
  1940. GraphicsContextBase.__init__(self)
  1941. self._fillcolor = (0.0, 0.0, 0.0)
  1942. self._effective_alphas = (1.0, 1.0)
  1943. self.file = file
  1944. self.parent = None
  1945. def __repr__(self):
  1946. d = dict(self.__dict__)
  1947. del d['file']
  1948. del d['parent']
  1949. return repr(d)
  1950. def stroke(self):
  1951. """
  1952. Predicate: does the path need to be stroked (its outline drawn)?
  1953. This tests for the various conditions that disable stroking
  1954. the path, in which case it would presumably be filled.
  1955. """
  1956. # _linewidth > 0: in pdf a line of width 0 is drawn at minimum
  1957. # possible device width, but e.g., agg doesn't draw at all
  1958. return (self._linewidth > 0 and self._alpha > 0 and
  1959. (len(self._rgb) <= 3 or self._rgb[3] != 0.0))
  1960. def fill(self, *args):
  1961. """
  1962. Predicate: does the path need to be filled?
  1963. An optional argument can be used to specify an alternative
  1964. _fillcolor, as needed by RendererPdf.draw_markers.
  1965. """
  1966. if len(args):
  1967. _fillcolor = args[0]
  1968. else:
  1969. _fillcolor = self._fillcolor
  1970. return (self._hatch or
  1971. (_fillcolor is not None and
  1972. (len(_fillcolor) <= 3 or _fillcolor[3] != 0.0)))
  1973. def paint(self):
  1974. """
  1975. Return the appropriate pdf operator to cause the path to be
  1976. stroked, filled, or both.
  1977. """
  1978. return Op.paint_path(self.fill(), self.stroke())
  1979. capstyles = {'butt': 0, 'round': 1, 'projecting': 2}
  1980. joinstyles = {'miter': 0, 'round': 1, 'bevel': 2}
  1981. def capstyle_cmd(self, style):
  1982. return [self.capstyles[style], Op.setlinecap]
  1983. def joinstyle_cmd(self, style):
  1984. return [self.joinstyles[style], Op.setlinejoin]
  1985. def linewidth_cmd(self, width):
  1986. return [width, Op.setlinewidth]
  1987. def dash_cmd(self, dashes):
  1988. offset, dash = dashes
  1989. if dash is None:
  1990. dash = []
  1991. offset = 0
  1992. return [list(dash), offset, Op.setdash]
  1993. def alpha_cmd(self, alpha, forced, effective_alphas):
  1994. name = self.file.alphaState(effective_alphas)
  1995. return [name, Op.setgstate]
  1996. def hatch_cmd(self, hatch, hatch_color):
  1997. if not hatch:
  1998. if self._fillcolor is not None:
  1999. return self.fillcolor_cmd(self._fillcolor)
  2000. else:
  2001. return [Name('DeviceRGB'), Op.setcolorspace_nonstroke]
  2002. else:
  2003. hatch_style = (hatch_color, self._fillcolor, hatch)
  2004. name = self.file.hatchPattern(hatch_style)
  2005. return [Name('Pattern'), Op.setcolorspace_nonstroke,
  2006. name, Op.setcolor_nonstroke]
  2007. def rgb_cmd(self, rgb):
  2008. if mpl.rcParams['pdf.inheritcolor']:
  2009. return []
  2010. if rgb[0] == rgb[1] == rgb[2]:
  2011. return [rgb[0], Op.setgray_stroke]
  2012. else:
  2013. return [*rgb[:3], Op.setrgb_stroke]
  2014. def fillcolor_cmd(self, rgb):
  2015. if rgb is None or mpl.rcParams['pdf.inheritcolor']:
  2016. return []
  2017. elif rgb[0] == rgb[1] == rgb[2]:
  2018. return [rgb[0], Op.setgray_nonstroke]
  2019. else:
  2020. return [*rgb[:3], Op.setrgb_nonstroke]
  2021. def push(self):
  2022. parent = GraphicsContextPdf(self.file)
  2023. parent.copy_properties(self)
  2024. parent.parent = self.parent
  2025. self.parent = parent
  2026. return [Op.gsave]
  2027. def pop(self):
  2028. assert self.parent is not None
  2029. self.copy_properties(self.parent)
  2030. self.parent = self.parent.parent
  2031. return [Op.grestore]
  2032. def clip_cmd(self, cliprect, clippath):
  2033. """Set clip rectangle. Calls `.pop()` and `.push()`."""
  2034. cmds = []
  2035. # Pop graphics state until we hit the right one or the stack is empty
  2036. while ((self._cliprect, self._clippath) != (cliprect, clippath)
  2037. and self.parent is not None):
  2038. cmds.extend(self.pop())
  2039. # Unless we hit the right one, set the clip polygon
  2040. if ((self._cliprect, self._clippath) != (cliprect, clippath) or
  2041. self.parent is None):
  2042. cmds.extend(self.push())
  2043. if self._cliprect != cliprect:
  2044. cmds.extend([cliprect, Op.rectangle, Op.clip, Op.endpath])
  2045. if self._clippath != clippath:
  2046. path, affine = clippath.get_transformed_path_and_affine()
  2047. cmds.extend(
  2048. PdfFile.pathOperations(path, affine, simplify=False) +
  2049. [Op.clip, Op.endpath])
  2050. return cmds
  2051. commands = (
  2052. # must come first since may pop
  2053. (('_cliprect', '_clippath'), clip_cmd),
  2054. (('_alpha', '_forced_alpha', '_effective_alphas'), alpha_cmd),
  2055. (('_capstyle',), capstyle_cmd),
  2056. (('_fillcolor',), fillcolor_cmd),
  2057. (('_joinstyle',), joinstyle_cmd),
  2058. (('_linewidth',), linewidth_cmd),
  2059. (('_dashes',), dash_cmd),
  2060. (('_rgb',), rgb_cmd),
  2061. # must come after fillcolor and rgb
  2062. (('_hatch', '_hatch_color'), hatch_cmd),
  2063. )
  2064. def delta(self, other):
  2065. """
  2066. Copy properties of other into self and return PDF commands
  2067. needed to transform self into other.
  2068. """
  2069. cmds = []
  2070. fill_performed = False
  2071. for params, cmd in self.commands:
  2072. different = False
  2073. for p in params:
  2074. ours = getattr(self, p)
  2075. theirs = getattr(other, p)
  2076. try:
  2077. if ours is None or theirs is None:
  2078. different = ours is not theirs
  2079. else:
  2080. different = bool(ours != theirs)
  2081. except ValueError:
  2082. ours = np.asarray(ours)
  2083. theirs = np.asarray(theirs)
  2084. different = (ours.shape != theirs.shape or
  2085. np.any(ours != theirs))
  2086. if different:
  2087. break
  2088. # Need to update hatching if we also updated fillcolor
  2089. if params == ('_hatch', '_hatch_color') and fill_performed:
  2090. different = True
  2091. if different:
  2092. if params == ('_fillcolor',):
  2093. fill_performed = True
  2094. theirs = [getattr(other, p) for p in params]
  2095. cmds.extend(cmd(self, *theirs))
  2096. for p in params:
  2097. setattr(self, p, getattr(other, p))
  2098. return cmds
  2099. def copy_properties(self, other):
  2100. """
  2101. Copy properties of other into self.
  2102. """
  2103. GraphicsContextBase.copy_properties(self, other)
  2104. fillcolor = getattr(other, '_fillcolor', self._fillcolor)
  2105. effective_alphas = getattr(other, '_effective_alphas',
  2106. self._effective_alphas)
  2107. self._fillcolor = fillcolor
  2108. self._effective_alphas = effective_alphas
  2109. def finalize(self):
  2110. """
  2111. Make sure every pushed graphics state is popped.
  2112. """
  2113. cmds = []
  2114. while self.parent is not None:
  2115. cmds.extend(self.pop())
  2116. return cmds
  2117. class PdfPages:
  2118. """
  2119. A multi-page PDF file.
  2120. Examples
  2121. --------
  2122. >>> import matplotlib.pyplot as plt
  2123. >>> # Initialize:
  2124. >>> with PdfPages('foo.pdf') as pdf:
  2125. ... # As many times as you like, create a figure fig and save it:
  2126. ... fig = plt.figure()
  2127. ... pdf.savefig(fig)
  2128. ... # When no figure is specified the current figure is saved
  2129. ... pdf.savefig()
  2130. Notes
  2131. -----
  2132. In reality `PdfPages` is a thin wrapper around `PdfFile`, in order to avoid
  2133. confusion when using `~.pyplot.savefig` and forgetting the format argument.
  2134. """
  2135. __slots__ = ('_file', 'keep_empty')
  2136. def __init__(self, filename, keep_empty=True, metadata=None):
  2137. """
  2138. Create a new PdfPages object.
  2139. Parameters
  2140. ----------
  2141. filename : str or path-like or file-like
  2142. Plots using `PdfPages.savefig` will be written to a file at this
  2143. location. The file is opened at once and any older file with the
  2144. same name is overwritten.
  2145. keep_empty : bool, optional
  2146. If set to False, then empty pdf files will be deleted automatically
  2147. when closed.
  2148. metadata : dict, optional
  2149. Information dictionary object (see PDF reference section 10.2.1
  2150. 'Document Information Dictionary'), e.g.:
  2151. ``{'Creator': 'My software', 'Author': 'Me', 'Title': 'Awesome'}``.
  2152. The standard keys are 'Title', 'Author', 'Subject', 'Keywords',
  2153. 'Creator', 'Producer', 'CreationDate', 'ModDate', and
  2154. 'Trapped'. Values have been predefined for 'Creator', 'Producer'
  2155. and 'CreationDate'. They can be removed by setting them to `None`.
  2156. """
  2157. self._file = PdfFile(filename, metadata=metadata)
  2158. self.keep_empty = keep_empty
  2159. def __enter__(self):
  2160. return self
  2161. def __exit__(self, exc_type, exc_val, exc_tb):
  2162. self.close()
  2163. def close(self):
  2164. """
  2165. Finalize this object, making the underlying file a complete
  2166. PDF file.
  2167. """
  2168. self._file.finalize()
  2169. self._file.close()
  2170. if (self.get_pagecount() == 0 and not self.keep_empty and
  2171. not self._file.passed_in_file_object):
  2172. os.remove(self._file.fh.name)
  2173. self._file = None
  2174. def infodict(self):
  2175. """
  2176. Return a modifiable information dictionary object
  2177. (see PDF reference section 10.2.1 'Document Information
  2178. Dictionary').
  2179. """
  2180. return self._file.infoDict
  2181. def savefig(self, figure=None, **kwargs):
  2182. """
  2183. Save a `.Figure` to this file as a new page.
  2184. Any other keyword arguments are passed to `~.Figure.savefig`.
  2185. Parameters
  2186. ----------
  2187. figure : `.Figure` or int, optional
  2188. Specifies what figure is saved to file. If not specified, the
  2189. active figure is saved. If a `.Figure` instance is provided, this
  2190. figure is saved. If an int is specified, the figure instance to
  2191. save is looked up by number.
  2192. """
  2193. if not isinstance(figure, Figure):
  2194. if figure is None:
  2195. manager = Gcf.get_active()
  2196. else:
  2197. manager = Gcf.get_fig_manager(figure)
  2198. if manager is None:
  2199. raise ValueError("No figure {}".format(figure))
  2200. figure = manager.canvas.figure
  2201. # Force use of pdf backend, as PdfPages is tightly coupled with it.
  2202. try:
  2203. orig_canvas = figure.canvas
  2204. figure.canvas = FigureCanvasPdf(figure)
  2205. figure.savefig(self, format="pdf", **kwargs)
  2206. finally:
  2207. figure.canvas = orig_canvas
  2208. def get_pagecount(self):
  2209. """Return the current number of pages in the multipage pdf file."""
  2210. return len(self._file.pageList)
  2211. def attach_note(self, text, positionRect=[-100, -100, 0, 0]):
  2212. """
  2213. Add a new text note to the page to be saved next. The optional
  2214. positionRect specifies the position of the new note on the
  2215. page. It is outside the page per default to make sure it is
  2216. invisible on printouts.
  2217. """
  2218. self._file.newTextnote(text, positionRect)
  2219. class FigureCanvasPdf(FigureCanvasBase):
  2220. """
  2221. The canvas the figure renders into. Calls the draw and print fig
  2222. methods, creates the renderers, etc...
  2223. Attributes
  2224. ----------
  2225. figure : `matplotlib.figure.Figure`
  2226. A high-level Figure instance
  2227. """
  2228. fixed_dpi = 72
  2229. filetypes = {'pdf': 'Portable Document Format'}
  2230. def get_default_filetype(self):
  2231. return 'pdf'
  2232. @_check_savefig_extra_args
  2233. def print_pdf(self, filename, *,
  2234. dpi=72, # dpi to use for images
  2235. bbox_inches_restore=None, metadata=None):
  2236. self.figure.set_dpi(72) # there are 72 pdf points to an inch
  2237. width, height = self.figure.get_size_inches()
  2238. if isinstance(filename, PdfPages):
  2239. file = filename._file
  2240. else:
  2241. file = PdfFile(filename, metadata=metadata)
  2242. try:
  2243. file.newPage(width, height)
  2244. renderer = MixedModeRenderer(
  2245. self.figure, width, height, dpi,
  2246. RendererPdf(file, dpi, height, width),
  2247. bbox_inches_restore=bbox_inches_restore)
  2248. self.figure.draw(renderer)
  2249. renderer.finalize()
  2250. if not isinstance(filename, PdfPages):
  2251. file.finalize()
  2252. finally:
  2253. if isinstance(filename, PdfPages): # finish off this page
  2254. file.endStream()
  2255. else: # we opened the file above; now finish it off
  2256. file.close()
  2257. FigureManagerPdf = FigureManagerBase
  2258. @_Backend.export
  2259. class _BackendPdf(_Backend):
  2260. FigureCanvas = FigureCanvasPdf