backend_pdf.py 103 KB

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