diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index 75d84997be9f..ca95c8081c47 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -9,7 +9,7 @@ LayoutItem = dataclasses.make_dataclass( - "LayoutItem", ["char", "glyph_idx", "x", "prev_kern"]) + "LayoutItem", ["ft_object", "char", "glyph_idx", "x", "prev_kern"]) def warn_on_missing_glyph(codepoint): @@ -63,6 +63,7 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT): if prev_glyph_idx is not None else 0.) x += kern glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING) - yield LayoutItem(char, glyph_idx, x, kern) + ft_object = font.get_glyph_to_font().get(glyph_idx, font) + yield LayoutItem(ft_object, char, glyph_idx, x, kern) x += glyph.linearHoriAdvance / 65536 prev_glyph_idx = glyph_idx diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 3224fb90e3a9..d51a50581378 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -2,6 +2,7 @@ Common functionality between the PDF and PS backends. """ +import os from io import BytesIO import functools @@ -37,7 +38,11 @@ def get_glyphs_subset(fontfile, characters): options = subset.Options(glyph_names=True, recommended_glyphs=True) # prevent subsetting FontForge Timestamp and other tables - options.drop_tables += ['FFTM', 'PfEd'] + options.drop_tables += ['FFTM', 'PfEd', 'BDF'] + + # if fontfile is a ttc, specify font number + if os.path.splitext(fontfile)[1] == ".ttc": + options.font_number = 0 with subset.load_font(fontfile, options) as font: subsetter = subset.Subsetter(options=options) @@ -136,7 +141,7 @@ def _get_font_afm(self, prop): return _cached_get_afm_from_fname(fname) def _get_font_ttf(self, prop): - fname = font_manager.findfont(prop) + fname = font_manager.find_fontsprop(prop) font = font_manager.get_font(fname) font.clear() font.set_size(prop.get_size_in_points(), 72) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index b937c64fce95..b5a2f85affc1 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -35,7 +35,7 @@ from matplotlib.backend_bases import ( _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, RendererBase) -from matplotlib.font_manager import findfont, get_font +from matplotlib.font_manager import find_fontsprop, get_font from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, LOAD_DEFAULT, LOAD_NO_AUTOHINT) from matplotlib.mathtext import MathTextParser @@ -251,7 +251,7 @@ def _get_agg_font(self, prop): """ Get the font for text instance t, caching for efficiency """ - fname = findfont(prop) + fname = find_fontsprop(prop) font = get_font(fname) font.clear() diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 9980d49af85b..df575ea371c7 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -33,7 +33,7 @@ GraphicsContextBase, RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure -from matplotlib.font_manager import findfont, get_font +from matplotlib.font_manager import findfont, find_fontsprop, get_font from matplotlib.afm import AFM import matplotlib.type1font as type1font import matplotlib.dviread as dviread @@ -861,20 +861,28 @@ def fontName(self, fontprop): """ if isinstance(fontprop, str): - filename = fontprop + filenames = [fontprop] elif mpl.rcParams['pdf.use14corefonts']: - filename = findfont( - fontprop, fontext='afm', directory=RendererPdf._afm_font_dir) + filenames = find_fontsprop( + fontprop, fontext='afm', directory=RendererPdf._afm_font_dir + ).values() else: - filename = findfont(fontprop) - - Fx = self.fontNames.get(filename) - if Fx is None: - Fx = next(self._internal_font_seq) - self.fontNames[filename] = Fx - _log.debug('Assigning font %s = %r', Fx, filename) - - return Fx + filenames = find_fontsprop(fontprop).values() + first_Fx = None + for fname in filenames: + Fx = self.fontNames.get(fname) + if not first_Fx: + first_Fx = Fx + if Fx is None: + Fx = next(self._internal_font_seq) + self.fontNames[fname] = Fx + _log.debug('Assigning font %s = %r', Fx, fname) + if not first_Fx: + first_Fx = Fx + + # find_fontsprop's first value always adheres to + # findfont's value, so technically no behaviour change + return first_Fx def dviFontName(self, dvifont): """ @@ -1143,7 +1151,6 @@ def get_char_width(charcode): width = font.load_char( s, flags=LOAD_NO_SCALE | LOAD_NO_HINTING).horiAdvance return cvt(width) - with warnings.catch_warnings(): # Ignore 'Required glyph missing from current font' warning # from ft2font: here we're just building the widths table, but @@ -2331,7 +2338,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): fonttype = 1 else: font = self._get_font_ttf(prop) - self.file._character_tracker.track(font, s) + char_to_font = font.fill_glyphs(s) + for char, font in char_to_font.items(): + self.file._character_tracker.track(font, chr(char)) fonttype = mpl.rcParams['pdf.fonttype'] if gc.get_url() is not None: @@ -2371,22 +2380,27 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # the regular text show command (TJ) with appropriate kerning between # chunks, whereas multibyte characters use the XObject command (Do). else: - # List of (start_x, [prev_kern, char, char, ...]), w/o zero kerns. + # List of (ft_object, start_x, [prev_kern, char, char, ...]), + # w/o zero kerns. singlebyte_chunks = [] - # List of (start_x, glyph_index). + # List of (ft_object, start_x, glyph_index). multibyte_glyphs = [] prev_was_multibyte = True + prev_font = font for item in _text_helpers.layout( s, font, kern_mode=KERNING_UNFITTED): if _font_supports_glyph(fonttype, ord(item.char)): - if prev_was_multibyte: - singlebyte_chunks.append((item.x, [])) + if prev_was_multibyte or item.ft_object != prev_font: + singlebyte_chunks.append((item.ft_object, item.x, [])) + prev_font = item.ft_object if item.prev_kern: - singlebyte_chunks[-1][1].append(item.prev_kern) - singlebyte_chunks[-1][1].append(item.char) + singlebyte_chunks[-1][2].append(item.prev_kern) + singlebyte_chunks[-1][2].append(item.char) prev_was_multibyte = False else: - multibyte_glyphs.append((item.x, item.glyph_idx)) + multibyte_glyphs.append( + (item.ft_object, item.x, item.glyph_idx) + ) prev_was_multibyte = True # Do the rotation and global translation as a single matrix # concatenation up front @@ -2396,10 +2410,12 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): -math.sin(a), math.cos(a), x, y, Op.concat_matrix) # Emit all the 1-byte characters in a BT/ET group. - self.file.output(Op.begin_text, - self.file.fontName(prop), fontsize, Op.selectfont) + + self.file.output(Op.begin_text) prev_start_x = 0 - for start_x, kerns_or_chars in singlebyte_chunks: + for ft_object, start_x, kerns_or_chars in singlebyte_chunks: + ft_name = self.file.fontName(ft_object.fname) + self.file.output(ft_name, fontsize, Op.selectfont) self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0) self.file.output( # See pdf spec "Text space details" for the 1000/fontsize @@ -2411,14 +2427,16 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): prev_start_x = start_x self.file.output(Op.end_text) # Then emit all the multibyte characters, one at a time. - for start_x, glyph_idx in multibyte_glyphs: - self._draw_xobject_glyph(font, fontsize, glyph_idx, start_x, 0) + for ft_object, start_x, glyph_idx in multibyte_glyphs: + self._draw_xobject_glyph( + ft_object, fontsize, glyph_idx, start_x, 0 + ) self.file.output(Op.grestore) - def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): + def _draw_xobject_glyph(self, ft_object, fontsize, glyph_idx, x, y): """Draw a multibyte character from a Type 3 font as an XObject.""" - symbol_name = font.get_glyph_name(glyph_idx) - name = self.file._get_xobject_symbol_name(font.fname, symbol_name) + symbol_name = ft_object.get_glyph_name(glyph_idx) + name = self.file._get_xobject_symbol_name(ft_object.fname, symbol_name) self.file.output( Op.gsave, 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 9d45575eb13d..6c88622deecc 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -23,6 +23,7 @@ # - setWeights function needs improvement # - 'light' is an invalid weight value, remove it. +from collections import OrderedDict import dataclasses from functools import lru_cache import json @@ -1308,6 +1309,117 @@ def findfont(self, prop, fontext='ttf', directory=None, prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params) + def find_fontsprop(self, prop, fontext='ttf', directory=None, + fallback_to_default=True, rebuild_if_missing=True): + """ + Find font families that most closely matches 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 + ------- + OrderedDict + key, value pair of families and their corresponding filepaths. + + Notes + ----- + This is a plugin to original findfont API, which only returns a + single font for given font properties. Instead, this API returns + an OrderedDict containing multiple fonts and their filepaths which + closely match the given font properties. + Since this internally uses original API, there's no change + to the logic of performing the nearest neighbor search. + See `findfont` for more details. + + """ + # print("finding font!") + + 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) + ffamily = prop.get_family() + + fpaths = OrderedDict() + for fidx in range(len(ffamily)): + cprop = prop.copy() + + # set current prop's family + cprop.set_family(ffamily[fidx]) + + # do not fall back to default font + fpath = self._findfontsprop_cached( + ffamily[fidx], cprop, fontext, directory, + False, rebuild_if_missing, rc_params + ) + if fpath: + fpaths[ffamily[fidx]] = fpath + + # 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().set_family(dfamily) + fpath = self._findfontsprop_cached( + dfamily, cprop, fontext, directory, + True, rebuild_if_missing, rc_params + ) + fpaths[dfamily] = fpath + else: + raise ValueError("Failed to find any font, and fallback " + "to the default font was disabled.") + + return fpaths + + @lru_cache() + def _findfontsprop_cached( + self, family, prop, fontext, directory, + fallback_to_default, rebuild_if_missing, rc_params + ): + try: + return self._findfont_cached( + prop, fontext, directory, fallback_to_default, + rebuild_if_missing, rc_params + ) + except ValueError: + if not fallback_to_default: + if family.lower() 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 + ) + @lru_cache() def _findfont_cached(self, prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params): @@ -1401,9 +1513,21 @@ def is_opentype_cff_font(filename): @lru_cache(64) -def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id): - return ft2font.FT2Font( - filename, hinting_factor, _kerning_factor=_kerning_factor) +def _get_font(fpaths, hinting_factor, *, _kerning_factor, thread_id): + ftobjects = [] + for fpath in fpaths[1:]: + ftobject = ft2font.FT2Font( + fpath, hinting_factor, + _kerning_factor=_kerning_factor + ) + ftobjects.append(ftobject) + + ft2font_object = ft2font.FT2Font( + fpaths[0], hinting_factor, + _fallback_list=ftobjects, + _kerning_factor=_kerning_factor + ) + return ft2font_object # FT2Font objects cannot be used across fork()s because they reference the same @@ -1417,11 +1541,14 @@ def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id): def get_font(filename, hinting_factor=None): # 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) + if isinstance(filename, OrderedDict): + paths = tuple(_cached_realpath(fname) for fname in filename.values()) + else: + paths = (_cached_realpath(filename),) 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, + return _get_font(paths, hinting_factor, _kerning_factor=rcParams['text.kerning_factor'], thread_id=threading.get_ident()) @@ -1446,3 +1573,4 @@ def _load_fontmanager(*, try_read_cache=True): fontManager = _load_fontmanager() findfont = fontManager.findfont +find_fontsprop = fontManager.find_fontsprop diff --git a/lib/matplotlib/tests/baseline_images/test_agg/font_fallback.png b/lib/matplotlib/tests/baseline_images/test_agg/font_fallback.png new file mode 100644 index 000000000000..546beec003ea Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_agg/font_fallback.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type3.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type3.pdf new file mode 100644 index 000000000000..4ff499347a5e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type3.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type42.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type42.pdf new file mode 100644 index 000000000000..d5013dd41c9d Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type42.pdf differ diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 14573f5941f6..df67c397dc10 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -1,4 +1,5 @@ import io +from pathlib import Path import numpy as np from numpy.testing import assert_array_almost_equal @@ -7,7 +8,9 @@ from matplotlib import ( - collections, path, pyplot as plt, transforms as mtransforms, rcParams) + collections, path, pyplot as plt, transforms as mtransforms, rcParams, + font_manager as fm +) from matplotlib.image import imread from matplotlib.figure import Figure from matplotlib.testing.decorators import image_comparison @@ -251,3 +254,15 @@ def test_draw_path_collection_error_handling(): ax.scatter([1], [1]).set_paths(path.Path([(0, 1), (2, 3)])) with pytest.raises(TypeError): fig.canvas.draw() + + +@image_comparison(["font_fallback.png"]) +def test_font_fallback(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=15) + + fig, ax = plt.subplots() + ax.text(0.25, 0.475, "There are 多个汉字 in between!") diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 8e16eb2b7b94..98e72d8756c0 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -9,7 +9,9 @@ import pytest import matplotlib as mpl -from matplotlib import dviread, pyplot as plt, checkdep_usetex, rcParams +from matplotlib import ( + dviread, pyplot as plt, checkdep_usetex, rcParams, font_manager as fm +) from matplotlib.cbook import _get_data_path from matplotlib.ft2font import FT2Font from matplotlib.backends._backend_pdf_ps import get_glyphs_subset @@ -368,3 +370,29 @@ def test_glyphs_subset(): # since both objects are assigned same characters assert subfont.get_num_glyphs() == nosubfont.get_num_glyphs() + + +@image_comparison(["multi_font_type3.pdf"]) +def test_multi_font_type3(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27) + plt.rc('pdf', fonttype=3) + + fig = plt.figure() + fig.text(0.15, 0.475, "There are 多个汉字 in between!") + + +@image_comparison(["multi_font_type42.pdf"]) +def test_multi_font_type42(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27) + plt.rc('pdf', fonttype=42) + + fig = plt.figure() + fig.text(0.15, 0.475, "There are 多个汉字 in between!") diff --git a/src/forward.h b/src/forward.h new file mode 100644 index 000000000000..dd2f71e1d6c3 --- /dev/null +++ b/src/forward.h @@ -0,0 +1,5 @@ +#pragma once + +struct PyFT2Font; + +class FT2Font; diff --git a/src/ft2font.cpp b/src/ft2font.cpp index fdea4c39bec0..088e23d1d703 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -169,12 +169,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 FT_UInt 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))) { @@ -189,6 +185,15 @@ static FT_UInt ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode) 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) return ft_glyph_warn(charcode); + else 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 @@ -319,9 +324,14 @@ 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, + PyFT2Font *py_pointer) + : image(), face(NULL) { clear(); + py_font = py_pointer; FT_Error error = FT_Open_Face(_ft2Library, &open_args, 0, &face); @@ -353,6 +363,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 + set_fallbacks(fallback_list); } FT2Font::~FT2Font() @@ -364,6 +377,10 @@ FT2Font::~FT2Font() if (face) { FT_Done_Face(face); } + + for (size_t i = 0; i < fallbacks.size(); i++) { + Py_DECREF(fallbacks[i]->get_pyfont()); + } } void FT2Font::clear() @@ -376,6 +393,12 @@ void FT2Font::clear() } glyphs.clear(); + glyph_to_font.clear(); + char_to_font.clear(); + + for (unsigned int i = 0; i < fallbacks.size(); i++) { + fallbacks[i]->clear(); + } } void FT2Font::set_size(double ptsize, double dpi) @@ -387,8 +410,18 @@ void FT2Font::set_size(double ptsize, double dpi) } FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 }; FT_Set_Transform(face, &transform, 0); + + for (unsigned int i = 0; i < fallbacks.size(); i++) { + fallbacks[i]->set_size(ptsize, dpi); + } +} + +void FT2Font::set_fallbacks(std::vector &fallback_list) +{ + fallbacks.assign(fallback_list.begin(), fallback_list.end()); } + void FT2Font::set_charmap(int i) { if (i >= face->num_charmaps) { @@ -407,8 +440,19 @@ 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) { + // could potentially do something different? + } + // if left_ft_object is not the same the right_ft_object, + // do the exact same thing which set_text does. + return right_ft_object->get_kerning(left, right, mode, false); + } if (!FT_HAS_KERNING(face)) { return 0; } @@ -421,9 +465,25 @@ 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, FT_Vector &delta) +{ + if (!FT_HAS_KERNING(face)) { + return 0; + } + + if (!FT_Get_Kerning(face, left, right, mode, &delta)) { + return (int)(delta.x) / (hinting_factor << kerning_factor); + } else { + return 0; + } +} + void FT2Font::set_kerning_factor(int factor) { kerning_factor = factor; + for (unsigned int i = 0; i < fallbacks.size(); i ++){ + fallbacks[i]->set_kerning_factor(factor); + } } void FT2Font::set_text( @@ -452,28 +512,35 @@ void FT2Font::set_text( FT_BBox glyph_bbox; FT_Pos last_advance; - glyph_index = ft_get_char_index_or_warn(face, codepoints[n]); + FT_UInt final_glyph_index = 0; + 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, codepoints[n], flags, + charcode_error, glyph_error, false); + if (!was_found) { + ft_glyph_warn((FT_ULong)codepoints[n]); + + // render tofu + // ft_object_with_glyph == this + char_to_font[codepoints[n]] = ft_object_with_glyph; + glyph_to_font[final_glyph_index] = ft_object_with_glyph; + ft_object_with_glyph->load_glyph(final_glyph_index, flags, ft_object_with_glyph, false); + } + + glyph_index = final_glyph_index; // retrieve kerning distance and move pen position if (use_kerning && previous && glyph_index) { FT_Vector delta; - FT_Get_Kerning(face, previous, glyph_index, FT_KERNING_DEFAULT, &delta); + ft_object_with_glyph->get_kerning(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); - } - // 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); @@ -489,7 +556,6 @@ void FT2Font::set_text( pen.x += last_advance; previous = glyph_index; - glyphs.push_back(thisGlyph); } FT_Vector_Transform(&pen, &matrix); @@ -500,17 +566,174 @@ void FT2Font::set_text( } } -void FT2Font::load_char(long charcode, FT_Int32 flags) +void FT2Font::fill_glyphs( + size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, bool warn = 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_Matrix matrix; /* transformation matrix */ + + angle = angle / 360.0 * 2 * M_PI; + + // this computes width and height in subpixels so we have to divide 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_BBox glyph_bbox; + FT_Pos last_advance; + + 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, codepoints[n], flags, + charcode_error, glyph_error, false); + if (!was_found) { + if (warn) ft_glyph_warn((FT_ULong)codepoints[n]); + continue; + } + + glyph_index = final_glyph_index; + + // retrieve kerning distance and move pen position + if (use_kerning && previous && glyph_index) { + FT_Vector delta; + ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT, delta); + pen.x += delta.x / (hinting_factor << kerning_factor); + } + + // extract glyph image and store it in our table + + FT_Glyph &thisGlyph = glyphs[glyphs.size() - 1]; + + last_advance = ft_object_with_glyph->get_face()->glyph->advance.x; + FT_Glyph_Transform(thisGlyph, 0, &pen); + FT_Glyph_Transform(thisGlyph, &matrix, 0); + + FT_Glyph_Get_CBox(thisGlyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph_bbox); + + bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin); + bbox.xMax = std::max(bbox.xMax, glyph_bbox.xMax); + bbox.yMin = std::min(bbox.yMin, glyph_bbox.yMin); + bbox.yMax = std::max(bbox.yMax, glyph_bbox.yMax); + + pen.x += last_advance; + + previous = glyph_index; } - FT_Glyph thisGlyph; - if (FT_Error error = FT_Get_Glyph(face->glyph, &thisGlyph)) { - throw_ft_error("Could not get glyph", error); + + FT_Vector_Transform(&pen, &matrix); + advance = pen.x; + + if (bbox.xMin > bbox.xMax) { + bbox.xMin = bbox.yMin = bbox.xMax = bbox.yMax = 0; } - glyphs.push_back(thisGlyph); +} + +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 this 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(face, charcode); + + if (glyph_index || override) { + charcode_error = FT_Load_Glyph(face, glyph_index, flags); + FT_Glyph thisGlyph; + glyph_error = FT_Get_Glyph(face->glyph, &thisGlyph); + if (charcode_error || 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 (unsigned int 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; + } +} + +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; + } + + ft_object->load_glyph(glyph_index, flags); } void FT2Font::load_glyph(FT_UInt glyph_index, FT_Int32 flags) @@ -525,6 +748,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; @@ -615,8 +860,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 0863f3450b36..20787ec61e7f 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -1,10 +1,13 @@ /* -*- mode: c++; c-basic-offset: 4 -*- */ /* A python interface to FreeType */ +#pragma once +#include "forward.h" #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H #include #include +#include extern "C" { #include @@ -69,18 +72,34 @@ 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, PyFT2Font *py_pointer); virtual ~FT2Font(); void clear(); void set_size(double ptsize, double dpi); void set_charmap(int i); + void set_fallbacks(std::vector &fallback_list); 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); + void fill_glyphs( + size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, bool warn); + 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 load_parent_glyph(std::vector &parent_glyphs, long charcode, FT_Int32 flags); void get_width_height(long *width, long *height); void get_bitmap_offset(long *x, long *y); long get_descent(); @@ -89,14 +108,32 @@ 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 &get_face() { return face; } + + PyFT2Font *get_pyfont() + { + return py_font; + } + + std::unordered_map &get_glyph_to_font() + { + return glyph_to_font; + } + + std::unordered_map &get_char_to_font() + { + return char_to_font; + } + FT2Image &get_image() { return image; @@ -123,6 +160,10 @@ class FT2Font 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; + PyFT2Font *py_font; FT_BBox bbox; FT_Pos advance; long hinting_factor; diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 06cbc22dad9c..b08e937e3aa8 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1,3 +1,4 @@ +#include "forward.h" #include "mplutils.h" #include "ft2font.h" #include "py_converters.h" @@ -178,25 +179,25 @@ typedef struct static PyTypeObject PyGlyphType; -static PyObject * -PyGlyph_new(const FT_Face &face, const FT_Glyph &glyph, size_t ind, long hinting_factor) +static PyObject *PyGlyph_new(FT2Font *&parent_ft_object, FT2Font *&ft_object) { PyGlyph *self; self = (PyGlyph *)PyGlyphType.tp_alloc(&PyGlyphType, 0); - - self->glyphInd = ind; - - FT_Glyph_Get_CBox(glyph, ft_glyph_bbox_subpixels, &self->bbox); - - self->width = face->glyph->metrics.width / hinting_factor; - self->height = face->glyph->metrics.height; - self->horiBearingX = face->glyph->metrics.horiBearingX / hinting_factor; - self->horiBearingY = face->glyph->metrics.horiBearingY; - self->horiAdvance = face->glyph->metrics.horiAdvance; - self->linearHoriAdvance = face->glyph->linearHoriAdvance / hinting_factor; - self->vertBearingX = face->glyph->metrics.vertBearingX; - self->vertBearingY = face->glyph->metrics.vertBearingY; - self->vertAdvance = face->glyph->metrics.vertAdvance; + const long hinting_factor = ft_object->get_hinting_factor(); + + self->glyphInd = ft_object->get_last_glyph_index(); + + ft_object->get_cbox(self->bbox); + + self->width = ft_object->get_face()->glyph->metrics.width / hinting_factor; + self->height = ft_object->get_face()->glyph->metrics.height; + self->horiBearingX = ft_object->get_face()->glyph->metrics.horiBearingX / hinting_factor; + self->horiBearingY = ft_object->get_face()->glyph->metrics.horiBearingY; + self->horiAdvance = ft_object->get_face()->glyph->metrics.horiAdvance; + self->linearHoriAdvance = ft_object->get_face()->glyph->linearHoriAdvance / hinting_factor; + self->vertBearingX = ft_object->get_face()->glyph->metrics.vertBearingX; + self->vertBearingY = ft_object->get_face()->glyph->metrics.vertBearingY; + self->vertAdvance = ft_object->get_face()->glyph->metrics.vertAdvance; return (PyObject *)self; } @@ -254,7 +255,7 @@ static PyTypeObject *PyGlyph_init_type(PyObject *m, PyTypeObject *type) * FT2Font * */ -typedef struct +struct PyFT2Font { PyObject_HEAD FT2Font *x; @@ -264,7 +265,8 @@ typedef struct Py_ssize_t shape[2]; Py_ssize_t strides[2]; Py_ssize_t suboffsets[2]; -} PyFT2Font; + std::vector fallbacks; +}; static unsigned long read_from_file_callback(FT_Stream stream, unsigned long offset, @@ -361,15 +363,17 @@ 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 + }; 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; } @@ -382,6 +386,20 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; + if (fallback_list && PyList_Check(fallback_list)) { + Py_ssize_t size = PyList_Size(fallback_list); + + for (int i = 0; i < size; ++i) { + PyObject* item = PyList_GetItem(fallback_list, i); + + // TODO: check whether item is actually an FT2Font + FT2Font *fback = reinterpret_cast(item)->x; + self->fallbacks.push_back(fback); + } + + Py_INCREF(fallback_list); + } + 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"))) { @@ -403,7 +421,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, self->fallbacks, self)), Py_CLEAR(self->py_file), -1); CALL_CPP_INIT("FT2Font->set_kerning_factor", (self->x->set_kerning_factor(kerning_factor))); @@ -503,12 +521,13 @@ static PyObject *PyFT2Font_get_kerning(PyFT2Font *self, PyObject *args, PyObject { 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,6 +604,78 @@ static PyObject *PyFT2Font_set_text(PyFT2Font *self, PyObject *args, PyObject *k return convert_xys_to_array(xys); } +const char *PyFT2Font_fill_glyphs__doc__ = + "fill_glyphs(string, angle, flags=32)\n" + "--\n\n" + "Fill (and cache) a text *string* and *angle*.\n" + "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" + "the default value is LOAD_FORCE_AUTOHINT.\n" + "A mapping of character to FT2Font object is returned.\n"; + +static PyObject *PyFT2Font_fill_glyphs(PyFT2Font *self, PyObject *args, PyObject *kwds) +{ + PyObject *textobj; + double angle = 0.0; + bool warn = false; + FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; + const char *names[] = { "string", "angle", "flags", NULL }; + + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "O|di:fill_glyphs", (char **)names, &textobj, &angle, &flags)) { + return NULL; + } + + std::vector codepoints; + size_t size; + + if (PyUnicode_Check(textobj)) { + size = PyUnicode_GET_LENGTH(textobj); + codepoints.resize(size); +#if defined(PYPY_VERSION) && (PYPY_VERSION_NUM < 0x07040000) + // PyUnicode_ReadChar is available from PyPy 7.3.2, but wheels do not + // specify the micro-release version, so put the version bound at 7.4 + // to prevent generating wheels unusable on PyPy 7.3.{0,1}. + Py_UNICODE *unistr = PyUnicode_AsUnicode(textobj); + for (size_t i = 0; i < size; ++i) { + codepoints[i] = unistr[i]; + } +#else + for (size_t i = 0; i < size; ++i) { + codepoints[i] = PyUnicode_ReadChar(textobj, i); + } +#endif + } else { + PyErr_SetString(PyExc_TypeError, "String must be str"); + return NULL; + } + + uint32_t* codepoints_array = NULL; + if (size > 0) { + codepoints_array = &codepoints[0]; + } + CALL_CPP("fill_glyphs", self->x->fill_glyphs(size, codepoints_array, angle, flags, warn)); + + PyObject *char_to_font; + if (!(char_to_font = PyDict_New())) { + return NULL; + } + std::unordered_map from_ft = self->x->get_char_to_font(); + + for (std::pair &itr : from_ft) { + PyObject *key = NULL, *val = NULL; + bool error = (!(key = PyLong_FromLong(itr.first)) + || !(val = reinterpret_cast(itr.second->get_pyfont())) + || (PyDict_SetItem(char_to_font, key, val) == -1)); + Py_XDECREF(key); + Py_XDECREF(val); + if (error) { + Py_DECREF(char_to_font); + return NULL; + } + } + return char_to_font; +} + const char *PyFT2Font_get_num_glyphs__doc__ = "get_num_glyphs()\n" "--\n\n" @@ -595,8 +686,66 @@ static PyObject *PyFT2Font_get_num_glyphs(PyFT2Font *self, PyObject *args, PyObj return PyLong_FromLong(self->x->get_num_glyphs()); } +const char *PyFT2Font_get_char_to_font__doc__ = + "get_char_to_font()\n" + "--\n\n" + "Return a cache of characters mapped to FT2Font objects.\n"; + +static PyObject *PyFT2Font_get_char_to_font(PyFT2Font *self, PyObject *args, PyObject *kwds) +{ + PyObject *char_to_font; + if (!(char_to_font = PyDict_New())) { + return NULL; + } + std::unordered_map from_ft = self->x->get_char_to_font(); + + for (std::pair &itr : from_ft) { + PyObject *key = NULL, *val = NULL; + bool error = (!(key = PyLong_FromLong(itr.first)) + || !(val = reinterpret_cast(itr.second->get_pyfont())) + || (PyDict_SetItem(char_to_font, key, val) == -1)); + Py_XDECREF(key); + Py_XDECREF(val); + if (error) { + Py_DECREF(char_to_font); + return NULL; + } + } + return char_to_font; +} + +const char *PyFT2Font_get_glyph_to_font__doc__ = + "get_glyph_to_font()\n" + "--\n\n" + "Return a cache of glyph indexes mapped to FT2Font objects.\n"; + +static PyObject *PyFT2Font_get_glyph_to_font(PyFT2Font *self, PyObject *args, PyObject *kwds) +{ + PyObject *glyph_to_font; + if (!(glyph_to_font = PyDict_New())) { + return NULL; + } + std::unordered_map from_ft = self->x->get_glyph_to_font(); + + for (std::pair &itr : from_ft) { + PyObject *key = NULL, *val = NULL; + bool error = (!(key = PyLong_FromLong(itr.first)) + || !(val = reinterpret_cast(itr.second->get_pyfont())) + || (PyDict_SetItem(glyph_to_font, key, val) == -1)); + // Py_XDECREF(key); + // Py_XDECREF(val); + Py_INCREF(val); + Py_INCREF(key); + if (error) { + Py_DECREF(glyph_to_font); + return NULL; + } + } + return glyph_to_font; +} + const char *PyFT2Font_load_char__doc__ = - "load_char(charcode, flags=32)\n" + "load_char(charcode, fallback=False, flags=32)\n" "--\n\n" "Load character with *charcode* in current fontfile and set glyph.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" @@ -615,23 +764,22 @@ const char *PyFT2Font_load_char__doc__ = 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_new(self->x->get_face(), - self->x->get_last_glyph(), - self->x->get_last_glyph_index(), - self->x->get_hinting_factor()); + return PyGlyph_new(self->x, ft_object); } const char *PyFT2Font_load_glyph__doc__ = @@ -655,22 +803,21 @@ static PyObject *PyFT2Font_load_glyph(PyFT2Font *self, PyObject *args, PyObject { 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_new(self->x->get_face(), - self->x->get_last_glyph(), - self->x->get_last_glyph_index(), - self->x->get_hinting_factor()); + return PyGlyph_new(self->x, ft_object); } const char *PyFT2Font_get_width_height__doc__ = @@ -816,10 +963,13 @@ static PyObject *PyFT2Font_get_glyph_name(PyFT2Font *self, PyObject *args, PyObj { unsigned int glyph_number; char buffer[128]; + int fallback = 1; + + // parse kwds too 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); } @@ -855,7 +1005,7 @@ static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args, PyObject const char *PyFT2Font_get_char_index__doc__ = - "get_char_index(codepoint)\n" + "get_char_index(codepoint, fallback=True)\n" "--\n\n" "Return the glyph index corresponding to a character *codepoint*.\n"; @@ -863,12 +1013,13 @@ static PyObject *PyFT2Font_get_char_index(PyFT2Font *self, PyObject *args, PyObj { 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); } @@ -1485,7 +1636,10 @@ static PyTypeObject *PyFT2Font_init_type(PyObject *m, PyTypeObject *type) {"select_charmap", (PyCFunction)PyFT2Font_select_charmap, METH_VARARGS, PyFT2Font_select_charmap__doc__}, {"get_kerning", (PyCFunction)PyFT2Font_get_kerning, METH_VARARGS, PyFT2Font_get_kerning__doc__}, {"set_text", (PyCFunction)PyFT2Font_set_text, METH_VARARGS|METH_KEYWORDS, PyFT2Font_set_text__doc__}, + {"fill_glyphs", (PyCFunction)PyFT2Font_fill_glyphs, METH_VARARGS|METH_KEYWORDS, PyFT2Font_fill_glyphs__doc__}, {"get_num_glyphs", (PyCFunction)PyFT2Font_get_num_glyphs, METH_NOARGS, PyFT2Font_get_num_glyphs__doc__}, + {"get_char_to_font", (PyCFunction)PyFT2Font_get_char_to_font, METH_NOARGS, PyFT2Font_get_char_to_font__doc__}, + {"get_glyph_to_font", (PyCFunction)PyFT2Font_get_glyph_to_font, METH_NOARGS, PyFT2Font_get_glyph_to_font__doc__}, {"load_char", (PyCFunction)PyFT2Font_load_char, METH_VARARGS|METH_KEYWORDS, PyFT2Font_load_char__doc__}, {"load_glyph", (PyCFunction)PyFT2Font_load_glyph, METH_VARARGS|METH_KEYWORDS, PyFT2Font_load_glyph__doc__}, {"get_width_height", (PyCFunction)PyFT2Font_get_width_height, METH_NOARGS, PyFT2Font_get_width_height__doc__},