backend_pdf.py 94 KB

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