backend_pdf.py 104 KB

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