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

Skip to content

Commit ef58a59

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 f7e7e46 commit ef58a59

File tree

9 files changed

+120
-81
lines changed

9 files changed

+120
-81
lines changed

doc/api/next_api_changes/deprecations.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,9 @@ Flags containing "U" passed to `.cbook.to_filehandle` and `.cbook.open_file_cm`
4141
Please remove "U" from flags passed to `.cbook.to_filehandle` and
4242
`.cbook.open_file_cm`. This is consistent with their removal from `open` in
4343
Python 3.9.
44+
45+
PDF and PS character tracking internals
46+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
47+
The ``used_characters`` attribute and ``track_characters`` and
48+
``merge_used_characters`` methods of `.RendererPdf`, `.PdfFile`, and
49+
`.RendererPS` are deprecated.

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
@@ -142,6 +142,7 @@ class RendererBase:
142142
"""
143143

144144
def __init__(self):
145+
super().__init__()
145146
self._texmanager = None
146147
self._text2path = textpath.TextToPath()
147148

lib/matplotlib/backends/_backend_pdf_ps.py

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

1818

19+
class CharacterTracker:
20+
"""
21+
Helper for font subsetting by the pdf and ps backends.
22+
23+
Maintains a mapping of font paths to the set of character codepoints that
24+
are being used from that font.
25+
"""
26+
27+
def __init__(self):
28+
self.used = {}
29+
30+
@mpl.cbook.deprecated("3.3")
31+
@property
32+
def used_characters(self):
33+
d = {}
34+
for fname, chars in self.used.items():
35+
realpath, stat_key = mpl.cbook.get_realpath_and_stat(fname)
36+
d[stat_key] = (realpath, chars)
37+
return d
38+
39+
def track(self, font, s):
40+
"""Record that string *s* is being typeset using font *font*."""
41+
if isinstance(font, str):
42+
# Unused, can be removed after removal of track_characters.
43+
fname = font
44+
else:
45+
fname = font.fname
46+
self.used.setdefault(fname, set()).update(map(ord, s))
47+
48+
def merge(self, other):
49+
"""Update self with a font path to character codepoints."""
50+
for fname, charset in other.items():
51+
self.used.setdefault(fname, set()).update(charset)
52+
53+
1954
class RendererPDFPSBase(RendererBase):
2055
# The following attributes must be defined by the subclasses:
2156
# - _afm_font_dir
2257
# - _use_afm_rc_name
2358

59+
def __init__(self, width, height):
60+
super().__init__()
61+
self.width = width
62+
self.height = height
63+
2464
def flipy(self):
2565
# docstring inherited
2666
return False # y increases from bottom to top.

lib/matplotlib/backends/backend_pdf.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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,7 @@ 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 = {}
518+
self._character_tracker = _backend_pdf_ps.CharacterTracker()
517519

518520
self.alphaStates = {} # maps alpha values to graphics state objects
519521
self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1))
@@ -550,6 +552,11 @@ def __init__(self, filename, metadata=None):
550552
'ProcSet': procsets}
551553
self.writeObject(self.resourceObject, resources)
552554

555+
@cbook.deprecated("3.3")
556+
@property
557+
def used_characters(self):
558+
return self.file._character_tracker.used_characters
559+
553560
def newPage(self, width, height):
554561
self.endStream()
555562

@@ -724,10 +731,9 @@ def writeFonts(self):
724731
else:
725732
# a normal TrueType font
726733
_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])
734+
chars = self._character_tracker.used.get(filename)
735+
if chars:
736+
fonts[Fx] = self.embedTTF(filename, chars)
731737
self.writeObject(self.fontObject, fonts)
732738

733739
def _write_afm_font(self, filename):
@@ -1675,9 +1681,7 @@ def afm_font_cache(self, _cache=cbook.maxdict(50)):
16751681
_use_afm_rc_name = "pdf.use14corefonts"
16761682

16771683
def __init__(self, file, image_dpi, height, width):
1678-
RendererBase.__init__(self)
1679-
self.height = height
1680-
self.width = width
1684+
super().__init__(width, height)
16811685
self.file = file
16821686
self.gc = self.new_gc()
16831687
self.mathtext_parser = MathTextParser("Pdf")
@@ -1713,22 +1717,14 @@ def check_gc(self, gc, fillcolor=None):
17131717
gc._fillcolor = orig_fill
17141718
gc._effective_alphas = orig_alphas
17151719

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

17331729
def get_image_magnification(self):
17341730
return self.image_dpi/72.0
@@ -1938,7 +1934,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
19381934
# TODO: fix positioning and encoding
19391935
width, height, descent, glyphs, rects, used_characters = \
19401936
self.mathtext_parser.parse(s, 72, prop)
1941-
self.merge_used_characters(used_characters)
1937+
self.file._character_tracker.merge(used_characters)
19421938

19431939
# When using Type 3 fonts, we can't use character codes higher
19441940
# than 255, so we use the "Do" command to render those
@@ -2101,7 +2097,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
21012097
fonttype = 1
21022098
else:
21032099
font = self._get_font_ttf(prop)
2104-
self.track_characters(font, s)
2100+
self.file._character_tracker.track(font, s)
21052101
fonttype = rcParams['pdf.fonttype']
21062102
# We can't subset all OpenType fonts, so switch to Type 42
21072103
# in that case.

lib/matplotlib/backends/backend_ps.py

Lines changed: 43 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
@@ -202,9 +201,7 @@ def __init__(self, width, height, pswriter, imagedpi=72):
202201
# Although postscript itself is dpi independent, we need to inform the
203202
# image code about a requested dpi to generate high resolution images
204203
# and them scale them before embedding them.
205-
RendererBase.__init__(self)
206-
self.width = width
207-
self.height = height
204+
super().__init__(width, height)
208205
self._pswriter = pswriter
209206
if rcParams['text.usetex']:
210207
self.textcnt = 0
@@ -224,21 +221,22 @@ def __init__(self, width, height, pswriter, imagedpi=72):
224221
self._clip_paths = {}
225222
self._path_collection_id = 0
226223

227-
self.used_characters = {}
224+
self._character_tracker = _backend_pdf_ps.CharacterTracker()
228225
self.mathtext_parser = MathTextParser("PS")
229226

230-
def track_characters(self, font, s):
227+
@cbook.deprecated("3.3")
228+
@property
229+
def used_characters(self):
230+
return self._character_tracker.used_characters
231+
232+
@cbook.deprecated("3.3")
233+
def track_characters(self, *args, **kwargs):
231234
"""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))
235+
self._character_tracker.track(*args, **kwargs)
236236

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)
237+
@cbook.deprecated("3.3")
238+
def merge_used_characters(self, *args, **kwargs):
239+
self._character_tracker.merge(*args, **kwargs)
242240

243241
def set_color(self, r, g, b, store=1):
244242
if (r, g, b) != self.color:
@@ -621,7 +619,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
621619
else:
622620
font = self._get_font_ttf(prop)
623621
font.set_text(s, 0, flags=LOAD_NO_HINTING)
624-
self.track_characters(font, s)
622+
self._character_tracker.track(font, s)
625623

626624
self.set_color(*gc.get_rgb())
627625
ps_name = (font.postscript_name
@@ -650,7 +648,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
650648

651649
width, height, descent, pswriter, used_characters = \
652650
self.mathtext_parser.parse(s, 72, prop)
653-
self.merge_used_characters(used_characters)
651+
self._character_tracker.merge(used_characters)
654652
self.set_color(*gc.get_rgb())
655653
thetext = pswriter.getvalue()
656654
self._pswriter.write(f"""\
@@ -980,46 +978,40 @@ def print_figure_impl(fh):
980978
Ndict = len(psDefs)
981979
print("%%BeginProlog", file=fh)
982980
if not rcParams['ps.useafm']:
983-
Ndict += len(ps_renderer.used_characters)
981+
Ndict += len(ps_renderer._character_tracker.used)
984982
print("/mpldict %d dict def" % Ndict, file=fh)
985983
print("mpldict begin", file=fh)
986984
for d in psDefs:
987985
d = d.strip()
988986
for l in d.split('\n'):
989987
print(l.strip(), file=fh)
990988
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
989+
for font_path, chars \
990+
in ps_renderer._character_tracker.used.items():
991+
if not chars:
992+
continue
993+
font = get_font(font_path)
994+
glyph_ids = [font.get_char_index(c) for c in chars]
995+
fonttype = rcParams['ps.fonttype']
996+
# Can't use more than 255 chars from a single Type 3 font.
997+
if len(glyph_ids) > 255:
998+
fonttype = 42
999+
# The ttf to ps (subsetting) support doesn't work for
1000+
# OpenType fonts that are Postscript inside (like the STIX
1001+
# fonts). This will simply turn that off to avoid errors.
1002+
if is_opentype_cff_font(font_path):
1003+
raise RuntimeError(
1004+
"OpenType CFF fonts can not be saved using "
1005+
"the internal Postscript backend at this "
1006+
"time; consider using the Cairo backend")
1007+
fh.flush()
1008+
try:
1009+
convert_ttf_to_ps(os.fsencode(font_path),
1010+
fh, fonttype, glyph_ids)
1011+
except RuntimeError:
1012+
_log.warning("The PostScript backend does not "
1013+
"currently support the selected font.")
1014+
raise
10231015
print("end", file=fh)
10241016
print("%%EndProlog", file=fh)
10251017

lib/matplotlib/cbook/__init__.py

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

526526

527+
@deprecated("3.3", alternative="os.path.realpath and os.stat")
527528
@functools.lru_cache()
528529
def get_realpath_and_stat(path):
529530
realpath = os.path.realpath(path)

lib/matplotlib/font_manager.py

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

13401340

13411341
def get_font(filename, hinting_factor=None):
1342+
# Resolving the path avoids embedding the font twice in pdf/ps output if a
1343+
# single font is selected using two different relative paths.
1344+
filename = os.path.realpath(filename)
13421345
if hinting_factor is None:
13431346
hinting_factor = rcParams['text.hinting_factor']
13441347
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
@@ -32,7 +32,6 @@
3232

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

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

0 commit comments

Comments
 (0)
0