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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2A PDF Matplotlib backend.
4Author: Jouni K Seppänen <jks@iki.fi> and others.
5"""
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
24import numpy as np
25from PIL import Image
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
45_log = logging.getLogger(__name__)
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.
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.
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.
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
95@_api.deprecated("3.6", alternative="a vendored copy of _fill")
96def fill(strings, linelen=75):
97 return _fill(strings, linelen=linelen)
100def _fill(strings, linelen=75):
101 """
102 Make one string from sequence of strings, with whitespace in between.
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)
122def _create_pdf_info_dict(backend, metadata):
123 """
124 Create a PDF infoDict based on user-supplied metadata.
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.
130 Metadata is verified to have the correct keys and their expected types. Any
131 unknown keys/types will raise a warning.
133 Parameters
134 ----------
135 backend : str
136 The name of the backend to use in the Producer value.
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.
143 If any value is *None*, then the key will be removed. This can be used
144 to remove any pre-defined values.
146 Returns
147 -------
148 dict[str, Union[str, datetime, Name]]
149 A validated dictionary of metadata.
150 """
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()
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}
169 def is_string_like(x):
170 return isinstance(x, str)
171 is_string_like.text_for_warning = "an instance of str"
173 def is_date(x):
174 return isinstance(x, datetime)
175 is_date.text_for_warning = "an instance of datetime.datetime"
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"}'
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'])
206 return info
209def _datetime_to_pdf(d):
210 """
211 Convert a datetime to a PDF string representing it.
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
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 """
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))
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 """
256 vertices = _calculate_quad_point_coordinates(x, y, width,
257 height, angle)
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.
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))
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
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'})
304def pdfRepr(obj):
305 """Map Python objects to PDF syntax."""
307 # Some objects defined later have their own pdfRepr method.
308 if hasattr(obj, 'pdfRepr'):
309 return obj.pdfRepr()
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'.')
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]
325 # Integers are written as such.
326 elif isinstance(obj, (int, np.integer)):
327 return b"%d" % obj
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'))
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')')
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 ])
356 # Lists.
357 elif isinstance(obj, (list, tuple)):
358 return _fill([b"[", *[pdfRepr(val) for val in obj], b"]"])
360 # The null keyword.
361 elif obj is None:
362 return b'null'
364 # A date.
365 elif isinstance(obj, datetime):
366 return pdfRepr(_datetime_to_pdf(obj))
368 # A bounding box
369 elif isinstance(obj, BboxBase):
370 return _fill([pdfRepr(val) for val in obj.bounds])
372 else:
373 raise TypeError("Don't know a PDF representation for {} objects"
374 .format(type(obj)))
377def _font_supports_glyph(fonttype, glyph):
378 """
379 Returns True if the font is able to provide codepoint *glyph* in a PDF.
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()
392class Reference:
393 """
394 PDF reference object.
396 Use PdfFile.reserveObject() to create References.
397 """
399 def __init__(self, id):
400 self.id = id
402 def __repr__(self):
403 return "<Reference %d>" % self.id
405 def pdfRepr(self):
406 return b"%d 0 R" % self.id
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")
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)}}
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')
430 def __repr__(self):
431 return "<Name %s>" % self.name
433 def __str__(self):
434 return '/' + self.name.decode('ascii')
436 def __eq__(self, other):
437 return isinstance(other, Name) and self.name == other.name
439 def __lt__(self, other):
440 return isinstance(other, Name) and self.name < other.name
442 def __hash__(self):
443 return hash(self.name)
445 @staticmethod
446 @_api.deprecated("3.6")
447 def hexify(match):
448 return '#%02x' % ord(match.group())
450 def pdfRepr(self):
451 return b'/' + self.name
454@_api.deprecated("3.6")
455class Operator:
456 __slots__ = ('op',)
458 def __init__(self, op):
459 self.op = op
461 def __repr__(self):
462 return '<Operator %s>' % self.op
464 def pdfRepr(self):
465 return self.op
468class Verbatim:
469 """Store verbatim PDF command content for later inclusion in the stream."""
470 def __init__(self, x):
471 self._x = x
473 def pdfRepr(self):
474 return self._x
477class Op(Enum):
478 """PDF operators (not an exhaustive list)."""
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'
518 op = _api.deprecated('3.6')(property(lambda self: self.value))
520 def pdfRepr(self):
521 return self.value
523 @classmethod
524 def paint_path(cls, fill, stroke):
525 """
526 Return the PDF operator to paint a path.
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
547class Stream:
548 """
549 PDF stream object.
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')
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})
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()
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')
603 write(pdfRepr(dict))
604 write(b"\nstream\n")
606 def end(self):
607 """Finalize stream."""
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)
622 def write(self, data):
623 """Write some data on the stream."""
625 if self.compressobj is None:
626 self.file.write(data)
627 else:
628 compressed = self.compressobj.compress(data)
629 self.file.write(compressed)
631 def _flush(self):
632 """Flush the compression object."""
634 if self.compressobj is not None:
635 compressed = self.compressobj.flush()
636 self.file.write(compressed)
637 self.compressobj = None
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
686class PdfFile:
687 """PDF file object."""
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.
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'}``.
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__()
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
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")
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')
742 root = {'Type': Name('Catalog'),
743 'Pages': self.pagesObject}
744 self.writeObject(self.rootObject, root)
746 self.infoDict = _create_pdf_info_dict('pdf', metadata or {})
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()
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 = []
764 self._images = {}
765 self._image_seq = (Name(f'I{i}') for i in itertools.count(1))
767 self.markers = {}
768 self.multi_byte_charprocs = {}
770 self.paths = []
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 = []
780 # The PDF spec recommends to include every procset
781 procsets = [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()]
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)
794 def newPage(self, width, height):
795 self.endStream()
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))
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)
820 # Clear the list of annotations for the next page
821 self.pageAnnotations = []
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)
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 )
841 # encode to string using base 26
842 hashed = hash(frozenset(charmap.keys())) % ((sys.maxsize + 1) * 2)
843 prefix = toStr(hashed, 26)
845 # get first 6 characters from prefix
846 return prefix[:6] + "+" + ps_name
848 def finalize(self):
849 """Write out the various deferred objects and the pdf end matter."""
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()
877 # Finalize the file
878 self.writeXref()
879 self.writeTrailer()
881 def close(self):
882 """Flush all buffers and free all resources."""
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()
892 def write(self, data):
893 if self.currentstream is None:
894 self.fh.write(data)
895 else:
896 self.currentstream.write(data)
898 def output(self, *data):
899 self.write(_fill([pdfRepr(x) for x in data]))
900 self.write(b'\n')
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)
906 def endStream(self):
907 if self.currentstream is not None:
908 self.currentstream.end()
909 self.currentstream = None
911 def outputStream(self, ref, data, *, extra=None):
912 self.beginStream(ref.id, None, extra)
913 self.currentstream.write(data)
914 self.endStream()
916 def _write_annotations(self):
917 for annotsObject, annotations in self._annotations:
918 self.writeObject(annotsObject, annotations)
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 """
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
947 # find_fontsprop's first value always adheres to
948 # findfont's value, so technically no behaviour change
949 return first_Fx
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 """
958 dvi_info = self.dviFontInfo.get(dvifont.texname)
959 if dvi_info is not None:
960 return dvi_info.pdfname
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))
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
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)
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
1014 def _embedTeXFont(self, fontinfo):
1015 _log.debug('Embedding TeX font %s - fontinfo=%s',
1016 fontinfo.dvifont.texname, fontinfo.__dict__)
1018 # Widths
1019 widthsObject = self.reserveObject('font widths')
1020 self.writeObject(widthsObject, fontinfo.dvifont.widths)
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 }
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 }
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
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'])
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
1069 self.writeObject(fontdictObject, fontdict)
1070 return fontdictObject
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')
1078 italic_angle = t1font.prop['ItalicAngle']
1079 fixed_pitch = t1font.prop['isFixedPitch']
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
1107 ft2font = get_font(fontfile)
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 }
1126 self.writeObject(fontdescObject, descriptor)
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})
1133 return fontdescObject
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])
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"""
1163 def embedTTF(self, filename, characters):
1164 """Embed the TTF font from the named file into the document."""
1166 font = get_font(filename)
1167 fonttype = mpl.rcParams['pdf.fonttype']
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)
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]
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 }
1207 from encodings import cp1252
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)
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()
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
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)
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
1278 # Write everything out
1279 self.writeObject(fontdictObject, fontdict)
1280 self.writeObject(fontdescObject, descriptor)
1281 self.writeObject(widthsObject, widths)
1282 self.writeObject(charprocsObject, charprocs)
1284 return fontdictObject
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')
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 )
1304 # We need this ref for XObjects
1305 full_font = font
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)
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 }
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 }
1334 # Make fontfile stream
1335 descriptor['FontFile2'] = fontfileObject
1336 self.outputStream(
1337 fontfileObject, fontdata.getvalue(),
1338 extra={'Length1': fontdata.getbuffer().nbytes})
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]
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
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)
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)))
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)
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)
1410 name = self._get_xobject_glyph_name(filename, charname)
1411 self.multi_byte_charprocs[name] = charprocObject
1413 # CIDToGIDMap stream
1414 cid_to_gid_map = "".join(cid_to_gid_map).encode("utf-16be")
1415 self.outputStream(cidToGidMapObject, cid_to_gid_map)
1417 # ToUnicode CMap
1418 self.outputStream(toUnicodeMapObject, unicode_cmap)
1420 descriptor['MaxWidth'] = max_width
1422 # Write everything out
1423 self.writeObject(cidFontDictObject, cidFontDict)
1424 self.writeObject(type0FontDictObject, type0FontDict)
1425 self.writeObject(fontdescObject, descriptor)
1426 self.writeObject(wObject, w)
1428 return type0FontDictObject
1430 # Beginning of main embedTTF function...
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
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
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 }
1475 if fonttype == 3:
1476 return embedTTFType3(font, characters, descriptor)
1477 elif fonttype == 42:
1478 return embedTTFType42(font, characters, descriptor)
1480 def alphaState(self, alpha):
1481 """Return name of an ExtGState that sets alpha to the given value."""
1483 state = self.alphaStates.get(alpha, None)
1484 if state is not None:
1485 return state[0]
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
1493 def _soft_mask_state(self, smask):
1494 """
1495 Return an ExtGState that sets the soft mask to the given shading.
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.
1503 Returns
1504 -------
1505 Name
1506 """
1508 state = self._soft_mask_states.get(smask, None)
1509 if state is not None:
1510 return state[0]
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
1545 def writeExtGSTates(self):
1546 self.writeObject(
1547 self._extGStateObject,
1548 dict([
1549 *self.alphaStates.values(),
1550 *self._soft_mask_states.values()
1551 ])
1552 )
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()
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)
1570 pattern = self.hatchPatterns.get(hatch_style, None)
1571 if pattern is not None:
1572 return pattern
1574 name = next(self._hatch_pattern_seq)
1575 self.hatchPatterns[hatch_style] = name
1576 return name
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]})
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)
1605 self.output(mpl.rcParams['hatch.linewidth'], Op.setlinewidth)
1607 self.output(*self.pathOperations(
1608 Path.hatch(hatch),
1609 Affine2D().scale(sidelen),
1610 simplify=False))
1611 self.output(Op.fill_stroke)
1613 self.endStream()
1614 self.writeObject(self.hatchObject, hatchDict)
1616 def addGouraudTriangles(self, points, colors):
1617 """
1618 Add a Gouraud triangle shading.
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).
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
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)
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 })
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
1679 self.write(streamarr.tobytes())
1680 self.endStream()
1681 self.writeObject(self.gouraudObject, gouraudDict)
1683 def imageObject(self, image):
1684 """Return name of an image XObject representing the given image."""
1686 entry = self._images.get(id(image), None)
1687 if entry is not None:
1688 return entry[1]
1690 name = next(self._image_seq)
1691 ob = self.reserveObject(f'image {name}')
1692 self._images[id(image)] = (image, name, ob)
1693 return name
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
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
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()
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)
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
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()
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
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()
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))]
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)
1916 def reserveObject(self, name=''):
1917 """
1918 Reserve an ID for an indirect object.
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)
1927 def recordXref(self, id):
1928 self.xrefTable[id][0] = self.fh.tell() - self.tell_base
1930 def writeObject(self, object, contents):
1931 self.recordXref(object.id)
1932 object.write(contents, self)
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)
1947 def writeInfoDict(self):
1948 """Write out the info dictionary, checking it for good form"""
1950 self.infoObject = self.reserveObject('info')
1951 self.writeObject(self.infoObject, self.infoDict)
1953 def writeTrailer(self):
1954 """Write out the PDF trailer."""
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)
1965class RendererPdf(_backend_pdf_ps.RendererPDFPSBase):
1967 _afm_font_dir = cbook._get_data_path("fonts/pdfcorefonts")
1968 _use_afm_rc_name = "pdf.use14corefonts"
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
1976 def finalize(self):
1977 self.file.output(*self.gc.finalize())
1979 def check_gc(self, gc, fillcolor=None):
1980 orig_fill = getattr(gc, '_fillcolor', (0., 0., 0.))
1981 gc._fillcolor = fillcolor
1983 orig_alphas = getattr(gc, '_effective_alphas', (1.0, 1.0))
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)
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])
1998 delta = self.gc.delta(gc)
1999 if delta:
2000 self.file.output(*delta)
2002 # Restore gc to avoid unwanted side effects
2003 gc._fillcolor = orig_fill
2004 gc._effective_alphas = orig_alphas
2006 def get_image_magnification(self):
2007 return self.image_dpi/72.0
2009 def draw_image(self, gc, x, y, im, transform=None):
2010 # docstring inherited
2012 h, w = im.shape[:2]
2013 if w == 0 or h == 0:
2014 return
2016 if transform is None:
2017 # If there's no transform, alpha has already been applied
2018 gc.set_alpha(1.0)
2020 self.check_gc(gc)
2022 w = 72.0 * w / self.image_dpi
2023 h = 72.0 * h / self.image_dpi
2025 imob = self.file.imageObject(im)
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()
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)
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())
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)
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
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
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
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)
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)
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):
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())
2118 def draw_markers(self, gc, marker_path, marker_trans, path, trans,
2119 rgbFace=None):
2120 # docstring inherited
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
2130 self.check_gc(gc, rgbFace)
2131 fill = gc.fill(rgbFace)
2132 stroke = gc.stroke()
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())
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)
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)
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)
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
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
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)
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)
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)
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))
2219 fonttype = mpl.rcParams['pdf.fonttype']
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)
2228 self.check_gc(gc, gc._rgb)
2229 prev_font = None, None
2230 oldx, oldy = 0, 0
2231 unsupported_chars = []
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)
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)
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)
2261 # Pop off the global transformation
2262 self.file.output(Op.grestore)
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
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))
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]]
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
2313 # Create a transform to map the dvi contents to the canvas.
2314 mytrans = Affine2D().rotate_deg(angle).translate(x, y)
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)
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)
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')
2352 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
2353 # docstring inherited
2355 # TODO: combine consecutive texts into one BT/ET delimited section
2357 self.check_gc(gc, gc._rgb)
2358 if ismath:
2359 return self.draw_mathtext(gc, x, y, s, prop, angle)
2361 fontsize = prop.get_size_in_points()
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']
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))
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)
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.
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)
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 )
2463 def new_gc(self):
2464 # docstring inherited
2465 return GraphicsContextPdf(self.file)
2468class GraphicsContextPdf(GraphicsContextBase):
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
2477 def __repr__(self):
2478 d = dict(self.__dict__)
2479 del d['file']
2480 del d['parent']
2481 return repr(d)
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))
2494 def fill(self, *args):
2495 """
2496 Predicate: does the path need to be filled?
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)))
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())
2516 capstyles = {'butt': 0, 'round': 1, 'projecting': 2}
2517 joinstyles = {'miter': 0, 'round': 1, 'bevel': 2}
2519 def capstyle_cmd(self, style):
2520 return [self.capstyles[style], Op.setlinecap]
2522 def joinstyle_cmd(self, style):
2523 return [self.joinstyles[style], Op.setlinejoin]
2525 def linewidth_cmd(self, width):
2526 return [width, Op.setlinewidth]
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]
2535 def alpha_cmd(self, alpha, forced, effective_alphas):
2536 name = self.file.alphaState(effective_alphas)
2537 return [name, Op.setgstate]
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]
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]
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]
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]
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]
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
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 )
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
2639 # Need to update hatching if we also updated fillcolor
2640 if params == ('_hatch', '_hatch_color') and fill_performed:
2641 different = True
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
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
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
2673class PdfPages:
2674 """
2675 A multi-page PDF file.
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()
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')
2695 def __init__(self, filename, keep_empty=True, metadata=None):
2696 """
2697 Create a new PdfPages object.
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.
2706 keep_empty : bool, optional
2707 If set to False, then empty pdf files will be deleted automatically
2708 when closed.
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'}``.
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
2723 def __enter__(self):
2724 return self
2726 def __exit__(self, exc_type, exc_val, exc_tb):
2727 self.close()
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
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
2749 def savefig(self, figure=None, **kwargs):
2750 """
2751 Save a `.Figure` to this file as a new page.
2753 Any other keyword arguments are passed to `~.Figure.savefig`.
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
2776 def get_pagecount(self):
2777 """Return the current number of pages in the multipage pdf file."""
2778 return len(self._file.pageList)
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)
2790class FigureCanvasPdf(FigureCanvasBase):
2791 # docstring inherited
2793 fixed_dpi = 72
2794 filetypes = {'pdf': 'Portable Document Format'}
2796 def get_default_filetype(self):
2797 return 'pdf'
2799 def print_pdf(self, filename, *,
2800 bbox_inches_restore=None, metadata=None):
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()
2825 def draw(self):
2826 self.figure.draw_without_rendering()
2827 return super().draw()
2830FigureManagerPdf = FigureManagerBase
2833@_Backend.export
2834class _BackendPdf(_Backend):
2835 FigureCanvas = FigureCanvasPdf