Skip to content

Commit 6078f52

Browse files
committed
ps: Fix font subset handling
Previously, this was supposed to "upgrade" type 3 to type 42 if the number of glyphs overflowed. However, as `CharacterTracker` can suggest a new subset for other reasons (i.e., multiple glyphs for the same character or a glyph for multiple characters may go to a second subset), we do need proper subset handling here as well. Since that is now done, we can drop the "promotion" from type 3 to type 42, as we don't get too many glyphs in each embedded font.
1 parent 6f3a6bb commit 6078f52

File tree

3 files changed

+42
-30
lines changed

3 files changed

+42
-30
lines changed

lib/matplotlib/backends/_backend_pdf_ps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
from fontTools.ttLib import TTFont
2323

2424

25+
_FONT_MAX_GLYPH = {
26+
3: 256,
27+
42: 65536,
28+
}
29+
30+
2531
@functools.lru_cache(50)
2632
def _cached_get_afm_from_fname(fname):
2733
with open(fname, "rb") as fh:

lib/matplotlib/backends/backend_pdf.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,6 @@ def pdfRepr(obj):
368368
"objects")
369369

370370

371-
_FONT_MAX_GLYPH = {
372-
3: 256,
373-
42: 65536,
374-
}
375-
376-
377371
class Reference:
378372
"""
379373
PDF reference object.
@@ -691,7 +685,7 @@ def __init__(self, filename, metadata=None):
691685
self._fontNames = {} # maps filenames to internal font names
692686
self._dviFontInfo = {} # maps pdf names to dvifonts
693687
self._character_tracker = _backend_pdf_ps.CharacterTracker(
694-
_FONT_MAX_GLYPH.get(mpl.rcParams['pdf.fonttype'], 0))
688+
_backend_pdf_ps._FONT_MAX_GLYPH.get(mpl.rcParams['ps.fonttype'], 0))
695689

696690
self.alphaStates = {} # maps alpha values to graphics state objects
697691
self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1))

lib/matplotlib/backends/backend_ps.py

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,16 @@ def _move_path_to_path_or_stream(src, dst):
8888
shutil.move(src, dst, copy_function=shutil.copyfile)
8989

9090

91-
def _font_to_ps_type3(font_path, glyph_indices):
91+
def _font_to_ps_type3(font_path, subset_index, glyph_indices):
9292
"""
9393
Subset *glyphs_indices* from the font at *font_path* into a Type 3 font.
9494
9595
Parameters
9696
----------
9797
font_path : path-like
9898
Path to the font to be subsetted.
99+
subset_index : int
100+
The subset of the above font being created.
99101
glyph_indices : set[int]
100102
The glyphs to include in the subsetted font.
101103
@@ -111,15 +113,15 @@ def _font_to_ps_type3(font_path, glyph_indices):
111113
%!PS-Adobe-3.0 Resource-Font
112114
%%Creator: Converted from TrueType to Type 3 by Matplotlib.
113115
10 dict begin
114-
/FontName /{font_name} def
116+
/FontName /{font_name}-{subset} def
115117
/PaintType 0 def
116118
/FontMatrix [{inv_units_per_em} 0 0 {inv_units_per_em} 0 0] def
117119
/FontBBox [{bbox}] def
118120
/FontType 3 def
119121
/Encoding [{encoding}] def
120122
/CharStrings {num_glyphs} dict dup begin
121123
/.notdef 0 def
122-
""".format(font_name=font.postscript_name,
124+
""".format(font_name=font.postscript_name, subset=subset_index,
123125
inv_units_per_em=1 / font.units_per_EM,
124126
bbox=" ".join(map(str, font.bbox)),
125127
encoding=" ".join(f"/{font.get_glyph_name(glyph_index)}"
@@ -168,20 +170,22 @@ def _font_to_ps_type3(font_path, glyph_indices):
168170
return preamble + "\n".join(entries) + postamble
169171

170172

171-
def _font_to_ps_type42(font_path, glyph_indices, fh):
173+
def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh):
172174
"""
173175
Subset *glyph_indices* from the font at *font_path* into a Type 42 font at *fh*.
174176
175177
Parameters
176178
----------
177179
font_path : path-like
178180
Path to the font to be subsetted.
181+
subset_index : int
182+
The subset of the above font being created.
179183
glyph_indices : set[int]
180184
The glyphs to include in the subsetted font.
181185
fh : file-like
182186
Where to write the font.
183187
"""
184-
_log.debug("SUBSET %s characters: %s", font_path, glyph_indices)
188+
_log.debug("SUBSET %s:%d characters: %s", font_path, subset_index, glyph_indices)
185189
try:
186190
kw = {}
187191
# fix this once we support loading more fonts from a collection
@@ -192,25 +196,27 @@ def _font_to_ps_type42(font_path, glyph_indices, fh):
192196
_backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset):
193197
fontdata = _backend_pdf_ps.font_as_file(subset).getvalue()
194198
_log.debug(
195-
"SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size,
196-
len(fontdata)
199+
"SUBSET %s:%d %d -> %d", font_path, subset_index,
200+
os.stat(font_path).st_size, len(fontdata)
197201
)
198-
fh.write(_serialize_type42(font, subset, fontdata))
202+
fh.write(_serialize_type42(font, subset_index, subset, fontdata))
199203
except RuntimeError:
200204
_log.warning(
201205
"The PostScript backend does not currently support the selected font (%s).",
202206
font_path)
203207
raise
204208

205209

206-
def _serialize_type42(font, subset, fontdata):
210+
def _serialize_type42(font, subset_index, subset, fontdata):
207211
"""
208212
Output a PostScript Type-42 format representation of font
209213
210214
Parameters
211215
----------
212216
font : fontTools.ttLib.ttFont.TTFont
213217
The original font object
218+
subset_index : int
219+
The subset of the above font to be created.
214220
subset : fontTools.ttLib.ttFont.TTFont
215221
The subset font object
216222
fontdata : bytes
@@ -231,7 +237,7 @@ def _serialize_type42(font, subset, fontdata):
231237
10 dict begin
232238
/FontType 42 def
233239
/FontMatrix [1 0 0 1 0 0] def
234-
/FontName /{name.getDebugName(6)} def
240+
/FontName /{name.getDebugName(6)}-{subset_index} def
235241
/FontInfo 7 dict dup begin
236242
/FullName ({name.getDebugName(4)}) def
237243
/FamilyName ({name.getDebugName(1)}) def
@@ -425,7 +431,8 @@ def __init__(self, width, height, pswriter, imagedpi=72):
425431
self._clip_paths = {}
426432
self._path_collection_id = 0
427433

428-
self._character_tracker = _backend_pdf_ps.CharacterTracker()
434+
self._character_tracker = _backend_pdf_ps.CharacterTracker(
435+
_backend_pdf_ps._FONT_MAX_GLYPH.get(mpl.rcParams['ps.fonttype'], 0))
429436
self._logwarn_once = functools.cache(_log.warning)
430437

431438
def _is_transparent(self, rgb_or_rgba):
@@ -793,12 +800,16 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
793800
else:
794801
language = mtext.get_language() if mtext is not None else None
795802
font = self._get_font_ttf(prop)
796-
self._character_tracker.track(font, s)
797803
for item in _text_helpers.layout(s, font, language=language):
804+
# NOTE: We ignore the character code in the subset, because PS uses the
805+
# glyph name to write text. The subset is only used to ensure that each
806+
# one does not overflow format limits.
807+
subset, _ = self._character_tracker.track_glyph(
808+
item.ft_object, item.char, item.glyph_index)
798809
ps_name = (item.ft_object.postscript_name
799810
.encode("ascii", "replace").decode("ascii"))
800811
glyph_name = item.ft_object.get_glyph_name(item.glyph_index)
801-
stream.append((ps_name, item.x, glyph_name))
812+
stream.append((f'{ps_name}-{subset}', item.x, glyph_name))
802813
self.set_color(*gc.get_rgb())
803814

804815
for ps_name, group in itertools. \
@@ -827,11 +838,15 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
827838
f"{angle:g} rotate\n")
828839
lastfont = None
829840
for font, fontsize, ccode, glyph_index, ox, oy in glyphs:
830-
self._character_tracker.track_glyph(font, ccode, glyph_index)
831-
if (font.postscript_name, fontsize) != lastfont:
832-
lastfont = font.postscript_name, fontsize
841+
# NOTE: We ignore the character code in the subset, because PS uses the
842+
# glyph name to write text. The subset is only used to ensure that each one
843+
# does not overflow format limits.
844+
subset, _ = self._character_tracker.track_glyph(
845+
font, ccode, glyph_index)
846+
if (font.postscript_name, subset, fontsize) != lastfont:
847+
lastfont = font.postscript_name, subset, fontsize
833848
self._pswriter.write(
834-
f"/{font.postscript_name} {fontsize} selectfont\n")
849+
f"/{font.postscript_name}-{subset} {fontsize} selectfont\n")
835850
glyph_name = font.get_glyph_name(glyph_index)
836851
self._pswriter.write(
837852
f"{ox:g} {oy:g} moveto\n"
@@ -1071,18 +1086,15 @@ def print_figure_impl(fh):
10711086
print("\n".join(_psDefs), file=fh)
10721087
if not mpl.rcParams['ps.useafm']:
10731088
for font, subsets in ps_renderer._character_tracker.used.items():
1074-
for charmap in subsets:
1089+
for subset, charmap in enumerate(subsets):
10751090
if not charmap:
10761091
continue
10771092
fonttype = mpl.rcParams['ps.fonttype']
1078-
# Can't use more than 255 chars from a single Type 3 font.
1079-
if len(charmap) > 255:
1080-
fonttype = 42
10811093
fh.flush()
10821094
if fonttype == 3:
1083-
fh.write(_font_to_ps_type3(font, charmap.values()))
1095+
fh.write(_font_to_ps_type3(font, subset, charmap.values()))
10841096
else: # Type 42 only.
1085-
_font_to_ps_type42(font, charmap.values(), fh)
1097+
_font_to_ps_type42(font, subset, charmap.values(), fh)
10861098
print("end", file=fh)
10871099
print("%%EndProlog", file=fh)
10881100

0 commit comments

Comments
 (0)