Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions doc/release/next_whats_new/ttc_fonts.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Support for loading TrueType Collection fonts
---------------------------------------------

TrueType Collection fonts (commonly found as files with a ``.ttc`` extension) are now
supported. Namely, Matplotlib will include these file extensions in its scan for system
fonts, and will add all sub-fonts to its list of available fonts (i.e., the list from
`~.font_manager.get_font_names`).

From most high-level API, this means you should be able to specify the name of any
sub-font in a collection just as you would any other font. Note that at this time, there
is no way to specify the entire collection with any sort of automated selection of the
internal sub-fonts.

In the low-level API, to ensure backwards-compatibility while facilitating this new
support, a `.FontPath` instance (comprised of a font path and a sub-font index, with
behaviour similar to a `str`) may be passed to the font management API in place of a
simple `os.PathLike` path. Any font management API that previously returned a string path
now returns a `.FontPath` instance instead.
10 changes: 5 additions & 5 deletions lib/matplotlib/backends/_backend_pdf_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont:

Parameters
----------
fontfile : str
fontfile : FontPath
Path to the font file
glyphs : set[GlyphIndexType]
Set of glyph indices to include in subset.
Expand Down Expand Up @@ -80,8 +80,7 @@ def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont:
'xref', # The cross-reference table (some Apple font tooling information).
]
# if fontfile is a ttc, specify font number
if fontfile.endswith(".ttc"):
options.font_number = 0
options.font_number = fontfile.face_index

font = subset.load_font(fontfile, options)
subsetter = subset.Subsetter(options=options)
Expand Down Expand Up @@ -267,11 +266,12 @@ def track_glyph(self, font: FT2Font, chars: str | CharacterCodeType,
charcode = chars
chars = chr(chars)

glyph_map = self.glyph_maps.setdefault(font.fname, GlyphMap())
font_path = font_manager.FontPath(font.fname, font.face_index)
glyph_map = self.glyph_maps.setdefault(font_path, GlyphMap())
if result := glyph_map.get(chars, glyph):
return result

subset_maps = self.used.setdefault(font.fname, [{}])
subset_maps = self.used.setdefault(font_path, [{}])
use_next_charmap = (
# Multi-character glyphs always go in the non-0 subset.
len(chars) > 1 or
Expand Down
29 changes: 17 additions & 12 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
RendererBase)
from matplotlib.backends.backend_mixed import MixedModeRenderer
from matplotlib.figure import Figure
from matplotlib.font_manager import get_font, fontManager as _fontManager
from matplotlib.font_manager import FontPath, get_font, fontManager as _fontManager
from matplotlib._afm import AFM
from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags
from matplotlib.transforms import Affine2D, BboxBase
Expand Down Expand Up @@ -894,8 +894,10 @@ def fontName(self, fontprop, subset=0):
as the filename of the font.
"""

if isinstance(fontprop, str):
if isinstance(fontprop, FontPath):
filenames = [fontprop]
elif isinstance(fontprop, str):
filenames = [FontPath(fontprop, 0)]
elif mpl.rcParams['pdf.use14corefonts']:
filenames = _fontManager._find_fonts_by_props(
fontprop, fontext='afm', directory=RendererPdf._afm_font_dir
Expand Down Expand Up @@ -935,7 +937,7 @@ def writeFonts(self):
_log.debug('Embedding Type-1 font %s from dvi.', dvifont.texname)
fonts[pdfname] = self._embedTeXFont(dvifont)
for (filename, subset), Fx in sorted(self._fontNames.items()):
_log.debug('Embedding font %s:%d.', filename, subset)
_log.debug('Embedding font %r:%d.', filename, subset)
if filename.endswith('.afm'):
# from pdf.use14corefonts
_log.debug('Writing AFM font.')
Expand Down Expand Up @@ -986,10 +988,11 @@ def _embedTeXFont(self, dvifont):

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

_log.debug("SUBSET %s:%d characters: %s", filename, subset_index, charmap)
_log.debug("SUBSET %r:%d characters: %s", filename, subset_index, charmap)
with _backend_pdf_ps.get_glyphs_subset(filename,
charmap.values()) as subset:
fontdata = _backend_pdf_ps.font_as_file(subset)
_log.debug(
"SUBSET %s:%d %d -> %d", filename, subset_index,
"SUBSET %r:%d %d -> %d", filename, subset_index,
os.stat(filename).st_size, fontdata.getbuffer().nbytes
)

Expand Down Expand Up @@ -2137,13 +2140,13 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
for font, fontsize, ccode, glyph_index, ox, oy in glyphs:
subset_index, subset_charcode = self.file._character_tracker.track_glyph(
font, ccode, glyph_index)
fontname = font.fname
font_path = FontPath(font.fname, font.face_index)
self._setup_textpos(ox, oy, 0, oldx, oldy)
oldx, oldy = ox, oy
if (fontname, subset_index, fontsize) != prev_font:
self.file.output(self.file.fontName(fontname, subset_index), fontsize,
if (font_path, subset_index, fontsize) != prev_font:
self.file.output(self.file.fontName(font_path, subset_index), fontsize,
Op.selectfont)
prev_font = fontname, subset_index, fontsize
prev_font = font_path, subset_index, fontsize
self.file.output(self._encode_glyphs([subset_charcode], fonttype),
Op.show)
self.file.output(Op.end_text)
Expand Down Expand Up @@ -2338,7 +2341,9 @@ def output_singlebyte_chunk(kerns_or_chars):
item.ft_object, item.char, item.glyph_index)
if (item.ft_object, subset) != prev_font:
output_singlebyte_chunk(singlebyte_chunk)
ft_name = self.file.fontName(item.ft_object.fname, subset)
font_path = FontPath(item.ft_object.fname,
item.ft_object.face_index)
ft_name = self.file.fontName(font_path, subset)
self.file.output(ft_name, fontsize, Op.selectfont)
self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0)
prev_font = (item.ft_object, subset)
Expand Down
25 changes: 13 additions & 12 deletions lib/matplotlib/backends/backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,17 @@

def _get_preamble():
"""Prepare a LaTeX preamble based on the rcParams configuration."""
font_size_pt = FontProperties(
size=mpl.rcParams["font.size"]
).get_size_in_points()
def _to_fontspec():
for command, family in [("setmainfont", "serif"),
("setsansfont", "sans\\-serif"),
("setmonofont", "monospace")]:
font_path = fm.findfont(family)
path = pathlib.Path(font_path)
yield r" \%s{%s}[Path=\detokenize{%s/}%s]" % (
command, path.name, path.parent.as_posix(),
f',FontIndex={font_path.face_index:d}' if path.suffix == '.ttc' else '')

font_size_pt = FontProperties(size=mpl.rcParams["font.size"]).get_size_in_points()
return "\n".join([
# Remove Matplotlib's custom command \mathdefault. (Not using
# \mathnormal instead since this looks odd with Computer Modern.)
Expand All @@ -63,15 +71,8 @@ def _get_preamble():
*([
r"\ifdefined\pdftexversion\else % non-pdftex case.",
r" \usepackage{fontspec}",
] + [
r" \%s{%s}[Path=\detokenize{%s/}]"
% (command, path.name, path.parent.as_posix())
for command, path in zip(
["setmainfont", "setsansfont", "setmonofont"],
[pathlib.Path(fm.findfont(family))
for family in ["serif", "sans\\-serif", "monospace"]]
)
] + [r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []),
*_to_fontspec(),
r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []),
# Documented as "must come last".
mpl.texmanager._usepackage_if_not_loaded("underscore", option="strings"),
])
Expand Down
12 changes: 4 additions & 8 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def _font_to_ps_type3(font_path, subset_index, glyph_indices):

Parameters
----------
font_path : path-like
font_path : FontPath
Path to the font to be subsetted.
subset_index : int
The subset of the above font being created.
Expand Down Expand Up @@ -176,7 +176,7 @@ def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh):

Parameters
----------
font_path : path-like
font_path : FontPath
Path to the font to be subsetted.
subset_index : int
The subset of the above font being created.
Expand All @@ -187,12 +187,8 @@ def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh):
"""
_log.debug("SUBSET %s:%d characters: %s", font_path, subset_index, glyph_indices)
try:
kw = {}
# fix this once we support loading more fonts from a collection
# https://github.com/matplotlib/matplotlib/issues/3135#issuecomment-571085541
if font_path.endswith('.ttc'):
kw['fontNumber'] = 0
with (fontTools.ttLib.TTFont(font_path, **kw) as font,
with (fontTools.ttLib.TTFont(font_path.path,
fontNumber=font_path.face_index) as font,
_backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset):
fontdata = _backend_pdf_ps.font_as_file(subset).getvalue()
_log.debug(
Expand Down
4 changes: 4 additions & 0 deletions lib/matplotlib/dviread.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,10 @@ def fname(self):
"""A fake filename"""
return self.texname.decode('latin-1')

@property
def face_index(self): # For compatibility with FT2Font.
return 0

def _get_fontmap(self, string):
"""Get the mapping from characters to the font that includes them.

Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/dviread.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ class DviFont:
def widths(self) -> list[int]: ...
@property
def fname(self) -> str: ...
@property
def face_index(self) -> int: ...
def resolve_path(self) -> Path: ...
@property
def subfont(self) -> int: ...
Expand Down
Loading
Loading