diff --git a/doc/users/next_whats_new/ttc_font_support.rst b/doc/users/next_whats_new/ttc_font_support.rst new file mode 100644 index 000000000000..7c0cdf418852 --- /dev/null +++ b/doc/users/next_whats_new/ttc_font_support.rst @@ -0,0 +1,7 @@ +TTC font collection support +--------------------------- + +Fonts in a TrueType collection file (TTC) can now be added and used. Internally, +the embedded TTF fonts are extracted and stored in the matplotlib cache +directory. Users upgrading to this version need to rebuild the font cache for +this feature to become effective. diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index f57fc9c051b0..5fe1d10cb207 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -35,6 +35,7 @@ from pathlib import Path import re import subprocess +import struct import sys import threading @@ -1099,7 +1100,7 @@ def __init__(self, size=None, weight='normal'): 'Matplotlib is building the font cache; this may take a moment.')) timer.start() try: - for fontext in ["afm", "ttf"]: + for fontext in ["afm", "ttf", "ttc"]: for path in [*findSystemFonts(paths, fontext=fontext), *findSystemFonts(fontext=fontext)]: try: @@ -1129,6 +1130,9 @@ def addfont(self, path): font = _afm.AFM(fh) prop = afmFontProperty(path, font) self.afmlist.append(prop) + elif Path(path).suffix.lower() == ".ttc": + for ttf_file in _split_ttc(path): + self.addfont(ttf_file) else: font = ft2font.FT2Font(path) prop = ttfFontProperty(font) @@ -1473,6 +1477,113 @@ def get_font(filename, hinting_factor=None): thread_id=threading.get_ident()) +def _split_ttc(ttc_path): + """Split a TTC file into TTF files""" + res = _read_ttc(ttc_path) + ttf_fonts, table_index, table_data = res + out_base = Path( + mpl.get_cachedir(), + os.path.basename(ttc_path) + "-" + ) + return _dump_ttf(out_base, ttf_fonts, table_index, table_data) + + +def _read_ttc(ttc_path): + """ + Read a TTC font collection + + Returns an internal list of TTF fonts, table index data, and table + contents. + """ + with open(ttc_path, "rb") as ttc_file: + def read(fmt): + """Read with struct format""" + size = struct.calcsize(fmt) + data = ttc_file.read(size) + return struct.unpack(fmt, data) + + tag, major, minor = read(">4sHH") # ttcf tag and version + if tag != b'ttcf': + _log.warning("Failed to read TTC file, invalid tag: %r", ttc_path) + return [], {}, {} + + if major > 2: + _log.info("TTC file format version > 2, parsing might fail: %r", + ttc_path) + + num_fonts = read(">I")[0] # Number of fonts + font_offsets = read(f">{num_fonts:d}I") # offsets of TTF font + + # Set of tables referenced by any font + table_index = {} # (offset, length): tag, chksum + + # List of TTF fonts + ttf_fonts = [] # (version, num_entries, triple, referenced tables) + + # Read TTF headers and directory tables + for font_offset in font_offsets: + ttc_file.seek(font_offset) + + version = read(">HH") # TTF format version + num_entries = read(">H")[0] # Number of entried in directory table + triple = read(">HHH") # Weird triple, often invalid + referenced_tables = [] + + for _ in range(num_entries): + tag, chksum, offset, length = read(">IIII") + referenced_tables.append((offset, length)) + table_index[(offset, length)] = tag, chksum + + ttf_fonts.append((version, num_entries, triple, referenced_tables)) + + # Read data for all tables + table_data = {} + for (offset, length), (tag, chksum) in table_index.items(): + ttc_file.seek(offset) + table_data[(offset, length)] = ttc_file.read(length) + + _log.debug("Extracted %d tables for %d fonts from TTC file %r", + len(table_index), len(ttf_fonts), ttc_path) + return ttf_fonts, table_index, table_data + + +def _dump_ttf(base_name, ttf_fonts, table_index, table_data): + """Write each TTF font to a separate font""" + created_paths = [] + + # Dump TTF fonts into separate files + for i, font in enumerate(ttf_fonts): + version, num_entries, triple, referenced_tables = font + + def write(file, fmt, values): + raw = struct.pack(fmt, *values) + file.write(raw) + + out_path = f"{base_name}{i}.ttf" + created_paths.append(out_path) + with open(out_path, "wb") as ttf_file: + + write(ttf_file, ">HH", version) + write(ttf_file, ">H", (num_entries, )) + write(ttf_file, ">HHH", triple) + + # Length of header and directory + file_offset = 12 + len(referenced_tables) * 16 + + # Write directory + for (offset, length) in referenced_tables: + tag, chksum, = table_index[(offset, length)] + write(ttf_file, ">IIII", (tag, chksum, file_offset, length)) + file_offset += length + + # Write tables + for table_coord in referenced_tables: + data = table_data[table_coord] + ttf_file.write(data) + _log.info("Created %r from TTC file", out_path) + return created_paths + + def _load_fontmanager(*, try_read_cache=True): fm_path = Path( mpl.get_cachedir(), f"fontlist-v{FontManager.__version__}.json") diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index ef8b467f1709..a74a12ed7cbc 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -15,8 +15,8 @@ findfont, findSystemFonts, FontEntry, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, MSUserFontDirectories, _get_fontconfig_fonts, ft2font, - ttfFontProperty, cbook) -from matplotlib import pyplot as plt, rc_context + ttfFontProperty, cbook, _load_fontmanager) +from matplotlib import pyplot as plt, rc_context, get_cachedir has_fclist = shutil.which('fc-list') is not None @@ -297,11 +297,17 @@ def test_fontentry_dataclass_invalid_path(): @pytest.mark.skipif(sys.platform == 'win32', reason='Linux or OS only') def test_get_font_names(): + # Ensure fonts like 'mpltest' are not in cache + new_fm = _load_fontmanager(try_read_cache=False) + mpl_font_names = sorted(new_fm.get_font_names()) + paths_mpl = [cbook._get_data_path('fonts', subdir) for subdir in ['ttf']] fonts_mpl = findSystemFonts(paths_mpl, fontext='ttf') fonts_system = findSystemFonts(fontext='ttf') + # TTF extracted and cached from TTC + cached_fonts = findSystemFonts(get_cachedir(), fontext='ttf') ttf_fonts = [] - for path in fonts_mpl + fonts_system: + for path in fonts_mpl + fonts_system + cached_fonts: try: font = ft2font.FT2Font(path) prop = ttfFontProperty(font) @@ -309,7 +315,7 @@ def test_get_font_names(): except: pass available_fonts = sorted(list(set(ttf_fonts))) - mpl_font_names = sorted(fontManager.get_font_names()) + assert set(available_fonts) == set(mpl_font_names) assert len(available_fonts) == len(mpl_font_names) assert available_fonts == mpl_font_names pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy