From 85bacdccc52094ee885a394416f6aef2fc1d3545 Mon Sep 17 00:00:00 2001 From: Aitik Gupta Date: Mon, 26 Jul 2021 03:41:52 +0530 Subject: [PATCH] ENH: support font fallback for Agg renderer Co-authored-by: Aitik Gupta Co-authored-by: Elliott Sales de Andrade --- .circleci/config.yml | 1 + doc/api/ft2font.rst | 8 + doc/api/index.rst | 1 + doc/users/next_whats_new/font_fallback.rst | 27 +++ lib/matplotlib/backends/backend_agg.py | 4 +- lib/matplotlib/font_manager.py | 171 ++++++++++++-- lib/matplotlib/tests/test_ft2font.py | 78 +++++++ src/ft2font.cpp | 256 +++++++++++++++++---- src/ft2font.h | 32 ++- src/ft2font_wrapper.cpp | 162 +++++++++---- 10 files changed, 630 insertions(+), 110 deletions(-) create mode 100644 doc/api/ft2font.rst create mode 100644 doc/users/next_whats_new/font_fallback.rst create mode 100644 lib/matplotlib/tests/test_ft2font.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 6da35d469ac6..dd14ea8e01eb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,6 +55,7 @@ commands: texlive-latex-recommended \ texlive-pictures \ texlive-xetex \ + ttf-wqy-zenhei \ graphviz \ fonts-crosextra-carlito \ fonts-freefont-otf \ diff --git a/doc/api/ft2font.rst b/doc/api/ft2font.rst new file mode 100644 index 000000000000..a1f984abdda5 --- /dev/null +++ b/doc/api/ft2font.rst @@ -0,0 +1,8 @@ +********************** +``matplotlib.ft2font`` +********************** + +.. automodule:: matplotlib.ft2font + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/index.rst b/doc/api/index.rst index f2307be245be..8623a23e907e 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -120,6 +120,7 @@ Alphabetical list of modules: figure_api.rst font_manager_api.rst fontconfig_pattern_api.rst + ft2font.rst gridspec_api.rst hatch_api.rst image_api.rst diff --git a/doc/users/next_whats_new/font_fallback.rst b/doc/users/next_whats_new/font_fallback.rst new file mode 100644 index 000000000000..f4922e7844a1 --- /dev/null +++ b/doc/users/next_whats_new/font_fallback.rst @@ -0,0 +1,27 @@ +Font Fallback in Agg +-------------------- + +It is now possible to specify a list of fonts families and the Agg renderer +will try them in order to locate a required glyph. + +.. plot:: + :caption: Demonstration of mixed English and Chinese text with font fallback. + :alt: The phrase "There are 几个汉字 in between!" rendered in various fonts. + :include-source: True + + import matplotlib.pyplot as plt + + text = "There are 几个汉字 in between!" + + plt.rcParams["font.size"] = 20 + fig = plt.figure(figsize=(4.75, 1.85)) + fig.text(0.05, 0.85, text, family=["WenQuanYi Zen Hei"]) + fig.text(0.05, 0.65, text, family=["Noto Sans CJK JP"]) + fig.text(0.05, 0.45, text, family=["DejaVu Sans", "Noto Sans CJK JP"]) + fig.text(0.05, 0.25, text, family=["DejaVu Sans", "WenQuanYi Zen Hei"]) + + plt.show() + + +This currently only works with the Agg backend, but support for the vector +backends is planned for Matplotlib 3.7. diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 644961db111d..0d8a127dba8c 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -31,7 +31,7 @@ from matplotlib import _api, cbook from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) -from matplotlib.font_manager import findfont, get_font +from matplotlib.font_manager import fontManager as _fontManager, get_font from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, LOAD_DEFAULT, LOAD_NO_AUTOHINT) from matplotlib.mathtext import MathTextParser @@ -272,7 +272,7 @@ def _prepare_font(self, font_prop): """ Get the `.FT2Font` for *font_prop*, clear its buffer, and set its size. """ - font = get_font(findfont(font_prop)) + font = get_font(_fontManager._find_fonts_by_props(font_prop)) font.clear() size = font_prop.get_size_in_points() font.set_size(size, self.dpi) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index f57fc9c051b0..60965c16c046 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -167,11 +167,6 @@ ] -@lru_cache(64) -def _cached_realpath(path): - return os.path.realpath(path) - - def get_fontext_synonyms(fontext): """ Return a list of file extensions that are synonyms for @@ -1354,7 +1349,110 @@ def get_font_names(self): """Return the list of available fonts.""" return list(set([font.name for font in self.ttflist])) - @lru_cache() + def _find_fonts_by_props(self, prop, fontext='ttf', directory=None, + fallback_to_default=True, rebuild_if_missing=True): + """ + Find font families that most closely match the given properties. + + Parameters + ---------- + prop : str or `~matplotlib.font_manager.FontProperties` + The font properties to search for. This can be either a + `.FontProperties` object or a string defining a + `fontconfig patterns`_. + + fontext : {'ttf', 'afm'}, default: 'ttf' + The extension of the font file: + + - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf) + - 'afm': Adobe Font Metrics (.afm) + + directory : str, optional + If given, only search this directory and its subdirectories. + + fallback_to_default : bool + If True, will fallback to the default font family (usually + "DejaVu Sans" or "Helvetica") if none of the families were found. + + rebuild_if_missing : bool + Whether to rebuild the font cache and search again if the first + match appears to point to a nonexisting font (i.e., the font cache + contains outdated entries). + + Returns + ------- + list[str] + The paths of the fonts found + + Notes + ----- + This is an extension/wrapper of the original findfont API, which only + returns a single font for given font properties. Instead, this API + returns an dict containing multiple fonts and their filepaths + which closely match the given font properties. Since this internally + uses the original API, there's no change to the logic of performing the + nearest neighbor search. See `findfont` for more details. + + """ + + rc_params = tuple(tuple(rcParams[key]) for key in [ + "font.serif", "font.sans-serif", "font.cursive", "font.fantasy", + "font.monospace"]) + + prop = FontProperties._from_any(prop) + + fpaths = [] + for family in prop.get_family(): + cprop = prop.copy() + + # set current prop's family + cprop.set_family(family) + + # do not fall back to default font + try: + fpaths.append( + self._findfont_cached( + cprop, fontext, directory, + fallback_to_default=False, + rebuild_if_missing=rebuild_if_missing, + rc_params=rc_params, + ) + ) + except ValueError: + if family in font_family_aliases: + _log.warning( + "findfont: Generic family %r not found because " + "none of the following families were found: %s", + family, + ", ".join(self._expand_aliases(family)) + ) + else: + _log.warning( + 'findfont: Font family \'%s\' not found.', family + ) + + # only add default family if no other font was found and + # fallback_to_default is enabled + if not fpaths: + if fallback_to_default: + dfamily = self.defaultFamily[fontext] + cprop = prop.copy() + cprop.set_family(dfamily) + fpaths.append( + self._findfont_cached( + cprop, fontext, directory, + fallback_to_default=True, + rebuild_if_missing=rebuild_if_missing, + rc_params=rc_params, + ) + ) + else: + raise ValueError("Failed to find any font, and fallback " + "to the default font was disabled.") + + return fpaths + + @lru_cache(1024) def _findfont_cached(self, prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params): @@ -1447,9 +1545,19 @@ def is_opentype_cff_font(filename): @lru_cache(64) -def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id): +def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id): + first_fontpath, *rest = font_filepaths return ft2font.FT2Font( - filename, hinting_factor, _kerning_factor=_kerning_factor) + first_fontpath, hinting_factor, + _fallback_list=[ + ft2font.FT2Font( + fpath, hinting_factor, + _kerning_factor=_kerning_factor + ) + for fpath in rest + ], + _kerning_factor=_kerning_factor + ) # FT2Font objects cannot be used across fork()s because they reference the same @@ -1461,16 +1569,51 @@ def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id): os.register_at_fork(after_in_child=_get_font.cache_clear) -def get_font(filename, hinting_factor=None): +@lru_cache(64) +def _cached_realpath(path): # Resolving the path avoids embedding the font twice in pdf/ps output if a # single font is selected using two different relative paths. - filename = _cached_realpath(filename) + return os.path.realpath(path) + + +@_api.rename_parameter('3.6', "filepath", "font_filepaths") +def get_font(font_filepaths, hinting_factor=None): + """ + Get an `.ft2font.FT2Font` object given a list of file paths. + + Parameters + ---------- + font_filepaths : Iterable[str, Path, bytes], str, Path, bytes + Relative or absolute paths to the font files to be used. + + If a single string, bytes, or `pathlib.Path`, then it will be treated + as a list with that entry only. + + If more than one filepath is passed, then the returned FT2Font object + will fall back through the fonts, in the order given, to find a needed + glyph. + + Returns + ------- + `.ft2font.FT2Font` + + """ + if isinstance(font_filepaths, (str, Path, bytes)): + paths = (_cached_realpath(font_filepaths),) + else: + paths = tuple(_cached_realpath(fname) for fname in font_filepaths) + if hinting_factor is None: hinting_factor = rcParams['text.hinting_factor'] - # also key on the thread ID to prevent segfaults with multi-threading - return _get_font(filename, hinting_factor, - _kerning_factor=rcParams['text.kerning_factor'], - thread_id=threading.get_ident()) + + return _get_font( + # must be a tuple to be cached + paths, + hinting_factor, + _kerning_factor=rcParams['text.kerning_factor'], + # also key on the thread ID to prevent segfaults with multi-threading + thread_id=threading.get_ident() + ) def _load_fontmanager(*, try_read_cache=True): diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py new file mode 100644 index 000000000000..eee47f5b03f8 --- /dev/null +++ b/lib/matplotlib/tests/test_ft2font.py @@ -0,0 +1,78 @@ +from pathlib import Path +import io + +import pytest + +from matplotlib import ft2font +from matplotlib.testing.decorators import check_figures_equal +import matplotlib.font_manager as fm +import matplotlib.pyplot as plt + + +def test_fallback_errors(): + file_name = fm.findfont('DejaVu Sans') + + with pytest.raises(TypeError, match="Fallback list must be a list"): + # failing to be a list will fail before the 0 + ft2font.FT2Font(file_name, _fallback_list=(0,)) + + with pytest.raises( + TypeError, match="Fallback fonts must be FT2Font objects." + ): + ft2font.FT2Font(file_name, _fallback_list=[0]) + + +def test_ft2font_positive_hinting_factor(): + file_name = fm.findfont('DejaVu Sans') + with pytest.raises( + ValueError, match="hinting_factor must be greater than 0" + ): + ft2font.FT2Font(file_name, 0) + + +def test_fallback_smoke(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font wqy-zenhei.ttc may be missing") + + fp = fm.FontProperties(family=["Noto Sans CJK JP"]) + if Path(fm.findfont(fp)).name != "NotoSansCJK-Regular.ttc": + pytest.skip("Noto Sans CJK JP font may be missing.") + + plt.rcParams['font.size'] = 20 + fig = plt.figure(figsize=(4.75, 1.85)) + fig.text(0.05, 0.45, "There are 几个汉字 in between!", + family=['DejaVu Sans', "Noto Sans CJK JP"]) + fig.text(0.05, 0.25, "There are 几个汉字 in between!", + family=['DejaVu Sans', "WenQuanYi Zen Hei"]) + fig.text(0.05, 0.65, "There are 几个汉字 in between!", + family=["Noto Sans CJK JP"]) + fig.text(0.05, 0.85, "There are 几个汉字 in between!", + family=["WenQuanYi Zen Hei"]) + + # TODO enable fallback for other backends! + for fmt in ['png', 'raw']: # ["svg", "pdf", "ps"]: + fig.savefig(io.BytesIO(), format=fmt) + + +@pytest.mark.parametrize('family_name, file_name', + [("WenQuanYi Zen Hei", "wqy-zenhei.ttc"), + ("Noto Sans CJK JP", "NotoSansCJK-Regular.ttc")] + ) +@check_figures_equal(extensions=["png"]) +def test_font_fallback_chinese(fig_test, fig_ref, family_name, file_name): + fp = fm.FontProperties(family=[family_name]) + if Path(fm.findfont(fp)).name != file_name: + pytest.skip(f"Font {family_name} ({file_name}) is missing") + + text = ["There are", "几个汉字", "in between!"] + + plt.rcParams["font.size"] = 20 + test_fonts = [["DejaVu Sans", family_name]] * 3 + ref_fonts = [["DejaVu Sans"], [family_name], ["DejaVu Sans"]] + + for j, (txt, test_font, ref_font) in enumerate( + zip(text, test_fonts, ref_fonts) + ): + fig_ref.text(0.05, .85 - 0.15*j, txt, family=ref_font) + fig_test.text(0.05, .85 - 0.15*j, txt, family=test_font) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 56b4bb9b05b1..4454a4a51ac6 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -3,6 +3,7 @@ #define NO_IMPORT_ARRAY #include +#include #include #include #include @@ -183,12 +184,8 @@ FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, m_dirty = true; } -static FT_UInt ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode) +static void ft_glyph_warn(FT_ULong charcode) { - FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); - if (glyph_index) { - return glyph_index; - } PyObject *text_helpers = NULL, *tmp = NULL; if (!(text_helpers = PyImport_ImportModule("matplotlib._text_helpers")) || !(tmp = PyObject_CallMethod(text_helpers, "warn_on_missing_glyph", "k", charcode))) { @@ -200,9 +197,20 @@ static FT_UInt ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode) if (PyErr_Occurred()) { throw py::exception(); } - return 0; } +static FT_UInt +ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode, bool warn = true) +{ + FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); + if (glyph_index) { + return glyph_index; + } + if (warn) { + ft_glyph_warn(charcode); + } + return 0; +} // ft_outline_decomposer should be passed to FT_Outline_Decompose. On the // first pass, vertices and codes are set to NULL, and index is simply @@ -333,7 +341,10 @@ FT2Font::get_path() return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); } -FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_) : image(), face(NULL) +FT2Font::FT2Font(FT_Open_Args &open_args, + long hinting_factor_, + std::vector &fallback_list) + : image(), face(NULL) { clear(); @@ -360,6 +371,9 @@ FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_) : image(), face( FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 }; FT_Set_Transform(face, &transform, 0); + + // Set fallbacks + std::copy(fallback_list.begin(), fallback_list.end(), std::back_inserter(fallbacks)); } FT2Font::~FT2Font() @@ -383,6 +397,12 @@ void FT2Font::clear() } glyphs.clear(); + glyph_to_font.clear(); + char_to_font.clear(); + + for (size_t i = 0; i < fallbacks.size(); i++) { + fallbacks[i]->clear(); + } } void FT2Font::set_size(double ptsize, double dpi) @@ -394,6 +414,10 @@ void FT2Font::set_size(double ptsize, double dpi) } FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 }; FT_Set_Transform(face, &transform, 0); + + for (size_t i = 0; i < fallbacks.size(); i++) { + fallbacks[i]->set_size(ptsize, dpi); + } } void FT2Font::set_charmap(int i) @@ -414,12 +438,32 @@ void FT2Font::select_charmap(unsigned long i) } } -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode) +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback = false) +{ + if (fallback && glyph_to_font.find(left) != glyph_to_font.end() && + glyph_to_font.find(right) != glyph_to_font.end()) { + FT2Font *left_ft_object = glyph_to_font[left]; + FT2Font *right_ft_object = glyph_to_font[right]; + if (left_ft_object != right_ft_object) { + // we do not know how to do kerning between different fonts + return 0; + } + // if left_ft_object is the same as right_ft_object, + // do the exact same thing which set_text does. + return right_ft_object->get_kerning(left, right, mode, false); + } + else + { + FT_Vector delta; + return get_kerning(left, right, mode, delta); + } +} + +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta) { if (!FT_HAS_KERNING(face)) { return 0; } - FT_Vector delta; if (!FT_Get_Kerning(face, left, right, mode, &delta)) { return (int)(delta.x) / (hinting_factor << kerning_factor); @@ -431,6 +475,9 @@ int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode) void FT2Font::set_kerning_factor(int factor) { kerning_factor = factor; + for (size_t i = 0; i < fallbacks.size(); i++) { + fallbacks[i]->set_kerning_factor(factor); + } } void FT2Font::set_text( @@ -440,47 +487,54 @@ void FT2Font::set_text( angle = angle / 360.0 * 2 * M_PI; - // this computes width and height in subpixels so we have to divide by 64 + // this computes width and height in subpixels so we have to multiply by 64 matrix.xx = (FT_Fixed)(cos(angle) * 0x10000L); matrix.xy = (FT_Fixed)(-sin(angle) * 0x10000L); matrix.yx = (FT_Fixed)(sin(angle) * 0x10000L); matrix.yy = (FT_Fixed)(cos(angle) * 0x10000L); - FT_Bool use_kerning = FT_HAS_KERNING(face); - FT_UInt previous = 0; - clear(); bbox.xMin = bbox.yMin = 32000; bbox.xMax = bbox.yMax = -32000; - for (unsigned int n = 0; n < N; n++) { - FT_UInt glyph_index; + FT_UInt previous = 0; + FT2Font *previous_ft_object = NULL; + + for (size_t n = 0; n < N; n++) { + FT_UInt glyph_index = 0; FT_BBox glyph_bbox; FT_Pos last_advance; - glyph_index = ft_get_char_index_or_warn(face, codepoints[n]); + FT_Error charcode_error, glyph_error; + FT2Font *ft_object_with_glyph = this; + bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs, + char_to_font, glyph_to_font, codepoints[n], flags, + charcode_error, glyph_error, false); + if (!was_found) { + ft_glyph_warn((FT_ULong)codepoints[n]); + + // render missing glyph tofu + // come back to top-most font + ft_object_with_glyph = this; + char_to_font[codepoints[n]] = ft_object_with_glyph; + glyph_to_font[glyph_index] = ft_object_with_glyph; + ft_object_with_glyph->load_glyph(glyph_index, flags, ft_object_with_glyph, false); + } // retrieve kerning distance and move pen position - if (use_kerning && previous && glyph_index) { + if ((ft_object_with_glyph == previous_ft_object) && // if both fonts are the same + ft_object_with_glyph->has_kerning() && // if the font knows how to kern + previous && glyph_index // and we really have 2 glyphs + ) { FT_Vector delta; - FT_Get_Kerning(face, previous, glyph_index, FT_KERNING_DEFAULT, &delta); - pen.x += delta.x / (hinting_factor << kerning_factor); - } - if (FT_Error error = FT_Load_Glyph(face, glyph_index, flags)) { - throw_ft_error("Could not load glyph", error); + pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT, delta); } - // ignore errors, jump to next glyph // extract glyph image and store it in our table + FT_Glyph &thisGlyph = glyphs[glyphs.size() - 1]; - FT_Glyph thisGlyph; - if (FT_Error error = FT_Get_Glyph(face->glyph, &thisGlyph)) { - throw_ft_error("Could not get glyph", error); - } - // ignore errors, jump to next glyph - - last_advance = face->glyph->advance.x; + last_advance = ft_object_with_glyph->get_face()->glyph->advance.x; FT_Glyph_Transform(thisGlyph, 0, &pen); FT_Glyph_Transform(thisGlyph, &matrix, 0); xys.push_back(pen.x); @@ -496,7 +550,8 @@ void FT2Font::set_text( pen.x += last_advance; previous = glyph_index; - glyphs.push_back(thisGlyph); + previous_ft_object = ft_object_with_glyph; + } FT_Vector_Transform(&pen, &matrix); @@ -507,17 +562,110 @@ void FT2Font::set_text( } } -void FT2Font::load_char(long charcode, FT_Int32 flags) +void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback = false) +{ + // if this is parent FT2Font, cache will be filled in 2 ways: + // 1. set_text was previously called + // 2. set_text was not called and fallback was enabled + if (fallback && char_to_font.find(charcode) != char_to_font.end()) { + ft_object = char_to_font[charcode]; + // since it will be assigned to ft_object anyway + FT2Font *throwaway = NULL; + ft_object->load_char(charcode, flags, throwaway, false); + } else if (fallback) { + FT_UInt final_glyph_index; + FT_Error charcode_error, glyph_error; + FT2Font *ft_object_with_glyph = this; + bool was_found = load_char_with_fallback(ft_object_with_glyph, final_glyph_index, glyphs, char_to_font, + glyph_to_font, charcode, flags, charcode_error, glyph_error, true); + if (!was_found) { + ft_glyph_warn(charcode); + if (charcode_error) { + throw_ft_error("Could not load charcode", charcode_error); + } + else if (glyph_error) { + throw_ft_error("Could not load charcode", glyph_error); + } + } + ft_object = ft_object_with_glyph; + } else { + ft_object = this; + FT_UInt glyph_index = ft_get_char_index_or_warn(face, (FT_ULong)charcode); + + if (FT_Error error = FT_Load_Glyph(face, glyph_index, flags)) { + throw_ft_error("Could not load charcode", error); + } + FT_Glyph thisGlyph; + if (FT_Error error = FT_Get_Glyph(face->glyph, &thisGlyph)) { + throw_ft_error("Could not get glyph", error); + } + glyphs.push_back(thisGlyph); + } +} + +bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, + FT_UInt &final_glyph_index, + std::vector &parent_glyphs, + std::unordered_map &parent_char_to_font, + std::unordered_map &parent_glyph_to_font, + long charcode, + FT_Int32 flags, + FT_Error &charcode_error, + FT_Error &glyph_error, + bool override = false) { - FT_UInt glyph_index = ft_get_char_index_or_warn(face, (FT_ULong)charcode); - if (FT_Error error = FT_Load_Glyph(face, glyph_index, flags)) { - throw_ft_error("Could not load charcode", error); + FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); + + if (glyph_index || override) { + charcode_error = FT_Load_Glyph(face, glyph_index, flags); + if (charcode_error) { + return false; + } + + FT_Glyph thisGlyph; + glyph_error = FT_Get_Glyph(face->glyph, &thisGlyph); + if (glyph_error) { + return false; + } + + final_glyph_index = glyph_index; + + // cache the result for future + // need to store this for anytime a character is loaded from a parent + // FT2Font object or to generate a mapping of individual characters to fonts + ft_object_with_glyph = this; + parent_glyph_to_font[final_glyph_index] = this; + parent_char_to_font[charcode] = this; + parent_glyphs.push_back(thisGlyph); + return true; + } + + else { + for (size_t i = 0; i < fallbacks.size(); ++i) { + bool was_found = fallbacks[i]->load_char_with_fallback( + ft_object_with_glyph, final_glyph_index, parent_glyphs, parent_char_to_font, + parent_glyph_to_font, charcode, flags, charcode_error, glyph_error, override); + if (was_found) { + return true; + } + } + return false; } - FT_Glyph thisGlyph; - if (FT_Error error = FT_Get_Glyph(face->glyph, &thisGlyph)) { - throw_ft_error("Could not get glyph", error); +} + +void FT2Font::load_glyph(FT_UInt glyph_index, + FT_Int32 flags, + FT2Font *&ft_object, + bool fallback = false) +{ + // cache is only for parent FT2Font + if (fallback && glyph_to_font.find(glyph_index) != glyph_to_font.end()) { + ft_object = glyph_to_font[glyph_index]; + } else { + ft_object = this; } - glyphs.push_back(thisGlyph); + + ft_object->load_glyph(glyph_index, flags); } void FT2Font::load_glyph(FT_UInt glyph_index, FT_Int32 flags) @@ -532,6 +680,28 @@ void FT2Font::load_glyph(FT_UInt glyph_index, FT_Int32 flags) glyphs.push_back(thisGlyph); } +FT_UInt FT2Font::get_char_index(FT_ULong charcode, bool fallback = false) +{ + FT2Font *ft_object = NULL; + if (fallback && char_to_font.find(charcode) != char_to_font.end()) { + // fallback denotes whether we want to search fallback list. + // should call set_text/load_char_with_fallback to parent FT2Font before + // wanting to use fallback list here. (since that populates the cache) + ft_object = char_to_font[charcode]; + } else { + // set as self + ft_object = this; + } + + // historically, get_char_index never raises a warning + return ft_get_char_index_or_warn(ft_object->get_face(), charcode, false); +} + +void FT2Font::get_cbox(FT_BBox &bbox) +{ + FT_Glyph_Get_CBox(glyphs.back(), ft_glyph_bbox_subpixels, &bbox); +} + void FT2Font::get_width_height(long *width, long *height) { *width = advance; @@ -622,8 +792,14 @@ void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, im.draw_bitmap(&bitmap->bitmap, x + bitmap->left, y); } -void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer) +void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer, bool fallback = false) { + if (fallback && glyph_to_font.find(glyph_number) != glyph_to_font.end()) { + // cache is only for parent FT2Font + FT2Font *ft_object = glyph_to_font[glyph_number]; + ft_object->get_glyph_name(glyph_number, buffer, false); + return; + } if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ diff --git a/src/ft2font.h b/src/ft2font.h index 692be02f7ec1..cdcb979bdf3c 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -1,10 +1,12 @@ /* -*- mode: c++; c-basic-offset: 4 -*- */ /* A python interface to FreeType */ +#pragma once #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H #include #include +#include extern "C" { #include @@ -69,7 +71,7 @@ class FT2Font { public: - FT2Font(FT_Open_Args &open_args, long hinting_factor); + FT2Font(FT_Open_Args &open_args, long hinting_factor, std::vector &fallback_list); virtual ~FT2Font(); void clear(); void set_size(double ptsize, double dpi); @@ -77,9 +79,21 @@ class FT2Font void select_charmap(unsigned long i); void set_text( size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector &xys); - int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode); + int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback); + int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta); void set_kerning_factor(int factor); - void load_char(long charcode, FT_Int32 flags); + void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback); + bool load_char_with_fallback(FT2Font *&ft_object_with_glyph, + FT_UInt &final_glyph_index, + std::vector &parent_glyphs, + std::unordered_map &parent_char_to_font, + std::unordered_map &parent_glyph_to_font, + long charcode, + FT_Int32 flags, + FT_Error &charcode_error, + FT_Error &glyph_error, + bool override); + void load_glyph(FT_UInt glyph_index, FT_Int32 flags, FT2Font *&ft_object, bool fallback); void load_glyph(FT_UInt glyph_index, FT_Int32 flags); void get_width_height(long *width, long *height); void get_bitmap_offset(long *x, long *y); @@ -89,14 +103,17 @@ class FT2Font void get_xys(bool antialiased, std::vector &xys); void draw_glyphs_to_bitmap(bool antialiased); void draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased); - void get_glyph_name(unsigned int glyph_number, char *buffer); + void get_glyph_name(unsigned int glyph_number, char *buffer, bool fallback); long get_name_index(char *name); + FT_UInt get_char_index(FT_ULong charcode, bool fallback); + void get_cbox(FT_BBox &bbox); PyObject* get_path(); FT_Face const &get_face() const { return face; } + FT2Image &get_image() { return image; @@ -117,12 +134,19 @@ class FT2Font { return hinting_factor; } + FT_Bool has_kerning() const + { + return FT_HAS_KERNING(face); + } private: FT2Image image; FT_Face face; FT_Vector pen; /* untransformed origin */ std::vector glyphs; + std::vector fallbacks; + std::unordered_map glyph_to_font; + std::unordered_map char_to_font; FT_BBox bbox; FT_Pos advance; long hinting_factor; diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6f7d8fd33712..63d21184cc4c 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -168,19 +168,16 @@ typedef struct static PyTypeObject PyGlyphType; -static PyObject * -PyGlyph_from_FT2Font(const FT2Font *font) +static PyObject *PyGlyph_from_FT2Font(const FT2Font *font) { const FT_Face &face = font->get_face(); + const long hinting_factor = font->get_hinting_factor(); const FT_Glyph &glyph = font->get_last_glyph(); - size_t ind = font->get_last_glyph_index(); - long hinting_factor = font->get_hinting_factor(); PyGlyph *self; self = (PyGlyph *)PyGlyphType.tp_alloc(&PyGlyphType, 0); - self->glyphInd = ind; - + self->glyphInd = font->get_last_glyph_index(); FT_Glyph_Get_CBox(glyph, ft_glyph_bbox_subpixels, &self->bbox); self->width = face->glyph->metrics.width / hinting_factor; @@ -241,7 +238,7 @@ static PyTypeObject *PyGlyph_init_type() * FT2Font * */ -typedef struct +struct PyFT2Font { PyObject_HEAD FT2Font *x; @@ -250,7 +247,8 @@ typedef struct Py_ssize_t shape[2]; Py_ssize_t strides[2]; Py_ssize_t suboffsets[2]; -} PyFT2Font; + std::vector fallbacks; +}; static PyTypeObject PyFT2FontType; @@ -312,10 +310,24 @@ static PyObject *PyFT2Font_new(PyTypeObject *type, PyObject *args, PyObject *kwd } const char *PyFT2Font_init__doc__ = - "FT2Font(ttffile)\n" + "FT2Font(filename, hinting_factor=8, *, _fallback_list=None, _kerning_factor=0)\n" "--\n\n" "Create a new FT2Font object.\n" "\n" + "Parameters\n" + "----------\n" + "filename : str or file-like\n" + " The source of the font data in a format (ttf or ttc) that FreeType can read\n\n" + "hinting_factor : int, optional\n" + " Must be positive. Used to scale the hinting in the x-direction\n" + "_fallback_list : list of FT2Font, optional\n" + " A list of FT2Font objects used to find missing glyphs.\n\n" + " .. warning ::\n" + " This API is both private and provisional: do not use it directly\n\n" + "_kerning_factor : int, optional\n" + " Used to adjust the degree of kerning.\n\n" + " .. warning ::\n" + " This API is private: do not use it directly\n\n" "Attributes\n" "----------\n" "num_faces\n" @@ -349,17 +361,24 @@ const char *PyFT2Font_init__doc__ = static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) { - PyObject *filename = NULL, *open = NULL, *data = NULL; + PyObject *filename = NULL, *open = NULL, *data = NULL, *fallback_list = NULL; FT_Open_Args open_args; long hinting_factor = 8; int kerning_factor = 0; - const char *names[] = { "filename", "hinting_factor", "_kerning_factor", NULL }; - + const char *names[] = { + "filename", "hinting_factor", "_fallback_list", "_kerning_factor", NULL + }; + std::vector fallback_fonts; if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O|l$i:FT2Font", (char **)names, &filename, - &hinting_factor, &kerning_factor)) { + args, kwds, "O|l$Oi:FT2Font", (char **)names, &filename, + &hinting_factor, &fallback_list, &kerning_factor)) { return -1; } + if (hinting_factor <= 0) { + PyErr_SetString(PyExc_ValueError, + "hinting_factor must be greater than 0"); + goto exit; + } self->stream.base = NULL; self->stream.size = 0x7fffffff; // Unknown size. @@ -370,6 +389,37 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; + if (fallback_list) { + if (!PyList_Check(fallback_list)) { + PyErr_SetString(PyExc_TypeError, "Fallback list must be a list"); + goto exit; + } + Py_ssize_t size = PyList_Size(fallback_list); + + // go through fallbacks once to make sure the types are right + for (Py_ssize_t i = 0; i < size; ++i) { + // this returns a borrowed reference + PyObject* item = PyList_GetItem(fallback_list, i); + if (!PyObject_IsInstance(item, PyObject_Type(reinterpret_cast(self)))) { + PyErr_SetString(PyExc_TypeError, "Fallback fonts must be FT2Font objects."); + goto exit; + } + } + // go through a second time to add them to our lists + for (Py_ssize_t i = 0; i < size; ++i) { + // this returns a borrowed reference + PyObject* item = PyList_GetItem(fallback_list, i); + // Increase the ref count, we will undo this in dealloc this makes + // sure things do not get gc'd under us! + Py_INCREF(item); + self->fallbacks.push_back(item); + // Also (locally) cache the underlying FT2Font objects. As long as + // the Python objects are kept alive, these pointer are good. + FT2Font *fback = reinterpret_cast(item)->x; + fallback_fonts.push_back(fback); + } + } + if (PyBytes_Check(filename) || PyUnicode_Check(filename)) { if (!(open = PyDict_GetItemString(PyEval_GetBuiltins(), "open")) // Borrowed reference. || !(self->py_file = PyObject_CallFunction(open, "Os", filename, "rb"))) { @@ -391,7 +441,7 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) Py_CLEAR(data); CALL_CPP_FULL( - "FT2Font", (self->x = new FT2Font(open_args, hinting_factor)), + "FT2Font", (self->x = new FT2Font(open_args, hinting_factor, fallback_fonts)), Py_CLEAR(self->py_file), -1); CALL_CPP_INIT("FT2Font->set_kerning_factor", (self->x->set_kerning_factor(kerning_factor))); @@ -403,6 +453,10 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) static void PyFT2Font_dealloc(PyFT2Font *self) { delete self->x; + for (size_t i = 0; i < self->fallbacks.size(); i++) { + Py_DECREF(self->fallbacks[i]); + } + Py_XDECREF(self->py_file); Py_TYPE(self)->tp_free((PyObject *)self); } @@ -478,21 +532,22 @@ const char *PyFT2Font_get_kerning__doc__ = "get_kerning(self, left, right, mode)\n" "--\n\n" "Get the kerning between *left* and *right* glyph indices.\n" - "*mode* is a kerning mode constant:\n" - " KERNING_DEFAULT - Return scaled and grid-fitted kerning distances\n" - " KERNING_UNFITTED - Return scaled but un-grid-fitted kerning distances\n" - " KERNING_UNSCALED - Return the kerning vector in original font units\n"; + "*mode* is a kerning mode constant:\n\n" + " - KERNING_DEFAULT - Return scaled and grid-fitted kerning distances\n" + " - KERNING_UNFITTED - Return scaled but un-grid-fitted kerning distances\n" + " - KERNING_UNSCALED - Return the kerning vector in original font units\n"; static PyObject *PyFT2Font_get_kerning(PyFT2Font *self, PyObject *args) { FT_UInt left, right, mode; int result; + int fallback = 1; if (!PyArg_ParseTuple(args, "III:get_kerning", &left, &right, &mode)) { return NULL; } - CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode))); + CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode, (bool)fallback))); return PyLong_FromLong(result); } @@ -585,34 +640,36 @@ const char *PyFT2Font_load_char__doc__ = "Load character with *charcode* in current fontfile and set glyph.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" - "Return value is a Glyph object, with attributes\n" - " width # glyph width\n" - " height # glyph height\n" - " bbox # the glyph bbox (xmin, ymin, xmax, ymax)\n" - " horiBearingX # left side bearing in horizontal layouts\n" - " horiBearingY # top side bearing in horizontal layouts\n" - " horiAdvance # advance width for horizontal layout\n" - " vertBearingX # left side bearing in vertical layouts\n" - " vertBearingY # top side bearing in vertical layouts\n" - " vertAdvance # advance height for vertical layout\n"; + "Return value is a Glyph object, with attributes\n\n" + "- width: glyph width\n" + "- height: glyph height\n" + "- bbox: the glyph bbox (xmin, ymin, xmax, ymax)\n" + "- horiBearingX: left side bearing in horizontal layouts\n" + "- horiBearingY: top side bearing in horizontal layouts\n" + "- horiAdvance: advance width for horizontal layout\n" + "- vertBearingX: left side bearing in vertical layouts\n" + "- vertBearingY: top side bearing in vertical layouts\n" + "- vertAdvance: advance height for vertical layout\n"; static PyObject *PyFT2Font_load_char(PyFT2Font *self, PyObject *args, PyObject *kwds) { long charcode; + int fallback = 1; FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; const char *names[] = { "charcode", "flags", NULL }; /* This makes a technically incorrect assumption that FT_Int32 is int. In theory it can also be long, if the size of int is less than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "l|i:load_char", (char **)names, &charcode, &flags)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|i:load_char", (char **)names, &charcode, + &flags)) { return NULL; } - CALL_CPP("load_char", (self->x->load_char(charcode, flags))); + FT2Font *ft_object = NULL; + CALL_CPP("load_char", (self->x->load_char(charcode, flags, ft_object, (bool)fallback))); - return PyGlyph_from_FT2Font(self->x); + return PyGlyph_from_FT2Font(ft_object); } const char *PyFT2Font_load_glyph__doc__ = @@ -621,34 +678,36 @@ const char *PyFT2Font_load_glyph__doc__ = "Load character with *glyphindex* in current fontfile and set glyph.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" - "Return value is a Glyph object, with attributes\n" - " width # glyph width\n" - " height # glyph height\n" - " bbox # the glyph bbox (xmin, ymin, xmax, ymax)\n" - " horiBearingX # left side bearing in horizontal layouts\n" - " horiBearingY # top side bearing in horizontal layouts\n" - " horiAdvance # advance width for horizontal layout\n" - " vertBearingX # left side bearing in vertical layouts\n" - " vertBearingY # top side bearing in vertical layouts\n" - " vertAdvance # advance height for vertical layout\n"; + "Return value is a Glyph object, with attributes\n\n" + "- width: glyph width\n" + "- height: glyph height\n" + "- bbox: the glyph bbox (xmin, ymin, xmax, ymax)\n" + "- horiBearingX: left side bearing in horizontal layouts\n" + "- horiBearingY: top side bearing in horizontal layouts\n" + "- horiAdvance: advance width for horizontal layout\n" + "- vertBearingX: left side bearing in vertical layouts\n" + "- vertBearingY: top side bearing in vertical layouts\n" + "- vertAdvance: advance height for vertical layout\n"; static PyObject *PyFT2Font_load_glyph(PyFT2Font *self, PyObject *args, PyObject *kwds) { FT_UInt glyph_index; FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; + int fallback = 1; const char *names[] = { "glyph_index", "flags", NULL }; /* This makes a technically incorrect assumption that FT_Int32 is int. In theory it can also be long, if the size of int is less than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "I|i:load_glyph", (char **)names, &glyph_index, &flags)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "I|i:load_glyph", (char **)names, &glyph_index, + &flags)) { return NULL; } - CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags))); + FT2Font *ft_object = NULL; + CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags, ft_object, (bool)fallback))); - return PyGlyph_from_FT2Font(self->x); + return PyGlyph_from_FT2Font(ft_object); } const char *PyFT2Font_get_width_height__doc__ = @@ -794,10 +853,12 @@ static PyObject *PyFT2Font_get_glyph_name(PyFT2Font *self, PyObject *args) { unsigned int glyph_number; char buffer[128]; + int fallback = 1; + if (!PyArg_ParseTuple(args, "I:get_glyph_name", &glyph_number)) { return NULL; } - CALL_CPP("get_glyph_name", (self->x->get_glyph_name(glyph_number, buffer))); + CALL_CPP("get_glyph_name", (self->x->get_glyph_name(glyph_number, buffer, (bool)fallback))); return PyUnicode_FromString(buffer); } @@ -841,12 +902,13 @@ static PyObject *PyFT2Font_get_char_index(PyFT2Font *self, PyObject *args) { FT_UInt index; FT_ULong ccode; + int fallback = 1; if (!PyArg_ParseTuple(args, "k:get_char_index", &ccode)) { return NULL; } - index = FT_Get_Char_Index(self->x->get_face(), ccode); + CALL_CPP("get_char_index", index = self->x->get_char_index(ccode, (bool)fallback)); return PyLong_FromLong(index); }