diff --git a/doc/api/next_api_changes/deprecations/28842-ES.rst b/doc/api/next_api_changes/deprecations/28842-ES.rst new file mode 100644 index 000000000000..d88d3f7b9538 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28842-ES.rst @@ -0,0 +1,55 @@ +ft2font module-level constants replaced by enums +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `.ft2font`-level constants have been converted to `enum` classes, and all API using +them now take/return the new types. + +The following constants are now part of `.ft2font.Kerning` (without the ``KERNING_`` +prefix): + +- ``KERNING_DEFAULT`` +- ``KERNING_UNFITTED`` +- ``KERNING_UNSCALED`` + +The following constants are now part of `.ft2font.LoadFlags` (without the ``LOAD_`` +prefix): + +- ``LOAD_DEFAULT`` +- ``LOAD_NO_SCALE`` +- ``LOAD_NO_HINTING`` +- ``LOAD_RENDER`` +- ``LOAD_NO_BITMAP`` +- ``LOAD_VERTICAL_LAYOUT`` +- ``LOAD_FORCE_AUTOHINT`` +- ``LOAD_CROP_BITMAP`` +- ``LOAD_PEDANTIC`` +- ``LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH`` +- ``LOAD_NO_RECURSE`` +- ``LOAD_IGNORE_TRANSFORM`` +- ``LOAD_MONOCHROME`` +- ``LOAD_LINEAR_DESIGN`` +- ``LOAD_NO_AUTOHINT`` +- ``LOAD_TARGET_NORMAL`` +- ``LOAD_TARGET_LIGHT`` +- ``LOAD_TARGET_MONO`` +- ``LOAD_TARGET_LCD`` +- ``LOAD_TARGET_LCD_V`` + +The following constants are now part of `.ft2font.FaceFlags`: + +- ``EXTERNAL_STREAM`` +- ``FAST_GLYPHS`` +- ``FIXED_SIZES`` +- ``FIXED_WIDTH`` +- ``GLYPH_NAMES`` +- ``HORIZONTAL`` +- ``KERNING`` +- ``MULTIPLE_MASTERS`` +- ``SCALABLE`` +- ``SFNT`` +- ``VERTICAL`` + +The following constants are now part of `.ft2font.StyleFlags`: + +- ``ITALIC`` +- ``BOLD`` diff --git a/galleries/examples/misc/font_indexing.py b/galleries/examples/misc/font_indexing.py index 02d77e647bf3..31388737bcae 100644 --- a/galleries/examples/misc/font_indexing.py +++ b/galleries/examples/misc/font_indexing.py @@ -9,8 +9,7 @@ import os import matplotlib -from matplotlib.ft2font import (KERNING_DEFAULT, KERNING_UNFITTED, - KERNING_UNSCALED, FT2Font) +from matplotlib.ft2font import FT2Font, Kerning font = FT2Font( os.path.join(matplotlib.get_data_path(), 'fonts/ttf/DejaVuSans.ttf')) @@ -31,7 +30,7 @@ glyph = font.load_char(code) print(glyph.bbox) print(glyphd['A'], glyphd['V'], coded['A'], coded['V']) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_DEFAULT)) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNFITTED)) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNSCALED)) -print('AT', font.get_kerning(glyphd['A'], glyphd['T'], KERNING_UNSCALED)) +print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.DEFAULT)) +print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.UNFITTED)) +print('AV', font.get_kerning(glyphd['A'], glyphd['V'], Kerning.UNSCALED)) +print('AT', font.get_kerning(glyphd['A'], glyphd['T'], Kerning.UNSCALED)) diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py index 0cdf363fb6ba..8306e142c144 100644 --- a/galleries/examples/misc/ftface_props.py +++ b/galleries/examples/misc/ftface_props.py @@ -46,18 +46,10 @@ # vertical thickness of the underline print('Underline thickness:', font.underline_thickness) -for style in ('Italic', - 'Bold', - 'Scalable', - 'Fixed sizes', - 'Fixed width', - 'SFNT', - 'Horizontal', - 'Vertical', - 'Kerning', - 'Fast glyphs', - 'Multiple masters', - 'Glyph names', - 'External stream'): - bitpos = getattr(ft, style.replace(' ', '_').upper()) - 1 - print(f"{style+':':17}", bool(font.style_flags & (1 << bitpos))) +for flag in ft.StyleFlags: + name = flag.name.replace('_', ' ').title() + ':' + print(f"{name:17}", flag in font.style_flags) + +for flag in ft.FaceFlags: + name = flag.name.replace('_', ' ').title() + ':' + print(f"{name:17}", flag in font.face_flags) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index e1901cdd9b03..c9bb90608a85 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -29,7 +29,7 @@ from ._mathtext_data import ( latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) from .font_manager import FontProperties, findfont, get_font -from .ft2font import FT2Font, FT2Image, KERNING_DEFAULT +from .ft2font import FT2Font, FT2Image, Kerning, LoadFlags from packaging.version import parse as parse_version from pyparsing import __version__ as pyparsing_version @@ -227,14 +227,14 @@ class Fonts(abc.ABC): to do the actual drawing. """ - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): """ Parameters ---------- default_font_prop : `~.font_manager.FontProperties` The default non-math font, or the base font for Unicode (generic) font rendering. - load_glyph_flags : int + load_glyph_flags : `.ft2font.LoadFlags` Flags passed to the glyph loader (e.g. ``FT_Load_Glyph`` and ``FT_Load_Char`` for FreeType-based fonts). """ @@ -332,7 +332,7 @@ class TruetypeFonts(Fonts, metaclass=abc.ABCMeta): (through FT2Font). """ - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): super().__init__(default_font_prop, load_glyph_flags) # Per-instance cache. self._get_info = functools.cache(self._get_info) # type: ignore[method-assign] @@ -426,7 +426,7 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64 + return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64 return super().get_kern(font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi) @@ -448,7 +448,7 @@ class BakomaFonts(TruetypeFonts): 'ex': 'cmex10', } - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): self._stix_fallback = StixFonts(default_font_prop, load_glyph_flags) super().__init__(default_font_prop, load_glyph_flags) @@ -557,7 +557,7 @@ class UnicodeFonts(TruetypeFonts): 0x2212: 0x00A1, # Minus sign. } - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): # This must come first so the backend's owner is set correctly fallback_rc = mpl.rcParams['mathtext.fallback'] font_cls: type[TruetypeFonts] | None = { @@ -672,7 +672,7 @@ def get_sized_alternatives_for_symbol(self, fontname: str, class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta): _fontmap: dict[str | int, str] = {} - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): # This must come first so the backend's owner is set correctly if isinstance(self, DejaVuSerifFonts): self._fallback_font = StixFonts(default_font_prop, load_glyph_flags) @@ -776,7 +776,7 @@ class StixFonts(UnicodeFonts): _fallback_font = None _sans = False - def __init__(self, default_font_prop: FontProperties, load_glyph_flags: int): + def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags) for key, name in self._fontmap.items(): fullpath = findfont(name) diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index dc0540ea14e4..b9603b114bc2 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -7,7 +7,7 @@ import dataclasses from . import _api -from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING, FT2Font +from .ft2font import FT2Font, Kerning, LoadFlags @dataclasses.dataclass(frozen=True) @@ -43,7 +43,7 @@ def warn_on_missing_glyph(codepoint, fontnames): f"Matplotlib currently does not support {block} natively.") -def layout(string, font, *, kern_mode=KERNING_DEFAULT): +def layout(string, font, *, kern_mode=Kerning.DEFAULT): """ Render *string* with *font*. @@ -56,7 +56,7 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT): The string to be rendered. font : FT2Font The font. - kern_mode : int + kern_mode : Kerning A FreeType kerning mode. Yields @@ -76,7 +76,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) + glyph = font.load_glyph(glyph_idx, flags=LoadFlags.NO_HINTING) yield LayoutItem(font, 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 afea1db56831..a2a878d54156 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -161,7 +161,7 @@ def get_text_width_height_descent(self, s, prop, ismath): return w, h, d else: font = self._get_font_ttf(prop) - font.set_text(s, 0.0, flags=ft2font.LOAD_NO_HINTING) + font.set_text(s, 0.0, flags=ft2font.LoadFlags.NO_HINTING) w, h = font.get_width_height() d = font.get_descent() scale = 1 / 64 diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index ae361f0cceb4..c37427369267 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -31,8 +31,7 @@ from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) 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.ft2font import LoadFlags from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Bbox, BboxBase @@ -41,16 +40,16 @@ def get_hinting_flag(): mapping = { - 'default': LOAD_DEFAULT, - 'no_autohint': LOAD_NO_AUTOHINT, - 'force_autohint': LOAD_FORCE_AUTOHINT, - 'no_hinting': LOAD_NO_HINTING, - True: LOAD_FORCE_AUTOHINT, - False: LOAD_NO_HINTING, - 'either': LOAD_DEFAULT, - 'native': LOAD_NO_AUTOHINT, - 'auto': LOAD_FORCE_AUTOHINT, - 'none': LOAD_NO_HINTING, + 'default': LoadFlags.DEFAULT, + 'no_autohint': LoadFlags.NO_AUTOHINT, + 'force_autohint': LoadFlags.FORCE_AUTOHINT, + 'no_hinting': LoadFlags.NO_HINTING, + True: LoadFlags.FORCE_AUTOHINT, + False: LoadFlags.NO_HINTING, + 'either': LoadFlags.DEFAULT, + 'native': LoadFlags.NO_AUTOHINT, + 'auto': LoadFlags.FORCE_AUTOHINT, + 'none': LoadFlags.NO_HINTING, } return mapping[mpl.rcParams['text.hinting']] diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 44b85cd3136d..5e76f8e0c9c9 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -35,8 +35,7 @@ from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager from matplotlib._afm import AFM -from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, - LOAD_NO_HINTING, KERNING_UNFITTED, FT2Font) +from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path from matplotlib.dates import UTC @@ -617,7 +616,7 @@ def _get_pdf_charprocs(font_path, glyph_ids): conv = 1000 / font.units_per_EM # Conversion to PS units (1/1000's). procs = {} for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LOAD_NO_SCALE) + g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) # NOTE: We should be using round(), but instead use # "(x+.5).astype(int)" to keep backcompat with the old ttconv code # (this is different for negative x's). @@ -1185,7 +1184,7 @@ def embedTTFType3(font, characters, descriptor): def get_char_width(charcode): s = ord(cp1252.decoding_table[charcode]) width = font.load_char( - s, flags=LOAD_NO_SCALE | LOAD_NO_HINTING).horiAdvance + s, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance return cvt(width) with warnings.catch_warnings(): # Ignore 'Required glyph missing from current font' warning @@ -1322,7 +1321,7 @@ def embedTTFType42(font, characters, descriptor): ccode = c gind = font.get_char_index(ccode) glyph = font.load_char(ccode, - flags=LOAD_NO_SCALE | LOAD_NO_HINTING) + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) widths.append((ccode, cvt(glyph.horiAdvance))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) @@ -1418,7 +1417,7 @@ def embedTTFType42(font, characters, descriptor): flags = 0 symbolic = False # ps_name.name in ('Cmsy10', 'Cmmi10', 'Cmex10') - if ff & FIXED_WIDTH: + if FaceFlags.FIXED_WIDTH in ff: flags |= 1 << 0 if 0: # TODO: serif flags |= 1 << 1 @@ -1426,7 +1425,7 @@ def embedTTFType42(font, characters, descriptor): flags |= 1 << 2 else: flags |= 1 << 5 - if sf & ITALIC: + if StyleFlags.ITALIC in sf: flags |= 1 << 6 if 0: # TODO: all caps flags |= 1 << 16 @@ -2379,7 +2378,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): multibyte_glyphs = [] prev_was_multibyte = True prev_font = font - for item in _text_helpers.layout(s, font, kern_mode=KERNING_UNFITTED): + for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED): if _font_supports_glyph(fonttype, ord(item.char)): if prev_was_multibyte or item.ft_object != prev_font: singlebyte_chunks.append((item.ft_object, item.x, [])) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 893c660c0b24..e98da538fc10 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -29,7 +29,7 @@ _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) from matplotlib.cbook import is_writable_file_like, file_requires_unicode from matplotlib.font_manager import get_font -from matplotlib.ft2font import LOAD_NO_SCALE +from matplotlib.ft2font import LoadFlags from matplotlib._mathtext_data import uni2type1 from matplotlib.path import Path from matplotlib.texmanager import TexManager @@ -148,7 +148,7 @@ def _font_to_ps_type3(font_path, chars): entries = [] for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LOAD_NO_SCALE) + g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) v, c = font.get_path() entries.append( "/%(name)s{%(bbox)s sc\n" % { diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 98731af3463f..9aa8dccde444 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -379,7 +379,7 @@ def ttfFontProperty(font): style = 'italic' elif sfnt2.find('regular') >= 0: style = 'normal' - elif font.style_flags & ft2font.ITALIC: + elif ft2font.StyleFlags.ITALIC in font.style_flags: style = 'italic' else: style = 'normal' @@ -428,7 +428,7 @@ def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal. for regex, weight in _weight_regexes: if re.search(regex, style, re.I): return weight - if font.style_flags & ft2font.BOLD: + if ft2font.StyleFlags.BOLD in font.style_flags: return 700 # "bold" return 500 # "medium", not "regular"! diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index b2eb8cea1cc8..88c65d139939 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,3 +1,4 @@ +from enum import Enum, Flag import sys from typing import BinaryIO, Literal, TypedDict, final, overload from typing_extensions import Buffer # < Py 3.12 @@ -7,42 +8,64 @@ from numpy.typing import NDArray __freetype_build_type__: str __freetype_version__: str -BOLD: int -EXTERNAL_STREAM: int -FAST_GLYPHS: int -FIXED_SIZES: int -FIXED_WIDTH: int -GLYPH_NAMES: int -HORIZONTAL: int -ITALIC: int -KERNING: int -KERNING_DEFAULT: int -KERNING_UNFITTED: int -KERNING_UNSCALED: int -LOAD_CROP_BITMAP: int -LOAD_DEFAULT: int -LOAD_FORCE_AUTOHINT: int -LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH: int -LOAD_IGNORE_TRANSFORM: int -LOAD_LINEAR_DESIGN: int -LOAD_MONOCHROME: int -LOAD_NO_AUTOHINT: int -LOAD_NO_BITMAP: int -LOAD_NO_HINTING: int -LOAD_NO_RECURSE: int -LOAD_NO_SCALE: int -LOAD_PEDANTIC: int -LOAD_RENDER: int -LOAD_TARGET_LCD: int -LOAD_TARGET_LCD_V: int -LOAD_TARGET_LIGHT: int -LOAD_TARGET_MONO: int -LOAD_TARGET_NORMAL: int -LOAD_VERTICAL_LAYOUT: int -MULTIPLE_MASTERS: int -SCALABLE: int -SFNT: int -VERTICAL: int + +class FaceFlags(Flag): + SCALABLE: int + FIXED_SIZES: int + FIXED_WIDTH: int + SFNT: int + HORIZONTAL: int + VERTICAL: int + KERNING: int + FAST_GLYPHS: int + MULTIPLE_MASTERS: int + GLYPH_NAMES: int + EXTERNAL_STREAM: int + HINTER: int + CID_KEYED: int + TRICKY: int + COLOR: int + # VARIATION: int # FT 2.9 + # SVG: int # FT 2.12 + # SBIX: int # FT 2.12 + # SBIX_OVERLAY: int # FT 2.12 + +class Kerning(Enum): + DEFAULT: int + UNFITTED: int + UNSCALED: int + +class LoadFlags(Flag): + DEFAULT: int + NO_SCALE: int + NO_HINTING: int + RENDER: int + NO_BITMAP: int + VERTICAL_LAYOUT: int + FORCE_AUTOHINT: int + CROP_BITMAP: int + PEDANTIC: int + IGNORE_GLOBAL_ADVANCE_WIDTH: int + NO_RECURSE: int + IGNORE_TRANSFORM: int + MONOCHROME: int + LINEAR_DESIGN: int + NO_AUTOHINT: int + COLOR: int + COMPUTE_METRICS: int # FT 2.6.1 + # BITMAP_METRICS_ONLY: int # FT 2.7.1 + # NO_SVG: int # FT 2.13.1 + # The following should be unique, but the above can be OR'd together. + TARGET_NORMAL: int + TARGET_LIGHT: int + TARGET_MONO: int + TARGET_LCD: int + TARGET_LCD_V: int + +class StyleFlags(Flag): + NORMAL: int + ITALIC: int + BOLD: int class _SfntHeadDict(TypedDict): version: tuple[int, int] @@ -184,7 +207,7 @@ class FT2Font(Buffer): def get_descent(self) -> int: ... def get_glyph_name(self, index: int) -> str: ... def get_image(self) -> NDArray[np.uint8]: ... - def get_kerning(self, left: int, right: int, mode: int) -> int: ... + def get_kerning(self, left: int, right: int, mode: Kerning) -> int: ... def get_name_index(self, name: str) -> int: ... def get_num_glyphs(self) -> int: ... def get_path(self) -> tuple[NDArray[np.float64], NDArray[np.int8]]: ... @@ -207,13 +230,13 @@ class FT2Font(Buffer): @overload def get_sfnt_table(self, name: Literal["pclt"]) -> _SfntPcltDict | None: ... def get_width_height(self) -> tuple[int, int]: ... - def load_char(self, charcode: int, flags: int = ...) -> Glyph: ... - def load_glyph(self, glyphindex: int, flags: int = ...) -> Glyph: ... + def load_char(self, charcode: int, flags: LoadFlags = ...) -> Glyph: ... + def load_glyph(self, glyphindex: int, flags: LoadFlags = ...) -> Glyph: ... def select_charmap(self, i: int) -> None: ... def set_charmap(self, i: int) -> None: ... def set_size(self, ptsize: float, dpi: float) -> None: ... def set_text( - self, string: str, angle: float = ..., flags: int = ... + self, string: str, angle: float = ..., flags: LoadFlags = ... ) -> NDArray[np.float64]: ... @property def ascender(self) -> int: ... @@ -222,7 +245,7 @@ class FT2Font(Buffer): @property def descender(self) -> int: ... @property - def face_flags(self) -> int: ... + def face_flags(self) -> FaceFlags: ... @property def family_name(self) -> str: ... @property @@ -246,7 +269,7 @@ class FT2Font(Buffer): @property def scalable(self) -> bool: ... @property - def style_flags(self) -> int: ... + def style_flags(self) -> StyleFlags: ... @property def style_name(self) -> str: ... @property diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index cee29c0d2feb..a88c35c15676 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -20,7 +20,7 @@ import matplotlib as mpl from matplotlib import _api, _mathtext -from matplotlib.ft2font import LOAD_NO_HINTING +from matplotlib.ft2font import LoadFlags from matplotlib.font_manager import FontProperties from ._mathtext import ( # noqa: F401, reexported API RasterParse, VectorParse, get_unicode_index) @@ -80,7 +80,7 @@ def parse(self, s, dpi=72, prop=None, *, antialiased=None): antialiased = mpl._val_or_rc(antialiased, 'text.antialiased') from matplotlib.backends import backend_agg load_glyph_flags = { - "vector": LOAD_NO_HINTING, + "vector": LoadFlags.NO_HINTING, "raster": backend_agg.get_hinting_flag(), }[self._output_type] return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 25b6a122c7ce..d15b892b3eea 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -251,7 +251,7 @@ def test_missing_family(caplog): def _test_threading(): import threading - from matplotlib.ft2font import LOAD_NO_HINTING + from matplotlib.ft2font import LoadFlags import matplotlib.font_manager as fm def loud_excepthook(args): @@ -266,7 +266,7 @@ def bad_idea(n): b.wait(timeout=5) for j in range(100): font = fm.get_font(fm.findfont("DejaVu Sans")) - font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING) + font.set_text(str(n), 0.0, flags=LoadFlags.NO_HINTING) threads = [ threading.Thread(target=bad_idea, name=f"bad_thread_{j}", args=(j,)) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index ace4cea5865e..7dc851b2c9cf 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -5,6 +5,7 @@ import numpy as np import pytest +import matplotlib as mpl from matplotlib import ft2font from matplotlib.testing.decorators import check_figures_equal import matplotlib.font_manager as fm @@ -43,10 +44,11 @@ def test_ft2font_dejavu_attrs(): assert font.num_fixed_sizes == 0 # All glyphs are scalable. assert font.num_charmaps == 5 # Other internal flags are set, so only check the ones we're allowed to test. - expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | - ft2font.KERNING | ft2font.GLYPH_NAMES) - assert (font.face_flags & expected_flags) == expected_flags - assert font.style_flags == 0 # Not italic or bold. + expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT | + ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.KERNING | + ft2font.FaceFlags.GLYPH_NAMES) + assert expected_flags in font.face_flags + assert font.style_flags == ft2font.StyleFlags.NORMAL assert font.scalable # From FontForge: Font Information → General tab → entry name below. assert font.units_per_EM == 2048 # Em Size. @@ -75,10 +77,10 @@ def test_ft2font_cm_attrs(): assert font.num_fixed_sizes == 0 # All glyphs are scalable. assert font.num_charmaps == 2 # Other internal flags are set, so only check the ones we're allowed to test. - expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | - ft2font.GLYPH_NAMES) - assert (font.face_flags & expected_flags) == expected_flags, font.face_flags - assert font.style_flags == 0 # Not italic or bold. + expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT | + ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.GLYPH_NAMES) + assert expected_flags in font.face_flags + assert font.style_flags == ft2font.StyleFlags.NORMAL assert font.scalable # From FontForge: Font Information → General tab → entry name below. assert font.units_per_EM == 2048 # Em Size. @@ -107,10 +109,10 @@ def test_ft2font_stix_bold_attrs(): assert font.num_fixed_sizes == 0 # All glyphs are scalable. assert font.num_charmaps == 3 # Other internal flags are set, so only check the ones we're allowed to test. - expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | - ft2font.GLYPH_NAMES) - assert (font.face_flags & expected_flags) == expected_flags, font.face_flags - assert font.style_flags == ft2font.BOLD + expected_flags = (ft2font.FaceFlags.SCALABLE | ft2font.FaceFlags.SFNT | + ft2font.FaceFlags.HORIZONTAL | ft2font.FaceFlags.GLYPH_NAMES) + assert expected_flags in font.face_flags + assert font.style_flags == ft2font.StyleFlags.BOLD assert font.scalable # From FontForge: Font Information → General tab → entry name below. assert font.units_per_EM == 1000 # Em Size. @@ -714,13 +716,37 @@ def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): font.set_size(100, 100) assert font.get_kerning(font.get_char_index(ord(left)), font.get_char_index(ord(right)), - ft2font.KERNING_UNSCALED) == unscaled + ft2font.Kerning.UNSCALED) == unscaled assert font.get_kerning(font.get_char_index(ord(left)), font.get_char_index(ord(right)), - ft2font.KERNING_UNFITTED) == unfitted + ft2font.Kerning.UNFITTED) == unfitted assert font.get_kerning(font.get_char_index(ord(left)), font.get_char_index(ord(right)), - ft2font.KERNING_DEFAULT) == default + ft2font.Kerning.DEFAULT) == default + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning.UNSCALED instead'): + k = ft2font.KERNING_UNSCALED + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning enum values instead'): + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + int(k)) == unscaled + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning.UNFITTED instead'): + k = ft2font.KERNING_UNFITTED + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning enum values instead'): + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + int(k)) == unfitted + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning.DEFAULT instead'): + k = ft2font.KERNING_DEFAULT + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='Use Kerning enum values instead'): + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + int(k)) == default def test_ft2font_set_text(): diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index c00966d6e6c3..83182e3f5400 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -8,7 +8,7 @@ from matplotlib.font_manager import ( FontProperties, get_font, fontManager as _fontManager ) -from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_TARGET_LIGHT +from matplotlib.ft2font import LoadFlags from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.texmanager import TexManager @@ -37,7 +37,7 @@ def _get_font(self, prop): return font def _get_hinting_flag(self): - return LOAD_NO_HINTING + return LoadFlags.NO_HINTING def _get_char_id(self, font, ccode): """ @@ -61,7 +61,7 @@ def get_text_width_height_descent(self, s, prop, ismath): return width * scale, height * scale, descent * scale font = self._get_font(prop) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) + font.set_text(s, 0.0, flags=LoadFlags.NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 @@ -190,7 +190,7 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) - font.load_char(ccode, flags=LOAD_NO_HINTING) + font.load_char(ccode, flags=LoadFlags.NO_HINTING) glyph_map_new[char_id] = font.get_path() xpositions.append(ox) @@ -241,11 +241,11 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, glyph_name_or_index = text.glyph_name_or_index if isinstance(glyph_name_or_index, str): index = font.get_name_index(glyph_name_or_index) - font.load_glyph(index, flags=LOAD_TARGET_LIGHT) + font.load_glyph(index, flags=LoadFlags.TARGET_LIGHT) elif isinstance(glyph_name_or_index, int): self._select_native_charmap(font) font.load_char( - glyph_name_or_index, flags=LOAD_TARGET_LIGHT) + glyph_name_or_index, flags=LoadFlags.TARGET_LIGHT) else: # Should not occur. raise TypeError(f"Glyph spec of unexpected type: " f"{glyph_name_or_index!r}") diff --git a/src/_enums.h b/src/_enums.h new file mode 100644 index 000000000000..18f3d9aac9fa --- /dev/null +++ b/src/_enums.h @@ -0,0 +1,95 @@ +#ifndef MPL_ENUMS_H +#define MPL_ENUMS_H + +#include + +// Extension for pybind11: Pythonic enums. +// This allows creating classes based on ``enum.*`` types. +// This code was copied from mplcairo, with some slight tweaks. +// The API is: +// +// - P11X_DECLARE_ENUM(py_name: str, py_base_cls: str, ...: {str, enum value}): +// py_name: The name to expose in the module. +// py_base_cls: The name of the enum base class to use. +// ...: The enum name/value pairs to expose. +// +// Use this macro to declare an enum and its values. +// +// - py11x::bind_enums(m: pybind11::module): +// m: The module to use to register the enum classes. +// +// Place this in PYBIND11_MODULE to register the enums declared by P11X_DECLARE_ENUM. + +// a1 includes the opening brace and a2 the closing brace. +// This definition is compatible with older compiler versions compared to +// #define P11X_ENUM_TYPE(...) decltype(std::map{std::pair __VA_ARGS__})::mapped_type +#define P11X_ENUM_TYPE(a1, a2, ...) decltype(std::pair a1, a2)::second_type + +#define P11X_CAT2(a, b) a##b +#define P11X_CAT(a, b) P11X_CAT2(a, b) + +namespace p11x { + namespace { + namespace py = pybind11; + + // Holder is (py_base_cls, [(name, value), ...]) before module init; + // converted to the Python class object after init. + auto enums = std::unordered_map{}; + + auto bind_enums(py::module mod) -> void + { + for (auto& [py_name, spec]: enums) { + auto const& [py_base_cls, pairs] = + spec.cast>(); + mod.attr(py::cast(py_name)) = spec = + py::module::import("enum").attr(py_base_cls.c_str())( + py_name, pairs, py::arg("module") = mod.attr("__name__")); + } + } + } +} + +// Immediately converting the args to a vector outside of the lambda avoids +// name collisions. +#define P11X_DECLARE_ENUM(py_name, py_base_cls, ...) \ + namespace p11x { \ + namespace { \ + [[maybe_unused]] auto const P11X_CAT(enum_placeholder_, __COUNTER__) = \ + [](auto args) { \ + py::gil_scoped_acquire gil; \ + using int_t = std::underlying_type_t; \ + auto pairs = std::vector>{}; \ + for (auto& [k, v]: args) { \ + pairs.emplace_back(k, int_t(v)); \ + } \ + p11x::enums[py_name] = pybind11::cast(std::pair{py_base_cls, pairs}); \ + return 0; \ + } (std::vector{std::pair __VA_ARGS__}); \ + } \ + } \ + namespace pybind11::detail { \ + template<> struct type_caster { \ + using type = P11X_ENUM_TYPE(__VA_ARGS__); \ + static_assert(std::is_enum_v, "Not an enum"); \ + PYBIND11_TYPE_CASTER(type, _(py_name)); \ + bool load(handle src, bool) { \ + auto cls = p11x::enums.at(py_name); \ + PyObject* tmp = nullptr; \ + if (pybind11::isinstance(src, cls) \ + && (tmp = PyNumber_Index(src.attr("value").ptr()))) { \ + auto ival = PyLong_AsLong(tmp); \ + value = decltype(value)(ival); \ + Py_DECREF(tmp); \ + return !(ival == -1 && !PyErr_Occurred()); \ + } else { \ + return false; \ + } \ + } \ + static handle cast(decltype(value) obj, return_value_policy, handle) { \ + auto cls = p11x::enums.at(py_name); \ + return cls(std::underlying_type_t(obj)).inc_ref(); \ + } \ + }; \ + } + +#endif /* MPL_ENUMS_H */ diff --git a/src/ft2font.cpp b/src/ft2font.cpp index dc6bc419d43e..c0e8b7c27125 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -354,7 +354,8 @@ void FT2Font::select_charmap(unsigned long i) } } -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback = false) +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, + bool fallback = false) { if (fallback && glyph_to_font.find(left) != glyph_to_font.end() && glyph_to_font.find(right) != glyph_to_font.end()) { @@ -375,7 +376,8 @@ int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallbac } } -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta) +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, + FT_Vector &delta) { if (!FT_HAS_KERNING(face)) { return 0; diff --git a/src/ft2font.h b/src/ft2font.h index 79b0e1ccc518..5524930d5ad0 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -79,8 +79,8 @@ class FT2Font void select_charmap(unsigned long i); void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, std::vector &xys); - 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); + int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, bool fallback); + int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, FT_Vector &delta); void set_kerning_factor(int factor); void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback); bool load_char_with_fallback(FT2Font *&ft_object_with_glyph, diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 8d332d2af4d4..59400d4a2de5 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -4,6 +4,7 @@ #include #include "ft2font.h" +#include "_enums.h" #include #include @@ -12,12 +13,218 @@ namespace py = pybind11; using namespace pybind11::literals; +/********************************************************************** + * Enumerations + * */ + +const char *Kerning__doc__ = R"""( + Kerning modes for `.FT2Font.get_kerning`. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +P11X_DECLARE_ENUM( + "Kerning", "Enum", + {"DEFAULT", FT_KERNING_DEFAULT}, + {"UNFITTED", FT_KERNING_UNFITTED}, + {"UNSCALED", FT_KERNING_UNSCALED}, +); + +const char *FaceFlags__doc__ = R"""( + Flags returned by `FT2Font.face_flags`. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +enum class FaceFlags : FT_Long { +#define DECLARE_FLAG(name) name = FT_FACE_FLAG_##name + DECLARE_FLAG(SCALABLE), + DECLARE_FLAG(FIXED_SIZES), + DECLARE_FLAG(FIXED_WIDTH), + DECLARE_FLAG(SFNT), + DECLARE_FLAG(HORIZONTAL), + DECLARE_FLAG(VERTICAL), + DECLARE_FLAG(KERNING), + DECLARE_FLAG(FAST_GLYPHS), + DECLARE_FLAG(MULTIPLE_MASTERS), + DECLARE_FLAG(GLYPH_NAMES), + DECLARE_FLAG(EXTERNAL_STREAM), + DECLARE_FLAG(HINTER), + DECLARE_FLAG(CID_KEYED), + DECLARE_FLAG(TRICKY), + DECLARE_FLAG(COLOR), +#ifdef FT_FACE_FLAG_VARIATION // backcompat: ft 2.9.0. + DECLARE_FLAG(VARIATION), +#endif +#ifdef FT_FACE_FLAG_SVG // backcompat: ft 2.12.0. + DECLARE_FLAG(SVG), +#endif +#ifdef FT_FACE_FLAG_SBIX // backcompat: ft 2.12.0. + DECLARE_FLAG(SBIX), +#endif +#ifdef FT_FACE_FLAG_SBIX_OVERLAY // backcompat: ft 2.12.0. + DECLARE_FLAG(SBIX_OVERLAY), +#endif +#undef DECLARE_FLAG +}; + +P11X_DECLARE_ENUM( + "FaceFlags", "Flag", + {"SCALABLE", FaceFlags::SCALABLE}, + {"FIXED_SIZES", FaceFlags::FIXED_SIZES}, + {"FIXED_WIDTH", FaceFlags::FIXED_WIDTH}, + {"SFNT", FaceFlags::SFNT}, + {"HORIZONTAL", FaceFlags::HORIZONTAL}, + {"VERTICAL", FaceFlags::VERTICAL}, + {"KERNING", FaceFlags::KERNING}, + {"FAST_GLYPHS", FaceFlags::FAST_GLYPHS}, + {"MULTIPLE_MASTERS", FaceFlags::MULTIPLE_MASTERS}, + {"GLYPH_NAMES", FaceFlags::GLYPH_NAMES}, + {"EXTERNAL_STREAM", FaceFlags::EXTERNAL_STREAM}, + {"HINTER", FaceFlags::HINTER}, + {"CID_KEYED", FaceFlags::CID_KEYED}, + {"TRICKY", FaceFlags::TRICKY}, + {"COLOR", FaceFlags::COLOR}, + // backcompat: ft 2.9.0. + // {"VARIATION", FaceFlags::VARIATION}, + // backcompat: ft 2.12.0. + // {"SVG", FaceFlags::SVG}, + // backcompat: ft 2.12.0. + // {"SBIX", FaceFlags::SBIX}, + // backcompat: ft 2.12.0. + // {"SBIX_OVERLAY", FaceFlags::SBIX_OVERLAY}, +); + +const char *LoadFlags__doc__ = R"""( + Flags for `FT2Font.load_char`, `FT2Font.load_glyph`, and `FT2Font.set_text`. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +enum class LoadFlags : FT_Int32 { +#define DECLARE_FLAG(name) name = FT_LOAD_##name + DECLARE_FLAG(DEFAULT), + DECLARE_FLAG(NO_SCALE), + DECLARE_FLAG(NO_HINTING), + DECLARE_FLAG(RENDER), + DECLARE_FLAG(NO_BITMAP), + DECLARE_FLAG(VERTICAL_LAYOUT), + DECLARE_FLAG(FORCE_AUTOHINT), + DECLARE_FLAG(CROP_BITMAP), + DECLARE_FLAG(PEDANTIC), + DECLARE_FLAG(IGNORE_GLOBAL_ADVANCE_WIDTH), + DECLARE_FLAG(NO_RECURSE), + DECLARE_FLAG(IGNORE_TRANSFORM), + DECLARE_FLAG(MONOCHROME), + DECLARE_FLAG(LINEAR_DESIGN), + DECLARE_FLAG(NO_AUTOHINT), + DECLARE_FLAG(COLOR), +#ifdef FT_LOAD_COMPUTE_METRICS // backcompat: ft 2.6.1. + DECLARE_FLAG(COMPUTE_METRICS), +#endif +#ifdef FT_LOAD_BITMAP_METRICS_ONLY // backcompat: ft 2.7.1. + DECLARE_FLAG(LOAD_BITMAP_METRICS_ONLY), +#endif +#ifdef FT_LOAD_NO_SVG // backcompat: ft 2.13.1. + DECLARE_FLAG(LOAD_NO_SVG), +#endif + DECLARE_FLAG(TARGET_NORMAL), + DECLARE_FLAG(TARGET_LIGHT), + DECLARE_FLAG(TARGET_MONO), + DECLARE_FLAG(TARGET_LCD), + DECLARE_FLAG(TARGET_LCD_V), +#undef DECLARE_FLAG +}; + +P11X_DECLARE_ENUM( + "LoadFlags", "Flag", + {"DEFAULT", LoadFlags::DEFAULT}, + {"NO_SCALE", LoadFlags::NO_SCALE}, + {"NO_HINTING", LoadFlags::NO_HINTING}, + {"RENDER", LoadFlags::RENDER}, + {"NO_BITMAP", LoadFlags::NO_BITMAP}, + {"VERTICAL_LAYOUT", LoadFlags::VERTICAL_LAYOUT}, + {"FORCE_AUTOHINT", LoadFlags::FORCE_AUTOHINT}, + {"CROP_BITMAP", LoadFlags::CROP_BITMAP}, + {"PEDANTIC", LoadFlags::PEDANTIC}, + {"IGNORE_GLOBAL_ADVANCE_WIDTH", LoadFlags::IGNORE_GLOBAL_ADVANCE_WIDTH}, + {"NO_RECURSE", LoadFlags::NO_RECURSE}, + {"IGNORE_TRANSFORM", LoadFlags::IGNORE_TRANSFORM}, + {"MONOCHROME", LoadFlags::MONOCHROME}, + {"LINEAR_DESIGN", LoadFlags::LINEAR_DESIGN}, + {"NO_AUTOHINT", LoadFlags::NO_AUTOHINT}, + {"COLOR", LoadFlags::COLOR}, + // backcompat: ft 2.6.1. + {"COMPUTE_METRICS", LoadFlags::COMPUTE_METRICS}, + // backcompat: ft 2.7.1. + // {"BITMAP_METRICS_ONLY", LoadFlags::BITMAP_METRICS_ONLY}, + // backcompat: ft 2.13.1. + // {"NO_SVG", LoadFlags::NO_SVG}, + // These must be unique, but the others can be OR'd together; I don't know if + // there's any way to really enforce that. + {"TARGET_NORMAL", LoadFlags::TARGET_NORMAL}, + {"TARGET_LIGHT", LoadFlags::TARGET_LIGHT}, + {"TARGET_MONO", LoadFlags::TARGET_MONO}, + {"TARGET_LCD", LoadFlags::TARGET_LCD}, + {"TARGET_LCD_V", LoadFlags::TARGET_LCD_V}, +); + +const char *StyleFlags__doc__ = R"""( + Flags returned by `FT2Font.style_flags`. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +enum class StyleFlags : FT_Long { +#define DECLARE_FLAG(name) name = FT_STYLE_FLAG_##name + NORMAL = 0, + DECLARE_FLAG(ITALIC), + DECLARE_FLAG(BOLD), +#undef DECLARE_FLAG +}; + +P11X_DECLARE_ENUM( + "StyleFlags", "Flag", + {"NORMAL", StyleFlags::NORMAL}, + {"ITALIC", StyleFlags::ITALIC}, + {"BOLD", StyleFlags::BOLD}, +); + /********************************************************************** * FT2Image * */ -const char *PyFT2Image_draw_rect_filled__doc__ = - "Draw a filled rectangle to the image."; +const char *PyFT2Image__doc__ = R"""( + An image buffer for drawing glyphs. +)"""; + +const char *PyFT2Image_init__doc__ = R"""( + Parameters + ---------- + width, height : int + The dimensions of the image buffer. +)"""; + +const char *PyFT2Image_draw_rect_filled__doc__ = R"""( + Draw a filled rectangle to the image. + + Parameters + ---------- + x0, y0, x1, y1 : float + The bounds of the rectangle from (x0, y0) to (x1, y1). +)"""; static void PyFT2Image_draw_rect_filled(FT2Image *self, double x0, double y0, double x1, double y1) @@ -44,6 +251,17 @@ typedef struct FT_BBox bbox; } PyGlyph; +const char *PyGlyph__doc__ = R"""( + Information about a single glyph. + + You cannot create instances of this object yourself, but must use + `.FT2Font.load_char` or `.FT2Font.load_glyph` to generate one. This object may be + used in a call to `.FT2Font.draw_glyph_to_bitmap`. + + For more information on the various metrics, see `the FreeType documentation + `_. +)"""; + static PyGlyph * PyGlyph_from_FT2Font(const FT2Font *font) { @@ -93,6 +311,26 @@ struct PyFT2Font } }; +const char *PyFT2Font__doc__ = R"""( + An object representing a single font face. + + Outside of the font itself and querying its properties, this object provides methods + for processing text strings into glyph shapes. + + Commonly, one will use `FT2Font.set_text` to load some glyph metrics and outlines. + Then `FT2Font.draw_glyphs_to_bitmap` and `FT2Font.get_image` may be used to get a + rendered form of the loaded string. + + For single characters, `FT2Font.load_char` or `FT2Font.load_glyph` may be used, + either directly for their return values, or to use `FT2Font.draw_glyph_to_bitmap` or + `FT2Font.get_path`. + + Useful metrics may be examined via the `Glyph` return values or + `FT2Font.get_kerning`. Most dimensions are given in 26.6 or 16.6 fixed-point + integers representing subpixels. Divide these values by 64 to produce floating-point + pixels. +)"""; + static unsigned long read_from_file_callback(FT_Stream stream, unsigned long offset, unsigned char *buffer, unsigned long count) @@ -146,28 +384,27 @@ ft_glyph_warn(FT_ULong charcode, std::set family_names) warn_on_missing_glyph(charcode, ss.str()); } -const char *PyFT2Font_init__doc__ = - "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" -; +const char *PyFT2Font_init__doc__ = R"""( + Parameters + ---------- + filename : str or file-like + The source of the font data in a format (ttf or ttc) that FreeType can read. + + hinting_factor : int, optional + Must be positive. Used to scale the hinting in the x-direction. + + _fallback_list : list of FT2Font, optional + A list of FT2Font objects used to find missing glyphs. + + .. warning:: + This API is both private and provisional: do not use it directly. + + _kerning_factor : int, optional + Used to adjust the degree of kerning. + + .. warning:: + This API is private: do not use it directly. +)"""; static PyFT2Font * PyFT2Font_init(py::object filename, long hinting_factor = 8, @@ -237,8 +474,16 @@ PyFT2Font_clear(PyFT2Font *self) self->x->clear(); } -const char *PyFT2Font_set_size__doc__ = - "Set the point size and dpi of the text."; +const char *PyFT2Font_set_size__doc__ = R"""( + Set the size of the text. + + Parameters + ---------- + ptsize : float + The size of the text in points. + dpi : float + The DPI used for rendering the text. +)"""; static void PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi) @@ -246,8 +491,23 @@ PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi) self->x->set_size(ptsize, dpi); } -const char *PyFT2Font_set_charmap__doc__ = - "Make the i-th charmap current."; +const char *PyFT2Font_set_charmap__doc__ = R"""( + Make the i-th charmap current. + + For more details on character mapping, see the `FreeType documentation + `_. + + Parameters + ---------- + i : int + The charmap number in the range [0, `.num_charmaps`). + + See Also + -------- + .num_charmaps + .select_charmap + .get_charmap +)"""; static void PyFT2Font_set_charmap(PyFT2Font *self, int i) @@ -255,8 +515,23 @@ PyFT2Font_set_charmap(PyFT2Font *self, int i) self->x->set_charmap(i); } -const char *PyFT2Font_select_charmap__doc__ = - "Select a charmap by its FT_Encoding number."; +const char *PyFT2Font_select_charmap__doc__ = R"""( + Select a charmap by its FT_Encoding number. + + For more details on character mapping, see the `FreeType documentation + `_. + + Parameters + ---------- + i : int + The charmap in the form defined by FreeType: + https://freetype.org/freetype2/docs/reference/ft2-character_mapping.html#ft_encoding + + See Also + -------- + .set_charmap + .get_charmap +)"""; static void PyFT2Font_select_charmap(PyFT2Font *self, unsigned long i) @@ -264,38 +539,85 @@ PyFT2Font_select_charmap(PyFT2Font *self, unsigned long i) self->x->select_charmap(i); } -const char *PyFT2Font_get_kerning__doc__ = - "Get the kerning between *left* and *right* glyph indices.\n" - "\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"; +const char *PyFT2Font_get_kerning__doc__ = R"""( + Get the kerning between two glyphs. + + Parameters + ---------- + left, right : int + The glyph indices. Note these are not characters nor character codes. + Use `.get_char_index` to convert character codes to glyph indices. + + mode : Kerning + A kerning mode constant: + + - ``DEFAULT`` - Return scaled and grid-fitted kerning distances. + - ``UNFITTED`` - Return scaled but un-grid-fitted kerning distances. + - ``UNSCALED`` - Return the kerning vector in original font units. + + .. versionchanged:: 3.10 + This now takes a `.ft2font.Kerning` value instead of an `int`. + + Returns + ------- + int + The kerning adjustment between the two glyphs. +)"""; static int -PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, FT_UInt mode) +PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, + std::variant mode_or_int) { bool fallback = true; + FT_Kerning_Mode mode; + + if (auto value = std::get_if(&mode_or_int)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="mode", "obj_type"_a="parameter as int", + "alternative"_a="Kerning enum values"); + mode = static_cast(*value); + } else if (auto value = std::get_if(&mode_or_int)) { + mode = *value; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("mode must be Kerning or int"); + } return self->x->get_kerning(left, right, mode, fallback); } -const char *PyFT2Font_get_fontmap__doc__ = - "Get a mapping between characters and the font that includes them.\n" - "A dictionary mapping unicode characters to PyFT2Font objects."; +const char *PyFT2Font_get_fontmap__doc__ = R"""( + Get a mapping between characters and the font that includes them. + + .. warning:: + This API uses the fallback list and is both private and provisional: do not use + it directly. + + Parameters + ---------- + text : str + The characters for which to find fonts. + + Returns + ------- + dict[str, FT2Font] + A dictionary mapping unicode characters to `.FT2Font` objects. +)"""; static py::dict PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) { std::set codepoints; + py::dict char_to_font; for (auto code : text) { - codepoints.insert(code); - } + if (!codepoints.insert(code).second) { + continue; + } - py::dict char_to_font; - for (auto code : codepoints) { py::object target_font; int index; if (self->x->get_char_fallback_index(code, index)) { @@ -315,20 +637,52 @@ PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) return char_to_font; } -const char *PyFT2Font_set_text__doc__ = - "Set the 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" - "You must call this before `.draw_glyphs_to_bitmap`.\n" - "A sequence of x,y positions in 26.6 subpixels is returned; divide by 64 for pixels.\n"; +const char *PyFT2Font_set_text__doc__ = R"""( + Set the text *string* and *angle*. + + You must call this before `.draw_glyphs_to_bitmap`. + + Parameters + ---------- + string : str + The text to prepare rendering information for. + angle : float + The angle at which to render the supplied text. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. + + .. versionchanged:: 3.10 + This now takes an `.ft2font.LoadFlags` instead of an int. + + Returns + ------- + np.ndarray[double] + A sequence of x,y glyph positions in 26.6 subpixels; divide by 64 for pixels. +)"""; static py::array_t PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0, - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) { std::vector xys; + LoadFlags flags; + + if (auto value = std::get_if(&flags_or_int)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="flags", "obj_type"_a="parameter as int", + "alternative"_a="LoadFlags enum values"); + flags = static_cast(*value); + } else if (auto value = std::get_if(&flags_or_int)) { + flags = *value; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("flags must be LoadFlags or int"); + } - self->x->set_text(text, angle, flags, xys); + self->x->set_text(text, angle, static_cast(flags), xys); py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; py::array_t result(dims); @@ -338,8 +692,7 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0 return result; } -const char *PyFT2Font_get_num_glyphs__doc__ = - "Return the number of loaded glyphs."; +const char *PyFT2Font_get_num_glyphs__doc__ = "Return the number of loaded glyphs."; static size_t PyFT2Font_get_num_glyphs(PyFT2Font *self) @@ -347,64 +700,130 @@ PyFT2Font_get_num_glyphs(PyFT2Font *self) return self->x->get_num_glyphs(); } -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\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"; +const char *PyFT2Font_load_char__doc__ = R"""( + Load character in current fontfile and set glyph. + + Parameters + ---------- + charcode : int + The character code to prepare rendering information for. This code must be in + the charmap, or else a ``.notdef`` glyph may be returned instead. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. + + .. versionchanged:: 3.10 + This now takes an `.ft2font.LoadFlags` instead of an int. + + Returns + ------- + Glyph + The glyph information corresponding to the specified character. + + See Also + -------- + .load_glyph + .select_charmap + .set_charmap +)"""; static PyGlyph * PyFT2Font_load_char(PyFT2Font *self, long charcode, - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) { bool fallback = true; FT2Font *ft_object = NULL; + LoadFlags flags; + + if (auto value = std::get_if(&flags_or_int)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="flags", "obj_type"_a="parameter as int", + "alternative"_a="LoadFlags enum values"); + flags = static_cast(*value); + } else if (auto value = std::get_if(&flags_or_int)) { + flags = *value; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("flags must be LoadFlags or int"); + } - self->x->load_char(charcode, flags, ft_object, fallback); + self->x->load_char(charcode, static_cast(flags), ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } -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\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"; +const char *PyFT2Font_load_glyph__doc__ = R"""( + Load glyph index in current fontfile and set glyph. + + Note that the glyph index is specific to a font, and not universal like a Unicode + code point. + + Parameters + ---------- + glyph_index : int + The glyph index to prepare rendering information for. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. + + .. versionchanged:: 3.10 + This now takes an `.ft2font.LoadFlags` instead of an int. + + Returns + ------- + Glyph + The glyph information corresponding to the specified index. + + See Also + -------- + .load_char +)"""; static PyGlyph * PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) { bool fallback = true; FT2Font *ft_object = NULL; + LoadFlags flags; + + if (auto value = std::get_if(&flags_or_int)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="flags", "obj_type"_a="parameter as int", + "alternative"_a="LoadFlags enum values"); + flags = static_cast(*value); + } else if (auto value = std::get_if(&flags_or_int)) { + flags = *value; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("flags must be LoadFlags or int"); + } - self->x->load_glyph(glyph_index, flags, ft_object, fallback); + self->x->load_glyph(glyph_index, static_cast(flags), ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } -const char *PyFT2Font_get_width_height__doc__ = - "Get the width and height in 26.6 subpixels of the current string set by `.set_text`.\n" - "The rotation of the string is accounted for. To get width and height\n" - "in pixels, divide these values by 64.\n"; +const char *PyFT2Font_get_width_height__doc__ = R"""( + Get the dimensions of the current string set by `.set_text`. + + The rotation of the string is accounted for. + + Returns + ------- + width, height : float + The width and height in 26.6 subpixels of the current string. To get width and + height in pixels, divide these values by 64. + + See Also + -------- + .get_bitmap_offset + .get_descent +)"""; static py::tuple PyFT2Font_get_width_height(PyFT2Font *self) @@ -416,9 +835,22 @@ PyFT2Font_get_width_height(PyFT2Font *self) return py::make_tuple(width, height); } -const char *PyFT2Font_get_bitmap_offset__doc__ = - "Get the (x, y) offset in 26.6 subpixels for the bitmap if ink hangs left or below (0, 0).\n" - "Since Matplotlib only supports left-to-right text, y is always 0.\n"; +const char *PyFT2Font_get_bitmap_offset__doc__ = R"""( + Get the (x, y) offset for the bitmap if ink hangs left or below (0, 0). + + Since Matplotlib only supports left-to-right text, y is always 0. + + Returns + ------- + x, y : float + The x and y offset in 26.6 subpixels of the bitmap. To get x and y in pixels, + divide these values by 64. + + See Also + -------- + .get_width_height + .get_descent +)"""; static py::tuple PyFT2Font_get_bitmap_offset(PyFT2Font *self) @@ -430,10 +862,22 @@ PyFT2Font_get_bitmap_offset(PyFT2Font *self) return py::make_tuple(x, y); } -const char *PyFT2Font_get_descent__doc__ = - "Get the descent in 26.6 subpixels of the current string set by `.set_text`.\n" - "The rotation of the string is accounted for. To get the descent\n" - "in pixels, divide this value by 64.\n"; +const char *PyFT2Font_get_descent__doc__ = R"""( + Get the descent of the current string set by `.set_text`. + + The rotation of the string is accounted for. + + Returns + ------- + int + The descent in 26.6 subpixels of the bitmap. To get the descent in pixels, + divide these values by 64. + + See Also + -------- + .get_bitmap_offset + .get_width_height +)"""; static long PyFT2Font_get_descent(PyFT2Font *self) @@ -441,10 +885,20 @@ PyFT2Font_get_descent(PyFT2Font *self) return self->x->get_descent(); } -const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = - "Draw the glyphs that were loaded by `.set_text` to the bitmap.\n" - "\n" - "The bitmap size will be automatically set to include the glyphs.\n"; +const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = R"""( + Draw the glyphs that were loaded by `.set_text` to the bitmap. + + The bitmap size will be automatically set to include the glyphs. + + Parameters + ---------- + antialiased : bool, default: True + Whether to render glyphs 8-bit antialiased or in pure black-and-white. + + See Also + -------- + .draw_glyph_to_bitmap +)"""; static void PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, bool antialiased = true) @@ -452,16 +906,31 @@ PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, bool antialiased = true) self->x->draw_glyphs_to_bitmap(antialiased); } -const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = - "Draw a single glyph to the bitmap at pixel locations x, y.\n" - "\n" - "Note it is your responsibility to create the image manually\n" - "with the correct size before this call is made.\n" - "\n" - "If you want automatic layout, use `.set_text` in combinations with\n" - "`.draw_glyphs_to_bitmap`. This function is instead intended for people\n" - "who want to render individual glyphs (e.g., returned by `.load_char`)\n" - "at precise locations.\n"; +const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( + Draw a single glyph to the bitmap at pixel locations x, y. + + Note it is your responsibility to create the image manually with the correct size + before this call is made. + + If you want automatic layout, use `.set_text` in combinations with + `.draw_glyphs_to_bitmap`. This function is instead intended for people who want to + render individual glyphs (e.g., returned by `.load_char`) at precise locations. + + Parameters + ---------- + image : FT2Image + The image buffer on which to draw the glyph. + x, y : float + The pixel location at which to draw the glyph. + glyph : Glyph + The glyph to draw. + antialiased : bool, default: True + Whether to render glyphs 8-bit antialiased or in pure black-and-white. + + See Also + -------- + .draw_glyphs_to_bitmap +)"""; static void PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image, double xd, double yd, @@ -470,12 +939,28 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image, double xd, doub self->x->draw_glyph_to_bitmap(image, xd, yd, glyph->glyphInd, antialiased); } -const char *PyFT2Font_get_glyph_name__doc__ = - "Retrieve the ASCII name of a given glyph *index* in a face.\n" - "\n" - "Due to Matplotlib's internal design, for fonts that do not contain glyph\n" - "names (per FT_FACE_FLAG_GLYPH_NAMES), this returns a made-up name which\n" - "does *not* roundtrip through `.get_name_index`.\n"; +const char *PyFT2Font_get_glyph_name__doc__ = R"""( + Retrieve the ASCII name of a given glyph *index* in a face. + + Due to Matplotlib's internal design, for fonts that do not contain glyph names (per + ``FT_FACE_FLAG_GLYPH_NAMES``), this returns a made-up name which does *not* + roundtrip through `.get_name_index`. + + Parameters + ---------- + index : int + The glyph number to query. + + Returns + ------- + str + The name of the glyph, or if the font does not contain names, a name synthesized + by Matplotlib. + + See Also + -------- + .get_name_index +)"""; static py::str PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) @@ -488,9 +973,18 @@ PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) return buffer; } -const char *PyFT2Font_get_charmap__doc__ = - "Return a dict that maps the character codes of the selected charmap\n" - "(Unicode by default) to their corresponding glyph indices.\n"; +const char *PyFT2Font_get_charmap__doc__ = R"""( + Return a mapping of character codes to glyph indices in the font. + + The charmap is Unicode by default, but may be changed by `.set_charmap` or + `.select_charmap`. + + Returns + ------- + dict[int, int] + A dictionary of the selected charmap mapping character codes to their + corresponding glyph indices. +)"""; static py::dict PyFT2Font_get_charmap(PyFT2Font *self) @@ -505,9 +999,26 @@ PyFT2Font_get_charmap(PyFT2Font *self) return charmap; } +const char *PyFT2Font_get_char_index__doc__ = R"""( + Return the glyph index corresponding to a character code point. + + Parameters + ---------- + codepoint : int + A character code point in the current charmap (which defaults to Unicode.) + + Returns + ------- + int + The corresponding glyph index. -const char *PyFT2Font_get_char_index__doc__ = - "Return the glyph index corresponding to a character *codepoint*."; + See Also + -------- + .set_charmap + .select_charmap + .get_glyph_name + .get_name_index +)"""; static FT_UInt PyFT2Font_get_char_index(PyFT2Font *self, FT_ULong ccode) @@ -517,11 +1028,18 @@ PyFT2Font_get_char_index(PyFT2Font *self, FT_ULong ccode) return self->x->get_char_index(ccode, fallback); } +const char *PyFT2Font_get_sfnt__doc__ = R"""( + Load the entire SFNT names table. -const char *PyFT2Font_get_sfnt__doc__ = - "Load the entire SFNT names table, as a dict whose keys are\n" - "(platform-ID, ISO-encoding-scheme, language-code, and description)\n" - "tuples.\n"; + Returns + ------- + dict[tuple[int, int, int, int], bytes] + The SFNT names table; the dictionary keys are tuples of: + + (platform-ID, ISO-encoding-scheme, language-code, description) + + and the values are the direct information from the font table. +)"""; static py::dict PyFT2Font_get_sfnt(PyFT2Font *self) @@ -552,9 +1070,24 @@ PyFT2Font_get_sfnt(PyFT2Font *self) return names; } -const char *PyFT2Font_get_name_index__doc__ = - "Return the glyph index of a given glyph *name*.\n" - "The glyph index 0 means 'undefined character code'.\n"; +const char *PyFT2Font_get_name_index__doc__ = R"""( + Return the glyph index of a given glyph *name*. + + Parameters + ---------- + name : str + The name of the glyph to query. + + Returns + ------- + int + The corresponding glyph index; 0 means 'undefined character code'. + + See Also + -------- + .get_char_index + .get_glyph_name +)"""; static long PyFT2Font_get_name_index(PyFT2Font *self, char *glyphname) @@ -562,8 +1095,24 @@ PyFT2Font_get_name_index(PyFT2Font *self, char *glyphname) return self->x->get_name_index(glyphname); } -const char *PyFT2Font_get_ps_font_info__doc__ = - "Return the information in the PS Font Info structure."; +const char *PyFT2Font_get_ps_font_info__doc__ = R"""( + Return the information in the PS Font Info structure. + + For more information, see the `FreeType documentation on this structure + `_. + + Returns + ------- + version : str + notice : str + full_name : str + family_name : str + weight : str + italic_angle : int + is_fixed_pitch : bool + underline_position : int + underline_thickness : int +)"""; static py::tuple PyFT2Font_get_ps_font_info(PyFT2Font *self) @@ -587,9 +1136,20 @@ PyFT2Font_get_ps_font_info(PyFT2Font *self) fontinfo.underline_thickness); } -const char *PyFT2Font_get_sfnt_table__doc__ = - "Return one of the following SFNT tables: head, maxp, OS/2, hhea, " - "vhea, post, or pclt."; +const char *PyFT2Font_get_sfnt_table__doc__ = R"""( + Return one of the SFNT tables. + + Parameters + ---------- + name : {"head", "maxp", "OS/2", "hhea", "vhea", "post", "pclt"} + Which table to return. + + Returns + ------- + dict[str, Any] + The corresponding table; for more information, see `the FreeType documentation + `_. +)"""; static std::optional PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) @@ -770,8 +1330,23 @@ PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) } } -const char *PyFT2Font_get_path__doc__ = - "Get the path data from the currently loaded glyph as a tuple of vertices, codes."; +const char *PyFT2Font_get_path__doc__ = R"""( + Get the path data from the currently loaded glyph. + + Returns + ------- + vertices : np.ndarray[double] + The (N, 2) array of vertices describing the current glyph. + codes : np.ndarray[np.uint8] + The (N, ) array of codes corresponding to the vertices. + + See Also + -------- + .get_image + .load_char + .load_glyph + .set_text +)"""; static py::tuple PyFT2Font_get_path(PyFT2Font *self) @@ -796,8 +1371,17 @@ PyFT2Font_get_path(PyFT2Font *self) return py::make_tuple(vertices_arr, codes_arr); } -const char *PyFT2Font_get_image__doc__ = - "Return the underlying image buffer for this font object."; +const char *PyFT2Font_get_image__doc__ = R"""( + Return the underlying image buffer for this font object. + + Returns + ------- + np.ndarray[int] + + See Also + -------- + .get_path +)"""; static py::array PyFT2Font_get_image(PyFT2Font *self) @@ -847,16 +1431,16 @@ PyFT2Font_style_name(PyFT2Font *self) return name; } -static FT_Long +static FaceFlags PyFT2Font_face_flags(PyFT2Font *self) { - return self->x->get_face()->face_flags; + return static_cast(self->x->get_face()->face_flags); } -static FT_Long +static StyleFlags PyFT2Font_style_flags(PyFT2Font *self) { - return self->x->get_face()->style_flags; + return static_cast(self->x->get_face()->style_flags); } static FT_Long @@ -952,6 +1536,77 @@ PyFT2Font_fname(PyFT2Font *self) } } +static py::object +ft2font__getattr__(std::string name) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + +#define DEPRECATE_ATTR_FROM_ENUM(attr_, alternative_, real_value_) \ + do { \ + if (name == #attr_) { \ + warn("since"_a="3.10", "name"_a=#attr_, "obj_type"_a="attribute", \ + "alternative"_a=#alternative_); \ + return py::cast(static_cast(real_value_)); \ + } \ + } while(0) + DEPRECATE_ATTR_FROM_ENUM(KERNING_DEFAULT, Kerning.DEFAULT, FT_KERNING_DEFAULT); + DEPRECATE_ATTR_FROM_ENUM(KERNING_UNFITTED, Kerning.UNFITTED, FT_KERNING_UNFITTED); + DEPRECATE_ATTR_FROM_ENUM(KERNING_UNSCALED, Kerning.UNSCALED, FT_KERNING_UNSCALED); + +#undef DEPRECATE_ATTR_FROM_ENUM + +#define DEPRECATE_ATTR_FROM_FLAG(attr_, enum_, value_) \ + do { \ + if (name == #attr_) { \ + warn("since"_a="3.10", "name"_a=#attr_, "obj_type"_a="attribute", \ + "alternative"_a=#enum_ "." #value_); \ + return py::cast(enum_::value_); \ + } \ + } while(0) + + DEPRECATE_ATTR_FROM_FLAG(LOAD_DEFAULT, LoadFlags, DEFAULT); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_SCALE, LoadFlags, NO_SCALE); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_HINTING, LoadFlags, NO_HINTING); + DEPRECATE_ATTR_FROM_FLAG(LOAD_RENDER, LoadFlags, RENDER); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_BITMAP, LoadFlags, NO_BITMAP); + DEPRECATE_ATTR_FROM_FLAG(LOAD_VERTICAL_LAYOUT, LoadFlags, VERTICAL_LAYOUT); + DEPRECATE_ATTR_FROM_FLAG(LOAD_FORCE_AUTOHINT, LoadFlags, FORCE_AUTOHINT); + DEPRECATE_ATTR_FROM_FLAG(LOAD_CROP_BITMAP, LoadFlags, CROP_BITMAP); + DEPRECATE_ATTR_FROM_FLAG(LOAD_PEDANTIC, LoadFlags, PEDANTIC); + DEPRECATE_ATTR_FROM_FLAG(LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH, LoadFlags, + IGNORE_GLOBAL_ADVANCE_WIDTH); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_RECURSE, LoadFlags, NO_RECURSE); + DEPRECATE_ATTR_FROM_FLAG(LOAD_IGNORE_TRANSFORM, LoadFlags, IGNORE_TRANSFORM); + DEPRECATE_ATTR_FROM_FLAG(LOAD_MONOCHROME, LoadFlags, MONOCHROME); + DEPRECATE_ATTR_FROM_FLAG(LOAD_LINEAR_DESIGN, LoadFlags, LINEAR_DESIGN); + DEPRECATE_ATTR_FROM_FLAG(LOAD_NO_AUTOHINT, LoadFlags, NO_AUTOHINT); + + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_NORMAL, LoadFlags, TARGET_NORMAL); + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_LIGHT, LoadFlags, TARGET_LIGHT); + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_MONO, LoadFlags, TARGET_MONO); + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_LCD, LoadFlags, TARGET_LCD); + DEPRECATE_ATTR_FROM_FLAG(LOAD_TARGET_LCD_V, LoadFlags, TARGET_LCD_V); + + DEPRECATE_ATTR_FROM_FLAG(SCALABLE, FaceFlags, SCALABLE); + DEPRECATE_ATTR_FROM_FLAG(FIXED_SIZES, FaceFlags, FIXED_SIZES); + DEPRECATE_ATTR_FROM_FLAG(FIXED_WIDTH, FaceFlags, FIXED_WIDTH); + DEPRECATE_ATTR_FROM_FLAG(SFNT, FaceFlags, SFNT); + DEPRECATE_ATTR_FROM_FLAG(HORIZONTAL, FaceFlags, HORIZONTAL); + DEPRECATE_ATTR_FROM_FLAG(VERTICAL, FaceFlags, VERTICAL); + DEPRECATE_ATTR_FROM_FLAG(KERNING, FaceFlags, KERNING); + DEPRECATE_ATTR_FROM_FLAG(FAST_GLYPHS, FaceFlags, FAST_GLYPHS); + DEPRECATE_ATTR_FROM_FLAG(MULTIPLE_MASTERS, FaceFlags, MULTIPLE_MASTERS); + DEPRECATE_ATTR_FROM_FLAG(GLYPH_NAMES, FaceFlags, GLYPH_NAMES); + DEPRECATE_ATTR_FROM_FLAG(EXTERNAL_STREAM, FaceFlags, EXTERNAL_STREAM); + + DEPRECATE_ATTR_FROM_FLAG(ITALIC, StyleFlags, ITALIC); + DEPRECATE_ATTR_FROM_FLAG(BOLD, StyleFlags, BOLD); +#undef DEPRECATE_ATTR_FROM_FLAG + + throw py::attribute_error( + "module 'matplotlib.ft2font' has no attribute {!r}"_s.format(name)); +} + PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) { if (FT_Init_FreeType(&_ft2Library)) { // initialize library @@ -962,8 +1617,15 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) FT_Library_Version(_ft2Library, &major, &minor, &patch); snprintf(version_string, sizeof(version_string), "%d.%d.%d", major, minor, patch); - py::class_(m, "FT2Image", py::is_final(), py::buffer_protocol()) - .def(py::init(), "width"_a, "height"_a) + p11x::bind_enums(m); + p11x::enums["Kerning"].attr("__doc__") = Kerning__doc__; + p11x::enums["LoadFlags"].attr("__doc__") = LoadFlags__doc__; + p11x::enums["FaceFlags"].attr("__doc__") = FaceFlags__doc__; + p11x::enums["StyleFlags"].attr("__doc__") = StyleFlags__doc__; + + py::class_(m, "FT2Image", py::is_final(), py::buffer_protocol(), + PyFT2Image__doc__) + .def(py::init(), "width"_a, "height"_a, PyFT2Image_init__doc__) .def("draw_rect_filled", &PyFT2Image_draw_rect_filled, "x0"_a, "y0"_a, "x1"_a, "y1"_a, PyFT2Image_draw_rect_filled__doc__) @@ -973,23 +1635,32 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) return py::buffer_info(self.get_buffer(), shape, strides); }); - py::class_(m, "Glyph", py::is_final()) + py::class_(m, "Glyph", py::is_final(), PyGlyph__doc__) .def(py::init<>([]() -> PyGlyph { // Glyph is not useful from Python, so mark it as not constructible. throw std::runtime_error("Glyph is not constructible"); })) - .def_readonly("width", &PyGlyph::width) - .def_readonly("height", &PyGlyph::height) - .def_readonly("horiBearingX", &PyGlyph::horiBearingX) - .def_readonly("horiBearingY", &PyGlyph::horiBearingY) - .def_readonly("horiAdvance", &PyGlyph::horiAdvance) - .def_readonly("linearHoriAdvance", &PyGlyph::linearHoriAdvance) - .def_readonly("vertBearingX", &PyGlyph::vertBearingX) - .def_readonly("vertBearingY", &PyGlyph::vertBearingY) - .def_readonly("vertAdvance", &PyGlyph::vertAdvance) - .def_property_readonly("bbox", &PyGlyph_get_bbox); - - py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol()) + .def_readonly("width", &PyGlyph::width, "The glyph's width.") + .def_readonly("height", &PyGlyph::height, "The glyph's height.") + .def_readonly("horiBearingX", &PyGlyph::horiBearingX, + "Left side bearing for horizontal layout.") + .def_readonly("horiBearingY", &PyGlyph::horiBearingY, + "Top side bearing for horizontal layout.") + .def_readonly("horiAdvance", &PyGlyph::horiAdvance, + "Advance width for horizontal layout.") + .def_readonly("linearHoriAdvance", &PyGlyph::linearHoriAdvance, + "The advance width of the unhinted glyph.") + .def_readonly("vertBearingX", &PyGlyph::vertBearingX, + "Left side bearing for vertical layout.") + .def_readonly("vertBearingY", &PyGlyph::vertBearingY, + "Top side bearing for vertical layout.") + .def_readonly("vertAdvance", &PyGlyph::vertAdvance, + "Advance height for vertical layout.") + .def_property_readonly("bbox", &PyGlyph_get_bbox, + "The control box of the glyph."); + + py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), + PyFT2Font__doc__) .def(py::init(&PyFT2Font_init), "filename"_a, "hinting_factor"_a=8, py::kw_only(), "_fallback_list"_a=py::none(), "_kerning_factor"_a=0, @@ -1004,16 +1675,16 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, PyFT2Font_get_kerning__doc__) .def("set_text", &PyFT2Font_set_text, - "string"_a, "angle"_a=0.0, "flags"_a=FT_LOAD_FORCE_AUTOHINT, + "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, PyFT2Font_set_text__doc__) .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, PyFT2Font_get_fontmap__doc__) .def("get_num_glyphs", &PyFT2Font_get_num_glyphs, PyFT2Font_get_num_glyphs__doc__) .def("load_char", &PyFT2Font_load_char, - "charcode"_a, "flags"_a=FT_LOAD_FORCE_AUTOHINT, + "charcode"_a, "flags"_a=LoadFlags::FORCE_AUTOHINT, PyFT2Font_load_char__doc__) .def("load_glyph", &PyFT2Font_load_glyph, - "glyph_index"_a, "flags"_a=FT_LOAD_FORCE_AUTOHINT, + "glyph_index"_a, "flags"_a=LoadFlags::FORCE_AUTOHINT, PyFT2Font_load_glyph__doc__) .def("get_width_height", &PyFT2Font_get_width_height, PyFT2Font_get_width_height__doc__) @@ -1050,14 +1721,15 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def_property_readonly("style_name", &PyFT2Font_style_name, "Style name.") .def_property_readonly("face_flags", &PyFT2Font_face_flags, - "Face flags; see the ft2font constants.") + "Face flags; see `.FaceFlags`.") .def_property_readonly("style_flags", &PyFT2Font_style_flags, - "Style flags; see the ft2font constants.") + "Style flags; see `.StyleFlags`.") .def_property_readonly("num_glyphs", &PyFT2Font_num_glyphs, "Number of glyphs in the face.") .def_property_readonly("num_fixed_sizes", &PyFT2Font_num_fixed_sizes, "Number of bitmap in the face.") - .def_property_readonly("num_charmaps", &PyFT2Font_num_charmaps) + .def_property_readonly("num_charmaps", &PyFT2Font_num_charmaps, + "Number of charmaps in the face.") .def_property_readonly("scalable", &PyFT2Font_scalable, "Whether face is scalable; attributes after this one " "are only defined for scalable faces.") @@ -1080,7 +1752,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) "Vertical position of the underline bar.") .def_property_readonly("underline_thickness", &PyFT2Font_underline_thickness, "Thickness of the underline bar.") - .def_property_readonly("fname", &PyFT2Font_fname) + .def_property_readonly("fname", &PyFT2Font_fname, + "The original filename for this object.") .def_buffer([](PyFT2Font &self) -> py::buffer_info { FT2Image &im = self.x->get_image(); @@ -1091,40 +1764,5 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) m.attr("__freetype_version__") = version_string; m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE; - m.attr("SCALABLE") = FT_FACE_FLAG_SCALABLE; - m.attr("FIXED_SIZES") = FT_FACE_FLAG_FIXED_SIZES; - m.attr("FIXED_WIDTH") = FT_FACE_FLAG_FIXED_WIDTH; - m.attr("SFNT") = FT_FACE_FLAG_SFNT; - m.attr("HORIZONTAL") = FT_FACE_FLAG_HORIZONTAL; - m.attr("VERTICAL") = FT_FACE_FLAG_VERTICAL; - m.attr("KERNING") = FT_FACE_FLAG_KERNING; - m.attr("FAST_GLYPHS") = FT_FACE_FLAG_FAST_GLYPHS; - m.attr("MULTIPLE_MASTERS") = FT_FACE_FLAG_MULTIPLE_MASTERS; - m.attr("GLYPH_NAMES") = FT_FACE_FLAG_GLYPH_NAMES; - m.attr("EXTERNAL_STREAM") = FT_FACE_FLAG_EXTERNAL_STREAM; - m.attr("ITALIC") = FT_STYLE_FLAG_ITALIC; - m.attr("BOLD") = FT_STYLE_FLAG_BOLD; - m.attr("KERNING_DEFAULT") = (int)FT_KERNING_DEFAULT; - m.attr("KERNING_UNFITTED") = (int)FT_KERNING_UNFITTED; - m.attr("KERNING_UNSCALED") = (int)FT_KERNING_UNSCALED; - m.attr("LOAD_DEFAULT") = FT_LOAD_DEFAULT; - m.attr("LOAD_NO_SCALE") = FT_LOAD_NO_SCALE; - m.attr("LOAD_NO_HINTING") = FT_LOAD_NO_HINTING; - m.attr("LOAD_RENDER") = FT_LOAD_RENDER; - m.attr("LOAD_NO_BITMAP") = FT_LOAD_NO_BITMAP; - m.attr("LOAD_VERTICAL_LAYOUT") = FT_LOAD_VERTICAL_LAYOUT; - m.attr("LOAD_FORCE_AUTOHINT") = FT_LOAD_FORCE_AUTOHINT; - m.attr("LOAD_CROP_BITMAP") = FT_LOAD_CROP_BITMAP; - m.attr("LOAD_PEDANTIC") = FT_LOAD_PEDANTIC; - m.attr("LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH") = FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH; - m.attr("LOAD_NO_RECURSE") = FT_LOAD_NO_RECURSE; - m.attr("LOAD_IGNORE_TRANSFORM") = FT_LOAD_IGNORE_TRANSFORM; - m.attr("LOAD_MONOCHROME") = FT_LOAD_MONOCHROME; - m.attr("LOAD_LINEAR_DESIGN") = FT_LOAD_LINEAR_DESIGN; - m.attr("LOAD_NO_AUTOHINT") = (unsigned long)FT_LOAD_NO_AUTOHINT; - m.attr("LOAD_TARGET_NORMAL") = (unsigned long)FT_LOAD_TARGET_NORMAL; - m.attr("LOAD_TARGET_LIGHT") = (unsigned long)FT_LOAD_TARGET_LIGHT; - m.attr("LOAD_TARGET_MONO") = (unsigned long)FT_LOAD_TARGET_MONO; - m.attr("LOAD_TARGET_LCD") = (unsigned long)FT_LOAD_TARGET_LCD; - m.attr("LOAD_TARGET_LCD_V") = (unsigned long)FT_LOAD_TARGET_LCD_V; + m.def("__getattr__", ft2font__getattr__); } diff --git a/src/meson.build b/src/meson.build index fda013b82b13..a7018f0db094 100644 --- a/src/meson.build +++ b/src/meson.build @@ -110,8 +110,29 @@ extension_data = { }, } +if cpp.get_id() == 'msvc' + # This flag fixes some bugs with the macro processing, namely + # https://learn.microsoft.com/en-us/cpp/preprocessor/preprocessor-experimental-overview?view=msvc-170#macro-arguments-are-unpacked + if cpp.has_argument('/Zc:preprocessor') + # This flag was added in MSVC 2019 version 16.5, which deprecated the one below. + new_preprocessor = '/Zc:preprocessor' + else + # Since we currently support any version of MSVC 2019 (vc142), we'll stick with the + # older flag, added in MSVC 2017 version 15.8. + new_preprocessor = '/experimental:preprocessor' + endif +else + new_preprocessor = [] +endif + foreach ext, kwargs : extension_data - py3.extension_module(ext, install: true, kwargs: kwargs) + additions = { + 'cpp_args': [new_preprocessor] + kwargs.get('cpp_args', []), + } + py3.extension_module( + ext, + install: true, + kwargs: kwargs + additions) endforeach if get_option('macosx') and host_machine.system() == 'darwin'