Skip to content

Commit e84ce2b

Browse files
authored
Merge pull request matplotlib#30334 from QuLogic/ttc-loading
Add support for loading all fonts from collections
2 parents 950fa0f + b2aa1f2 commit e84ce2b

File tree

23 files changed

+3320
-67
lines changed

23 files changed

+3320
-67
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Support for loading TrueType Collection fonts
2+
---------------------------------------------
3+
4+
TrueType Collection fonts (commonly found as files with a ``.ttc`` extension) are now
5+
supported. Namely, Matplotlib will include these file extensions in its scan for system
6+
fonts, and will add all sub-fonts to its list of available fonts (i.e., the list from
7+
`~.font_manager.get_font_names`).
8+
9+
From most high-level API, this means you should be able to specify the name of any
10+
sub-font in a collection just as you would any other font. Note that at this time, there
11+
is no way to specify the entire collection with any sort of automated selection of the
12+
internal sub-fonts.
13+
14+
In the low-level API, to ensure backwards-compatibility while facilitating this new
15+
support, a `.FontPath` instance (comprised of a font path and a sub-font index, with
16+
behaviour similar to a `str`) may be passed to the font management API in place of a
17+
simple `os.PathLike` path. Any font management API that previously returned a string path
18+
now returns a `.FontPath` instance instead.

lib/matplotlib/backends/_backend_pdf_ps.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont:
4242
4343
Parameters
4444
----------
45-
fontfile : str
45+
fontfile : FontPath
4646
Path to the font file
4747
glyphs : set[GlyphIndexType]
4848
Set of glyph indices to include in subset.
@@ -80,8 +80,7 @@ def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont:
8080
'xref', # The cross-reference table (some Apple font tooling information).
8181
]
8282
# if fontfile is a ttc, specify font number
83-
if fontfile.endswith(".ttc"):
84-
options.font_number = 0
83+
options.font_number = fontfile.face_index
8584

8685
font = subset.load_font(fontfile, options)
8786
subsetter = subset.Subsetter(options=options)
@@ -267,11 +266,12 @@ def track_glyph(self, font: FT2Font, chars: str | CharacterCodeType,
267266
charcode = chars
268267
chars = chr(chars)
269268

270-
glyph_map = self.glyph_maps.setdefault(font.fname, GlyphMap())
269+
font_path = font_manager.FontPath(font.fname, font.face_index)
270+
glyph_map = self.glyph_maps.setdefault(font_path, GlyphMap())
271271
if result := glyph_map.get(chars, glyph):
272272
return result
273273

274-
subset_maps = self.used.setdefault(font.fname, [{}])
274+
subset_maps = self.used.setdefault(font_path, [{}])
275275
use_next_charmap = (
276276
# Multi-character glyphs always go in the non-0 subset.
277277
len(chars) > 1 or

lib/matplotlib/backends/backend_pdf.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
RendererBase)
3333
from matplotlib.backends.backend_mixed import MixedModeRenderer
3434
from matplotlib.figure import Figure
35-
from matplotlib.font_manager import get_font, fontManager as _fontManager
35+
from matplotlib.font_manager import FontPath, get_font, fontManager as _fontManager
3636
from matplotlib._afm import AFM
3737
from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags
3838
from matplotlib.transforms import Affine2D, BboxBase
@@ -894,8 +894,10 @@ def fontName(self, fontprop, subset=0):
894894
as the filename of the font.
895895
"""
896896

897-
if isinstance(fontprop, str):
897+
if isinstance(fontprop, FontPath):
898898
filenames = [fontprop]
899+
elif isinstance(fontprop, str):
900+
filenames = [FontPath(fontprop, 0)]
899901
elif mpl.rcParams['pdf.use14corefonts']:
900902
filenames = _fontManager._find_fonts_by_props(
901903
fontprop, fontext='afm', directory=RendererPdf._afm_font_dir
@@ -935,7 +937,7 @@ def writeFonts(self):
935937
_log.debug('Embedding Type-1 font %s from dvi.', dvifont.texname)
936938
fonts[pdfname] = self._embedTeXFont(dvifont)
937939
for (filename, subset), Fx in sorted(self._fontNames.items()):
938-
_log.debug('Embedding font %s:%d.', filename, subset)
940+
_log.debug('Embedding font %r:%d.', filename, subset)
939941
if filename.endswith('.afm'):
940942
# from pdf.use14corefonts
941943
_log.debug('Writing AFM font.')
@@ -986,10 +988,11 @@ def _embedTeXFont(self, dvifont):
986988

987989
# Reduce the font to only the glyphs used in the document, get the encoding
988990
# for that subset, and compute various properties based on the encoding.
989-
charmap = self._character_tracker.used[dvifont.fname][0]
991+
font_path = FontPath(dvifont.fname, dvifont.face_index)
992+
charmap = self._character_tracker.used[font_path][0]
990993
chars = {
991994
# DVI type 1 fonts always map single glyph to single character.
992-
ord(self._character_tracker.subset_to_unicode(dvifont.fname, 0, ccode))
995+
ord(self._character_tracker.subset_to_unicode(font_path, 0, ccode))
993996
for ccode in charmap
994997
}
995998
t1font = t1font.subset(chars, self._get_subset_prefix(charmap.values()))
@@ -1241,12 +1244,12 @@ def embedTTFType42(font, subset_index, charmap, descriptor):
12411244
wObject = self.reserveObject('Type 0 widths')
12421245
toUnicodeMapObject = self.reserveObject('ToUnicode map')
12431246

1244-
_log.debug("SUBSET %s:%d characters: %s", filename, subset_index, charmap)
1247+
_log.debug("SUBSET %r:%d characters: %s", filename, subset_index, charmap)
12451248
with _backend_pdf_ps.get_glyphs_subset(filename,
12461249
charmap.values()) as subset:
12471250
fontdata = _backend_pdf_ps.font_as_file(subset)
12481251
_log.debug(
1249-
"SUBSET %s:%d %d -> %d", filename, subset_index,
1252+
"SUBSET %r:%d %d -> %d", filename, subset_index,
12501253
os.stat(filename).st_size, fontdata.getbuffer().nbytes
12511254
)
12521255

@@ -2137,13 +2140,13 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
21372140
for font, fontsize, ccode, glyph_index, ox, oy in glyphs:
21382141
subset_index, subset_charcode = self.file._character_tracker.track_glyph(
21392142
font, ccode, glyph_index)
2140-
fontname = font.fname
2143+
font_path = FontPath(font.fname, font.face_index)
21412144
self._setup_textpos(ox, oy, 0, oldx, oldy)
21422145
oldx, oldy = ox, oy
2143-
if (fontname, subset_index, fontsize) != prev_font:
2144-
self.file.output(self.file.fontName(fontname, subset_index), fontsize,
2146+
if (font_path, subset_index, fontsize) != prev_font:
2147+
self.file.output(self.file.fontName(font_path, subset_index), fontsize,
21452148
Op.selectfont)
2146-
prev_font = fontname, subset_index, fontsize
2149+
prev_font = font_path, subset_index, fontsize
21472150
self.file.output(self._encode_glyphs([subset_charcode], fonttype),
21482151
Op.show)
21492152
self.file.output(Op.end_text)
@@ -2338,7 +2341,9 @@ def output_singlebyte_chunk(kerns_or_chars):
23382341
item.ft_object, item.char, item.glyph_index)
23392342
if (item.ft_object, subset) != prev_font:
23402343
output_singlebyte_chunk(singlebyte_chunk)
2341-
ft_name = self.file.fontName(item.ft_object.fname, subset)
2344+
font_path = FontPath(item.ft_object.fname,
2345+
item.ft_object.face_index)
2346+
ft_name = self.file.fontName(font_path, subset)
23422347
self.file.output(ft_name, fontsize, Op.selectfont)
23432348
self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0)
23442349
prev_font = (item.ft_object, subset)

lib/matplotlib/backends/backend_pgf.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,17 @@
3838

3939
def _get_preamble():
4040
"""Prepare a LaTeX preamble based on the rcParams configuration."""
41-
font_size_pt = FontProperties(
42-
size=mpl.rcParams["font.size"]
43-
).get_size_in_points()
41+
def _to_fontspec():
42+
for command, family in [("setmainfont", "serif"),
43+
("setsansfont", "sans\\-serif"),
44+
("setmonofont", "monospace")]:
45+
font_path = fm.findfont(family)
46+
path = pathlib.Path(font_path)
47+
yield r" \%s{%s}[Path=\detokenize{%s/}%s]" % (
48+
command, path.name, path.parent.as_posix(),
49+
f',FontIndex={font_path.face_index:d}' if path.suffix == '.ttc' else '')
50+
51+
font_size_pt = FontProperties(size=mpl.rcParams["font.size"]).get_size_in_points()
4452
return "\n".join([
4553
# Remove Matplotlib's custom command \mathdefault. (Not using
4654
# \mathnormal instead since this looks odd with Computer Modern.)
@@ -63,15 +71,8 @@ def _get_preamble():
6371
*([
6472
r"\ifdefined\pdftexversion\else % non-pdftex case.",
6573
r" \usepackage{fontspec}",
66-
] + [
67-
r" \%s{%s}[Path=\detokenize{%s/}]"
68-
% (command, path.name, path.parent.as_posix())
69-
for command, path in zip(
70-
["setmainfont", "setsansfont", "setmonofont"],
71-
[pathlib.Path(fm.findfont(family))
72-
for family in ["serif", "sans\\-serif", "monospace"]]
73-
)
74-
] + [r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []),
74+
*_to_fontspec(),
75+
r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []),
7576
# Documented as "must come last".
7677
mpl.texmanager._usepackage_if_not_loaded("underscore", option="strings"),
7778
])

lib/matplotlib/backends/backend_ps.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def _font_to_ps_type3(font_path, subset_index, glyph_indices):
9494
9595
Parameters
9696
----------
97-
font_path : path-like
97+
font_path : FontPath
9898
Path to the font to be subsetted.
9999
subset_index : int
100100
The subset of the above font being created.
@@ -176,7 +176,7 @@ def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh):
176176
177177
Parameters
178178
----------
179-
font_path : path-like
179+
font_path : FontPath
180180
Path to the font to be subsetted.
181181
subset_index : int
182182
The subset of the above font being created.
@@ -187,12 +187,8 @@ def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh):
187187
"""
188188
_log.debug("SUBSET %s:%d characters: %s", font_path, subset_index, glyph_indices)
189189
try:
190-
kw = {}
191-
# fix this once we support loading more fonts from a collection
192-
# https://github.com/matplotlib/matplotlib/issues/3135#issuecomment-571085541
193-
if font_path.endswith('.ttc'):
194-
kw['fontNumber'] = 0
195-
with (fontTools.ttLib.TTFont(font_path, **kw) as font,
190+
with (fontTools.ttLib.TTFont(font_path.path,
191+
fontNumber=font_path.face_index) as font,
196192
_backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset):
197193
fontdata = _backend_pdf_ps.font_as_file(subset).getvalue()
198194
_log.debug(

lib/matplotlib/dviread.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,10 @@ def fname(self):
719719
"""A fake filename"""
720720
return self.texname.decode('latin-1')
721721

722+
@property
723+
def face_index(self): # For compatibility with FT2Font.
724+
return 0
725+
722726
def _get_fontmap(self, string):
723727
"""Get the mapping from characters to the font that includes them.
724728

lib/matplotlib/dviread.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ class DviFont:
7878
def widths(self) -> list[int]: ...
7979
@property
8080
def fname(self) -> str: ...
81+
@property
82+
def face_index(self) -> int: ...
8183
def resolve_path(self) -> Path: ...
8284
@property
8385
def subfont(self) -> int: ...

0 commit comments

Comments
 (0)