8000 Merge pull request #15686 from sauerburger/unrealpath · dchudz/matplotlib@6f4f19e · GitHub 8000
[go: up one dir, main page]

Skip to content

Commit 6f4f19e

Browse files
authored
Merge pull request matplotlib#15686 from sauerburger/unrealpath
Simplify and unify character tracking in pdf and ps backends (with linked fonts)
2 parents 24caa1b + 6a126a4 commit 6f4f19e

File tree

9 files changed

+126
-84
lines changed

9 files changed

+126
-84
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
@@ -319,6 +319,10 @@
319319
"lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolGrid:1",
320320
"lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolMinorGrid:1"
321321
],
322+
"matplotlib.backends._backend_pdf_ps.CharacterTracker": [
323+
"lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.PdfFile:1",
324+
"lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS:1"
325+
],
322326
"matplotlib.backends._backend_pdf_ps.RendererPDFPSBase": [
323327
"lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf:1",
324328
"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: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,8 @@ def __init__(self, filename, metadata=None):
449449
'Trapped'. Values have been predefined for 'Creator', 'Producer'
450450
and 'CreationDate'. They can be removed by setting them to `None`.
451451
"""
452+
super().__init__()
453+
452454
self._object_seq = itertools.count(1) # consumed by reserveObject
453455
self.xrefTable = [[0, 65535, 'the zero object']]
454456
self.passed_in_file_object = False
@@ -511,7 +513,7 @@ def __init__(self, filename, metadata=None):
511513
self.dviFontInfo = {} # maps dvi font names to embedding information
512514
# differently encoded Type-1 fonts may share the same descriptor
513515
self.type1Descriptors = {}
514-
self.used_characters = {}
516+
self._character_tracker = _backend_pdf_ps.CharacterTracker()
515517

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

553+
@cbook.deprecated("3.3")
554+
@property
555+
def used_characters(self):
556+
return self.file._character_tracker.used_characters
557+
551558
def newPage(self, width, height):
552559
self.endStream()
553560

@@ -718,10 +725,9 @@ def writeFonts(self):
718725
else:
719726
# a normal TrueType font
720727
_log.debug('Writing TrueType font.')
721-
realpath, stat_key = cbook.get_realpath_and_stat(filename)
722-
chars = self.used_characters.get(stat_key)
723-
if chars is not None and len(chars[1]):
724-
fonts[Fx] = self.embedTTF(realpath, chars[1])
728+
chars = self._character_tracker.used.get(filename)
729+
if chars:
730+
fonts[Fx] = self.embedTTF(filename, chars)
725731
self.writeObject(self.fontObject, fonts)
726732

727733
def _write_afm_font(self, filename):
@@ -862,9 +868,11 @@ def createType1Descriptor(self, t1font, fontfile):
862868
return fontdescObject
863869

864870
def _get_xobject_symbol_name(self, filename, symbol_name):
865-
return "%s-%s" % (
871+
Fx = self.fontName(filename)
872+
return "-".join([
873+
Fx.name.decode(),
866874
os.path.splitext(os.path.basename(filename))[0],
867-
symbol_name)
875+
symbol_name])
868876

869877
_identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin
870878
12 dict begin
@@ -1669,9 +1677,7 @@ def afm_font_cache(self, _cache=cbook.maxdict(50)):
16691677
_use_afm_rc_name = "pdf.use14corefonts"
16701678

16711679
def __init__(self, file, image_dpi, height, width):
1672-
RendererBase.__init__(self)
1673-
self.height = height
1674-
self.width = width
1680+
super().__init__(width, height)
16751681
self.file = file
16761682
self.gc = self.new_gc()
16771683
self.mathtext_parser = MathTextParser("Pdf")
@@ -1707,22 +1713,14 @@ def check_gc(self, gc, fillcolor=None):
17071713
gc._fillcolor = orig_fill
17081714
gc._effective_alphas = orig_alphas
17091715

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

17271725
def get_image_magnification(self):
17281726
return self.image_dpi/72.0
@@ -1932,7 +1930,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
19321930
# TODO: fix positioning and encoding
19331931
width, height, descent, glyphs, rects, used_characters = \
19341932
self.mathtext_parser.parse(s, 72, prop)
1935-
self.merge_used_characters(used_characters)
1933+
self.file._character_tracker.merge(used_characters)
19361934

19371935
# When using Type 3 fonts, we can't use character codes higher
19381936
# than 255, so we use the "Do" command to render those
@@ -2095,7 +2093,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
20952093
fonttype = 1
20962094
else:
20972095
font = self._get_font_ttf(prop)
2098-
self.track_characters(font, s)
2096+
self.file._character_tracker.track(font, s)
20992097
fonttype = rcParams['pdf.fonttype']
21002098
# We can't subset all OpenType fonts, so switch to Type 42
21012099
# 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+
10000 "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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,9 +1238,10 @@ def findfont(self, prop, fontext='ttf', directory=None,
12381238
rc_params = tuple(tuple(rcParams[key]) for key in [
12391239
"font.serif", "font.sans-serif", "font.cursive", "font.fantasy",
12401240
"font.monospace"])
1241-
return self._findfont_cached(
1241+
filename = self._findfont_cached(
12421242
prop, fontext, directory, fallback_to_default, rebuild_if_missing,
12431243
rc_params)
1244+
return os.path.realpath(filename)
12441245

12451246
@lru_cache()
12461247
def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
@@ -1339,6 +1340,9 @@ def is_opentype_cff_font(filename):
13391340

13401341

13411342
def get_font(filename, hinting_factor=None):
1343+
# Resolving the path avoids embedding the font twice in pdf/ps output if a
1344+
# single font is selected using two different relative paths.
1345+
filename = os.path.realpath(filename)
13421346
if hinting_factor is None:
13431347
hinting_factor = rcParams['text.hinting_factor']
13441348
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