8000 Simplify and unify character tracking in pdf and ps backends. · matplotlib/matplotlib@443b7d2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 443b7d2

Browse files
committed
Simplify and unify character tracking in pdf and ps backends.
Instead of trying to resolve font paths to absolute files and key off by inode(!), just track fonts using whatever names they use, and simplify used_characters to be a straight mapping of filenames to character ids (making the attribute private -- with a backcompat shim) at the same time). The previous approach would avoid embedding the same file twice if it is given under two different filenames (hardlinks to the same file...), but it would fail if the user passes a relative path, chdir()s to another directory, and passes another different font with the same filename, because of the lru_cache(). None of these seem likely to happen in practice, and in any case we can cover most of it by making the font paths absolute before passing them to FreeType (which is going to open the file anyways, so the cost of making them absolute doesn't matter).
1 parent 34d486e commit 443b7d2

File tree

9 files changed

+95
-83
lines changed

9 files changed

+95
-83
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Deprecations
2+
````````````
3+
4+
The ``used_characters`` attribute of `.RendererPdf` and `.RendererPS` is
5+
deprecated. The ``track_characters`` and ``merge_used_characters`` methods
6+
of `.RendererPdf` are deprecated; use the same methods on the underlying
7+
`.PdfFile` instead.

doc/missing-references.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,10 @@
357357
"lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolGrid:1",
358358
"lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolMinorGrid:1"
359359
],
360+
"matplotlib.backends._backend_pdf_ps.CharacterTracker": [
361+
"lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.PdfFile:1",
362+
"lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS:1"
363+
],
360364
"matplotlib.backends._backend_pdf_ps.RendererPDFPSBase": [
361365
"lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf:1",
362366
"lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS:1"

lib/matplotlib/backend_bases.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class RendererBase:
148148
"""
149149

150150
def __init__(self):
151+
super().__init__()
151152
self._texmanager = None
152153
self._text2path = textpath.TextToPath()
153154

lib/matplotlib/backends/_backend_pdf_ps.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,43 @@ def _cached_get_afm_from_fname(fname):
1616
return AFM(fh)
1717

1818

19+
class CharacterTracker:
20+
def __init__(self):
21+
super().__init__()
22+
self._used_characters = {}
23+
24+
@mpl.cbook.deprecated("3.3")
25+
@property
26+
def used_characters(self):
27+
d = {}
28+
for fname, chars in self._used_characters.items():
29+
realpath, stat_key = mpl.cbook.get_realpath_and_stat(fname)
30+
d[stat_key] = (realpath, chars)
31+
return d
32+
33+
def track_characters(self, font, s):
34+
"""Keeps track of which characters are required from each font."""
35+
if isinstance(font, str):
36+
fname = font # never actually used.
37+
else:
38+
fname = font.fname
39+
self._used_characters.setdefault(fname, set()).update(map(ord, s))
40+
41+
def merge_used_characters(self, other):
42+
for fname, charset in other.items():
43+
self._used_characters.setdefault(fname, set()).update(charset)
44+
45+
1946
class RendererPDFPSBase(RendererBase):
2047
# The following attributes must be defined by the subclasses:
2148
# - _afm_font_dir
2249
# - _use_afm_rc_name
2350

51+
def __init__(self, width, height):
52+
super().__init__()
53+
self.width = width
54+
self.height = height
55+
2456
def flipy(self):
2557
# docstring inherited
2658
return False # y increases from bottom to top.

lib/matplotlib/backends/backend_pdf.py

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ def _flush(self):
429429
self.compressobj = None
430430

431431

432-
class PdfFile:
432+
class PdfFile(_backend_pdf_ps.CharacterTracker):
433433
"""PDF file object."""
434434

435435
def __init__(self, filename, metadata=None):
@@ -451,6 +451,8 @@ def __init__(self, filename, metadata=None):
451451
for `'Creator'`, `'Producer'` and `'CreationDate'`. They
452452
can be removed by setting them to `None`.
453453
"""
454+
super().__init__()
455+
454456
self._object_seq = itertools.count(1) # consumed by reserveObject
455457
self.xrefTable = [[0, 65535, 'the zero object']]
456458
self.passed_in_file_object = False
@@ -513,7 +515,6 @@ def __init__(self, filename, metadata=None):
513515
self.dviFontInfo = {} # maps dvi font names to embedding information
514516
# differently encoded Type-1 fonts may share the same descriptor
515517
self.type1Descriptors = {}
516-
self.used_characters = {}
517518

518519
self.alphaStates = {} # maps alpha values to graphics state objects
519520
self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1))
@@ -724,10 +725,9 @@ def writeFonts(self):
724725
else:
725726
# a normal TrueType font
726727
_log.debug('Writing TrueType font.')
727-
realpath, stat_key = cbook.get_realpath_and_stat(filename)
728-
chars = self.used_characters.get(stat_key)
729-
if chars is not None and len(chars[1]):
730-
fonts[Fx] = self.embedTTF(realpath, chars[1])
728+
chars = self._used_characters.get(filename)
729+
if chars:
730+
fonts[Fx] = self.embedTTF(filename, chars)
731731
self.writeObject(self.fontObject, fonts)
732732

733733
def _write_afm_font(self, filename):
@@ -1673,9 +1673,7 @@ def afm_font_cache(self, _cache=cbook.maxdict(50)):
16731673
_use_afm_rc_name = "pdf.use14corefonts"
16741674

16751675
def __init__(self, file, image_dpi, height, width):
1676-
RendererBase.__init__(self)
1677-
self.height = height
1678-
self.width = width
1676+
super().__init__(width, height)
16791677
self.file = file
16801678
self.gc = self.new_gc()
16811679
self.mathtext_parser = MathTextParser("Pdf")
@@ -1711,22 +1709,13 @@ def check_gc(self, gc, fillcolor=None):
17111709
gc._fillcolor = orig_fill
17121710
gc._effective_alphas = orig_alphas
17131711

1714-
def track_characters(self, font, s):
1715-
"""Keeps track of which characters are required from each font."""
1716-
if isinstance(font, str):
1717-
fname = font
1718-
else:
1719-
fname = font.fname
1720-
realpath, stat_key = cbook.get_realpath_and_stat(fname)
1721-
used_characters = self.file.used_characters.setdefault(
1722-
stat_key, (realpath, set()))
1723-
used_characters[1].update(map(ord, s))
1724-
1725-
def merge_used_characters(self, other):
1726-
for stat_key, (realpath, charset) in other.items():
1727-
used_characters = self.file.used_characters.setdefault(
1728-
stat_key, (realpath, set()))
1729-
used_characters[1].update(charset)
1712+
@cbook.deprecated("3.3", alternative="renderer.file.track_characters")
1713+
def track_characters(self, *args, **kwargs):
1714+
self.file.track_characters(*args, **kwargs)
1715+
1716+
@cbook.deprecated("3.3", alternative="renderer.file.merge_used_characters")
1717+
def merge_used_characters(self, *args, **kwargs):
1718+
self.file.merge_used_characters(*args, **kwargs)
17301719

17311720
def get_image_magnification(self):
17321721
return self.image_dpi/72.0
@@ -1936,7 +1925,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
19361925
# TODO: fix positioning and encoding
19371926
width, height, descent, glyphs, rects, used_characters = \
19381927
self.mathtext_parser.parse(s, 72, prop)
1939-
self.merge_used_characters(used_characters)
1928+
self.file.merge_used_characters(used_characters)
19401929

19411930
# When using Type 3 fonts, we can't use character codes higher
19421931
# than 255, so we use the "Do" command to render those
@@ -2099,7 +2088,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
20992088
fonttype = 1
21002089
else:
21012090
font = self._get_font_ttf(prop)
2102-
self.track_characters(font, s)
2091+
self.file.track_characters(font, s)
21032092
fonttype = rcParams['pdf.fonttype']
21042093
# We can't subset all OpenType fonts, so switch to Type 42
21052094
# in that case.

lib/matplotlib/backends/backend_ps.py

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
from matplotlib.backend_bases import (
2626
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
2727
RendererBase)
28-
from matplotlib.cbook import (get_realpath_and_stat, is_writable_file_like,
29-
file_requires_unicode)
28+
from matplotlib.cbook import is_writable_file_like, file_requires_unicode
3029
from matplotlib.font_manager import is_opentype_cff_font, get_font
3130
from matplotlib.ft2font import LOAD_NO_HINTING
3231
from matplotlib.ttconv import convert_ttf_to_ps
@@ -184,7 +183,8 @@ def _move_path_to_path_or_stream(src, dst):
184183
shutil.move(src, dst, copy_function=shutil.copyfile)
185184

186185

187-
class RendererPS(_backend_pdf_ps.RendererPDFPSBase):
186+
class RendererPS(_backend_pdf_ps.RendererPDFPSBase,
187+
_backend_pdf_ps.CharacterTracker):
188188
"""
189189
The renderer handles all the drawing primitives using a graphics
190190
context instance that controls the colors/styles.
@@ -202,7 +202,7 @@ def __init__(self, width, height, pswriter, imagedpi=72):
202202
# Although postscript itself is dpi independent, we need to inform the
203203
# image code about a requested dpi to generate high resolution images
204204
# and them scale them before embedding them.
205-
RendererBase.__init__(self)
205+
super().__init__(width, height)
206206
self.width = width
207207
self.height = height
208208
self._pswriter = pswriter
@@ -224,22 +224,8 @@ def __init__(self, width, height, pswriter, imagedpi=72):
224224
self._clip_paths = {}
225225
self._path_collection_id = 0
226226

227-
self.used_characters = {}
228227
self.mathtext_parser = MathTextParser("PS")
229228

230-
def track_characters(self, font, s):
231-
"""Keeps track of which characters are required from each font."""
232-
realpath, stat_key = get_realpath_and_stat(font.fname)
233-
used_characters = self.used_characters.setdefault(
234-
stat_key, (realpath, set()))
235-
used_characters[1].update(map(ord, s))
236-
237-
def merge_used_characters(self, other):
238-
for stat_key, (realpath, charset) in other.items():
239-
used_characters = self.used_characters.setdefault(
240-
stat_key, (realpath, set()))
241-
used_characters[1].update(charset)
242-
243229
def set_color(self, r, g, b, store=1):
244230
if (r, g, b) != self.color:
245231
if r == g and r == b:
@@ -980,46 +966,39 @@ def print_figure_impl(fh):
980966
Ndict = len(psDefs)
981967
print("%%BeginProlog", file=fh)
982968
if not rcParams['ps.useafm']:
983-
Ndict += len(ps_renderer.used_characters)
969+
Ndict += len(ps_renderer._used_characters)
984970
print("/mpldict %d dict def" % Ndict, file=fh)
985971
print("mpldict begin", file=fh)
986972
for d in psDefs:
987973
d = d.strip()
988974
for l in d.split('\n'):
989975
print(l.strip(), file=fh)
990976
if not rcParams['ps.useafm']:
991-
for font_filename, chars in \
992-
ps_renderer.used_characters.values():
993-
if len(chars):
994-
font = get_font(font_filename)
995-
glyph_ids = [font.get_char_index(c) for c in chars]
996-
997-
fonttype = rcParams['ps.fonttype']
998-
999-
# Can not use more than 255 characters from a
1000-
# single font for Type 3
1001-
if len(glyph_ids) > 255:
1002-
fonttype = 42
1003-
1004-
# The ttf to ps (subsetting) support doesn't work for
1005-
# OpenType fonts that are Postscript inside (like the
1006-
# STIX fonts). This will simply turn that off to avoid
1007-
# errors.
1008-
if is_opentype_cff_font(font_filename):
1009-
raise RuntimeError(
1010-
"OpenType CFF fonts can not be saved using "
1011-
"the internal Postscript backend at this "
1012-
"time; consider using the Cairo backend")
1013-
else:
1014-
fh.flush()
1015-
try:
1016-
convert_ttf_to_ps(os.fsencode(font_filename),
1017-
fh, fonttype, glyph_ids)
1018-
except RuntimeError:
1019-
_log.warning("The PostScript backend does not "
1020-
"currently support the selected "
1021-
"font.")
1022-
raise
977+
for font_path, chars in ps_renderer._used_characters.items():
978+
if not chars:
979+
continue
980+
font = get_font(font_path)
981+
glyph_ids = [font.get_char_index(c) for c in chars]
982+
fonttype = rcParams['ps.fonttype']
983+
# Can't use more than 255 chars from a single Type 3 font.
984+
if len(glyph_ids) > 255:
985+
fonttype = 42
986+
# The ttf to ps (subsetting) support doesn't work for
987+
# OpenType fonts that are Postscript inside (like the STIX
988+
# fonts). This will simply turn that off to avoid errors.
989+
if is_opentype_cff_font(font_path):
990+
raise RuntimeError(
991+
"OpenType CFF fonts can not be saved using "
992+
"the internal Postscript backend at this "
993+
"time; consider using the Cairo backend")
994+
fh.flush()
995+
try:
996+
convert_ttf_to_ps(os.fsencode(font_path),
997+
fh, fonttype, glyph_ids)
998+
except RuntimeError:
999+
_log.warning("The PostScript backend does not "
1000+
"currently support the selected font.")
1001+
raise
10231002
print("end", file=fh)
10241003
print("%%EndProlog", file=fh)
10251004

lib/matplotlib/cbook/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ def flatten(seq, scalarp=is_scalar_or_string):
483483
yield from flatten(item, scalarp)
484484

485485

486+
@deprecated("3.3", alternative="os.path.realpath and os.stat")
486487
@functools.lru_cache()
487488
def get_realpath_and_stat(path):
488489
realpath = os.path.realpath(path)

lib/matplotlib/font_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,6 +1335,9 @@ def is_opentype_cff_font(filename):
13351335

13361336

13371337
def get_font(filename, hinting_factor=None):
1338+
# Resolving the path avoids embedding the font twice in pdf/ps output if a
1339+
# single font is selected using two different relative paths.
1340+
filename = os.path.realpath(filename)
13381341
if hinting_factor is None:
13391342
hinting_factor = rcParams['text.hinting_factor']
13401343
return _get_font(os.fspath(filename), hinting_factor,

lib/matplotlib/mathtext.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131

3232
from matplotlib import cbook, colors as mcolors, rcParams
3333
from matplotlib.afm import AFM
34-
from matplotlib.cbook import get_realpath_and_stat
3534
from matplotlib.ft2font import FT2Image, KERNING_DEFAULT, LOAD_NO_HINTING
3635
from matplotlib.font_manager import findfont, FontProperties, get_font
3736
from matplotlib._mathtext_data import (latex_to_bakoma, latex_to_standard,
@@ -484,10 +483,7 @@ def render_glyph(self, ox, oy, facename, font_class, sym, fontsize, dpi):
484483
- *dpi*: The dpi to draw at.
485484
"""
486485
info = self._get_info(facename, font_class, sym, fontsize, dpi)
487-
realpath, stat_key = get_realpath_and_stat(info.font.fname)
488-
used_characters = self.used_characters.setdefault(
489-
stat_key, (realpath, set()))
490-
used_characters[1].add(info.num)
486+
self.used_characters.setdefault(info.font.fname, set()).add(info.num)
491487
self.mathtext_backend.render_glyph(ox, oy, info)
492488

493489
def render_rect_filled(self, x1, y1, x2, y2):

0 commit comments

Comments
 (0)
0