Coverage for /usr/lib/python3/dist-packages/matplotlib/backends/backend_pdf.py: 63%

1433 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2A PDF Matplotlib backend. 

3 

4Author: Jouni K Seppänen <jks@iki.fi> and others. 

5""" 

6 

7import codecs 

8from datetime import datetime 

9from enum import Enum 

10from functools import total_ordering 

11from io import BytesIO 

12import itertools 

13import logging 

14import math 

15import os 

16import string 

17import struct 

18import sys 

19import time 

20import types 

21import warnings 

22import zlib 

23 

24import numpy as np 

25from PIL import Image 

26 

27import matplotlib as mpl 

28from matplotlib import _api, _text_helpers, _type1font, cbook, dviread 

29from matplotlib._pylab_helpers import Gcf 

30from matplotlib.backend_bases import ( 

31 _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, 

32 RendererBase) 

33from matplotlib.backends.backend_mixed import MixedModeRenderer 

34from matplotlib.figure import Figure 

35from matplotlib.font_manager import get_font, fontManager as _fontManager 

36from matplotlib._afm import AFM 

37from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, 

38 LOAD_NO_HINTING, KERNING_UNFITTED, FT2Font) 

39from matplotlib.transforms import Affine2D, BboxBase 

40from matplotlib.path import Path 

41from matplotlib.dates import UTC 

42from matplotlib import _path 

43from . import _backend_pdf_ps 

44 

45_log = logging.getLogger(__name__) 

46 

47# Overview 

48# 

49# The low-level knowledge about pdf syntax lies mainly in the pdfRepr 

50# function and the classes Reference, Name, Operator, and Stream. The 

51# PdfFile class knows about the overall structure of pdf documents. 

52# It provides a "write" method for writing arbitrary strings in the 

53# file, and an "output" method that passes objects through the pdfRepr 

54# function before writing them in the file. The output method is 

55# called by the RendererPdf class, which contains the various draw_foo 

56# methods. RendererPdf contains a GraphicsContextPdf instance, and 

57# each draw_foo calls self.check_gc before outputting commands. This 

58# method checks whether the pdf graphics state needs to be modified 

59# and outputs the necessary commands. GraphicsContextPdf represents 

60# the graphics state, and its "delta" method returns the commands that 

61# modify the state. 

62 

63# Add "pdf.use14corefonts: True" in your configuration file to use only 

64# the 14 PDF core fonts. These fonts do not need to be embedded; every 

65# PDF viewing application is required to have them. This results in very 

66# light PDF files you can use directly in LaTeX or ConTeXt documents 

67# generated with pdfTeX, without any conversion. 

68 

69# These fonts are: Helvetica, Helvetica-Bold, Helvetica-Oblique, 

70# Helvetica-BoldOblique, Courier, Courier-Bold, Courier-Oblique, 

71# Courier-BoldOblique, Times-Roman, Times-Bold, Times-Italic, 

72# Times-BoldItalic, Symbol, ZapfDingbats. 

73# 

74# Some tricky points: 

75# 

76# 1. The clip path can only be widened by popping from the state 

77# stack. Thus the state must be pushed onto the stack before narrowing 

78# the clip path. This is taken care of by GraphicsContextPdf. 

79# 

80# 2. Sometimes it is necessary to refer to something (e.g., font, 

81# image, or extended graphics state, which contains the alpha value) 

82# in the page stream by a name that needs to be defined outside the 

83# stream. PdfFile provides the methods fontName, imageObject, and 

84# alphaState for this purpose. The implementations of these methods 

85# should perhaps be generalized. 

86 

87# TODOs: 

88# 

89# * encoding of fonts, including mathtext fonts and Unicode support 

90# * TTF support has lots of small TODOs, e.g., how do you know if a font 

91# is serif/sans-serif, or symbolic/non-symbolic? 

92# * draw_quad_mesh 

93 

94 

95@_api.deprecated("3.6", alternative="a vendored copy of _fill") 

96def fill(strings, linelen=75): 

97 return _fill(strings, linelen=linelen) 

98 

99 

100def _fill(strings, linelen=75): 

101 """ 

102 Make one string from sequence of strings, with whitespace in between. 

103 

104 The whitespace is chosen to form lines of at most *linelen* characters, 

105 if possible. 

106 """ 

107 currpos = 0 

108 lasti = 0 

109 result = [] 

110 for i, s in enumerate(strings): 

111 length = len(s) 

112 if currpos + length < linelen: 

113 currpos += length + 1 

114 else: 

115 result.append(b' '.join(strings[lasti:i])) 

116 lasti = i 

117 currpos = length 

118 result.append(b' '.join(strings[lasti:])) 

119 return b'\n'.join(result) 

120 

121 

122def _create_pdf_info_dict(backend, metadata): 

123 """ 

124 Create a PDF infoDict based on user-supplied metadata. 

125 

126 A default ``Creator``, ``Producer``, and ``CreationDate`` are added, though 

127 the user metadata may override it. The date may be the current time, or a 

128 time set by the ``SOURCE_DATE_EPOCH`` environment variable. 

129 

130 Metadata is verified to have the correct keys and their expected types. Any 

131 unknown keys/types will raise a warning. 

132 

133 Parameters 

134 ---------- 

135 backend : str 

136 The name of the backend to use in the Producer value. 

137 

138 metadata : dict[str, Union[str, datetime, Name]] 

139 A dictionary of metadata supplied by the user with information 

140 following the PDF specification, also defined in 

141 `~.backend_pdf.PdfPages` below. 

142 

143 If any value is *None*, then the key will be removed. This can be used 

144 to remove any pre-defined values. 

145 

146 Returns 

147 ------- 

148 dict[str, Union[str, datetime, Name]] 

149 A validated dictionary of metadata. 

150 """ 

151 

152 # get source date from SOURCE_DATE_EPOCH, if set 

153 # See https://reproducible-builds.org/specs/source-date-epoch/ 

154 source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") 

155 if source_date_epoch: 

156 source_date = datetime.utcfromtimestamp(int(source_date_epoch)) 

157 source_date = source_date.replace(tzinfo=UTC) 

158 else: 

159 source_date = datetime.today() 

160 

161 info = { 

162 'Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org', 

163 'Producer': f'Matplotlib {backend} backend v{mpl.__version__}', 

164 'CreationDate': source_date, 

165 **metadata 

166 } 

167 info = {k: v for (k, v) in info.items() if v is not None} 

168 

169 def is_string_like(x): 

170 return isinstance(x, str) 

171 is_string_like.text_for_warning = "an instance of str" 

172 

173 def is_date(x): 

174 return isinstance(x, datetime) 

175 is_date.text_for_warning = "an instance of datetime.datetime" 

176 

177 def check_trapped(x): 

178 if isinstance(x, Name): 

179 return x.name in (b'True', b'False', b'Unknown') 

180 else: 

181 return x in ('True', 'False', 'Unknown') 

182 check_trapped.text_for_warning = 'one of {"True", "False", "Unknown"}' 

183 

184 keywords = { 

185 'Title': is_string_like, 

186 'Author': is_string_like, 

187 'Subject': is_string_like, 

188 'Keywords': is_string_like, 

189 'Creator': is_string_like, 

190 'Producer': is_string_like, 

191 'CreationDate': is_date, 

192 'ModDate': is_date, 

193 'Trapped': check_trapped, 

194 } 

195 for k in info: 

196 if k not in keywords: 

197 _api.warn_external(f'Unknown infodict keyword: {k!r}. ' 

198 f'Must be one of {set(keywords)!r}.') 

199 elif not keywords[k](info[k]): 

200 _api.warn_external(f'Bad value for infodict keyword {k}. ' 

201 f'Got {info[k]!r} which is not ' 

202 f'{keywords[k].text_for_warning}.') 

203 if 'Trapped' in info: 

204 info['Trapped'] = Name(info['Trapped']) 

205 

206 return info 

207 

208 

209def _datetime_to_pdf(d): 

210 """ 

211 Convert a datetime to a PDF string representing it. 

212 

213 Used for PDF and PGF. 

214 """ 

215 r = d.strftime('D:%Y%m%d%H%M%S') 

216 z = d.utcoffset() 

217 if z is not None: 

218 z = z.seconds 

219 else: 

220 if time.daylight: 

221 z = time.altzone 

222 else: 

223 z = time.timezone 

224 if z == 0: 

225 r += 'Z' 

226 elif z < 0: 

227 r += "+%02d'%02d'" % ((-z) // 3600, (-z) % 3600) 

228 else: 

229 r += "-%02d'%02d'" % (z // 3600, z % 3600) 

230 return r 

231 

232 

233def _calculate_quad_point_coordinates(x, y, width, height, angle=0): 

234 """ 

235 Calculate the coordinates of rectangle when rotated by angle around x, y 

236 """ 

237 

238 angle = math.radians(-angle) 

239 sin_angle = math.sin(angle) 

240 cos_angle = math.cos(angle) 

241 a = x + height * sin_angle 

242 b = y + height * cos_angle 

243 c = x + width * cos_angle + height * sin_angle 

244 d = y - width * sin_angle + height * cos_angle 

245 e = x + width * cos_angle 

246 f = y - width * sin_angle 

247 return ((x, y), (e, f), (c, d), (a, b)) 

248 

249 

250def _get_coordinates_of_block(x, y, width, height, angle=0): 

251 """ 

252 Get the coordinates of rotated rectangle and rectangle that covers the 

253 rotated rectangle. 

254 """ 

255 

256 vertices = _calculate_quad_point_coordinates(x, y, width, 

257 height, angle) 

258 

259 # Find min and max values for rectangle 

260 # adjust so that QuadPoints is inside Rect 

261 # PDF docs says that QuadPoints should be ignored if any point lies 

262 # outside Rect, but for Acrobat it is enough that QuadPoints is on the 

263 # border of Rect. 

264 

265 pad = 0.00001 if angle % 90 else 0 

266 min_x = min(v[0] for v in vertices) - pad 

267 min_y = min(v[1] for v in vertices) - pad 

268 max_x = max(v[0] for v in vertices) + pad 

269 max_y = max(v[1] for v in vertices) + pad 

270 return (tuple(itertools.chain.from_iterable(vertices)), 

271 (min_x, min_y, max_x, max_y)) 

272 

273 

274def _get_link_annotation(gc, x, y, width, height, angle=0): 

275 """ 

276 Create a link annotation object for embedding URLs. 

277 """ 

278 quadpoints, rect = _get_coordinates_of_block(x, y, width, height, angle) 

279 link_annotation = { 

280 'Type': Name('Annot'), 

281 'Subtype': Name('Link'), 

282 'Rect': rect, 

283 'Border': [0, 0, 0], 

284 'A': { 

285 'S': Name('URI'), 

286 'URI': gc.get_url(), 

287 }, 

288 } 

289 if angle % 90: 

290 # Add QuadPoints 

291 link_annotation['QuadPoints'] = quadpoints 

292 return link_annotation 

293 

294 

295# PDF strings are supposed to be able to include any eight-bit data, except 

296# that unbalanced parens and backslashes must be escaped by a backslash. 

297# However, sf bug #2708559 shows that the carriage return character may get 

298# read as a newline; these characters correspond to \gamma and \Omega in TeX's 

299# math font encoding. Escaping them fixes the bug. 

300_str_escapes = str.maketrans({ 

301 '\\': '\\\\', '(': '\\(', ')': '\\)', '\n': '\\n', '\r': '\\r'}) 

302 

303 

304def pdfRepr(obj): 

305 """Map Python objects to PDF syntax.""" 

306 

307 # Some objects defined later have their own pdfRepr method. 

308 if hasattr(obj, 'pdfRepr'): 

309 return obj.pdfRepr() 

310 

311 # Floats. PDF does not have exponential notation (1.0e-10) so we 

312 # need to use %f with some precision. Perhaps the precision 

313 # should adapt to the magnitude of the number? 

314 elif isinstance(obj, (float, np.floating)): 

315 if not np.isfinite(obj): 

316 raise ValueError("Can only output finite numbers in PDF") 

317 r = b"%.10f" % obj 

318 return r.rstrip(b'0').rstrip(b'.') 

319 

320 # Booleans. Needs to be tested before integers since 

321 # isinstance(True, int) is true. 

322 elif isinstance(obj, bool): 

323 return [b'false', b'true'][obj] 

324 

325 # Integers are written as such. 

326 elif isinstance(obj, (int, np.integer)): 

327 return b"%d" % obj 

328 

329 # Non-ASCII Unicode strings are encoded in UTF-16BE with byte-order mark. 

330 elif isinstance(obj, str): 

331 return pdfRepr(obj.encode('ascii') if obj.isascii() 

332 else codecs.BOM_UTF16_BE + obj.encode('UTF-16BE')) 

333 

334 # Strings are written in parentheses, with backslashes and parens 

335 # escaped. Actually balanced parens are allowed, but it is 

336 # simpler to escape them all. TODO: cut long strings into lines; 

337 # I believe there is some maximum line length in PDF. 

338 # Despite the extra decode/encode, translate is faster than regex. 

339 elif isinstance(obj, bytes): 

340 return ( 

341 b'(' + 

342 obj.decode('latin-1').translate(_str_escapes).encode('latin-1') 

343 + b')') 

344 

345 # Dictionaries. The keys must be PDF names, so if we find strings 

346 # there, we make Name objects from them. The values may be 

347 # anything, so the caller must ensure that PDF names are 

348 # represented as Name objects. 

349 elif isinstance(obj, dict): 

350 return _fill([ 

351 b"<<", 

352 *[Name(k).pdfRepr() + b" " + pdfRepr(v) for k, v in obj.items()], 

353 b">>", 

354 ]) 

355 

356 # Lists. 

357 elif isinstance(obj, (list, tuple)): 

358 return _fill([b"[", *[pdfRepr(val) for val in obj], b"]"]) 

359 

360 # The null keyword. 

361 elif obj is None: 

362 return b'null' 

363 

364 # A date. 

365 elif isinstance(obj, datetime): 

366 return pdfRepr(_datetime_to_pdf(obj)) 

367 

368 # A bounding box 

369 elif isinstance(obj, BboxBase): 

370 return _fill([pdfRepr(val) for val in obj.bounds]) 

371 

372 else: 

373 raise TypeError("Don't know a PDF representation for {} objects" 

374 .format(type(obj))) 

375 

376 

377def _font_supports_glyph(fonttype, glyph): 

378 """ 

379 Returns True if the font is able to provide codepoint *glyph* in a PDF. 

380 

381 For a Type 3 font, this method returns True only for single-byte 

382 characters. For Type 42 fonts this method return True if the character is 

383 from the Basic Multilingual Plane. 

384 """ 

385 if fonttype == 3: 

386 return glyph <= 255 

387 if fonttype == 42: 

388 return glyph <= 65535 

389 raise NotImplementedError() 

390 

391 

392class Reference: 

393 """ 

394 PDF reference object. 

395 

396 Use PdfFile.reserveObject() to create References. 

397 """ 

398 

399 def __init__(self, id): 

400 self.id = id 

401 

402 def __repr__(self): 

403 return "<Reference %d>" % self.id 

404 

405 def pdfRepr(self): 

406 return b"%d 0 R" % self.id 

407 

408 def write(self, contents, file): 

409 write = file.write 

410 write(b"%d 0 obj\n" % self.id) 

411 write(pdfRepr(contents)) 

412 write(b"\nendobj\n") 

413 

414 

415@total_ordering 

416class Name: 

417 """PDF name object.""" 

418 __slots__ = ('name',) 

419 _hexify = {c: '#%02x' % c 

420 for c in {*range(256)} - {*range(ord('!'), ord('~') + 1)}} 

421 

422 def __init__(self, name): 

423 if isinstance(name, Name): 

424 self.name = name.name 

425 else: 

426 if isinstance(name, bytes): 

427 name = name.decode('ascii') 

428 self.name = name.translate(self._hexify).encode('ascii') 

429 

430 def __repr__(self): 

431 return "<Name %s>" % self.name 

432 

433 def __str__(self): 

434 return '/' + self.name.decode('ascii') 

435 

436 def __eq__(self, other): 

437 return isinstance(other, Name) and self.name == other.name 

438 

439 def __lt__(self, other): 

440 return isinstance(other, Name) and self.name < other.name 

441 

442 def __hash__(self): 

443 return hash(self.name) 

444 

445 @staticmethod 

446 @_api.deprecated("3.6") 

447 def hexify(match): 

448 return '#%02x' % ord(match.group()) 

449 

450 def pdfRepr(self): 

451 return b'/' + self.name 

452 

453 

454@_api.deprecated("3.6") 

455class Operator: 

456 __slots__ = ('op',) 

457 

458 def __init__(self, op): 

459 self.op = op 

460 

461 def __repr__(self): 

462 return '<Operator %s>' % self.op 

463 

464 def pdfRepr(self): 

465 return self.op 

466 

467 

468class Verbatim: 

469 """Store verbatim PDF command content for later inclusion in the stream.""" 

470 def __init__(self, x): 

471 self._x = x 

472 

473 def pdfRepr(self): 

474 return self._x 

475 

476 

477class Op(Enum): 

478 """PDF operators (not an exhaustive list).""" 

479 

480 close_fill_stroke = b'b' 

481 fill_stroke = b'B' 

482 fill = b'f' 

483 closepath = b'h' 

484 close_stroke = b's' 

485 stroke = b'S' 

486 endpath = b'n' 

487 begin_text = b'BT' 

488 end_text = b'ET' 

489 curveto = b'c' 

490 rectangle = b're' 

491 lineto = b'l' 

492 moveto = b'm' 

493 concat_matrix = b'cm' 

494 use_xobject = b'Do' 

495 setgray_stroke = b'G' 

496 setgray_nonstroke = b'g' 

497 setrgb_stroke = b'RG' 

498 setrgb_nonstroke = b'rg' 

499 setcolorspace_stroke = b'CS' 

500 setcolorspace_nonstroke = b'cs' 

501 setcolor_stroke = b'SCN' 

502 setcolor_nonstroke = b'scn' 

503 setdash = b'd' 

504 setlinejoin = b'j' 

505 setlinecap = b'J' 

506 setgstate = b'gs' 

507 gsave = b'q' 

508 grestore = b'Q' 

509 textpos = b'Td' 

510 selectfont = b'Tf' 

511 textmatrix = b'Tm' 

512 show = b'Tj' 

513 showkern = b'TJ' 

514 setlinewidth = b'w' 

515 clip = b'W' 

516 shading = b'sh' 

517 

518 op = _api.deprecated('3.6')(property(lambda self: self.value)) 

519 

520 def pdfRepr(self): 

521 return self.value 

522 

523 @classmethod 

524 def paint_path(cls, fill, stroke): 

525 """ 

526 Return the PDF operator to paint a path. 

527 

528 Parameters 

529 ---------- 

530 fill : bool 

531 Fill the path with the fill color. 

532 stroke : bool 

533 Stroke the outline of the path with the line color. 

534 """ 

535 if stroke: 

536 if fill: 

537 return cls.fill_stroke 

538 else: 

539 return cls.stroke 

540 else: 

541 if fill: 

542 return cls.fill 

543 else: 

544 return cls.endpath 

545 

546 

547class Stream: 

548 """ 

549 PDF stream object. 

550 

551 This has no pdfRepr method. Instead, call begin(), then output the 

552 contents of the stream by calling write(), and finally call end(). 

553 """ 

554 __slots__ = ('id', 'len', 'pdfFile', 'file', 'compressobj', 'extra', 'pos') 

555 

556 def __init__(self, id, len, file, extra=None, png=None): 

557 """ 

558 Parameters 

559 ---------- 

560 id : int 

561 Object id of the stream. 

562 len : Reference or None 

563 An unused Reference object for the length of the stream; 

564 None means to use a memory buffer so the length can be inlined. 

565 file : PdfFile 

566 The underlying object to write the stream to. 

567 extra : dict from Name to anything, or None 

568 Extra key-value pairs to include in the stream header. 

569 png : dict or None 

570 If the data is already png encoded, the decode parameters. 

571 """ 

572 self.id = id # object id 

573 self.len = len # id of length object 

574 self.pdfFile = file 

575 self.file = file.fh # file to which the stream is written 

576 self.compressobj = None # compression object 

577 if extra is None: 

578 self.extra = dict() 

579 else: 

580 self.extra = extra.copy() 

581 if png is not None: 

582 self.extra.update({'Filter': Name('FlateDecode'), 

583 'DecodeParms': png}) 

584 

585 self.pdfFile.recordXref(self.id) 

586 if mpl.rcParams['pdf.compression'] and not png: 

587 self.compressobj = zlib.compressobj( 

588 mpl.rcParams['pdf.compression']) 

589 if self.len is None: 

590 self.file = BytesIO() 

591 else: 

592 self._writeHeader() 

593 self.pos = self.file.tell() 

594 

595 def _writeHeader(self): 

596 write = self.file.write 

597 write(b"%d 0 obj\n" % self.id) 

598 dict = self.extra 

599 dict['Length'] = self.len 

600 if mpl.rcParams['pdf.compression']: 

601 dict['Filter'] = Name('FlateDecode') 

602 

603 write(pdfRepr(dict)) 

604 write(b"\nstream\n") 

605 

606 def end(self): 

607 """Finalize stream.""" 

608 

609 self._flush() 

610 if self.len is None: 

611 contents = self.file.getvalue() 

612 self.len = len(contents) 

613 self.file = self.pdfFile.fh 

614 self._writeHeader() 

615 self.file.write(contents) 

616 self.file.write(b"\nendstream\nendobj\n") 

617 else: 

618 length = self.file.tell() - self.pos 

619 self.file.write(b"\nendstream\nendobj\n") 

620 self.pdfFile.writeObject(self.len, length) 

621 

622 def write(self, data): 

623 """Write some data on the stream.""" 

624 

625 if self.compressobj is None: 

626 self.file.write(data) 

627 else: 

628 compressed = self.compressobj.compress(data) 

629 self.file.write(compressed) 

630 

631 def _flush(self): 

632 """Flush the compression object.""" 

633 

634 if self.compressobj is not None: 

635 compressed = self.compressobj.flush() 

636 self.file.write(compressed) 

637 self.compressobj = None 

638 

639 

640def _get_pdf_charprocs(font_path, glyph_ids): 

641 font = get_font(font_path, hinting_factor=1) 

642 conv = 1000 / font.units_per_EM # Conversion to PS units (1/1000's). 

643 procs = {} 

644 for glyph_id in glyph_ids: 

645 g = font.load_glyph(glyph_id, LOAD_NO_SCALE) 

646 # NOTE: We should be using round(), but instead use 

647 # "(x+.5).astype(int)" to keep backcompat with the old ttconv code 

648 # (this is different for negative x's). 

649 d1 = (np.array([g.horiAdvance, 0, *g.bbox]) * conv + .5).astype(int) 

650 v, c = font.get_path() 

651 v = (v * 64).astype(int) # Back to TrueType's internal units (1/64's). 

652 # Backcompat with old ttconv code: control points between two quads are 

653 # omitted if they are exactly at the midpoint between the control of 

654 # the quad before and the quad after, but ttconv used to interpolate 

655 # *after* conversion to PS units, causing floating point errors. Here 

656 # we reproduce ttconv's logic, detecting these "implicit" points and 

657 # re-interpolating them. Note that occasionally (e.g. with DejaVu Sans 

658 # glyph "0") a point detected as "implicit" is actually explicit, and 

659 # will thus be shifted by 1. 

660 quads, = np.nonzero(c == 3) 

661 quads_on = quads[1::2] 

662 quads_mid_on = np.array( 

663 sorted({*quads_on} & {*(quads - 1)} & {*(quads + 1)}), int) 

664 implicit = quads_mid_on[ 

665 (v[quads_mid_on] # As above, use astype(int), not // division 

666 == ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int)) 

667 .all(axis=1)] 

668 if (font.postscript_name, glyph_id) in [ 

669 ("DejaVuSerif-Italic", 77), # j 

670 ("DejaVuSerif-Italic", 135), # \AA 

671 ]: 

672 v[:, 0] -= 1 # Hard-coded backcompat (FreeType shifts glyph by 1). 

673 v = (v * conv + .5).astype(int) # As above re: truncation vs rounding. 

674 v[implicit] = (( # Fix implicit points; again, truncate. 

675 (v[implicit - 1] + v[implicit + 1]) / 2).astype(int)) 

676 procs[font.get_glyph_name(glyph_id)] = ( 

677 " ".join(map(str, d1)).encode("ascii") + b" d1\n" 

678 + _path.convert_to_string( 

679 Path(v, c), None, None, False, None, -1, 

680 # no code for quad Beziers triggers auto-conversion to cubics. 

681 [b"m", b"l", b"", b"c", b"h"], True) 

682 + b"f") 

683 return procs 

684 

685 

686class PdfFile: 

687 """PDF file object.""" 

688 

689 def __init__(self, filename, metadata=None): 

690 """ 

691 Parameters 

692 ---------- 

693 filename : str or path-like or file-like 

694 Output target; if a string, a file will be opened for writing. 

695 

696 metadata : dict from strings to strings and dates 

697 Information dictionary object (see PDF reference section 10.2.1 

698 'Document Information Dictionary'), e.g.: 

699 ``{'Creator': 'My software', 'Author': 'Me', 'Title': 'Awesome'}``. 

700 

701 The standard keys are 'Title', 'Author', 'Subject', 'Keywords', 

702 'Creator', 'Producer', 'CreationDate', 'ModDate', and 

703 'Trapped'. Values have been predefined for 'Creator', 'Producer' 

704 and 'CreationDate'. They can be removed by setting them to `None`. 

705 """ 

706 super().__init__() 

707 

708 self._object_seq = itertools.count(1) # consumed by reserveObject 

709 self.xrefTable = [[0, 65535, 'the zero object']] 

710 self.passed_in_file_object = False 

711 self.original_file_like = None 

712 self.tell_base = 0 

713 fh, opened = cbook.to_filehandle(filename, "wb", return_opened=True) 

714 if not opened: 

715 try: 

716 self.tell_base = filename.tell() 

717 except IOError: 

718 fh = BytesIO() 

719 self.original_file_like = filename 

720 else: 

721 fh = filename 

722 self.passed_in_file_object = True 

723 

724 self.fh = fh 

725 self.currentstream = None # stream object to write to, if any 

726 fh.write(b"%PDF-1.4\n") # 1.4 is the first version to have alpha 

727 # Output some eight-bit chars as a comment so various utilities 

728 # recognize the file as binary by looking at the first few 

729 # lines (see note in section 3.4.1 of the PDF reference). 

730 fh.write(b"%\254\334 \253\272\n") 

731 

732 self.rootObject = self.reserveObject('root') 

733 self.pagesObject = self.reserveObject('pages') 

734 self.pageList = [] 

735 self.fontObject = self.reserveObject('fonts') 

736 self._extGStateObject = self.reserveObject('extended graphics states') 

737 self.hatchObject = self.reserveObject('tiling patterns') 

738 self.gouraudObject = self.reserveObject('Gouraud triangles') 

739 self.XObjectObject = self.reserveObject('external objects') 

740 self.resourceObject = self.reserveObject('resources') 

741 

742 root = {'Type': Name('Catalog'), 

743 'Pages': self.pagesObject} 

744 self.writeObject(self.rootObject, root) 

745 

746 self.infoDict = _create_pdf_info_dict('pdf', metadata or {}) 

747 

748 self.fontNames = {} # maps filenames to internal font names 

749 self._internal_font_seq = (Name(f'F{i}') for i in itertools.count(1)) 

750 self.dviFontInfo = {} # maps dvi font names to embedding information 

751 # differently encoded Type-1 fonts may share the same descriptor 

752 self.type1Descriptors = {} 

753 self._character_tracker = _backend_pdf_ps.CharacterTracker() 

754 

755 self.alphaStates = {} # maps alpha values to graphics state objects 

756 self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1)) 

757 self._soft_mask_states = {} 

758 self._soft_mask_seq = (Name(f'SM{i}') for i in itertools.count(1)) 

759 self._soft_mask_groups = [] 

760 self.hatchPatterns = {} 

761 self._hatch_pattern_seq = (Name(f'H{i}') for i in itertools.count(1)) 

762 self.gouraudTriangles = [] 

763 

764 self._images = {} 

765 self._image_seq = (Name(f'I{i}') for i in itertools.count(1)) 

766 

767 self.markers = {} 

768 self.multi_byte_charprocs = {} 

769 

770 self.paths = [] 

771 

772 # A list of annotations for each page. Each entry is a tuple of the 

773 # overall Annots object reference that's inserted into the page object, 

774 # followed by a list of the actual annotations. 

775 self._annotations = [] 

776 # For annotations added before a page is created; mostly for the 

777 # purpose of newTextnote. 

778 self.pageAnnotations = [] 

779 

780 # The PDF spec recommends to include every procset 

781 procsets = [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()] 

782 

783 # Write resource dictionary. 

784 # Possibly TODO: more general ExtGState (graphics state dictionaries) 

785 # ColorSpace Pattern Shading Properties 

786 resources = {'Font': self.fontObject, 

787 'XObject': self.XObjectObject, 

788 'ExtGState': self._extGStateObject, 

789 'Pattern': self.hatchObject, 

790 'Shading': self.gouraudObject, 

791 'ProcSet': procsets} 

792 self.writeObject(self.resourceObject, resources) 

793 

794 def newPage(self, width, height): 

795 self.endStream() 

796 

797 self.width, self.height = width, height 

798 contentObject = self.reserveObject('page contents') 

799 annotsObject = self.reserveObject('annotations') 

800 thePage = {'Type': Name('Page'), 

801 'Parent': self.pagesObject, 

802 'Resources': self.resourceObject, 

803 'MediaBox': [0, 0, 72 * width, 72 * height], 

804 'Contents': contentObject, 

805 'Annots': annotsObject, 

806 } 

807 pageObject = self.reserveObject('page') 

808 self.writeObject(pageObject, thePage) 

809 self.pageList.append(pageObject) 

810 self._annotations.append((annotsObject, self.pageAnnotations)) 

811 

812 self.beginStream(contentObject.id, 

813 self.reserveObject('length of content stream')) 

814 # Initialize the pdf graphics state to match the default Matplotlib 

815 # graphics context (colorspace and joinstyle). 

816 self.output(Name('DeviceRGB'), Op.setcolorspace_stroke) 

817 self.output(Name('DeviceRGB'), Op.setcolorspace_nonstroke) 

818 self.output(GraphicsContextPdf.joinstyles['round'], Op.setlinejoin) 

819 

820 # Clear the list of annotations for the next page 

821 self.pageAnnotations = [] 

822 

823 def newTextnote(self, text, positionRect=[-100, -100, 0, 0]): 

824 # Create a new annotation of type text 

825 theNote = {'Type': Name('Annot'), 

826 'Subtype': Name('Text'), 

827 'Contents': text, 

828 'Rect': positionRect, 

829 } 

830 self.pageAnnotations.append(theNote) 

831 

832 def _get_subsetted_psname(self, ps_name, charmap): 

833 def toStr(n, base): 

834 if n < base: 

835 return string.ascii_uppercase[n] 

836 else: 

837 return ( 

838 toStr(n // base, base) + string.ascii_uppercase[n % base] 

839 ) 

840 

841 # encode to string using base 26 

842 hashed = hash(frozenset(charmap.keys())) % ((sys.maxsize + 1) * 2) 

843 prefix = toStr(hashed, 26) 

844 

845 # get first 6 characters from prefix 

846 return prefix[:6] + "+" + ps_name 

847 

848 def finalize(self): 

849 """Write out the various deferred objects and the pdf end matter.""" 

850 

851 self.endStream() 

852 self._write_annotations() 

853 self.writeFonts() 

854 self.writeExtGSTates() 

855 self._write_soft_mask_groups() 

856 self.writeHatches() 

857 self.writeGouraudTriangles() 

858 xobjects = { 

859 name: ob for image, name, ob in self._images.values()} 

860 for tup in self.markers.values(): 

861 xobjects[tup[0]] = tup[1] 

862 for name, value in self.multi_byte_charprocs.items(): 

863 xobjects[name] = value 

864 for name, path, trans, ob, join, cap, padding, filled, stroked \ 

865 in self.paths: 

866 xobjects[name] = ob 

867 self.writeObject(self.XObjectObject, xobjects) 

868 self.writeImages() 

869 self.writeMarkers() 

870 self.writePathCollectionTemplates() 

871 self.writeObject(self.pagesObject, 

872 {'Type': Name('Pages'), 

873 'Kids': self.pageList, 

874 'Count': len(self.pageList)}) 

875 self.writeInfoDict() 

876 

877 # Finalize the file 

878 self.writeXref() 

879 self.writeTrailer() 

880 

881 def close(self): 

882 """Flush all buffers and free all resources.""" 

883 

884 self.endStream() 

885 if self.passed_in_file_object: 

886 self.fh.flush() 

887 else: 

888 if self.original_file_like is not None: 

889 self.original_file_like.write(self.fh.getvalue()) 

890 self.fh.close() 

891 

892 def write(self, data): 

893 if self.currentstream is None: 

894 self.fh.write(data) 

895 else: 

896 self.currentstream.write(data) 

897 

898 def output(self, *data): 

899 self.write(_fill([pdfRepr(x) for x in data])) 

900 self.write(b'\n') 

901 

902 def beginStream(self, id, len, extra=None, png=None): 

903 assert self.currentstream is None 

904 self.currentstream = Stream(id, len, self, extra, png) 

905 

906 def endStream(self): 

907 if self.currentstream is not None: 

908 self.currentstream.end() 

909 self.currentstream = None 

910 

911 def outputStream(self, ref, data, *, extra=None): 

912 self.beginStream(ref.id, None, extra) 

913 self.currentstream.write(data) 

914 self.endStream() 

915 

916 def _write_annotations(self): 

917 for annotsObject, annotations in self._annotations: 

918 self.writeObject(annotsObject, annotations) 

919 

920 def fontName(self, fontprop): 

921 """ 

922 Select a font based on fontprop and return a name suitable for 

923 Op.selectfont. If fontprop is a string, it will be interpreted 

924 as the filename of the font. 

925 """ 

926 

927 if isinstance(fontprop, str): 

928 filenames = [fontprop] 

929 elif mpl.rcParams['pdf.use14corefonts']: 

930 filenames = _fontManager._find_fonts_by_props( 

931 fontprop, fontext='afm', directory=RendererPdf._afm_font_dir 

932 ) 

933 else: 

934 filenames = _fontManager._find_fonts_by_props(fontprop) 

935 first_Fx = None 

936 for fname in filenames: 

937 Fx = self.fontNames.get(fname) 

938 if not first_Fx: 

939 first_Fx = Fx 

940 if Fx is None: 

941 Fx = next(self._internal_font_seq) 

942 self.fontNames[fname] = Fx 

943 _log.debug('Assigning font %s = %r', Fx, fname) 

944 if not first_Fx: 

945 first_Fx = Fx 

946 

947 # find_fontsprop's first value always adheres to 

948 # findfont's value, so technically no behaviour change 

949 return first_Fx 

950 

951 def dviFontName(self, dvifont): 

952 """ 

953 Given a dvi font object, return a name suitable for Op.selectfont. 

954 This registers the font information in ``self.dviFontInfo`` if not yet 

955 registered. 

956 """ 

957 

958 dvi_info = self.dviFontInfo.get(dvifont.texname) 

959 if dvi_info is not None: 

960 return dvi_info.pdfname 

961 

962 tex_font_map = dviread.PsfontsMap(dviread._find_tex_file('pdftex.map')) 

963 psfont = tex_font_map[dvifont.texname] 

964 if psfont.filename is None: 

965 raise ValueError( 

966 "No usable font file found for {} (TeX: {}); " 

967 "the font may lack a Type-1 version" 

968 .format(psfont.psname, dvifont.texname)) 

969 

970 pdfname = next(self._internal_font_seq) 

971 _log.debug('Assigning font %s = %s (dvi)', pdfname, dvifont.texname) 

972 self.dviFontInfo[dvifont.texname] = types.SimpleNamespace( 

973 dvifont=dvifont, 

974 pdfname=pdfname, 

975 fontfile=psfont.filename, 

976 basefont=psfont.psname, 

977 encodingfile=psfont.encoding, 

978 effects=psfont.effects) 

979 return pdfname 

980 

981 def writeFonts(self): 

982 fonts = {} 

983 for dviname, info in sorted(self.dviFontInfo.items()): 

984 Fx = info.pdfname 

985 _log.debug('Embedding Type-1 font %s from dvi.', dviname) 

986 fonts[Fx] = self._embedTeXFont(info) 

987 for filename in sorted(self.fontNames): 

988 Fx = self.fontNames[filename] 

989 _log.debug('Embedding font %s.', filename) 

990 if filename.endswith('.afm'): 

991 # from pdf.use14corefonts 

992 _log.debug('Writing AFM font.') 

993 fonts[Fx] = self._write_afm_font(filename) 

994 else: 

995 # a normal TrueType font 

996 _log.debug('Writing TrueType font.') 

997 chars = self._character_tracker.used.get(filename) 

998 if chars: 

999 fonts[Fx] = self.embedTTF(filename, chars) 

1000 self.writeObject(self.fontObject, fonts) 

1001 

1002 def _write_afm_font(self, filename): 

1003 with open(filename, 'rb') as fh: 

1004 font = AFM(fh) 

1005 fontname = font.get_fontname() 

1006 fontdict = {'Type': Name('Font'), 

1007 'Subtype': Name('Type1'), 

1008 'BaseFont': Name(fontname), 

1009 'Encoding': Name('WinAnsiEncoding')} 

1010 fontdictObject = self.reserveObject('font dictionary') 

1011 self.writeObject(fontdictObject, fontdict) 

1012 return fontdictObject 

1013 

1014 def _embedTeXFont(self, fontinfo): 

1015 _log.debug('Embedding TeX font %s - fontinfo=%s', 

1016 fontinfo.dvifont.texname, fontinfo.__dict__) 

1017 

1018 # Widths 

1019 widthsObject = self.reserveObject('font widths') 

1020 self.writeObject(widthsObject, fontinfo.dvifont.widths) 

1021 

1022 # Font dictionary 

1023 fontdictObject = self.reserveObject('font dictionary') 

1024 fontdict = { 

1025 'Type': Name('Font'), 

1026 'Subtype': Name('Type1'), 

1027 'FirstChar': 0, 

1028 'LastChar': len(fontinfo.dvifont.widths) - 1, 

1029 'Widths': widthsObject, 

1030 } 

1031 

1032 # Encoding (if needed) 

1033 if fontinfo.encodingfile is not None: 

1034 fontdict['Encoding'] = { 

1035 'Type': Name('Encoding'), 

1036 'Differences': [ 

1037 0, *map(Name, dviread._parse_enc(fontinfo.encodingfile))], 

1038 } 

1039 

1040 # If no file is specified, stop short 

1041 if fontinfo.fontfile is None: 

1042 _log.warning( 

1043 "Because of TeX configuration (pdftex.map, see updmap option " 

1044 "pdftexDownloadBase14) the font %s is not embedded. This is " 

1045 "deprecated as of PDF 1.5 and it may cause the consumer " 

1046 "application to show something that was not intended.", 

1047 fontinfo.basefont) 

1048 fontdict['BaseFont'] = Name(fontinfo.basefont) 

1049 self.writeObject(fontdictObject, fontdict) 

1050 return fontdictObject 

1051 

1052 # We have a font file to embed - read it in and apply any effects 

1053 t1font = _type1font.Type1Font(fontinfo.fontfile) 

1054 if fontinfo.effects: 

1055 t1font = t1font.transform(fontinfo.effects) 

1056 fontdict['BaseFont'] = Name(t1font.prop['FontName']) 

1057 

1058 # Font descriptors may be shared between differently encoded 

1059 # Type-1 fonts, so only create a new descriptor if there is no 

1060 # existing descriptor for this font. 

1061 effects = (fontinfo.effects.get('slant', 0.0), 

1062 fontinfo.effects.get('extend', 1.0)) 

1063 fontdesc = self.type1Descriptors.get((fontinfo.fontfile, effects)) 

1064 if fontdesc is None: 

1065 fontdesc = self.createType1Descriptor(t1font, fontinfo.fontfile) 

1066 self.type1Descriptors[(fontinfo.fontfile, effects)] = fontdesc 

1067 fontdict['FontDescriptor'] = fontdesc 

1068 

1069 self.writeObject(fontdictObject, fontdict) 

1070 return fontdictObject 

1071 

1072 def createType1Descriptor(self, t1font, fontfile): 

1073 # Create and write the font descriptor and the font file 

1074 # of a Type-1 font 

1075 fontdescObject = self.reserveObject('font descriptor') 

1076 fontfileObject = self.reserveObject('font file') 

1077 

1078 italic_angle = t1font.prop['ItalicAngle'] 

1079 fixed_pitch = t1font.prop['isFixedPitch'] 

1080 

1081 flags = 0 

1082 # fixed width 

1083 if fixed_pitch: 

1084 flags |= 1 << 0 

1085 # TODO: serif 

1086 if 0: 

1087 flags |= 1 << 1 

1088 # TODO: symbolic (most TeX fonts are) 

1089 if 1: 

1090 flags |= 1 << 2 

1091 # non-symbolic 

1092 else: 

1093 flags |= 1 << 5 

1094 # italic 

1095 if italic_angle: 

1096 flags |= 1 << 6 

1097 # TODO: all caps 

1098 if 0: 

1099 flags |= 1 << 16 

1100 # TODO: small caps 

1101 if 0: 

1102 flags |= 1 << 17 

1103 # TODO: force bold 

1104 if 0: 

1105 flags |= 1 << 18 

1106 

1107 ft2font = get_font(fontfile) 

1108 

1109 descriptor = { 

1110 'Type': Name('FontDescriptor'), 

1111 'FontName': Name(t1font.prop['FontName']), 

1112 'Flags': flags, 

1113 'FontBBox': ft2font.bbox, 

1114 'ItalicAngle': italic_angle, 

1115 'Ascent': ft2font.ascender, 

1116 'Descent': ft2font.descender, 

1117 'CapHeight': 1000, # TODO: find this out 

1118 'XHeight': 500, # TODO: this one too 

1119 'FontFile': fontfileObject, 

1120 'FontFamily': t1font.prop['FamilyName'], 

1121 'StemV': 50, # TODO 

1122 # (see also revision 3874; but not all TeX distros have AFM files!) 

1123 # 'FontWeight': a number where 400 = Regular, 700 = Bold 

1124 } 

1125 

1126 self.writeObject(fontdescObject, descriptor) 

1127 

1128 self.outputStream(fontfileObject, b"".join(t1font.parts[:2]), 

1129 extra={'Length1': len(t1font.parts[0]), 

1130 'Length2': len(t1font.parts[1]), 

1131 'Length3': 0}) 

1132 

1133 return fontdescObject 

1134 

1135 def _get_xobject_glyph_name(self, filename, glyph_name): 

1136 Fx = self.fontName(filename) 

1137 return "-".join([ 

1138 Fx.name.decode(), 

1139 os.path.splitext(os.path.basename(filename))[0], 

1140 glyph_name]) 

1141 

1142 _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin 

114312 dict begin 

1144begincmap 

1145/CIDSystemInfo 

1146<< /Registry (Adobe) 

1147 /Ordering (UCS) 

1148 /Supplement 0 

1149>> def 

1150/CMapName /Adobe-Identity-UCS def 

1151/CMapType 2 def 

11521 begincodespacerange 

1153<0000> <ffff> 

1154endcodespacerange 

1155%d beginbfrange 

1156%s 

1157endbfrange 

1158endcmap 

1159CMapName currentdict /CMap defineresource pop 

1160end 

1161end""" 

1162 

1163 def embedTTF(self, filename, characters): 

1164 """Embed the TTF font from the named file into the document.""" 

1165 

1166 font = get_font(filename) 

1167 fonttype = mpl.rcParams['pdf.fonttype'] 

1168 

1169 def cvt(length, upe=font.units_per_EM, nearest=True): 

1170 """Convert font coordinates to PDF glyph coordinates.""" 

1171 value = length / upe * 1000 

1172 if nearest: 

1173 return round(value) 

1174 # Best(?) to round away from zero for bounding boxes and the like. 

1175 if value < 0: 

1176 return math.floor(value) 

1177 else: 

1178 return math.ceil(value) 

1179 

1180 def embedTTFType3(font, characters, descriptor): 

1181 """The Type 3-specific part of embedding a Truetype font""" 

1182 widthsObject = self.reserveObject('font widths') 

1183 fontdescObject = self.reserveObject('font descriptor') 

1184 fontdictObject = self.reserveObject('font dictionary') 

1185 charprocsObject = self.reserveObject('character procs') 

1186 differencesArray = [] 

1187 firstchar, lastchar = 0, 255 

1188 bbox = [cvt(x, nearest=False) for x in font.bbox] 

1189 

1190 fontdict = { 

1191 'Type': Name('Font'), 

1192 'BaseFont': ps_name, 

1193 'FirstChar': firstchar, 

1194 'LastChar': lastchar, 

1195 'FontDescriptor': fontdescObject, 

1196 'Subtype': Name('Type3'), 

1197 'Name': descriptor['FontName'], 

1198 'FontBBox': bbox, 

1199 'FontMatrix': [.001, 0, 0, .001, 0, 0], 

1200 'CharProcs': charprocsObject, 

1201 'Encoding': { 

1202 'Type': Name('Encoding'), 

1203 'Differences': differencesArray}, 

1204 'Widths': widthsObject 

1205 } 

1206 

1207 from encodings import cp1252 

1208 

1209 # Make the "Widths" array 

1210 def get_char_width(charcode): 

1211 s = ord(cp1252.decoding_table[charcode]) 

1212 width = font.load_char( 

1213 s, flags=LOAD_NO_SCALE | LOAD_NO_HINTING).horiAdvance 

1214 return cvt(width) 

1215 with warnings.catch_warnings(): 

1216 # Ignore 'Required glyph missing from current font' warning 

1217 # from ft2font: here we're just building the widths table, but 

1218 # the missing glyphs may not even be used in the actual string. 

1219 warnings.filterwarnings("ignore") 

1220 widths = [get_char_width(charcode) 

1221 for charcode in range(firstchar, lastchar+1)] 

1222 descriptor['MaxWidth'] = max(widths) 

1223 

1224 # Make the "Differences" array, sort the ccodes < 255 from 

1225 # the multi-byte ccodes, and build the whole set of glyph ids 

1226 # that we need from this font. 

1227 glyph_ids = [] 

1228 differences = [] 

1229 multi_byte_chars = set() 

1230 for c in characters: 

1231 ccode = c 

1232 gind = font.get_char_index(ccode) 

1233 glyph_ids.append(gind) 

1234 glyph_name = font.get_glyph_name(gind) 

1235 if ccode <= 255: 

1236 differences.append((ccode, glyph_name)) 

1237 else: 

1238 multi_byte_chars.add(glyph_name) 

1239 differences.sort() 

1240 

1241 last_c = -2 

1242 for c, name in differences: 

1243 if c != last_c + 1: 

1244 differencesArray.append(c) 

1245 differencesArray.append(Name(name)) 

1246 last_c = c 

1247 

1248 # Make the charprocs array. 

1249 rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) 

1250 charprocs = {} 

1251 for charname in sorted(rawcharprocs): 

1252 stream = rawcharprocs[charname] 

1253 charprocDict = {} 

1254 # The 2-byte characters are used as XObjects, so they 

1255 # need extra info in their dictionary 

1256 if charname in multi_byte_chars: 

1257 charprocDict = {'Type': Name('XObject'), 

1258 'Subtype': Name('Form'), 

1259 'BBox': bbox} 

1260 # Each glyph includes bounding box information, 

1261 # but xpdf and ghostscript can't handle it in a 

1262 # Form XObject (they segfault!!!), so we remove it 

1263 # from the stream here. It's not needed anyway, 

1264 # since the Form XObject includes it in its BBox 

1265 # value. 

1266 stream = stream[stream.find(b"d1") + 2:] 

1267 charprocObject = self.reserveObject('charProc') 

1268 self.outputStream(charprocObject, stream, extra=charprocDict) 

1269 

1270 # Send the glyphs with ccode > 255 to the XObject dictionary, 

1271 # and the others to the font itself 

1272 if charname in multi_byte_chars: 

1273 name = self._get_xobject_glyph_name(filename, charname) 

1274 self.multi_byte_charprocs[name] = charprocObject 

1275 else: 

1276 charprocs[charname] = charprocObject 

1277 

1278 # Write everything out 

1279 self.writeObject(fontdictObject, fontdict) 

1280 self.writeObject(fontdescObject, descriptor) 

1281 self.writeObject(widthsObject, widths) 

1282 self.writeObject(charprocsObject, charprocs) 

1283 

1284 return fontdictObject 

1285 

1286 def embedTTFType42(font, characters, descriptor): 

1287 """The Type 42-specific part of embedding a Truetype font""" 

1288 fontdescObject = self.reserveObject('font descriptor') 

1289 cidFontDictObject = self.reserveObject('CID font dictionary') 

1290 type0FontDictObject = self.reserveObject('Type 0 font dictionary') 

1291 cidToGidMapObject = self.reserveObject('CIDToGIDMap stream') 

1292 fontfileObject = self.reserveObject('font file stream') 

1293 wObject = self.reserveObject('Type 0 widths') 

1294 toUnicodeMapObject = self.reserveObject('ToUnicode map') 

1295 

1296 subset_str = "".join(chr(c) for c in characters) 

1297 _log.debug("SUBSET %s characters: %s", filename, subset_str) 

1298 fontdata = _backend_pdf_ps.get_glyphs_subset(filename, subset_str) 

1299 _log.debug( 

1300 "SUBSET %s %d -> %d", filename, 

1301 os.stat(filename).st_size, fontdata.getbuffer().nbytes 

1302 ) 

1303 

1304 # We need this ref for XObjects 

1305 full_font = font 

1306 

1307 # reload the font object from the subset 

1308 # (all the necessary data could probably be obtained directly 

1309 # using fontLib.ttLib) 

1310 font = FT2Font(fontdata) 

1311 

1312 cidFontDict = { 

1313 'Type': Name('Font'), 

1314 'Subtype': Name('CIDFontType2'), 

1315 'BaseFont': ps_name, 

1316 'CIDSystemInfo': { 

1317 'Registry': 'Adobe', 

1318 'Ordering': 'Identity', 

1319 'Supplement': 0}, 

1320 'FontDescriptor': fontdescObject, 

1321 'W': wObject, 

1322 'CIDToGIDMap': cidToGidMapObject 

1323 } 

1324 

1325 type0FontDict = { 

1326 'Type': Name('Font'), 

1327 'Subtype': Name('Type0'), 

1328 'BaseFont': ps_name, 

1329 'Encoding': Name('Identity-H'), 

1330 'DescendantFonts': [cidFontDictObject], 

1331 'ToUnicode': toUnicodeMapObject 

1332 } 

1333 

1334 # Make fontfile stream 

1335 descriptor['FontFile2'] = fontfileObject 

1336 self.outputStream( 

1337 fontfileObject, fontdata.getvalue(), 

1338 extra={'Length1': fontdata.getbuffer().nbytes}) 

1339 

1340 # Make the 'W' (Widths) array, CidToGidMap and ToUnicode CMap 

1341 # at the same time 

1342 cid_to_gid_map = ['\0'] * 65536 

1343 widths = [] 

1344 max_ccode = 0 

1345 for c in characters: 

1346 ccode = c 

1347 gind = font.get_char_index(ccode) 

1348 glyph = font.load_char(ccode, 

1349 flags=LOAD_NO_SCALE | LOAD_NO_HINTING) 

1350 widths.append((ccode, cvt(glyph.horiAdvance))) 

1351 if ccode < 65536: 

1352 cid_to_gid_map[ccode] = chr(gind) 

1353 max_ccode = max(ccode, max_ccode) 

1354 widths.sort() 

1355 cid_to_gid_map = cid_to_gid_map[:max_ccode + 1] 

1356 

1357 last_ccode = -2 

1358 w = [] 

1359 max_width = 0 

1360 unicode_groups = [] 

1361 for ccode, width in widths: 

1362 if ccode != last_ccode + 1: 

1363 w.append(ccode) 

1364 w.append([width]) 

1365 unicode_groups.append([ccode, ccode]) 

1366 else: 

1367 w[-1].append(width) 

1368 unicode_groups[-1][1] = ccode 

1369 max_width = max(max_width, width) 

1370 last_ccode = ccode 

1371 

1372 unicode_bfrange = [] 

1373 for start, end in unicode_groups: 

1374 # Ensure the CID map contains only chars from BMP 

1375 if start > 65535: 

1376 continue 

1377 end = min(65535, end) 

1378 

1379 unicode_bfrange.append( 

1380 b"<%04x> <%04x> [%s]" % 

1381 (start, end, 

1382 b" ".join(b"<%04x>" % x for x in range(start, end+1)))) 

1383 unicode_cmap = (self._identityToUnicodeCMap % 

1384 (len(unicode_groups), b"\n".join(unicode_bfrange))) 

1385 

1386 # Add XObjects for unsupported chars 

1387 glyph_ids = [] 

1388 for ccode in characters: 

1389 if not _font_supports_glyph(fonttype, ccode): 

1390 gind = full_font.get_char_index(ccode) 

1391 glyph_ids.append(gind) 

1392 

1393 bbox = [cvt(x, nearest=False) for x in full_font.bbox] 

1394 rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) 

1395 for charname in sorted(rawcharprocs): 

1396 stream = rawcharprocs[charname] 

1397 charprocDict = {'Type': Name('XObject'), 

1398 'Subtype': Name('Form'), 

1399 'BBox': bbox} 

1400 # Each glyph includes bounding box information, 

1401 # but xpdf and ghostscript can't handle it in a 

1402 # Form XObject (they segfault!!!), so we remove it 

1403 # from the stream here. It's not needed anyway, 

1404 # since the Form XObject includes it in its BBox 

1405 # value. 

1406 stream = stream[stream.find(b"d1") + 2:] 

1407 charprocObject = self.reserveObject('charProc') 

1408 self.outputStream(charprocObject, stream, extra=charprocDict) 

1409 

1410 name = self._get_xobject_glyph_name(filename, charname) 

1411 self.multi_byte_charprocs[name] = charprocObject 

1412 

1413 # CIDToGIDMap stream 

1414 cid_to_gid_map = "".join(cid_to_gid_map).encode("utf-16be") 

1415 self.outputStream(cidToGidMapObject, cid_to_gid_map) 

1416 

1417 # ToUnicode CMap 

1418 self.outputStream(toUnicodeMapObject, unicode_cmap) 

1419 

1420 descriptor['MaxWidth'] = max_width 

1421 

1422 # Write everything out 

1423 self.writeObject(cidFontDictObject, cidFontDict) 

1424 self.writeObject(type0FontDictObject, type0FontDict) 

1425 self.writeObject(fontdescObject, descriptor) 

1426 self.writeObject(wObject, w) 

1427 

1428 return type0FontDictObject 

1429 

1430 # Beginning of main embedTTF function... 

1431 

1432 ps_name = self._get_subsetted_psname( 

1433 font.postscript_name, 

1434 font.get_charmap() 

1435 ) 

1436 ps_name = ps_name.encode('ascii', 'replace') 

1437 ps_name = Name(ps_name) 

1438 pclt = font.get_sfnt_table('pclt') or {'capHeight': 0, 'xHeight': 0} 

1439 post = font.get_sfnt_table('post') or {'italicAngle': (0, 0)} 

1440 ff = font.face_flags 

1441 sf = font.style_flags 

1442 

1443 flags = 0 

1444 symbolic = False # ps_name.name in ('Cmsy10', 'Cmmi10', 'Cmex10') 

1445 if ff & FIXED_WIDTH: 

1446 flags |= 1 << 0 

1447 if 0: # TODO: serif 

1448 flags |= 1 << 1 

1449 if symbolic: 

1450 flags |= 1 << 2 

1451 else: 

1452 flags |= 1 << 5 

1453 if sf & ITALIC: 

1454 flags |= 1 << 6 

1455 if 0: # TODO: all caps 

1456 flags |= 1 << 16 

1457 if 0: # TODO: small caps 

1458 flags |= 1 << 17 

1459 if 0: # TODO: force bold 

1460 flags |= 1 << 18 

1461 

1462 descriptor = { 

1463 'Type': Name('FontDescriptor'), 

1464 'FontName': ps_name, 

1465 'Flags': flags, 

1466 'FontBBox': [cvt(x, nearest=False) for x in font.bbox], 

1467 'Ascent': cvt(font.ascender, nearest=False), 

1468 'Descent': cvt(font.descender, nearest=False), 

1469 'CapHeight': cvt(pclt['capHeight'], nearest=False), 

1470 'XHeight': cvt(pclt['xHeight']), 

1471 'ItalicAngle': post['italicAngle'][1], # ??? 

1472 'StemV': 0 # ??? 

1473 } 

1474 

1475 if fonttype == 3: 

1476 return embedTTFType3(font, characters, descriptor) 

1477 elif fonttype == 42: 

1478 return embedTTFType42(font, characters, descriptor) 

1479 

1480 def alphaState(self, alpha): 

1481 """Return name of an ExtGState that sets alpha to the given value.""" 

1482 

1483 state = self.alphaStates.get(alpha, None) 

1484 if state is not None: 

1485 return state[0] 

1486 

1487 name = next(self._alpha_state_seq) 

1488 self.alphaStates[alpha] = \ 

1489 (name, {'Type': Name('ExtGState'), 

1490 'CA': alpha[0], 'ca': alpha[1]}) 

1491 return name 

1492 

1493 def _soft_mask_state(self, smask): 

1494 """ 

1495 Return an ExtGState that sets the soft mask to the given shading. 

1496 

1497 Parameters 

1498 ---------- 

1499 smask : Reference 

1500 Reference to a shading in DeviceGray color space, whose luminosity 

1501 is to be used as the alpha channel. 

1502 

1503 Returns 

1504 ------- 

1505 Name 

1506 """ 

1507 

1508 state = self._soft_mask_states.get(smask, None) 

1509 if state is not None: 

1510 return state[0] 

1511 

1512 name = next(self._soft_mask_seq) 

1513 groupOb = self.reserveObject('transparency group for soft mask') 

1514 self._soft_mask_states[smask] = ( 

1515 name, 

1516 { 

1517 'Type': Name('ExtGState'), 

1518 'AIS': False, 

1519 'SMask': { 

1520 'Type': Name('Mask'), 

1521 'S': Name('Luminosity'), 

1522 'BC': [1], 

1523 'G': groupOb 

1524 } 

1525 } 

1526 ) 

1527 self._soft_mask_groups.append(( 

1528 groupOb, 

1529 { 

1530 'Type': Name('XObject'), 

1531 'Subtype': Name('Form'), 

1532 'FormType': 1, 

1533 'Group': { 

1534 'S': Name('Transparency'), 

1535 'CS': Name('DeviceGray') 

1536 }, 

1537 'Matrix': [1, 0, 0, 1, 0, 0], 

1538 'Resources': {'Shading': {'S': smask}}, 

1539 'BBox': [0, 0, 1, 1] 

1540 }, 

1541 [Name('S'), Op.shading] 

1542 )) 

1543 return name 

1544 

1545 def writeExtGSTates(self): 

1546 self.writeObject( 

1547 self._extGStateObject, 

1548 dict([ 

1549 *self.alphaStates.values(), 

1550 *self._soft_mask_states.values() 

1551 ]) 

1552 ) 

1553 

1554 def _write_soft_mask_groups(self): 

1555 for ob, attributes, content in self._soft_mask_groups: 

1556 self.beginStream(ob.id, None, attributes) 

1557 self.output(*content) 

1558 self.endStream() 

1559 

1560 def hatchPattern(self, hatch_style): 

1561 # The colors may come in as numpy arrays, which aren't hashable 

1562 if hatch_style is not None: 

1563 edge, face, hatch = hatch_style 

1564 if edge is not None: 

1565 edge = tuple(edge) 

1566 if face is not None: 

1567 face = tuple(face) 

1568 hatch_style = (edge, face, hatch) 

1569 

1570 pattern = self.hatchPatterns.get(hatch_style, None) 

1571 if pattern is not None: 

1572 return pattern 

1573 

1574 name = next(self._hatch_pattern_seq) 

1575 self.hatchPatterns[hatch_style] = name 

1576 return name 

1577 

1578 def writeHatches(self): 

1579 hatchDict = dict() 

1580 sidelen = 72.0 

1581 for hatch_style, name in self.hatchPatterns.items(): 

1582 ob = self.reserveObject('hatch pattern') 

1583 hatchDict[name] = ob 

1584 res = {'Procsets': 

1585 [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()]} 

1586 self.beginStream( 

1587 ob.id, None, 

1588 {'Type': Name('Pattern'), 

1589 'PatternType': 1, 'PaintType': 1, 'TilingType': 1, 

1590 'BBox': [0, 0, sidelen, sidelen], 

1591 'XStep': sidelen, 'YStep': sidelen, 

1592 'Resources': res, 

1593 # Change origin to match Agg at top-left. 

1594 'Matrix': [1, 0, 0, 1, 0, self.height * 72]}) 

1595 

1596 stroke_rgb, fill_rgb, hatch = hatch_style 

1597 self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], 

1598 Op.setrgb_stroke) 

1599 if fill_rgb is not None: 

1600 self.output(fill_rgb[0], fill_rgb[1], fill_rgb[2], 

1601 Op.setrgb_nonstroke, 

1602 0, 0, sidelen, sidelen, Op.rectangle, 

1603 Op.fill) 

1604 

1605 self.output(mpl.rcParams['hatch.linewidth'], Op.setlinewidth) 

1606 

1607 self.output(*self.pathOperations( 

1608 Path.hatch(hatch), 

1609 Affine2D().scale(sidelen), 

1610 simplify=False)) 

1611 self.output(Op.fill_stroke) 

1612 

1613 self.endStream() 

1614 self.writeObject(self.hatchObject, hatchDict) 

1615 

1616 def addGouraudTriangles(self, points, colors): 

1617 """ 

1618 Add a Gouraud triangle shading. 

1619 

1620 Parameters 

1621 ---------- 

1622 points : np.ndarray 

1623 Triangle vertices, shape (n, 3, 2) 

1624 where n = number of triangles, 3 = vertices, 2 = x, y. 

1625 colors : np.ndarray 

1626 Vertex colors, shape (n, 3, 1) or (n, 3, 4) 

1627 as with points, but last dimension is either (gray,) 

1628 or (r, g, b, alpha). 

1629 

1630 Returns 

1631 ------- 

1632 Name, Reference 

1633 """ 

1634 name = Name('GT%d' % len(self.gouraudTriangles)) 

1635 ob = self.reserveObject(f'Gouraud triangle {name}') 

1636 self.gouraudTriangles.append((name, ob, points, colors)) 

1637 return name, ob 

1638 

1639 def writeGouraudTriangles(self): 

1640 gouraudDict = dict() 

1641 for name, ob, points, colors in self.gouraudTriangles: 

1642 gouraudDict[name] = ob 

1643 shape = points.shape 

1644 flat_points = points.reshape((shape[0] * shape[1], 2)) 

1645 colordim = colors.shape[2] 

1646 assert colordim in (1, 4) 

1647 flat_colors = colors.reshape((shape[0] * shape[1], colordim)) 

1648 if colordim == 4: 

1649 # strip the alpha channel 

1650 colordim = 3 

1651 points_min = np.min(flat_points, axis=0) - (1 << 8) 

1652 points_max = np.max(flat_points, axis=0) + (1 << 8) 

1653 factor = 0xffffffff / (points_max - points_min) 

1654 

1655 self.beginStream( 

1656 ob.id, None, 

1657 {'ShadingType': 4, 

1658 'BitsPerCoordinate': 32, 

1659 'BitsPerComponent': 8, 

1660 'BitsPerFlag': 8, 

1661 'ColorSpace': Name( 

1662 'DeviceRGB' if colordim == 3 else 'DeviceGray' 

1663 ), 

1664 'AntiAlias': False, 

1665 'Decode': ([points_min[0], points_max[0], 

1666 points_min[1], points_max[1]] 

1667 + [0, 1] * colordim), 

1668 }) 

1669 

1670 streamarr = np.empty( 

1671 (shape[0] * shape[1],), 

1672 dtype=[('flags', 'u1'), 

1673 ('points', '>u4', (2,)), 

1674 ('colors', 'u1', (colordim,))]) 

1675 streamarr['flags'] = 0 

1676 streamarr['points'] = (flat_points - points_min) * factor 

1677 streamarr['colors'] = flat_colors[:, :colordim] * 255.0 

1678 

1679 self.write(streamarr.tobytes()) 

1680 self.endStream() 

1681 self.writeObject(self.gouraudObject, gouraudDict) 

1682 

1683 def imageObject(self, image): 

1684 """Return name of an image XObject representing the given image.""" 

1685 

1686 entry = self._images.get(id(image), None) 

1687 if entry is not None: 

1688 return entry[1] 

1689 

1690 name = next(self._image_seq) 

1691 ob = self.reserveObject(f'image {name}') 

1692 self._images[id(image)] = (image, name, ob) 

1693 return name 

1694 

1695 def _unpack(self, im): 

1696 """ 

1697 Unpack image array *im* into ``(data, alpha)``, which have shape 

1698 ``(height, width, 3)`` (RGB) or ``(height, width, 1)`` (grayscale or 

1699 alpha), except that alpha is None if the image is fully opaque. 

1700 """ 

1701 im = im[::-1] 

1702 if im.ndim == 2: 

1703 return im, None 

1704 else: 

1705 rgb = im[:, :, :3] 

1706 rgb = np.array(rgb, order='C') 

1707 # PDF needs a separate alpha image 

1708 if im.shape[2] == 4: 

1709 alpha = im[:, :, 3][..., None] 

1710 if np.all(alpha == 255): 

1711 alpha = None 

1712 else: 

1713 alpha = np.array(alpha, order='C') 

1714 else: 

1715 alpha = None 

1716 return rgb, alpha 

1717 

1718 def _writePng(self, img): 

1719 """ 

1720 Write the image *img* into the pdf file using png 

1721 predictors with Flate compression. 

1722 """ 

1723 buffer = BytesIO() 

1724 img.save(buffer, format="png") 

1725 buffer.seek(8) 

1726 png_data = b'' 

1727 bit_depth = palette = None 

1728 while True: 

1729 length, type = struct.unpack(b'!L4s', buffer.read(8)) 

1730 if type in [b'IHDR', b'PLTE', b'IDAT']: 

1731 data = buffer.read(length) 

1732 if len(data) != length: 

1733 raise RuntimeError("truncated data") 

1734 if type == b'IHDR': 

1735 bit_depth = int(data[8]) 

1736 elif type == b'PLTE': 

1737 palette = data 

1738 elif type == b'IDAT': 

1739 png_data += data 

1740 elif type == b'IEND': 

1741 break 

1742 else: 

1743 buffer.seek(length, 1) 

1744 buffer.seek(4, 1) # skip CRC 

1745 return png_data, bit_depth, palette 

1746 

1747 def _writeImg(self, data, id, smask=None): 

1748 """ 

1749 Write the image *data*, of shape ``(height, width, 1)`` (grayscale) or 

1750 ``(height, width, 3)`` (RGB), as pdf object *id* and with the soft mask 

1751 (alpha channel) *smask*, which should be either None or a ``(height, 

1752 width, 1)`` array. 

1753 """ 

1754 height, width, color_channels = data.shape 

1755 obj = {'Type': Name('XObject'), 

1756 'Subtype': Name('Image'), 

1757 'Width': width, 

1758 'Height': height, 

1759 'ColorSpace': Name({1: 'DeviceGray', 

1760 3: 'DeviceRGB'}[color_channels]), 

1761 'BitsPerComponent': 8} 

1762 if smask: 

1763 obj['SMask'] = smask 

1764 if mpl.rcParams['pdf.compression']: 

1765 if data.shape[-1] == 1: 

1766 data = data.squeeze(axis=-1) 

1767 img = Image.fromarray(data) 

1768 img_colors = img.getcolors(maxcolors=256) 

1769 if color_channels == 3 and img_colors is not None: 

1770 # Convert to indexed color if there are 256 colors or fewer 

1771 # This can significantly reduce the file size 

1772 num_colors = len(img_colors) 

1773 # These constants were converted to IntEnums and deprecated in 

1774 # Pillow 9.2 

1775 dither = getattr(Image, 'Dither', Image).NONE 

1776 pmode = getattr(Image, 'Palette', Image).ADAPTIVE 

1777 img = img.convert( 

1778 mode='P', dither=dither, palette=pmode, colors=num_colors 

1779 ) 

1780 png_data, bit_depth, palette = self._writePng(img) 

1781 if bit_depth is None or palette is None: 

1782 raise RuntimeError("invalid PNG header") 

1783 palette = palette[:num_colors * 3] # Trim padding 

1784 obj['ColorSpace'] = Verbatim( 

1785 b'[/Indexed /DeviceRGB %d %s]' 

1786 % (num_colors - 1, pdfRepr(palette))) 

1787 obj['BitsPerComponent'] = bit_depth 

1788 color_channels = 1 

1789 else: 

1790 png_data, _, _ = self._writePng(img) 

1791 png = {'Predictor': 10, 'Colors': color_channels, 'Columns': width} 

1792 else: 

1793 png = None 

1794 self.beginStream( 

1795 id, 

1796 self.reserveObject('length of image stream'), 

1797 obj, 

1798 png=png 

1799 ) 

1800 if png: 

1801 self.currentstream.write(png_data) 

1802 else: 

1803 self.currentstream.write(data.tobytes()) 

1804 self.endStream() 

1805 

1806 def writeImages(self): 

1807 for img, name, ob in self._images.values(): 

1808 data, adata = self._unpack(img) 

1809 if adata is not None: 

1810 smaskObject = self.reserveObject("smask") 

1811 self._writeImg(adata, smaskObject.id) 

1812 else: 

1813 smaskObject = None 

1814 self._writeImg(data, ob.id, smaskObject) 

1815 

1816 def markerObject(self, path, trans, fill, stroke, lw, joinstyle, 

1817 capstyle): 

1818 """Return name of a marker XObject representing the given path.""" 

1819 # self.markers used by markerObject, writeMarkers, close: 

1820 # mapping from (path operations, fill?, stroke?) to 

1821 # [name, object reference, bounding box, linewidth] 

1822 # This enables different draw_markers calls to share the XObject 

1823 # if the gc is sufficiently similar: colors etc can vary, but 

1824 # the choices of whether to fill and whether to stroke cannot. 

1825 # We need a bounding box enclosing all of the XObject path, 

1826 # but since line width may vary, we store the maximum of all 

1827 # occurring line widths in self.markers. 

1828 # close() is somewhat tightly coupled in that it expects the 

1829 # first two components of each value in self.markers to be the 

1830 # name and object reference. 

1831 pathops = self.pathOperations(path, trans, simplify=False) 

1832 key = (tuple(pathops), bool(fill), bool(stroke), joinstyle, capstyle) 

1833 result = self.markers.get(key) 

1834 if result is None: 

1835 name = Name('M%d' % len(self.markers)) 

1836 ob = self.reserveObject('marker %d' % len(self.markers)) 

1837 bbox = path.get_extents(trans) 

1838 self.markers[key] = [name, ob, bbox, lw] 

1839 else: 

1840 if result[-1] < lw: 

1841 result[-1] = lw 

1842 name = result[0] 

1843 return name 

1844 

1845 def writeMarkers(self): 

1846 for ((pathops, fill, stroke, joinstyle, capstyle), 

1847 (name, ob, bbox, lw)) in self.markers.items(): 

1848 # bbox wraps the exact limits of the control points, so half a line 

1849 # will appear outside it. If the join style is miter and the line 

1850 # is not parallel to the edge, then the line will extend even 

1851 # further. From the PDF specification, Section 8.4.3.5, the miter 

1852 # limit is miterLength / lineWidth and from Table 52, the default 

1853 # is 10. With half the miter length outside, that works out to the 

1854 # following padding: 

1855 bbox = bbox.padded(lw * 5) 

1856 self.beginStream( 

1857 ob.id, None, 

1858 {'Type': Name('XObject'), 'Subtype': Name('Form'), 

1859 'BBox': list(bbox.extents)}) 

1860 self.output(GraphicsContextPdf.joinstyles[joinstyle], 

1861 Op.setlinejoin) 

1862 self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap) 

1863 self.output(*pathops) 

1864 self.output(Op.paint_path(fill, stroke)) 

1865 self.endStream() 

1866 

1867 def pathCollectionObject(self, gc, path, trans, padding, filled, stroked): 

1868 name = Name('P%d' % len(self.paths)) 

1869 ob = self.reserveObject('path %d' % len(self.paths)) 

1870 self.paths.append( 

1871 (name, path, trans, ob, gc.get_joinstyle(), gc.get_capstyle(), 

1872 padding, filled, stroked)) 

1873 return name 

1874 

1875 def writePathCollectionTemplates(self): 

1876 for (name, path, trans, ob, joinstyle, capstyle, padding, filled, 

1877 stroked) in self.paths: 

1878 pathops = self.pathOperations(path, trans, simplify=False) 

1879 bbox = path.get_extents(trans) 

1880 if not np.all(np.isfinite(bbox.extents)): 

1881 extents = [0, 0, 0, 0] 

1882 else: 

1883 bbox = bbox.padded(padding) 

1884 extents = list(bbox.extents) 

1885 self.beginStream( 

1886 ob.id, None, 

1887 {'Type': Name('XObject'), 'Subtype': Name('Form'), 

1888 'BBox': extents}) 

1889 self.output(GraphicsContextPdf.joinstyles[joinstyle], 

1890 Op.setlinejoin) 

1891 self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap) 

1892 self.output(*pathops) 

1893 self.output(Op.paint_path(filled, stroked)) 

1894 self.endStream() 

1895 

1896 @staticmethod 

1897 def pathOperations(path, transform, clip=None, simplify=None, sketch=None): 

1898 return [Verbatim(_path.convert_to_string( 

1899 path, transform, clip, simplify, sketch, 

1900 6, 

1901 [Op.moveto.value, Op.lineto.value, b'', Op.curveto.value, 

1902 Op.closepath.value], 

1903 True))] 

1904 

1905 def writePath(self, path, transform, clip=False, sketch=None): 

1906 if clip: 

1907 clip = (0.0, 0.0, self.width * 72, self.height * 72) 

1908 simplify = path.should_simplify 

1909 else: 

1910 clip = None 

1911 simplify = False 

1912 cmds = self.pathOperations(path, transform, clip, simplify=simplify, 

1913 sketch=sketch) 

1914 self.output(*cmds) 

1915 

1916 def reserveObject(self, name=''): 

1917 """ 

1918 Reserve an ID for an indirect object. 

1919 

1920 The name is used for debugging in case we forget to print out 

1921 the object with writeObject. 

1922 """ 

1923 id = next(self._object_seq) 

1924 self.xrefTable.append([None, 0, name]) 

1925 return Reference(id) 

1926 

1927 def recordXref(self, id): 

1928 self.xrefTable[id][0] = self.fh.tell() - self.tell_base 

1929 

1930 def writeObject(self, object, contents): 

1931 self.recordXref(object.id) 

1932 object.write(contents, self) 

1933 

1934 def writeXref(self): 

1935 """Write out the xref table.""" 

1936 self.startxref = self.fh.tell() - self.tell_base 

1937 self.write(b"xref\n0 %d\n" % len(self.xrefTable)) 

1938 for i, (offset, generation, name) in enumerate(self.xrefTable): 

1939 if offset is None: 

1940 raise AssertionError( 

1941 'No offset for object %d (%s)' % (i, name)) 

1942 else: 

1943 key = b"f" if name == 'the zero object' else b"n" 

1944 text = b"%010d %05d %b \n" % (offset, generation, key) 

1945 self.write(text) 

1946 

1947 def writeInfoDict(self): 

1948 """Write out the info dictionary, checking it for good form""" 

1949 

1950 self.infoObject = self.reserveObject('info') 

1951 self.writeObject(self.infoObject, self.infoDict) 

1952 

1953 def writeTrailer(self): 

1954 """Write out the PDF trailer.""" 

1955 

1956 self.write(b"trailer\n") 

1957 self.write(pdfRepr( 

1958 {'Size': len(self.xrefTable), 

1959 'Root': self.rootObject, 

1960 'Info': self.infoObject})) 

1961 # Could add 'ID' 

1962 self.write(b"\nstartxref\n%d\n%%%%EOF\n" % self.startxref) 

1963 

1964 

1965class RendererPdf(_backend_pdf_ps.RendererPDFPSBase): 

1966 

1967 _afm_font_dir = cbook._get_data_path("fonts/pdfcorefonts") 

1968 _use_afm_rc_name = "pdf.use14corefonts" 

1969 

1970 def __init__(self, file, image_dpi, height, width): 

1971 super().__init__(width, height) 

1972 self.file = file 

1973 self.gc = self.new_gc() 

1974 self.image_dpi = image_dpi 

1975 

1976 def finalize(self): 

1977 self.file.output(*self.gc.finalize()) 

1978 

1979 def check_gc(self, gc, fillcolor=None): 

1980 orig_fill = getattr(gc, '_fillcolor', (0., 0., 0.)) 

1981 gc._fillcolor = fillcolor 

1982 

1983 orig_alphas = getattr(gc, '_effective_alphas', (1.0, 1.0)) 

1984 

1985 if gc.get_rgb() is None: 

1986 # It should not matter what color here since linewidth should be 

1987 # 0 unless affected by global settings in rcParams, hence setting 

1988 # zero alpha just in case. 

1989 gc.set_foreground((0, 0, 0, 0), isRGBA=True) 

1990 

1991 if gc._forced_alpha: 

1992 gc._effective_alphas = (gc._alpha, gc._alpha) 

1993 elif fillcolor is None or len(fillcolor) < 4: 

1994 gc._effective_alphas = (gc._rgb[3], 1.0) 

1995 else: 

1996 gc._effective_alphas = (gc._rgb[3], fillcolor[3]) 

1997 

1998 delta = self.gc.delta(gc) 

1999 if delta: 

2000 self.file.output(*delta) 

2001 

2002 # Restore gc to avoid unwanted side effects 

2003 gc._fillcolor = orig_fill 

2004 gc._effective_alphas = orig_alphas 

2005 

2006 def get_image_magnification(self): 

2007 return self.image_dpi/72.0 

2008 

2009 def draw_image(self, gc, x, y, im, transform=None): 

2010 # docstring inherited 

2011 

2012 h, w = im.shape[:2] 

2013 if w == 0 or h == 0: 

2014 return 

2015 

2016 if transform is None: 

2017 # If there's no transform, alpha has already been applied 

2018 gc.set_alpha(1.0) 

2019 

2020 self.check_gc(gc) 

2021 

2022 w = 72.0 * w / self.image_dpi 

2023 h = 72.0 * h / self.image_dpi 

2024 

2025 imob = self.file.imageObject(im) 

2026 

2027 if transform is None: 

2028 self.file.output(Op.gsave, 

2029 w, 0, 0, h, x, y, Op.concat_matrix, 

2030 imob, Op.use_xobject, Op.grestore) 

2031 else: 

2032 tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values() 

2033 

2034 self.file.output(Op.gsave, 

2035 1, 0, 0, 1, x, y, Op.concat_matrix, 

2036 tr1, tr2, tr3, tr4, tr5, tr6, Op.concat_matrix, 

2037 imob, Op.use_xobject, Op.grestore) 

2038 

2039 def draw_path(self, gc, path, transform, rgbFace=None): 

2040 # docstring inherited 

2041 self.check_gc(gc, rgbFace) 

2042 self.file.writePath( 

2043 path, transform, 

2044 rgbFace is None and gc.get_hatch_path() is None, 

2045 gc.get_sketch_params()) 

2046 self.file.output(self.gc.paint()) 

2047 

2048 def draw_path_collection(self, gc, master_transform, paths, all_transforms, 

2049 offsets, offset_trans, facecolors, edgecolors, 

2050 linewidths, linestyles, antialiaseds, urls, 

2051 offset_position): 

2052 # We can only reuse the objects if the presence of fill and 

2053 # stroke (and the amount of alpha for each) is the same for 

2054 # all of them 

2055 can_do_optimization = True 

2056 facecolors = np.asarray(facecolors) 

2057 edgecolors = np.asarray(edgecolors) 

2058 

2059 if not len(facecolors): 

2060 filled = False 

2061 can_do_optimization = not gc.get_hatch() 

2062 else: 

2063 if np.all(facecolors[:, 3] == facecolors[0, 3]): 

2064 filled = facecolors[0, 3] != 0.0 

2065 else: 

2066 can_do_optimization = False 

2067 

2068 if not len(edgecolors): 

2069 stroked = False 

2070 else: 

2071 if np.all(np.asarray(linewidths) == 0.0): 

2072 stroked = False 

2073 elif np.all(edgecolors[:, 3] == edgecolors[0, 3]): 

2074 stroked = edgecolors[0, 3] != 0.0 

2075 else: 

2076 can_do_optimization = False 

2077 

2078 # Is the optimization worth it? Rough calculation: 

2079 # cost of emitting a path in-line is len_path * uses_per_path 

2080 # cost of XObject is len_path + 5 for the definition, 

2081 # uses_per_path for the uses 

2082 len_path = len(paths[0].vertices) if len(paths) > 0 else 0 

2083 uses_per_path = self._iter_collection_uses_per_path( 

2084 paths, all_transforms, offsets, facecolors, edgecolors) 

2085 should_do_optimization = \ 

2086 len_path + uses_per_path + 5 < len_path * uses_per_path 

2087 

2088 if (not can_do_optimization) or (not should_do_optimization): 

2089 return RendererBase.draw_path_collection( 

2090 self, gc, master_transform, paths, all_transforms, 

2091 offsets, offset_trans, facecolors, edgecolors, 

2092 linewidths, linestyles, antialiaseds, urls, 

2093 offset_position) 

2094 

2095 padding = np.max(linewidths) 

2096 path_codes = [] 

2097 for i, (path, transform) in enumerate(self._iter_collection_raw_paths( 

2098 master_transform, paths, all_transforms)): 

2099 name = self.file.pathCollectionObject( 

2100 gc, path, transform, padding, filled, stroked) 

2101 path_codes.append(name) 

2102 

2103 output = self.file.output 

2104 output(*self.gc.push()) 

2105 lastx, lasty = 0, 0 

2106 for xo, yo, path_id, gc0, rgbFace in self._iter_collection( 

2107 gc, path_codes, offsets, offset_trans, 

2108 facecolors, edgecolors, linewidths, linestyles, 

2109 antialiaseds, urls, offset_position): 

2110 

2111 self.check_gc(gc0, rgbFace) 

2112 dx, dy = xo - lastx, yo - lasty 

2113 output(1, 0, 0, 1, dx, dy, Op.concat_matrix, path_id, 

2114 Op.use_xobject) 

2115 lastx, lasty = xo, yo 

2116 output(*self.gc.pop()) 

2117 

2118 def draw_markers(self, gc, marker_path, marker_trans, path, trans, 

2119 rgbFace=None): 

2120 # docstring inherited 

2121 

2122 # Same logic as in draw_path_collection 

2123 len_marker_path = len(marker_path) 

2124 uses = len(path) 

2125 if len_marker_path * uses < len_marker_path + uses + 5: 

2126 RendererBase.draw_markers(self, gc, marker_path, marker_trans, 

2127 path, trans, rgbFace) 

2128 return 

2129 

2130 self.check_gc(gc, rgbFace) 

2131 fill = gc.fill(rgbFace) 

2132 stroke = gc.stroke() 

2133 

2134 output = self.file.output 

2135 marker = self.file.markerObject( 

2136 marker_path, marker_trans, fill, stroke, self.gc._linewidth, 

2137 gc.get_joinstyle(), gc.get_capstyle()) 

2138 

2139 output(Op.gsave) 

2140 lastx, lasty = 0, 0 

2141 for vertices, code in path.iter_segments( 

2142 trans, 

2143 clip=(0, 0, self.file.width*72, self.file.height*72), 

2144 simplify=False): 

2145 if len(vertices): 

2146 x, y = vertices[-2:] 

2147 if not (0 <= x <= self.file.width * 72 

2148 and 0 <= y <= self.file.height * 72): 

2149 continue 

2150 dx, dy = x - lastx, y - lasty 

2151 output(1, 0, 0, 1, dx, dy, Op.concat_matrix, 

2152 marker, Op.use_xobject) 

2153 lastx, lasty = x, y 

2154 output(Op.grestore) 

2155 

2156 def draw_gouraud_triangle(self, gc, points, colors, trans): 

2157 self.draw_gouraud_triangles(gc, points.reshape((1, 3, 2)), 

2158 colors.reshape((1, 3, 4)), trans) 

2159 

2160 def draw_gouraud_triangles(self, gc, points, colors, trans): 

2161 assert len(points) == len(colors) 

2162 if len(points) == 0: 

2163 return 

2164 assert points.ndim == 3 

2165 assert points.shape[1] == 3 

2166 assert points.shape[2] == 2 

2167 assert colors.ndim == 3 

2168 assert colors.shape[1] == 3 

2169 assert colors.shape[2] in (1, 4) 

2170 

2171 shape = points.shape 

2172 points = points.reshape((shape[0] * shape[1], 2)) 

2173 tpoints = trans.transform(points) 

2174 tpoints = tpoints.reshape(shape) 

2175 name, _ = self.file.addGouraudTriangles(tpoints, colors) 

2176 output = self.file.output 

2177 

2178 if colors.shape[2] == 1: 

2179 # grayscale 

2180 gc.set_alpha(1.0) 

2181 self.check_gc(gc) 

2182 output(name, Op.shading) 

2183 return 

2184 

2185 alpha = colors[0, 0, 3] 

2186 if np.allclose(alpha, colors[:, :, 3]): 

2187 # single alpha value 

2188 gc.set_alpha(alpha) 

2189 self.check_gc(gc) 

2190 output(name, Op.shading) 

2191 else: 

2192 # varying alpha: use a soft mask 

2193 alpha = colors[:, :, 3][:, :, None] 

2194 _, smask_ob = self.file.addGouraudTriangles(tpoints, alpha) 

2195 gstate = self.file._soft_mask_state(smask_ob) 

2196 output(Op.gsave, gstate, Op.setgstate, 

2197 name, Op.shading, 

2198 Op.grestore) 

2199 

2200 def _setup_textpos(self, x, y, angle, oldx=0, oldy=0, oldangle=0): 

2201 if angle == oldangle == 0: 

2202 self.file.output(x - oldx, y - oldy, Op.textpos) 

2203 else: 

2204 angle = math.radians(angle) 

2205 self.file.output(math.cos(angle), math.sin(angle), 

2206 -math.sin(angle), math.cos(angle), 

2207 x, y, Op.textmatrix) 

2208 self.file.output(0, 0, Op.textpos) 

2209 

2210 def draw_mathtext(self, gc, x, y, s, prop, angle): 

2211 # TODO: fix positioning and encoding 

2212 width, height, descent, glyphs, rects = \ 

2213 self._text2path.mathtext_parser.parse(s, 72, prop) 

2214 

2215 if gc.get_url() is not None: 

2216 self.file._annotations[-1][1].append(_get_link_annotation( 

2217 gc, x, y, width, height, angle)) 

2218 

2219 fonttype = mpl.rcParams['pdf.fonttype'] 

2220 

2221 # Set up a global transformation matrix for the whole math expression 

2222 a = math.radians(angle) 

2223 self.file.output(Op.gsave) 

2224 self.file.output(math.cos(a), math.sin(a), 

2225 -math.sin(a), math.cos(a), 

2226 x, y, Op.concat_matrix) 

2227 

2228 self.check_gc(gc, gc._rgb) 

2229 prev_font = None, None 

2230 oldx, oldy = 0, 0 

2231 unsupported_chars = [] 

2232 

2233 self.file.output(Op.begin_text) 

2234 for font, fontsize, num, ox, oy in glyphs: 

2235 self.file._character_tracker.track_glyph(font, num) 

2236 fontname = font.fname 

2237 if not _font_supports_glyph(fonttype, num): 

2238 # Unsupported chars (i.e. multibyte in Type 3 or beyond BMP in 

2239 # Type 42) must be emitted separately (below). 

2240 unsupported_chars.append((font, fontsize, ox, oy, num)) 

2241 else: 

2242 self._setup_textpos(ox, oy, 0, oldx, oldy) 

2243 oldx, oldy = ox, oy 

2244 if (fontname, fontsize) != prev_font: 

2245 self.file.output(self.file.fontName(fontname), fontsize, 

2246 Op.selectfont) 

2247 prev_font = fontname, fontsize 

2248 self.file.output(self.encode_string(chr(num), fonttype), 

2249 Op.show) 

2250 self.file.output(Op.end_text) 

2251 

2252 for font, fontsize, ox, oy, num in unsupported_chars: 

2253 self._draw_xobject_glyph( 

2254 font, fontsize, font.get_char_index(num), ox, oy) 

2255 

2256 # Draw any horizontal lines in the math layout 

2257 for ox, oy, width, height in rects: 

2258 self.file.output(Op.gsave, ox, oy, width, height, 

2259 Op.rectangle, Op.fill, Op.grestore) 

2260 

2261 # Pop off the global transformation 

2262 self.file.output(Op.grestore) 

2263 

2264 def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): 

2265 # docstring inherited 

2266 texmanager = self.get_texmanager() 

2267 fontsize = prop.get_size_in_points() 

2268 dvifile = texmanager.make_dvi(s, fontsize) 

2269 with dviread.Dvi(dvifile, 72) as dvi: 

2270 page, = dvi 

2271 

2272 if gc.get_url() is not None: 

2273 self.file._annotations[-1][1].append(_get_link_annotation( 

2274 gc, x, y, page.width, page.height, angle)) 

2275 

2276 # Gather font information and do some setup for combining 

2277 # characters into strings. The variable seq will contain a 

2278 # sequence of font and text entries. A font entry is a list 

2279 # ['font', name, size] where name is a Name object for the 

2280 # font. A text entry is ['text', x, y, glyphs, x+w] where x 

2281 # and y are the starting coordinates, w is the width, and 

2282 # glyphs is a list; in this phase it will always contain just 

2283 # one one-character string, but later it may have longer 

2284 # strings interspersed with kern amounts. 

2285 oldfont, seq = None, [] 

2286 for x1, y1, dvifont, glyph, width in page.text: 

2287 if dvifont != oldfont: 

2288 pdfname = self.file.dviFontName(dvifont) 

2289 seq += [['font', pdfname, dvifont.size]] 

2290 oldfont = dvifont 

2291 seq += [['text', x1, y1, [bytes([glyph])], x1+width]] 

2292 

2293 # Find consecutive text strings with constant y coordinate and 

2294 # combine into a sequence of strings and kerns, or just one 

2295 # string (if any kerns would be less than 0.1 points). 

2296 i, curx, fontsize = 0, 0, None 

2297 while i < len(seq)-1: 

2298 elt, nxt = seq[i:i+2] 

2299 if elt[0] == 'font': 

2300 fontsize = elt[2] 

2301 elif elt[0] == nxt[0] == 'text' and elt[2] == nxt[2]: 

2302 offset = elt[4] - nxt[1] 

2303 if abs(offset) < 0.1: 

2304 elt[3][-1] += nxt[3][0] 

2305 elt[4] += nxt[4]-nxt[1] 

2306 else: 

2307 elt[3] += [offset*1000.0/fontsize, nxt[3][0]] 

2308 elt[4] = nxt[4] 

2309 del seq[i+1] 

2310 continue 

2311 i += 1 

2312 

2313 # Create a transform to map the dvi contents to the canvas. 

2314 mytrans = Affine2D().rotate_deg(angle).translate(x, y) 

2315 

2316 # Output the text. 

2317 self.check_gc(gc, gc._rgb) 

2318 self.file.output(Op.begin_text) 

2319 curx, cury, oldx, oldy = 0, 0, 0, 0 

2320 for elt in seq: 

2321 if elt[0] == 'font': 

2322 self.file.output(elt[1], elt[2], Op.selectfont) 

2323 elif elt[0] == 'text': 

2324 curx, cury = mytrans.transform((elt[1], elt[2])) 

2325 self._setup_textpos(curx, cury, angle, oldx, oldy) 

2326 oldx, oldy = curx, cury 

2327 if len(elt[3]) == 1: 

2328 self.file.output(elt[3][0], Op.show) 

2329 else: 

2330 self.file.output(elt[3], Op.showkern) 

2331 else: 

2332 assert False 

2333 self.file.output(Op.end_text) 

2334 

2335 # Then output the boxes (e.g., variable-length lines of square 

2336 # roots). 

2337 boxgc = self.new_gc() 

2338 boxgc.copy_properties(gc) 

2339 boxgc.set_linewidth(0) 

2340 pathops = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, 

2341 Path.CLOSEPOLY] 

2342 for x1, y1, h, w in page.boxes: 

2343 path = Path([[x1, y1], [x1+w, y1], [x1+w, y1+h], [x1, y1+h], 

2344 [0, 0]], pathops) 

2345 self.draw_path(boxgc, path, mytrans, gc._rgb) 

2346 

2347 def encode_string(self, s, fonttype): 

2348 if fonttype in (1, 3): 

2349 return s.encode('cp1252', 'replace') 

2350 return s.encode('utf-16be', 'replace') 

2351 

2352 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): 

2353 # docstring inherited 

2354 

2355 # TODO: combine consecutive texts into one BT/ET delimited section 

2356 

2357 self.check_gc(gc, gc._rgb) 

2358 if ismath: 

2359 return self.draw_mathtext(gc, x, y, s, prop, angle) 

2360 

2361 fontsize = prop.get_size_in_points() 

2362 

2363 if mpl.rcParams['pdf.use14corefonts']: 

2364 font = self._get_font_afm(prop) 

2365 fonttype = 1 

2366 else: 

2367 font = self._get_font_ttf(prop) 

2368 self.file._character_tracker.track(font, s) 

2369 fonttype = mpl.rcParams['pdf.fonttype'] 

2370 

2371 if gc.get_url() is not None: 

2372 font.set_text(s) 

2373 width, height = font.get_width_height() 

2374 self.file._annotations[-1][1].append(_get_link_annotation( 

2375 gc, x, y, width / 64, height / 64, angle)) 

2376 

2377 # If fonttype is neither 3 nor 42, emit the whole string at once 

2378 # without manual kerning. 

2379 if fonttype not in [3, 42]: 

2380 self.file.output(Op.begin_text, 

2381 self.file.fontName(prop), fontsize, Op.selectfont) 

2382 self._setup_textpos(x, y, angle) 

2383 self.file.output(self.encode_string(s, fonttype), 

2384 Op.show, Op.end_text) 

2385 

2386 # A sequence of characters is broken into multiple chunks. The chunking 

2387 # serves two purposes: 

2388 # - For Type 3 fonts, there is no way to access multibyte characters, 

2389 # as they cannot have a CIDMap. Therefore, in this case we break 

2390 # the string into chunks, where each chunk contains either a string 

2391 # of consecutive 1-byte characters or a single multibyte character. 

2392 # - A sequence of 1-byte characters is split into chunks to allow for 

2393 # kerning adjustments between consecutive chunks. 

2394 # 

2395 # Each chunk is emitted with a separate command: 1-byte characters use 

2396 # the regular text show command (TJ) with appropriate kerning between 

2397 # chunks, whereas multibyte characters use the XObject command (Do). 

2398 else: 

2399 # List of (ft_object, start_x, [prev_kern, char, char, ...]), 

2400 # w/o zero kerns. 

2401 singlebyte_chunks = [] 

2402 # List of (ft_object, start_x, glyph_index). 

2403 multibyte_glyphs = [] 

2404 prev_was_multibyte = True 

2405 prev_font = font 

2406 for item in _text_helpers.layout( 

2407 s, font, kern_mode=KERNING_UNFITTED): 

2408 if _font_supports_glyph(fonttype, ord(item.char)): 

2409 if prev_was_multibyte or item.ft_object != prev_font: 

2410 singlebyte_chunks.append((item.ft_object, item.x, [])) 

2411 prev_font = item.ft_object 

2412 if item.prev_kern: 

2413 singlebyte_chunks[-1][2].append(item.prev_kern) 

2414 singlebyte_chunks[-1][2].append(item.char) 

2415 prev_was_multibyte = False 

2416 else: 

2417 multibyte_glyphs.append( 

2418 (item.ft_object, item.x, item.glyph_idx) 

2419 ) 

2420 prev_was_multibyte = True 

2421 # Do the rotation and global translation as a single matrix 

2422 # concatenation up front 

2423 self.file.output(Op.gsave) 

2424 a = math.radians(angle) 

2425 self.file.output(math.cos(a), math.sin(a), 

2426 -math.sin(a), math.cos(a), 

2427 x, y, Op.concat_matrix) 

2428 # Emit all the 1-byte characters in a BT/ET group. 

2429 

2430 self.file.output(Op.begin_text) 

2431 prev_start_x = 0 

2432 for ft_object, start_x, kerns_or_chars in singlebyte_chunks: 

2433 ft_name = self.file.fontName(ft_object.fname) 

2434 self.file.output(ft_name, fontsize, Op.selectfont) 

2435 self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0) 

2436 self.file.output( 

2437 # See pdf spec "Text space details" for the 1000/fontsize 

2438 # (aka. 1000/T_fs) factor. 

2439 [-1000 * next(group) / fontsize if tp == float # a kern 

2440 else self.encode_string("".join(group), fonttype) 

2441 for tp, group in itertools.groupby(kerns_or_chars, type)], 

2442 Op.showkern) 

2443 prev_start_x = start_x 

2444 self.file.output(Op.end_text) 

2445 # Then emit all the multibyte characters, one at a time. 

2446 for ft_object, start_x, glyph_idx in multibyte_glyphs: 

2447 self._draw_xobject_glyph( 

2448 ft_object, fontsize, glyph_idx, start_x, 0 

2449 ) 

2450 self.file.output(Op.grestore) 

2451 

2452 def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): 

2453 """Draw a multibyte character from a Type 3 font as an XObject.""" 

2454 glyph_name = font.get_glyph_name(glyph_idx) 

2455 name = self.file._get_xobject_glyph_name(font.fname, glyph_name) 

2456 self.file.output( 

2457 Op.gsave, 

2458 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, 

2459 Name(name), Op.use_xobject, 

2460 Op.grestore, 

2461 ) 

2462 

2463 def new_gc(self): 

2464 # docstring inherited 

2465 return GraphicsContextPdf(self.file) 

2466 

2467 

2468class GraphicsContextPdf(GraphicsContextBase): 

2469 

2470 def __init__(self, file): 

2471 super().__init__() 

2472 self._fillcolor = (0.0, 0.0, 0.0) 

2473 self._effective_alphas = (1.0, 1.0) 

2474 self.file = file 

2475 self.parent = None 

2476 

2477 def __repr__(self): 

2478 d = dict(self.__dict__) 

2479 del d['file'] 

2480 del d['parent'] 

2481 return repr(d) 

2482 

2483 def stroke(self): 

2484 """ 

2485 Predicate: does the path need to be stroked (its outline drawn)? 

2486 This tests for the various conditions that disable stroking 

2487 the path, in which case it would presumably be filled. 

2488 """ 

2489 # _linewidth > 0: in pdf a line of width 0 is drawn at minimum 

2490 # possible device width, but e.g., agg doesn't draw at all 

2491 return (self._linewidth > 0 and self._alpha > 0 and 

2492 (len(self._rgb) <= 3 or self._rgb[3] != 0.0)) 

2493 

2494 def fill(self, *args): 

2495 """ 

2496 Predicate: does the path need to be filled? 

2497 

2498 An optional argument can be used to specify an alternative 

2499 _fillcolor, as needed by RendererPdf.draw_markers. 

2500 """ 

2501 if len(args): 

2502 _fillcolor = args[0] 

2503 else: 

2504 _fillcolor = self._fillcolor 

2505 return (self._hatch or 

2506 (_fillcolor is not None and 

2507 (len(_fillcolor) <= 3 or _fillcolor[3] != 0.0))) 

2508 

2509 def paint(self): 

2510 """ 

2511 Return the appropriate pdf operator to cause the path to be 

2512 stroked, filled, or both. 

2513 """ 

2514 return Op.paint_path(self.fill(), self.stroke()) 

2515 

2516 capstyles = {'butt': 0, 'round': 1, 'projecting': 2} 

2517 joinstyles = {'miter': 0, 'round': 1, 'bevel': 2} 

2518 

2519 def capstyle_cmd(self, style): 

2520 return [self.capstyles[style], Op.setlinecap] 

2521 

2522 def joinstyle_cmd(self, style): 

2523 return [self.joinstyles[style], Op.setlinejoin] 

2524 

2525 def linewidth_cmd(self, width): 

2526 return [width, Op.setlinewidth] 

2527 

2528 def dash_cmd(self, dashes): 

2529 offset, dash = dashes 

2530 if dash is None: 

2531 dash = [] 

2532 offset = 0 

2533 return [list(dash), offset, Op.setdash] 

2534 

2535 def alpha_cmd(self, alpha, forced, effective_alphas): 

2536 name = self.file.alphaState(effective_alphas) 

2537 return [name, Op.setgstate] 

2538 

2539 def hatch_cmd(self, hatch, hatch_color): 

2540 if not hatch: 

2541 if self._fillcolor is not None: 

2542 return self.fillcolor_cmd(self._fillcolor) 

2543 else: 

2544 return [Name('DeviceRGB'), Op.setcolorspace_nonstroke] 

2545 else: 

2546 hatch_style = (hatch_color, self._fillcolor, hatch) 

2547 name = self.file.hatchPattern(hatch_style) 

2548 return [Name('Pattern'), Op.setcolorspace_nonstroke, 

2549 name, Op.setcolor_nonstroke] 

2550 

2551 def rgb_cmd(self, rgb): 

2552 if mpl.rcParams['pdf.inheritcolor']: 

2553 return [] 

2554 if rgb[0] == rgb[1] == rgb[2]: 

2555 return [rgb[0], Op.setgray_stroke] 

2556 else: 

2557 return [*rgb[:3], Op.setrgb_stroke] 

2558 

2559 def fillcolor_cmd(self, rgb): 

2560 if rgb is None or mpl.rcParams['pdf.inheritcolor']: 

2561 return [] 

2562 elif rgb[0] == rgb[1] == rgb[2]: 

2563 return [rgb[0], Op.setgray_nonstroke] 

2564 else: 

2565 return [*rgb[:3], Op.setrgb_nonstroke] 

2566 

2567 def push(self): 

2568 parent = GraphicsContextPdf(self.file) 

2569 parent.copy_properties(self) 

2570 parent.parent = self.parent 

2571 self.parent = parent 

2572 return [Op.gsave] 

2573 

2574 def pop(self): 

2575 assert self.parent is not None 

2576 self.copy_properties(self.parent) 

2577 self.parent = self.parent.parent 

2578 return [Op.grestore] 

2579 

2580 def clip_cmd(self, cliprect, clippath): 

2581 """Set clip rectangle. Calls `.pop()` and `.push()`.""" 

2582 cmds = [] 

2583 # Pop graphics state until we hit the right one or the stack is empty 

2584 while ((self._cliprect, self._clippath) != (cliprect, clippath) 

2585 and self.parent is not None): 

2586 cmds.extend(self.pop()) 

2587 # Unless we hit the right one, set the clip polygon 

2588 if ((self._cliprect, self._clippath) != (cliprect, clippath) or 

2589 self.parent is None): 

2590 cmds.extend(self.push()) 

2591 if self._cliprect != cliprect: 

2592 cmds.extend([cliprect, Op.rectangle, Op.clip, Op.endpath]) 

2593 if self._clippath != clippath: 

2594 path, affine = clippath.get_transformed_path_and_affine() 

2595 cmds.extend( 

2596 PdfFile.pathOperations(path, affine, simplify=False) + 

2597 [Op.clip, Op.endpath]) 

2598 return cmds 

2599 

2600 commands = ( 

2601 # must come first since may pop 

2602 (('_cliprect', '_clippath'), clip_cmd), 

2603 (('_alpha', '_forced_alpha', '_effective_alphas'), alpha_cmd), 

2604 (('_capstyle',), capstyle_cmd), 

2605 (('_fillcolor',), fillcolor_cmd), 

2606 (('_joinstyle',), joinstyle_cmd), 

2607 (('_linewidth',), linewidth_cmd), 

2608 (('_dashes',), dash_cmd), 

2609 (('_rgb',), rgb_cmd), 

2610 # must come after fillcolor and rgb 

2611 (('_hatch', '_hatch_color'), hatch_cmd), 

2612 ) 

2613 

2614 def delta(self, other): 

2615 """ 

2616 Copy properties of other into self and return PDF commands 

2617 needed to transform self into other. 

2618 """ 

2619 cmds = [] 

2620 fill_performed = False 

2621 for params, cmd in self.commands: 

2622 different = False 

2623 for p in params: 

2624 ours = getattr(self, p) 

2625 theirs = getattr(other, p) 

2626 try: 

2627 if ours is None or theirs is None: 

2628 different = ours is not theirs 

2629 else: 

2630 different = bool(ours != theirs) 

2631 except ValueError: 

2632 ours = np.asarray(ours) 

2633 theirs = np.asarray(theirs) 

2634 different = (ours.shape != theirs.shape or 

2635 np.any(ours != theirs)) 

2636 if different: 

2637 break 

2638 

2639 # Need to update hatching if we also updated fillcolor 

2640 if params == ('_hatch', '_hatch_color') and fill_performed: 

2641 different = True 

2642 

2643 if different: 

2644 if params == ('_fillcolor',): 

2645 fill_performed = True 

2646 theirs = [getattr(other, p) for p in params] 

2647 cmds.extend(cmd(self, *theirs)) 

2648 for p in params: 

2649 setattr(self, p, getattr(other, p)) 

2650 return cmds 

2651 

2652 def copy_properties(self, other): 

2653 """ 

2654 Copy properties of other into self. 

2655 """ 

2656 super().copy_properties(other) 

2657 fillcolor = getattr(other, '_fillcolor', self._fillcolor) 

2658 effective_alphas = getattr(other, '_effective_alphas', 

2659 self._effective_alphas) 

2660 self._fillcolor = fillcolor 

2661 self._effective_alphas = effective_alphas 

2662 

2663 def finalize(self): 

2664 """ 

2665 Make sure every pushed graphics state is popped. 

2666 """ 

2667 cmds = [] 

2668 while self.parent is not None: 

2669 cmds.extend(self.pop()) 

2670 return cmds 

2671 

2672 

2673class PdfPages: 

2674 """ 

2675 A multi-page PDF file. 

2676 

2677 Examples 

2678 -------- 

2679 >>> import matplotlib.pyplot as plt 

2680 >>> # Initialize: 

2681 >>> with PdfPages('foo.pdf') as pdf: 

2682 ... # As many times as you like, create a figure fig and save it: 

2683 ... fig = plt.figure() 

2684 ... pdf.savefig(fig) 

2685 ... # When no figure is specified the current figure is saved 

2686 ... pdf.savefig() 

2687 

2688 Notes 

2689 ----- 

2690 In reality `PdfPages` is a thin wrapper around `PdfFile`, in order to avoid 

2691 confusion when using `~.pyplot.savefig` and forgetting the format argument. 

2692 """ 

2693 __slots__ = ('_file', 'keep_empty') 

2694 

2695 def __init__(self, filename, keep_empty=True, metadata=None): 

2696 """ 

2697 Create a new PdfPages object. 

2698 

2699 Parameters 

2700 ---------- 

2701 filename : str or path-like or file-like 

2702 Plots using `PdfPages.savefig` will be written to a file at this 

2703 location. The file is opened at once and any older file with the 

2704 same name is overwritten. 

2705 

2706 keep_empty : bool, optional 

2707 If set to False, then empty pdf files will be deleted automatically 

2708 when closed. 

2709 

2710 metadata : dict, optional 

2711 Information dictionary object (see PDF reference section 10.2.1 

2712 'Document Information Dictionary'), e.g.: 

2713 ``{'Creator': 'My software', 'Author': 'Me', 'Title': 'Awesome'}``. 

2714 

2715 The standard keys are 'Title', 'Author', 'Subject', 'Keywords', 

2716 'Creator', 'Producer', 'CreationDate', 'ModDate', and 

2717 'Trapped'. Values have been predefined for 'Creator', 'Producer' 

2718 and 'CreationDate'. They can be removed by setting them to `None`. 

2719 """ 

2720 self._file = PdfFile(filename, metadata=metadata) 

2721 self.keep_empty = keep_empty 

2722 

2723 def __enter__(self): 

2724 return self 

2725 

2726 def __exit__(self, exc_type, exc_val, exc_tb): 

2727 self.close() 

2728 

2729 def close(self): 

2730 """ 

2731 Finalize this object, making the underlying file a complete 

2732 PDF file. 

2733 """ 

2734 self._file.finalize() 

2735 self._file.close() 

2736 if (self.get_pagecount() == 0 and not self.keep_empty and 

2737 not self._file.passed_in_file_object): 

2738 os.remove(self._file.fh.name) 

2739 self._file = None 

2740 

2741 def infodict(self): 

2742 """ 

2743 Return a modifiable information dictionary object 

2744 (see PDF reference section 10.2.1 'Document Information 

2745 Dictionary'). 

2746 """ 

2747 return self._file.infoDict 

2748 

2749 def savefig(self, figure=None, **kwargs): 

2750 """ 

2751 Save a `.Figure` to this file as a new page. 

2752 

2753 Any other keyword arguments are passed to `~.Figure.savefig`. 

2754 

2755 Parameters 

2756 ---------- 

2757 figure : `.Figure` or int, default: the active figure 

2758 The figure, or index of the figure, that is saved to the file. 

2759 """ 

2760 if not isinstance(figure, Figure): 

2761 if figure is None: 

2762 manager = Gcf.get_active() 

2763 else: 

2764 manager = Gcf.get_fig_manager(figure) 

2765 if manager is None: 

2766 raise ValueError("No figure {}".format(figure)) 

2767 figure = manager.canvas.figure 

2768 # Force use of pdf backend, as PdfPages is tightly coupled with it. 

2769 try: 

2770 orig_canvas = figure.canvas 

2771 figure.canvas = FigureCanvasPdf(figure) 

2772 figure.savefig(self, format="pdf", **kwargs) 

2773 finally: 

2774 figure.canvas = orig_canvas 

2775 

2776 def get_pagecount(self): 

2777 """Return the current number of pages in the multipage pdf file.""" 

2778 return len(self._file.pageList) 

2779 

2780 def attach_note(self, text, positionRect=[-100, -100, 0, 0]): 

2781 """ 

2782 Add a new text note to the page to be saved next. The optional 

2783 positionRect specifies the position of the new note on the 

2784 page. It is outside the page per default to make sure it is 

2785 invisible on printouts. 

2786 """ 

2787 self._file.newTextnote(text, positionRect) 

2788 

2789 

2790class FigureCanvasPdf(FigureCanvasBase): 

2791 # docstring inherited 

2792 

2793 fixed_dpi = 72 

2794 filetypes = {'pdf': 'Portable Document Format'} 

2795 

2796 def get_default_filetype(self): 

2797 return 'pdf' 

2798 

2799 def print_pdf(self, filename, *, 

2800 bbox_inches_restore=None, metadata=None): 

2801 

2802 dpi = self.figure.dpi 

2803 self.figure.dpi = 72 # there are 72 pdf points to an inch 

2804 width, height = self.figure.get_size_inches() 

2805 if isinstance(filename, PdfPages): 

2806 file = filename._file 

2807 else: 

2808 file = PdfFile(filename, metadata=metadata) 

2809 try: 

2810 file.newPage(width, height) 

2811 renderer = MixedModeRenderer( 

2812 self.figure, width, height, dpi, 

2813 RendererPdf(file, dpi, height, width), 

2814 bbox_inches_restore=bbox_inches_restore) 

2815 self.figure.draw(renderer) 

2816 renderer.finalize() 

2817 if not isinstance(filename, PdfPages): 

2818 file.finalize() 

2819 finally: 

2820 if isinstance(filename, PdfPages): # finish off this page 

2821 file.endStream() 

2822 else: # we opened the file above; now finish it off 

2823 file.close() 

2824 

2825 def draw(self): 

2826 self.figure.draw_without_rendering() 

2827 return super().draw() 

2828 

2829 

2830FigureManagerPdf = FigureManagerBase 

2831 

2832 

2833@_Backend.export 

2834class _BackendPdf(_Backend): 

2835 FigureCanvas = FigureCanvasPdf