From b50bd8bcb8302e87a831aa61f472dd5edf17a88e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 15 Aug 2024 02:18:17 -0400 Subject: [PATCH 1/5] Convert ft2font extension to pybind11 --- doc/missing-references.json | 3 - lib/matplotlib/ft2font.pyi | 99 +- lib/matplotlib/tests/test_ft2font.py | 6 +- requirements/testing/mypy.txt | 2 +- src/ft2font.h | 3 - src/ft2font_wrapper.cpp | 1676 ++++++++++---------------- src/meson.build | 2 +- 7 files changed, 715 insertions(+), 1076 deletions(-) diff --git a/doc/missing-references.json b/doc/missing-references.json index 87c9ce9b716f..a0eb69308eb4 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -349,9 +349,6 @@ "Figure.stale_callback": [ "doc/users/explain/figure/interactive_guide.rst:333" ], - "Glyph": [ - "doc/gallery/misc/ftface_props.rst:25" - ], "Image": [ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.gci:4" ], diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 0a27411ff39c..b2eb8cea1cc8 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,4 +1,6 @@ +import sys from typing import BinaryIO, Literal, TypedDict, final, overload +from typing_extensions import Buffer # < Py 3.12 import numpy as np from numpy.typing import NDArray @@ -159,28 +161,7 @@ class _SfntPcltDict(TypedDict): serifStyle: int @final -class FT2Font: - ascender: int - bbox: tuple[int, int, int, int] - descender: int - face_flags: int - family_name: str - fname: str - height: int - max_advance_height: int - max_advance_width: int - num_charmaps: int - num_faces: int - num_fixed_sizes: int - num_glyphs: int - postscript_name: str - scalable: bool - style_flags: int - style_name: str - underline_position: int - underline_thickness: int - units_per_EM: int - +class FT2Font(Buffer): def __init__( self, filename: str | BinaryIO, @@ -189,6 +170,8 @@ class FT2Font: _fallback_list: list[FT2Font] | None = ..., _kerning_factor: int = ... ) -> None: ... + if sys.version_info[:2] >= (3, 12): + def __buffer__(self, flags: int) -> memoryview: ... def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( @@ -232,23 +215,73 @@ class FT2Font: def set_text( self, string: str, angle: float = ..., flags: int = ... ) -> NDArray[np.float64]: ... + @property + def ascender(self) -> int: ... + @property + def bbox(self) -> tuple[int, int, int, int]: ... + @property + def descender(self) -> int: ... + @property + def face_flags(self) -> int: ... + @property + def family_name(self) -> str: ... + @property + def fname(self) -> str: ... + @property + def height(self) -> int: ... + @property + def max_advance_height(self) -> int: ... + @property + def max_advance_width(self) -> int: ... + @property + def num_charmaps(self) -> int: ... + @property + def num_faces(self) -> int: ... + @property + def num_fixed_sizes(self) -> int: ... + @property + def num_glyphs(self) -> int: ... + @property + def postscript_name(self) -> str: ... + @property + def scalable(self) -> bool: ... + @property + def style_flags(self) -> int: ... + @property + def style_name(self) -> str: ... + @property + def underline_position(self) -> int: ... + @property + def underline_thickness(self) -> int: ... + @property + def units_per_EM(self) -> int: ... @final -class FT2Image: # TODO: When updating mypy>=1.4, subclass from Buffer. +class FT2Image(Buffer): def __init__(self, width: float, height: float) -> None: ... def draw_rect_filled(self, x0: float, y0: float, x1: float, y1: float) -> None: ... + if sys.version_info[:2] >= (3, 12): + def __buffer__(self, flags: int) -> memoryview: ... @final class Glyph: - width: int - height: int - horiBearingX: int - horiBearingY: int - horiAdvance: int - linearHoriAdvance: int - vertBearingX: int - vertBearingY: int - vertAdvance: int - + @property + def width(self) -> int: ... + @property + def height(self) -> int: ... + @property + def horiBearingX(self) -> int: ... + @property + def horiBearingY(self) -> int: ... + @property + def horiAdvance(self) -> int: ... + @property + def linearHoriAdvance(self) -> int: ... + @property + def vertBearingX(self) -> int: ... + @property + def vertBearingY(self) -> int: ... + @property + def vertAdvance(self) -> int: ... @property def bbox(self) -> tuple[int, int, int, int]: ... diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index f383901b7b31..1bfa990bd8f5 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -130,6 +130,8 @@ def test_ft2font_invalid_args(tmp_path): # filename argument. with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): ft2font.FT2Font(None) + with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): + ft2font.FT2Font(object()) # Not bytes or string, and has no read() method. file = tmp_path / 'invalid-font.ttf' file.write_text('This is not a valid font file.') with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), @@ -145,7 +147,7 @@ def test_ft2font_invalid_args(tmp_path): file = fm.findfont('DejaVu Sans') # hinting_factor argument. - with pytest.raises(TypeError, match='cannot be interpreted as an integer'): + with pytest.raises(TypeError, match='incompatible constructor arguments'): ft2font.FT2Font(file, 1.3) with pytest.raises(ValueError, match='hinting_factor must be greater than 0'): ft2font.FT2Font(file, 0) @@ -157,7 +159,7 @@ def test_ft2font_invalid_args(tmp_path): ft2font.FT2Font(file, _fallback_list=[0]) # type: ignore[list-item] # kerning_factor argument. - with pytest.raises(TypeError, match='cannot be interpreted as an integer'): + with pytest.raises(TypeError, match='incompatible constructor arguments'): ft2font.FT2Font(file, _kerning_factor=1.3) diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 4fec6a8c000f..0b65050b52de 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -1,7 +1,7 @@ # Extra pip requirements for the GitHub Actions mypy build mypy>=1.9 -typing-extensions>=4.1 +typing-extensions>=4.6 # Extra stubs distributed separately from the main pypi package pandas-stubs diff --git a/src/ft2font.h b/src/ft2font.h index 2f24bfb01f79..7891c6050341 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -6,9 +6,6 @@ #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H -#define PY_SSIZE_T_CLEAN -#include - #include #include #include diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6a05680a474c..27ba249ec916 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1,133 +1,41 @@ -#include "mplutils.h" -#include "ft2font.h" -#include "numpy_cpp.h" -#include "py_converters.h" -#include "py_exceptions.h" +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#include +#include +#include -// From Python -#include +#include "ft2font.h" +#include "numpy/arrayobject.h" -#include +#include #include +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; -static PyObject *convert_xys_to_array(std::vector &xys) +static py::array_t +convert_xys_to_array(std::vector &xys) { - npy_intp dims[] = {(npy_intp)xys.size() / 2, 2 }; - if (dims[0] > 0) { - auto obj = PyArray_SimpleNew(2, dims, NPY_DOUBLE); - auto array = reinterpret_cast(obj); - memcpy(PyArray_DATA(array), xys.data(), PyArray_NBYTES(array)); - return obj; - } else { - return PyArray_SimpleNew(2, dims, NPY_DOUBLE); + py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; + py::array_t result(dims); + if (xys.size() > 0) { + memcpy(result.mutable_data(), xys.data(), result.nbytes()); } + return result; } /********************************************************************** * FT2Image * */ -typedef struct -{ - PyObject_HEAD - FT2Image *x; - Py_ssize_t shape[2]; - Py_ssize_t strides[2]; - Py_ssize_t suboffsets[2]; -} PyFT2Image; - -static PyTypeObject PyFT2ImageType; - -static PyObject *PyFT2Image_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyFT2Image *self; - self = (PyFT2Image *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static int PyFT2Image_init(PyFT2Image *self, PyObject *args, PyObject *kwds) -{ - double width; - double height; - - if (!PyArg_ParseTuple(args, "dd:FT2Image", &width, &height)) { - return -1; - } - - CALL_CPP_INIT("FT2Image", (self->x = new FT2Image(width, height))); - - return 0; -} - -static void PyFT2Image_dealloc(PyFT2Image *self) -{ - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); -} - const char *PyFT2Image_draw_rect_filled__doc__ = - "draw_rect_filled(self, x0, y0, x1, y1)\n" - "--\n\n" - "Draw a filled rectangle to the image.\n"; - -static PyObject *PyFT2Image_draw_rect_filled(PyFT2Image *self, PyObject *args) -{ - double x0, y0, x1, y1; - - if (!PyArg_ParseTuple(args, "dddd:draw_rect_filled", &x0, &y0, &x1, &y1)) { - return NULL; - } - - CALL_CPP("draw_rect_filled", (self->x->draw_rect_filled(x0, y0, x1, y1))); + "Draw a filled rectangle to the image."; - Py_RETURN_NONE; -} - -static int PyFT2Image_get_buffer(PyFT2Image *self, Py_buffer *buf, int flags) +static void +PyFT2Image_draw_rect_filled(FT2Image *self, double x0, double y0, double x1, double y1) { - FT2Image *im = self->x; - - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = im->get_buffer(); - buf->len = im->get_width() * im->get_height(); - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 2; - self->shape[0] = im->get_height(); - self->shape[1] = im->get_width(); - buf->shape = self->shape; - self->strides[0] = im->get_width(); - self->strides[1] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} - -static PyTypeObject* PyFT2Image_init_type() -{ - static PyMethodDef methods[] = { - {"draw_rect_filled", (PyCFunction)PyFT2Image_draw_rect_filled, METH_VARARGS, PyFT2Image_draw_rect_filled__doc__}, - {NULL} - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyFT2Image_get_buffer; - - PyFT2ImageType.tp_name = "matplotlib.ft2font.FT2Image"; - PyFT2ImageType.tp_basicsize = sizeof(PyFT2Image); - PyFT2ImageType.tp_dealloc = (destructor)PyFT2Image_dealloc; - PyFT2ImageType.tp_flags = Py_TPFLAGS_DEFAULT; - PyFT2ImageType.tp_methods = methods; - PyFT2ImageType.tp_new = PyFT2Image_new; - PyFT2ImageType.tp_init = (initproc)PyFT2Image_init; - PyFT2ImageType.tp_as_buffer = &buffer_procs; - - return &PyFT2ImageType; + self->draw_rect_filled(x0, y0, x1, y1); } /********************************************************************** @@ -136,7 +44,6 @@ static PyTypeObject* PyFT2Image_init_type() typedef struct { - PyObject_HEAD size_t glyphInd; long width; long height; @@ -150,16 +57,14 @@ typedef struct FT_BBox bbox; } PyGlyph; -static PyTypeObject PyGlyphType; - -static PyObject *PyGlyph_from_FT2Font(const FT2Font *font) +static PyGlyph * +PyGlyph_from_FT2Font(const FT2Font *font) { const FT_Face &face = font->get_face(); const long hinting_factor = font->get_hinting_factor(); const FT_Glyph &glyph = font->get_last_glyph(); - PyGlyph *self; - self = (PyGlyph *)PyGlyphType.tp_alloc(&PyGlyphType, 0); + PyGlyph *self = new PyGlyph(); self->glyphInd = font->get_last_glyph_index(); FT_Glyph_Get_CBox(glyph, ft_glyph_bbox_subpixels, &self->bbox); @@ -174,48 +79,14 @@ static PyObject *PyGlyph_from_FT2Font(const FT2Font *font) self->vertBearingY = face->glyph->metrics.vertBearingY; self->vertAdvance = face->glyph->metrics.vertAdvance; - return (PyObject *)self; -} - -static void PyGlyph_dealloc(PyGlyph *self) -{ - Py_TYPE(self)->tp_free((PyObject *)self); -} - -static PyObject *PyGlyph_get_bbox(PyGlyph *self, void *closure) -{ - return Py_BuildValue( - "llll", self->bbox.xMin, self->bbox.yMin, self->bbox.xMax, self->bbox.yMax); + return self; } -static PyTypeObject *PyGlyph_init_type() +static py::tuple +PyGlyph_get_bbox(PyGlyph *self) { - static PyMemberDef members[] = { - {(char *)"width", T_LONG, offsetof(PyGlyph, width), READONLY, (char *)""}, - {(char *)"height", T_LONG, offsetof(PyGlyph, height), READONLY, (char *)""}, - {(char *)"horiBearingX", T_LONG, offsetof(PyGlyph, horiBearingX), READONLY, (char *)""}, - {(char *)"horiBearingY", T_LONG, offsetof(PyGlyph, horiBearingY), READONLY, (char *)""}, - {(char *)"horiAdvance", T_LONG, offsetof(PyGlyph, horiAdvance), READONLY, (char *)""}, - {(char *)"linearHoriAdvance", T_LONG, offsetof(PyGlyph, linearHoriAdvance), READONLY, (char *)""}, - {(char *)"vertBearingX", T_LONG, offsetof(PyGlyph, vertBearingX), READONLY, (char *)""}, - {(char *)"vertBearingY", T_LONG, offsetof(PyGlyph, vertBearingY), READONLY, (char *)""}, - {(char *)"vertAdvance", T_LONG, offsetof(PyGlyph, vertAdvance), READONLY, (char *)""}, - {NULL} - }; - - static PyGetSetDef getset[] = { - {(char *)"bbox", (getter)PyGlyph_get_bbox, NULL, NULL, NULL}, - {NULL} - }; - - PyGlyphType.tp_name = "matplotlib.ft2font.Glyph"; - PyGlyphType.tp_basicsize = sizeof(PyGlyph); - PyGlyphType.tp_dealloc = (destructor)PyGlyph_dealloc; - PyGlyphType.tp_flags = Py_TPFLAGS_DEFAULT; - PyGlyphType.tp_members = members; - PyGlyphType.tp_getset = getset; - - return &PyGlyphType; + return py::make_tuple(self->bbox.xMin, self->bbox.yMin, + self->bbox.xMax, self->bbox.yMax); } /********************************************************************** @@ -224,40 +95,33 @@ static PyTypeObject *PyGlyph_init_type() struct PyFT2Font { - PyObject_HEAD FT2Font *x; - PyObject *py_file; + py::object py_file; FT_StreamRec stream; - Py_ssize_t shape[2]; - Py_ssize_t strides[2]; - Py_ssize_t suboffsets[2]; - std::vector fallbacks; -}; + py::list fallbacks; -static PyTypeObject PyFT2FontType; + ~PyFT2Font() + { + delete this->x; + } +}; -static unsigned long read_from_file_callback(FT_Stream stream, - unsigned long offset, - unsigned char *buffer, - unsigned long count) +static unsigned long +read_from_file_callback(FT_Stream stream, unsigned long offset, unsigned char *buffer, + unsigned long count) { - PyObject *py_file = ((PyFT2Font *)stream->descriptor.pointer)->py_file; - PyObject *seek_result = NULL, *read_result = NULL; + PyFT2Font *self = (PyFT2Font *)stream->descriptor.pointer; Py_ssize_t n_read = 0; - if (!(seek_result = PyObject_CallMethod(py_file, "seek", "k", offset)) - || !(read_result = PyObject_CallMethod(py_file, "read", "k", count))) { - goto exit; - } - char *tmpbuf; - if (PyBytes_AsStringAndSize(read_result, &tmpbuf, &n_read) == -1) { - goto exit; - } - memcpy(buffer, tmpbuf, n_read); -exit: - Py_XDECREF(seek_result); - Py_XDECREF(read_result); - if (PyErr_Occurred()) { - PyErr_WriteUnraisable(py_file); + try { + char *tmpbuf; + auto seek_result = self->py_file.attr("seek")(offset); + auto read_result = self->py_file.attr("read")(count); + if (PyBytes_AsStringAndSize(read_result.ptr(), &tmpbuf, &n_read) == -1) { + throw py::error_already_set(); + } + memcpy(buffer, tmpbuf, n_read); + } catch (py::error_already_set &eas) { + eas.discard_as_unraisable(__func__); if (!count) { return 1; // Non-zero signals error, when count == 0. } @@ -265,28 +129,24 @@ static unsigned long read_from_file_callback(FT_Stream stream, return (unsigned long)n_read; } -static void close_file_callback(FT_Stream stream) +static void +close_file_callback(FT_Stream stream) { PyObject *type, *value, *traceback; PyErr_Fetch(&type, &value, &traceback); PyFT2Font *self = (PyFT2Font *)stream->descriptor.pointer; - PyObject *close_result = NULL; - if (!(close_result = PyObject_CallMethod(self->py_file, "close", ""))) { - goto exit; - } -exit: - Py_XDECREF(close_result); - Py_CLEAR(self->py_file); - if (PyErr_Occurred()) { - PyErr_WriteUnraisable((PyObject*)self); + try { + self->py_file.attr("close")(); + } catch (py::error_already_set &eas) { + eas.discard_as_unraisable(__func__); } + self->py_file = py::object(); PyErr_Restore(type, value, traceback); } static void ft_glyph_warn(FT_ULong charcode, std::set family_names) { - PyObject *text_helpers = NULL, *tmp = NULL; std::set::iterator it = family_names.begin(); std::stringstream ss; ss<<*it; @@ -294,33 +154,12 @@ ft_glyph_warn(FT_ULong charcode, std::set family_names) ss<<", "<<*it; } - if (!(text_helpers = PyImport_ImportModule("matplotlib._text_helpers")) || - !(tmp = PyObject_CallMethod(text_helpers, - "warn_on_missing_glyph", "(k, s)", - charcode, ss.str().c_str()))) { - goto exit; - } -exit: - Py_XDECREF(text_helpers); - Py_XDECREF(tmp); - if (PyErr_Occurred()) { - throw mpl::exception(); - } -} - -static PyObject *PyFT2Font_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyFT2Font *self; - self = (PyFT2Font *)type->tp_alloc(type, 0); - self->x = NULL; - self->py_file = NULL; - memset(&self->stream, 0, sizeof(FT_StreamRec)); - return (PyObject *)self; + auto text_helpers = py::module_::import("matplotlib._text_helpers"); + auto warn_on_missing_glyph = text_helpers.attr("warn_on_missing_glyph"); + warn_on_missing_glyph(charcode, ss.str()); } const char *PyFT2Font_init__doc__ = - "FT2Font(filename, hinting_factor=8, *, _fallback_list=None, _kerning_factor=0)\n" - "--\n\n" "Create a new FT2Font object.\n" "\n" "Parameters\n" @@ -341,353 +180,204 @@ const char *PyFT2Font_init__doc__ = "\n" " .. warning::\n" " This API is private: do not use it directly\n" - "\n" - "Attributes\n" - "----------\n" - "num_faces : int\n" - " Number of faces in file.\n" - "face_flags, style_flags : int\n" - " Face and style flags; see the ft2font constants.\n" - "num_glyphs : int\n" - " Number of glyphs in the face.\n" - "family_name, style_name : str\n" - " Face family and style name.\n" - "num_fixed_sizes : int\n" - " Number of bitmap in the face.\n" - "scalable : bool\n" - " Whether face is scalable; attributes after this one are only\n" - " defined for scalable faces.\n" - "bbox : tuple[int, int, int, int]\n" - " Face global bounding box (xmin, ymin, xmax, ymax).\n" - "units_per_EM : int\n" - " Number of font units covered by the EM.\n" - "ascender, descender : int\n" - " Ascender and descender in 26.6 units.\n" - "height : int\n" - " Height in 26.6 units; used to compute a default line spacing\n" - " (baseline-to-baseline distance).\n" - "max_advance_width, max_advance_height : int\n" - " Maximum horizontal and vertical cursor advance for all glyphs.\n" - "underline_position, underline_thickness : int\n" - " Vertical position and thickness of the underline bar.\n" - "postscript_name : str\n" - " PostScript name of the font.\n"; - -static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) +; + +static PyFT2Font * +PyFT2Font_init(py::object filename, long hinting_factor = 8, + py::object fallback_list_or_none = py::none(), int kerning_factor = 0) { - 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", "_fallback_list", "_kerning_factor", NULL - }; - std::vector fallback_fonts; - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O|l$Oi:FT2Font", (char **)names, &filename, - &hinting_factor, &fallback_list, &kerning_factor)) { - return -1; - } if (hinting_factor <= 0) { - PyErr_SetString(PyExc_ValueError, - "hinting_factor must be greater than 0"); - goto exit; + throw py::value_error("hinting_factor must be greater than 0"); } + PyFT2Font *self = new PyFT2Font(); + self->x = NULL; + memset(&self->stream, 0, sizeof(FT_StreamRec)); self->stream.base = NULL; self->stream.size = 0x7fffffff; // Unknown size. self->stream.pos = 0; self->stream.descriptor.pointer = self; self->stream.read = &read_from_file_callback; + FT_Open_Args open_args; memset((void *)&open_args, 0, sizeof(FT_Open_Args)); open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; - if (fallback_list) { - if (!PyList_Check(fallback_list)) { - PyErr_SetString(PyExc_TypeError, "Fallback list must be a list"); - goto exit; + std::vector fallback_fonts; + if (!fallback_list_or_none.is_none()) { + if (!py::isinstance(fallback_list_or_none)) { + throw py::type_error("Fallback list must be a list"); } - Py_ssize_t size = PyList_Size(fallback_list); + auto fallback_list = fallback_list_or_none.cast(); // go through fallbacks once to make sure the types are right - for (Py_ssize_t i = 0; i < size; ++i) { - // this returns a borrowed reference - PyObject* item = PyList_GetItem(fallback_list, i); - if (!PyObject_IsInstance(item, PyObject_Type(reinterpret_cast(self)))) { - PyErr_SetString(PyExc_TypeError, "Fallback fonts must be FT2Font objects."); - goto exit; + for (auto item : fallback_list) { + if (!py::isinstance(item)) { + throw py::type_error("Fallback fonts must be FT2Font objects."); } } // go through a second time to add them to our lists - for (Py_ssize_t i = 0; i < size; ++i) { - // this returns a borrowed reference - PyObject* item = PyList_GetItem(fallback_list, i); - // Increase the ref count, we will undo this in dealloc this makes - // sure things do not get gc'd under us! - Py_INCREF(item); - self->fallbacks.push_back(item); + for (auto item : fallback_list) { + self->fallbacks.append(item); // Also (locally) cache the underlying FT2Font objects. As long as // the Python objects are kept alive, these pointer are good. - FT2Font *fback = reinterpret_cast(item)->x; + FT2Font *fback = py::cast(item)->x; fallback_fonts.push_back(fback); } } - if (PyBytes_Check(filename) || PyUnicode_Check(filename)) { - if (!(open = PyDict_GetItemString(PyEval_GetBuiltins(), "open")) // Borrowed reference. - || !(self->py_file = PyObject_CallFunction(open, "Os", filename, "rb"))) { - goto exit; - } + if (py::isinstance(filename) || py::isinstance(filename)) { + self->py_file = py::module_::import("io").attr("open")(filename, "rb"); self->stream.close = &close_file_callback; - } else if (!PyObject_HasAttrString(filename, "read") - || !(data = PyObject_CallMethod(filename, "read", "i", 0)) - || !PyBytes_Check(data)) { - PyErr_SetString(PyExc_TypeError, - "First argument must be a path to a font file or a binary-mode file object"); - Py_CLEAR(data); - goto exit; } else { + try { + // This will catch various issues: + // 1. `read` not being an attribute. + // 2. `read` raising an error. + // 3. `read` returning something other than `bytes`. + auto data = filename.attr("read")(0).cast(); + } catch (const std::exception&) { + throw py::type_error( + "First argument must be a path to a font file or a binary-mode file object"); + } self->py_file = filename; self->stream.close = NULL; - Py_INCREF(filename); } - Py_CLEAR(data); - CALL_CPP_FULL( - "FT2Font", - (self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn)), - Py_CLEAR(self->py_file), -1); + self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn); - CALL_CPP_INIT("FT2Font->set_kerning_factor", (self->x->set_kerning_factor(kerning_factor))); + self->x->set_kerning_factor(kerning_factor); -exit: - return PyErr_Occurred() ? -1 : 0; -} - -static void PyFT2Font_dealloc(PyFT2Font *self) -{ - delete self->x; - for (size_t i = 0; i < self->fallbacks.size(); i++) { - Py_DECREF(self->fallbacks[i]); - } - - Py_XDECREF(self->py_file); - Py_TYPE(self)->tp_free((PyObject *)self); + return self; } const char *PyFT2Font_clear__doc__ = - "clear(self)\n" - "--\n\n" - "Clear all the glyphs, reset for a new call to `.set_text`.\n"; + "Clear all the glyphs, reset for a new call to `.set_text`."; -static PyObject *PyFT2Font_clear(PyFT2Font *self, PyObject *args) +static void +PyFT2Font_clear(PyFT2Font *self) { - CALL_CPP("clear", (self->x->clear())); - - Py_RETURN_NONE; + self->x->clear(); } const char *PyFT2Font_set_size__doc__ = - "set_size(self, ptsize, dpi)\n" - "--\n\n" - "Set the point size and dpi of the text.\n"; + "Set the point size and dpi of the text."; -static PyObject *PyFT2Font_set_size(PyFT2Font *self, PyObject *args) +static void +PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi) { - double ptsize; - double dpi; - - if (!PyArg_ParseTuple(args, "dd:set_size", &ptsize, &dpi)) { - return NULL; - } - - CALL_CPP("set_size", (self->x->set_size(ptsize, dpi))); - - Py_RETURN_NONE; + self->x->set_size(ptsize, dpi); } const char *PyFT2Font_set_charmap__doc__ = - "set_charmap(self, i)\n" - "--\n\n" - "Make the i-th charmap current.\n"; + "Make the i-th charmap current."; -static PyObject *PyFT2Font_set_charmap(PyFT2Font *self, PyObject *args) +static void +PyFT2Font_set_charmap(PyFT2Font *self, int i) { - int i; - - if (!PyArg_ParseTuple(args, "i:set_charmap", &i)) { - return NULL; - } - - CALL_CPP("set_charmap", (self->x->set_charmap(i))); - - Py_RETURN_NONE; + self->x->set_charmap(i); } const char *PyFT2Font_select_charmap__doc__ = - "select_charmap(self, i)\n" - "--\n\n" - "Select a charmap by its FT_Encoding number.\n"; + "Select a charmap by its FT_Encoding number."; -static PyObject *PyFT2Font_select_charmap(PyFT2Font *self, PyObject *args) +static void +PyFT2Font_select_charmap(PyFT2Font *self, unsigned long i) { - unsigned long i; - - if (!PyArg_ParseTuple(args, "k:select_charmap", &i)) { - return NULL; - } - - CALL_CPP("select_charmap", self->x->select_charmap(i)); - - Py_RETURN_NONE; + self->x->select_charmap(i); } const char *PyFT2Font_get_kerning__doc__ = - "get_kerning(self, left, right, mode)\n" - "--\n\n" "Get the kerning between *left* and *right* glyph indices.\n" - "*mode* is a kerning mode constant:\n\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"; -static PyObject *PyFT2Font_get_kerning(PyFT2Font *self, PyObject *args) +static int +PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, FT_UInt mode) { - FT_UInt left, right, mode; - int result; bool fallback = true; - if (!PyArg_ParseTuple(args, "III:get_kerning", &left, &right, &mode)) { - return NULL; - } - - CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode, fallback))); - - return PyLong_FromLong(result); + return self->x->get_kerning(left, right, mode, fallback); } const char *PyFT2Font_get_fontmap__doc__ = - "_get_fontmap(self, string)\n" - "--\n\n" "Get a mapping between characters and the font that includes them.\n" "A dictionary mapping unicode characters to PyFT2Font objects."; -static PyObject *PyFT2Font_get_fontmap(PyFT2Font *self, PyObject *args, PyObject *kwds) -{ - PyObject *textobj; - const char *names[] = { "string", NULL }; - - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O:_get_fontmap", (char **)names, &textobj)) { - return NULL; - } +static py::dict +PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) +{ std::set codepoints; - size_t size; - if (PyUnicode_Check(textobj)) { - size = PyUnicode_GET_LENGTH(textobj); - for (size_t i = 0; i < size; ++i) { - codepoints.insert(PyUnicode_ReadChar(textobj, i)); - } - } else { - PyErr_SetString(PyExc_TypeError, "string must be str"); - return NULL; - } - PyObject *char_to_font; - if (!(char_to_font = PyDict_New())) { - return NULL; + for (auto code : text) { + codepoints.insert(code); } - for (auto it = codepoints.begin(); it != codepoints.end(); ++it) { - auto x = *it; - PyObject* target_font; + + py::dict char_to_font; + for (auto code : codepoints) { + py::object target_font; int index; - if (self->x->get_char_fallback_index(x, index)) { + if (self->x->get_char_fallback_index(code, index)) { if (index >= 0) { target_font = self->fallbacks[index]; } else { - target_font = (PyObject *)self; + target_font = py::cast(self); } } else { // TODO Handle recursion! - target_font = (PyObject *)self; + target_font = py::cast(self); } - PyObject *key = NULL; - bool error = (!(key = PyUnicode_FromFormat("%c", x)) - || (PyDict_SetItem(char_to_font, key, target_font) == -1)); - Py_XDECREF(key); - if (error) { - Py_DECREF(char_to_font); - PyErr_SetString(PyExc_ValueError, "Something went very wrong"); - return NULL; - } + auto key = py::cast(std::u32string(1, code)); + char_to_font[key] = target_font; } return char_to_font; } - const char *PyFT2Font_set_text__doc__ = - "set_text(self, string, angle=0.0, flags=32)\n" - "--\n\n" "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"; -static PyObject *PyFT2Font_set_text(PyFT2Font *self, PyObject *args, PyObject *kwds) +static py::array_t +PyFT2Font_set_text(PyFT2Font *self, std::u32string text, double angle = 0.0, + FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) { - PyObject *textobj; - double angle = 0.0; - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; std::vector xys; - const char *names[] = { "string", "angle", "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, "O|di:set_text", (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); - for (size_t i = 0; i < size; ++i) { - codepoints[i] = PyUnicode_ReadChar(textobj, i); - } - } else { - PyErr_SetString(PyExc_TypeError, "set_text requires str-input."); - return NULL; + size = text.size(); + codepoints.resize(size); + for (size_t i = 0; i < size; ++i) { + codepoints[i] = text[i]; } uint32_t* codepoints_array = NULL; if (size > 0) { codepoints_array = &codepoints[0]; } - CALL_CPP("set_text", self->x->set_text(size, codepoints_array, angle, flags, xys)); + self->x->set_text(size, codepoints_array, angle, flags, xys); return convert_xys_to_array(xys); } const char *PyFT2Font_get_num_glyphs__doc__ = - "get_num_glyphs(self)\n" - "--\n\n" - "Return the number of loaded glyphs.\n"; + "Return the number of loaded glyphs."; -static PyObject *PyFT2Font_get_num_glyphs(PyFT2Font *self, PyObject *args) +static size_t +PyFT2Font_get_num_glyphs(PyFT2Font *self) { - return PyLong_FromSize_t(self->x->get_num_glyphs()); + return self->x->get_num_glyphs(); } const char *PyFT2Font_load_char__doc__ = - "load_char(self, charcode, 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" "the default value is LOAD_FORCE_AUTOHINT.\n" @@ -702,30 +392,19 @@ const char *PyFT2Font_load_char__doc__ = "- vertBearingY: top side bearing in vertical layouts\n" "- vertAdvance: advance height for vertical layout\n"; -static PyObject *PyFT2Font_load_char(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyGlyph * +PyFT2Font_load_char(PyFT2Font *self, long charcode, + FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) { - long charcode; bool fallback = true; - 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)) { - return NULL; - } - FT2Font *ft_object = NULL; - CALL_CPP("load_char", (self->x->load_char(charcode, flags, ft_object, fallback))); + + self->x->load_char(charcode, flags, ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } const char *PyFT2Font_load_glyph__doc__ = - "load_glyph(self, glyphindex, flags=32)\n" - "--\n\n" "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" @@ -740,99 +419,72 @@ const char *PyFT2Font_load_glyph__doc__ = "- vertBearingY: top side bearing in vertical layouts\n" "- vertAdvance: advance height for vertical layout\n"; -static PyObject *PyFT2Font_load_glyph(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyGlyph * +PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, + FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) { - FT_UInt glyph_index; - FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; bool fallback = true; - 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)) { - return NULL; - } - FT2Font *ft_object = NULL; - CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags, ft_object, fallback))); + + self->x->load_glyph(glyph_index, flags, ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } const char *PyFT2Font_get_width_height__doc__ = - "get_width_height(self)\n" - "--\n\n" "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"; -static PyObject *PyFT2Font_get_width_height(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_width_height(PyFT2Font *self) { long width, height; - CALL_CPP("get_width_height", (self->x->get_width_height(&width, &height))); + self->x->get_width_height(&width, &height); - return Py_BuildValue("ll", width, height); + return py::make_tuple(width, height); } const char *PyFT2Font_get_bitmap_offset__doc__ = - "get_bitmap_offset(self)\n" - "--\n\n" "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"; -static PyObject *PyFT2Font_get_bitmap_offset(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_bitmap_offset(PyFT2Font *self) { long x, y; - CALL_CPP("get_bitmap_offset", (self->x->get_bitmap_offset(&x, &y))); + self->x->get_bitmap_offset(&x, &y); - return Py_BuildValue("ll", x, y); + return py::make_tuple(x, y); } const char *PyFT2Font_get_descent__doc__ = - "get_descent(self)\n" - "--\n\n" "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"; -static PyObject *PyFT2Font_get_descent(PyFT2Font *self, PyObject *args) +static long +PyFT2Font_get_descent(PyFT2Font *self) { - long descent; - - CALL_CPP("get_descent", (descent = self->x->get_descent())); - - return PyLong_FromLong(descent); + return self->x->get_descent(); } const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = - "draw_glyphs_to_bitmap(self, antialiased=True)\n" - "--\n\n" "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"; -static PyObject *PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, PyObject *args, PyObject *kwds) +static void +PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, bool antialiased = true) { - bool antialiased = true; - const char *names[] = { "antialiased", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O&:draw_glyphs_to_bitmap", - (char **)names, &convert_bool, &antialiased)) { - return NULL; - } - - CALL_CPP("draw_glyphs_to_bitmap", (self->x->draw_glyphs_to_bitmap(antialiased))); - - Py_RETURN_NONE; + self->x->draw_glyphs_to_bitmap(antialiased); } const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = - "draw_glyph_to_bitmap(self, image, x, y, glyph, antialiased=True)\n" - "--\n\n" - "Draw a single glyph to *image* at pixel locations *x*, *y*\n" + "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" @@ -841,84 +493,48 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = "who want to render individual glyphs (e.g., returned by `.load_char`)\n" "at precise locations.\n"; -static PyObject *PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, PyObject *args, PyObject *kwds) +static void +PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image, double xd, double yd, + PyGlyph *glyph, bool antialiased = true) { - PyFT2Image *image; - double xd, yd; - PyGlyph *glyph; - bool antialiased = true; - const char *names[] = { "image", "x", "y", "glyph", "antialiased", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, - kwds, - "O!ddO!|O&:draw_glyph_to_bitmap", - (char **)names, - &PyFT2ImageType, - &image, - &xd, - &yd, - &PyGlyphType, - &glyph, - &convert_bool, - &antialiased)) { - return NULL; - } - - CALL_CPP("draw_glyph_to_bitmap", - self->x->draw_glyph_to_bitmap(*(image->x), xd, yd, glyph->glyphInd, antialiased)); - - Py_RETURN_NONE; + self->x->draw_glyph_to_bitmap(image, xd, yd, glyph->glyphInd, antialiased); } const char *PyFT2Font_get_glyph_name__doc__ = - "get_glyph_name(self, index)\n" - "--\n\n" "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"; -static PyObject *PyFT2Font_get_glyph_name(PyFT2Font *self, PyObject *args) +static py::str +PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) { - unsigned int glyph_number; std::string buffer; bool fallback = true; - if (!PyArg_ParseTuple(args, "I:get_glyph_name", &glyph_number)) { - return NULL; - } buffer.resize(128); - CALL_CPP("get_glyph_name", - (self->x->get_glyph_name(glyph_number, buffer, fallback))); - return PyUnicode_FromString(buffer.c_str()); + self->x->get_glyph_name(glyph_number, buffer, fallback); + // pybind11 uses the entire string's size(), so trim all the NULLs off the end. + auto len = buffer.find('\0'); + if (len != buffer.npos) { + buffer.resize(len); + } + return buffer; } const char *PyFT2Font_get_charmap__doc__ = - "get_charmap(self)\n" - "--\n\n" "Return a dict that maps the character codes of the selected charmap\n" "(Unicode by default) to their corresponding glyph indices.\n"; -static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args) +static py::dict +PyFT2Font_get_charmap(PyFT2Font *self) { - PyObject *charmap; - if (!(charmap = PyDict_New())) { - return NULL; - } + py::dict charmap; FT_UInt index; FT_ULong code = FT_Get_First_Char(self->x->get_face(), &index); while (index != 0) { - PyObject *key = NULL, *val = NULL; - bool error = (!(key = PyLong_FromLong(code)) - || !(val = PyLong_FromLong(index)) - || (PyDict_SetItem(charmap, key, val) == -1)); - Py_XDECREF(key); - Py_XDECREF(val); - if (error) { - Py_DECREF(charmap); - return NULL; - } + charmap[py::cast(code)] = py::cast(index); code = FT_Get_Next_Char(self->x->get_face(), code, &index); } return charmap; @@ -926,638 +542,632 @@ static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args) const char *PyFT2Font_get_char_index__doc__ = - "get_char_index(self, codepoint)\n" - "--\n\n" - "Return the glyph index corresponding to a character *codepoint*.\n"; + "Return the glyph index corresponding to a character *codepoint*."; -static PyObject *PyFT2Font_get_char_index(PyFT2Font *self, PyObject *args) +static FT_UInt +PyFT2Font_get_char_index(PyFT2Font *self, FT_ULong ccode) { - FT_UInt index; - FT_ULong ccode; bool fallback = true; - if (!PyArg_ParseTuple(args, "k:get_char_index", &ccode)) { - return NULL; - } - - CALL_CPP("get_char_index", index = self->x->get_char_index(ccode, fallback)); - - return PyLong_FromLong(index); + return self->x->get_char_index(ccode, fallback); } const char *PyFT2Font_get_sfnt__doc__ = - "get_sfnt(self)\n" - "--\n\n" "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"; -static PyObject *PyFT2Font_get_sfnt(PyFT2Font *self, PyObject *args) +static py::dict +PyFT2Font_get_sfnt(PyFT2Font *self) { - PyObject *names; - if (!(self->x->get_face()->face_flags & FT_FACE_FLAG_SFNT)) { - PyErr_SetString(PyExc_ValueError, "No SFNT name table"); - return NULL; + throw py::value_error("No SFNT name table"); } size_t count = FT_Get_Sfnt_Name_Count(self->x->get_face()); - names = PyDict_New(); - if (names == NULL) { - return NULL; - } + py::dict names; for (FT_UInt j = 0; j < count; ++j) { FT_SfntName sfnt; FT_Error error = FT_Get_Sfnt_Name(self->x->get_face(), j, &sfnt); if (error) { - Py_DECREF(names); - PyErr_SetString(PyExc_ValueError, "Could not get SFNT name"); - return NULL; + throw py::value_error("Could not get SFNT name"); } - PyObject *key = Py_BuildValue( - "HHHH", sfnt.platform_id, sfnt.encoding_id, sfnt.language_id, sfnt.name_id); - if (key == NULL) { - Py_DECREF(names); - return NULL; - } - - PyObject *val = PyBytes_FromStringAndSize((const char *)sfnt.string, sfnt.string_len); - if (val == NULL) { - Py_DECREF(key); - Py_DECREF(names); - return NULL; - } - - if (PyDict_SetItem(names, key, val)) { - Py_DECREF(key); - Py_DECREF(val); - Py_DECREF(names); - return NULL; - } - - Py_DECREF(key); - Py_DECREF(val); + auto key = py::make_tuple( + sfnt.platform_id, sfnt.encoding_id, sfnt.language_id, sfnt.name_id); + auto val = py::bytes(reinterpret_cast(sfnt.string), + sfnt.string_len); + names[key] = val; } return names; } const char *PyFT2Font_get_name_index__doc__ = - "get_name_index(self, name)\n" - "--\n\n" "Return the glyph index of a given glyph *name*.\n" "The glyph index 0 means 'undefined character code'.\n"; -static PyObject *PyFT2Font_get_name_index(PyFT2Font *self, PyObject *args) +static long +PyFT2Font_get_name_index(PyFT2Font *self, char *glyphname) { - char *glyphname; - long name_index; - if (!PyArg_ParseTuple(args, "s:get_name_index", &glyphname)) { - return NULL; - } - CALL_CPP("get_name_index", name_index = self->x->get_name_index(glyphname)); - return PyLong_FromLong(name_index); + return self->x->get_name_index(glyphname); } const char *PyFT2Font_get_ps_font_info__doc__ = - "get_ps_font_info(self)\n" - "--\n\n" - "Return the information in the PS Font Info structure.\n"; + "Return the information in the PS Font Info structure."; -static PyObject *PyFT2Font_get_ps_font_info(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_ps_font_info(PyFT2Font *self) { PS_FontInfoRec fontinfo; FT_Error error = FT_Get_PS_Font_Info(self->x->get_face(), &fontinfo); if (error) { - PyErr_SetString(PyExc_ValueError, "Could not get PS font info"); - return NULL; + throw py::value_error("Could not get PS font info"); } - return Py_BuildValue("ssssslbhH", - fontinfo.version ? fontinfo.version : "", - fontinfo.notice ? fontinfo.notice : "", - fontinfo.full_name ? fontinfo.full_name : "", - fontinfo.family_name ? fontinfo.family_name : "", - fontinfo.weight ? fontinfo.weight : "", - fontinfo.italic_angle, - fontinfo.is_fixed_pitch, - fontinfo.underline_position, - fontinfo.underline_thickness); + return py::make_tuple( + fontinfo.version ? fontinfo.version : "", + fontinfo.notice ? fontinfo.notice : "", + fontinfo.full_name ? fontinfo.full_name : "", + fontinfo.family_name ? fontinfo.family_name : "", + fontinfo.weight ? fontinfo.weight : "", + fontinfo.italic_angle, + fontinfo.is_fixed_pitch, + fontinfo.underline_position, + fontinfo.underline_thickness); } const char *PyFT2Font_get_sfnt_table__doc__ = - "get_sfnt_table(self, name)\n" - "--\n\n" "Return one of the following SFNT tables: head, maxp, OS/2, hhea, " - "vhea, post, or pclt.\n"; - -static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args) -{ - char *tagname; - if (!PyArg_ParseTuple(args, "s:get_sfnt_table", &tagname)) { - return NULL; - } - - int tag; - const char *tags[] = { "head", "maxp", "OS/2", "hhea", "vhea", "post", "pclt", NULL }; + "vhea, post, or pclt."; + +static std::optional +PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) +{ + FT_Sfnt_Tag tag; + const std::unordered_map names = { + {"head", FT_SFNT_HEAD}, + {"maxp", FT_SFNT_MAXP}, + {"OS/2", FT_SFNT_OS2}, + {"hhea", FT_SFNT_HHEA}, + {"vhea", FT_SFNT_VHEA}, + {"post", FT_SFNT_POST}, + {"pclt", FT_SFNT_PCLT}, + }; - for (tag = 0; tags[tag] != NULL; tag++) { - if (strncmp(tagname, tags[tag], 5) == 0) { - break; - } + try { + tag = names.at(tagname); + } catch (const std::out_of_range&) { + return std::nullopt; } - void *table = FT_Get_Sfnt_Table(self->x->get_face(), (FT_Sfnt_Tag)tag); + void *table = FT_Get_Sfnt_Table(self->x->get_face(), tag); if (!table) { - Py_RETURN_NONE; + return std::nullopt; } switch (tag) { - case 0: { - char head_dict[] = - "{s:(h,H), s:(h,H), s:l, s:l, s:H, s:H," - "s:(I,I), s:(I,I), s:h, s:h, s:h, s:h, s:H, s:H, s:h, s:h, s:h}"; - TT_Header *t = (TT_Header *)table; - return Py_BuildValue(head_dict, - "version", FIXED_MAJOR(t->Table_Version), FIXED_MINOR(t->Table_Version), - "fontRevision", FIXED_MAJOR(t->Font_Revision), FIXED_MINOR(t->Font_Revision), - "checkSumAdjustment", t->CheckSum_Adjust, - "magicNumber", t->Magic_Number, - "flags", t->Flags, - "unitsPerEm", t->Units_Per_EM, - // FreeType 2.6.1 defines these two timestamps as FT_Long, - // but they should be unsigned (fixed in 2.10.0): - // https://gitlab.freedesktop.org/freetype/freetype/-/commit/3e8ec291ffcfa03c8ecba1cdbfaa55f5577f5612 - // It's actually read from the file structure as two 32-bit - // values, so we need to cast down in size to prevent sign - // extension from producing huge 64-bit values. - "created", static_cast(t->Created[0]), static_cast(t->Created[1]), - "modified", static_cast(t->Modified[0]), static_cast(t->Modified[1]), - "xMin", t->xMin, - "yMin", t->yMin, - "xMax", t->xMax, - "yMax", t->yMax, - "macStyle", t->Mac_Style, - "lowestRecPPEM", t->Lowest_Rec_PPEM, - "fontDirectionHint", t->Font_Direction, - "indexToLocFormat", t->Index_To_Loc_Format, - "glyphDataFormat", t->Glyph_Data_Format); + case FT_SFNT_HEAD: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Table_Version), + FIXED_MINOR(t->Table_Version)), + "fontRevision"_a=py::make_tuple(FIXED_MAJOR(t->Font_Revision), + FIXED_MINOR(t->Font_Revision)), + "checkSumAdjustment"_a=t->CheckSum_Adjust, + "magicNumber"_a=t->Magic_Number, + "flags"_a=t->Flags, + "unitsPerEm"_a=t->Units_Per_EM, + // FreeType 2.6.1 defines these two timestamps as FT_Long, but they should + // be unsigned (fixed in 2.10.0): + // https://gitlab.freedesktop.org/freetype/freetype/-/commit/3e8ec291ffcfa03c8ecba1cdbfaa55f5577f5612 + // It's actually read from the file structure as two 32-bit values, so we + // need to cast down in size to prevent sign extension from producing huge + // 64-bit values. + "created"_a=py::make_tuple(static_cast(t->Created[0]), + static_cast(t->Created[1])), + "modified"_a=py::make_tuple(static_cast(t->Modified[0]), + static_cast(t->Modified[1])), + "xMin"_a=t->xMin, + "yMin"_a=t->yMin, + "xMax"_a=t->xMax, + "yMax"_a=t->yMax, + "macStyle"_a=t->Mac_Style, + "lowestRecPPEM"_a=t->Lowest_Rec_PPEM, + "fontDirectionHint"_a=t->Font_Direction, + "indexToLocFormat"_a=t->Index_To_Loc_Format, + "glyphDataFormat"_a=t->Glyph_Data_Format); } - case 1: { - char maxp_dict[] = - "{s:(h,H), s:H, s:H, s:H, s:H, s:H, s:H," - "s:H, s:H, s:H, s:H, s:H, s:H, s:H, s:H}"; - TT_MaxProfile *t = (TT_MaxProfile *)table; - return Py_BuildValue(maxp_dict, - "version", FIXED_MAJOR(t->version), FIXED_MINOR(t->version), - "numGlyphs", t->numGlyphs, - "maxPoints", t->maxPoints, - "maxContours", t->maxContours, - "maxComponentPoints", t->maxCompositePoints, - "maxComponentContours", t->maxCompositeContours, - "maxZones", t->maxZones, - "maxTwilightPoints", t->maxTwilightPoints, - "maxStorage", t->maxStorage, - "maxFunctionDefs", t->maxFunctionDefs, - "maxInstructionDefs", t->maxInstructionDefs, - "maxStackElements", t->maxStackElements, - "maxSizeOfInstructions", t->maxSizeOfInstructions, - "maxComponentElements", t->maxComponentElements, - "maxComponentDepth", t->maxComponentDepth); + case FT_SFNT_MAXP: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->version), + FIXED_MINOR(t->version)), + "numGlyphs"_a=t->numGlyphs, + "maxPoints"_a=t->maxPoints, + "maxContours"_a=t->maxContours, + "maxComponentPoints"_a=t->maxCompositePoints, + "maxComponentContours"_a=t->maxCompositeContours, + "maxZones"_a=t->maxZones, + "maxTwilightPoints"_a=t->maxTwilightPoints, + "maxStorage"_a=t->maxStorage, + "maxFunctionDefs"_a=t->maxFunctionDefs, + "maxInstructionDefs"_a=t->maxInstructionDefs, + "maxStackElements"_a=t->maxStackElements, + "maxSizeOfInstructions"_a=t->maxSizeOfInstructions, + "maxComponentElements"_a=t->maxComponentElements, + "maxComponentDepth"_a=t->maxComponentDepth); } - case 2: { - char os_2_dict[] = - "{s:H, s:h, s:H, s:H, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:h, s:h, s:h, s:h, s:y#, s:(kkkk)," - "s:y#, s:H, s:H, s:H}"; - TT_OS2 *t = (TT_OS2 *)table; - return Py_BuildValue(os_2_dict, - "version", t->version, - "xAvgCharWidth", t->xAvgCharWidth, - "usWeightClass", t->usWeightClass, - "usWidthClass", t->usWidthClass, - "fsType", t->fsType, - "ySubscriptXSize", t->ySubscriptXSize, - "ySubscriptYSize", t->ySubscriptYSize, - "ySubscriptXOffset", t->ySubscriptXOffset, - "ySubscriptYOffset", t->ySubscriptYOffset, - "ySuperscriptXSize", t->ySuperscriptXSize, - "ySuperscriptYSize", t->ySuperscriptYSize, - "ySuperscriptXOffset", t->ySuperscriptXOffset, - "ySuperscriptYOffset", t->ySuperscriptYOffset, - "yStrikeoutSize", t->yStrikeoutSize, - "yStrikeoutPosition", t->yStrikeoutPosition, - "sFamilyClass", t->sFamilyClass, - "panose", t->panose, Py_ssize_t(10), - "ulCharRange", t->ulUnicodeRange1, t->ulUnicodeRange2, t->ulUnicodeRange3, t->ulUnicodeRange4, - "achVendID", t->achVendID, Py_ssize_t(4), - "fsSelection", t->fsSelection, - "fsFirstCharIndex", t->usFirstCharIndex, - "fsLastCharIndex", t->usLastCharIndex); + case FT_SFNT_OS2: { + auto t = static_cast(table); + return py::dict( + "version"_a=t->version, + "xAvgCharWidth"_a=t->xAvgCharWidth, + "usWeightClass"_a=t->usWeightClass, + "usWidthClass"_a=t->usWidthClass, + "fsType"_a=t->fsType, + "ySubscriptXSize"_a=t->ySubscriptXSize, + "ySubscriptYSize"_a=t->ySubscriptYSize, + "ySubscriptXOffset"_a=t->ySubscriptXOffset, + "ySubscriptYOffset"_a=t->ySubscriptYOffset, + "ySuperscriptXSize"_a=t->ySuperscriptXSize, + "ySuperscriptYSize"_a=t->ySuperscriptYSize, + "ySuperscriptXOffset"_a=t->ySuperscriptXOffset, + "ySuperscriptYOffset"_a=t->ySuperscriptYOffset, + "yStrikeoutSize"_a=t->yStrikeoutSize, + "yStrikeoutPosition"_a=t->yStrikeoutPosition, + "sFamilyClass"_a=t->sFamilyClass, + "panose"_a=py::bytes(reinterpret_cast(t->panose), 10), + "ulCharRange"_a=py::make_tuple(t->ulUnicodeRange1, t->ulUnicodeRange2, + t->ulUnicodeRange3, t->ulUnicodeRange4), + "achVendID"_a=py::bytes(reinterpret_cast(t->achVendID), 4), + "fsSelection"_a=t->fsSelection, + "fsFirstCharIndex"_a=t->usFirstCharIndex, + "fsLastCharIndex"_a=t->usLastCharIndex); } - case 3: { - char hhea_dict[] = - "{s:(h,H), s:h, s:h, s:h, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:H}"; - TT_HoriHeader *t = (TT_HoriHeader *)table; - return Py_BuildValue(hhea_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "ascent", t->Ascender, - "descent", t->Descender, - "lineGap", t->Line_Gap, - "advanceWidthMax", t->advance_Width_Max, - "minLeftBearing", t->min_Left_Side_Bearing, - "minRightBearing", t->min_Right_Side_Bearing, - "xMaxExtent", t->xMax_Extent, - "caretSlopeRise", t->caret_Slope_Rise, - "caretSlopeRun", t->caret_Slope_Run, - "caretOffset", t->caret_Offset, - "metricDataFormat", t->metric_Data_Format, - "numOfLongHorMetrics", t->number_Of_HMetrics); + case FT_SFNT_HHEA: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "ascent"_a=t->Ascender, + "descent"_a=t->Descender, + "lineGap"_a=t->Line_Gap, + "advanceWidthMax"_a=t->advance_Width_Max, + "minLeftBearing"_a=t->min_Left_Side_Bearing, + "minRightBearing"_a=t->min_Right_Side_Bearing, + "xMaxExtent"_a=t->xMax_Extent, + "caretSlopeRise"_a=t->caret_Slope_Rise, + "caretSlopeRun"_a=t->caret_Slope_Run, + "caretOffset"_a=t->caret_Offset, + "metricDataFormat"_a=t->metric_Data_Format, + "numOfLongHorMetrics"_a=t->number_Of_HMetrics); } - case 4: { - char vhea_dict[] = - "{s:(h,H), s:h, s:h, s:h, s:H, s:h, s:h, s:h," - "s:h, s:h, s:h, s:h, s:H}"; - TT_VertHeader *t = (TT_VertHeader *)table; - return Py_BuildValue(vhea_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "vertTypoAscender", t->Ascender, - "vertTypoDescender", t->Descender, - "vertTypoLineGap", t->Line_Gap, - "advanceHeightMax", t->advance_Height_Max, - "minTopSideBearing", t->min_Top_Side_Bearing, - "minBottomSizeBearing", t->min_Bottom_Side_Bearing, - "yMaxExtent", t->yMax_Extent, - "caretSlopeRise", t->caret_Slope_Rise, - "caretSlopeRun", t->caret_Slope_Run, - "caretOffset", t->caret_Offset, - "metricDataFormat", t->metric_Data_Format, - "numOfLongVerMetrics", t->number_Of_VMetrics); + case FT_SFNT_VHEA: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "vertTypoAscender"_a=t->Ascender, + "vertTypoDescender"_a=t->Descender, + "vertTypoLineGap"_a=t->Line_Gap, + "advanceHeightMax"_a=t->advance_Height_Max, + "minTopSideBearing"_a=t->min_Top_Side_Bearing, + "minBottomSizeBearing"_a=t->min_Bottom_Side_Bearing, + "yMaxExtent"_a=t->yMax_Extent, + "caretSlopeRise"_a=t->caret_Slope_Rise, + "caretSlopeRun"_a=t->caret_Slope_Run, + "caretOffset"_a=t->caret_Offset, + "metricDataFormat"_a=t->metric_Data_Format, + "numOfLongVerMetrics"_a=t->number_Of_VMetrics); } - case 5: { - char post_dict[] = "{s:(h,H), s:(h,H), s:h, s:h, s:k, s:k, s:k, s:k, s:k}"; - TT_Postscript *t = (TT_Postscript *)table; - return Py_BuildValue(post_dict, - "format", FIXED_MAJOR(t->FormatType), FIXED_MINOR(t->FormatType), - "italicAngle", FIXED_MAJOR(t->italicAngle), FIXED_MINOR(t->italicAngle), - "underlinePosition", t->underlinePosition, - "underlineThickness", t->underlineThickness, - "isFixedPitch", t->isFixedPitch, - "minMemType42", t->minMemType42, - "maxMemType42", t->maxMemType42, - "minMemType1", t->minMemType1, - "maxMemType1", t->maxMemType1); + case FT_SFNT_POST: { + auto t = static_cast(table); + return py::dict( + "format"_a=py::make_tuple(FIXED_MAJOR(t->FormatType), + FIXED_MINOR(t->FormatType)), + "italicAngle"_a=py::make_tuple(FIXED_MAJOR(t->italicAngle), + FIXED_MINOR(t->italicAngle)), + "underlinePosition"_a=t->underlinePosition, + "underlineThickness"_a=t->underlineThickness, + "isFixedPitch"_a=t->isFixedPitch, + "minMemType42"_a=t->minMemType42, + "maxMemType42"_a=t->maxMemType42, + "minMemType1"_a=t->minMemType1, + "maxMemType1"_a=t->maxMemType1); } - case 6: { - char pclt_dict[] = - "{s:(h,H), s:k, s:H, s:H, s:H, s:H, s:H, s:H, s:y#, s:y#, s:b, " - "s:b, s:b}"; - TT_PCLT *t = (TT_PCLT *)table; - return Py_BuildValue(pclt_dict, - "version", FIXED_MAJOR(t->Version), FIXED_MINOR(t->Version), - "fontNumber", t->FontNumber, - "pitch", t->Pitch, - "xHeight", t->xHeight, - "style", t->Style, - "typeFamily", t->TypeFamily, - "capHeight", t->CapHeight, - "symbolSet", t->SymbolSet, - "typeFace", t->TypeFace, Py_ssize_t(16), - "characterComplement", t->CharacterComplement, Py_ssize_t(8), - "strokeWeight", t->StrokeWeight, - "widthType", t->WidthType, - "serifStyle", t->SerifStyle); + case FT_SFNT_PCLT: { + auto t = static_cast(table); + return py::dict( + "version"_a=py::make_tuple(FIXED_MAJOR(t->Version), + FIXED_MINOR(t->Version)), + "fontNumber"_a=t->FontNumber, + "pitch"_a=t->Pitch, + "xHeight"_a=t->xHeight, + "style"_a=t->Style, + "typeFamily"_a=t->TypeFamily, + "capHeight"_a=t->CapHeight, + "symbolSet"_a=t->SymbolSet, + "typeFace"_a=py::bytes(reinterpret_cast(t->TypeFace), 16), + "characterComplement"_a=py::bytes( + reinterpret_cast(t->CharacterComplement), 8), + "strokeWeight"_a=t->StrokeWeight, + "widthType"_a=t->WidthType, + "serifStyle"_a=t->SerifStyle); } default: - Py_RETURN_NONE; + return std::nullopt; } } const char *PyFT2Font_get_path__doc__ = - "get_path(self)\n" - "--\n\n" - "Get the path data from the currently loaded glyph as a tuple of vertices, " - "codes.\n"; + "Get the path data from the currently loaded glyph as a tuple of vertices, codes."; -static PyObject *PyFT2Font_get_path(PyFT2Font *self, PyObject *args) +static py::tuple +PyFT2Font_get_path(PyFT2Font *self) { std::vector vertices; std::vector codes; - CALL_CPP("get_path", self->x->get_path(vertices, codes)); + self->x->get_path(vertices, codes); - npy_intp length = codes.size(); - npy_intp vertices_dims[2] = { length, 2 }; - numpy::array_view vertices_arr(vertices_dims); - memcpy(vertices_arr.data(), vertices.data(), sizeof(double) * vertices.size()); - npy_intp codes_dims[1] = { length }; - numpy::array_view codes_arr(codes_dims); - memcpy(codes_arr.data(), codes.data(), codes.size()); + py::ssize_t length = codes.size(); + py::ssize_t vertices_dims[2] = { length, 2 }; + py::array_t vertices_arr(vertices_dims); + if (length > 0) { + memcpy(vertices_arr.mutable_data(), vertices.data(), vertices_arr.nbytes()); + } + py::ssize_t codes_dims[1] = { length }; + py::array_t codes_arr(codes_dims); + if (length > 0) { + memcpy(codes_arr.mutable_data(), codes.data(), codes_arr.nbytes()); + } - return Py_BuildValue("NN", vertices_arr.pyobj(), codes_arr.pyobj()); + return py::make_tuple(vertices_arr, codes_arr); } const char *PyFT2Font_get_image__doc__ = - "get_image(self)\n" - "--\n\n" - "Return the underlying image buffer for this font object.\n"; + "Return the underlying image buffer for this font object."; -static PyObject *PyFT2Font_get_image(PyFT2Font *self, PyObject *args) +static py::array +PyFT2Font_get_image(PyFT2Font *self) { FT2Image &im = self->x->get_image(); - npy_intp dims[] = {(npy_intp)im.get_height(), (npy_intp)im.get_width() }; - return PyArray_SimpleNewFromData(2, dims, NPY_UBYTE, im.get_buffer()); + py::ssize_t dims[] = { + static_cast(im.get_height()), + static_cast(im.get_width()) + }; + return py::array_t(dims, im.get_buffer()); } -static PyObject *PyFT2Font_postscript_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_postscript_name(PyFT2Font *self) { const char *ps_name = FT_Get_Postscript_Name(self->x->get_face()); if (ps_name == NULL) { ps_name = "UNAVAILABLE"; } - return PyUnicode_FromString(ps_name); + return ps_name; } -static PyObject *PyFT2Font_num_faces(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_num_faces(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_faces); + return self->x->get_face()->num_faces; } -static PyObject *PyFT2Font_family_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_family_name(PyFT2Font *self) { const char *name = self->x->get_face()->family_name; if (name == NULL) { name = "UNAVAILABLE"; } - return PyUnicode_FromString(name); + return name; } -static PyObject *PyFT2Font_style_name(PyFT2Font *self, void *closure) +static const char * +PyFT2Font_style_name(PyFT2Font *self) { const char *name = self->x->get_face()->style_name; if (name == NULL) { name = "UNAVAILABLE"; } - return PyUnicode_FromString(name); + return name; } -static PyObject *PyFT2Font_face_flags(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_face_flags(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->face_flags); + return self->x->get_face()->face_flags; } -static PyObject *PyFT2Font_style_flags(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_style_flags(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->style_flags); + return self->x->get_face()->style_flags; } -static PyObject *PyFT2Font_num_glyphs(PyFT2Font *self, void *closure) +static FT_Long +PyFT2Font_num_glyphs(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_glyphs); + return self->x->get_face()->num_glyphs; } -static PyObject *PyFT2Font_num_fixed_sizes(PyFT2Font *self, void *closure) +static FT_Int +PyFT2Font_num_fixed_sizes(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_fixed_sizes); + return self->x->get_face()->num_fixed_sizes; } -static PyObject *PyFT2Font_num_charmaps(PyFT2Font *self, void *closure) +static FT_Int +PyFT2Font_num_charmaps(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->num_charmaps); + return self->x->get_face()->num_charmaps; } -static PyObject *PyFT2Font_scalable(PyFT2Font *self, void *closure) +static bool +PyFT2Font_scalable(PyFT2Font *self) { if (FT_IS_SCALABLE(self->x->get_face())) { - Py_RETURN_TRUE; + return true; } - Py_RETURN_FALSE; + return false; } -static PyObject *PyFT2Font_units_per_EM(PyFT2Font *self, void *closure) +static FT_UShort +PyFT2Font_units_per_EM(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->units_per_EM); + return self->x->get_face()->units_per_EM; } -static PyObject *PyFT2Font_get_bbox(PyFT2Font *self, void *closure) +static py::tuple +PyFT2Font_get_bbox(PyFT2Font *self) { FT_BBox *bbox = &(self->x->get_face()->bbox); - return Py_BuildValue("llll", - bbox->xMin, bbox->yMin, bbox->xMax, bbox->yMax); + return py::make_tuple(bbox->xMin, bbox->yMin, bbox->xMax, bbox->yMax); } -static PyObject *PyFT2Font_ascender(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_ascender(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->ascender); + return self->x->get_face()->ascender; } -static PyObject *PyFT2Font_descender(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_descender(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->descender); + return self->x->get_face()->descender; } -static PyObject *PyFT2Font_height(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_height(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->height); + return self->x->get_face()->height; } -static PyObject *PyFT2Font_max_advance_width(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_max_advance_width(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->max_advance_width); + return self->x->get_face()->max_advance_width; } -static PyObject *PyFT2Font_max_advance_height(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_max_advance_height(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->max_advance_height); + return self->x->get_face()->max_advance_height; } -static PyObject *PyFT2Font_underline_position(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_underline_position(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->underline_position); + return self->x->get_face()->underline_position; } -static PyObject *PyFT2Font_underline_thickness(PyFT2Font *self, void *closure) +static FT_Short +PyFT2Font_underline_thickness(PyFT2Font *self) { - return PyLong_FromLong(self->x->get_face()->underline_thickness); + return self->x->get_face()->underline_thickness; } -static PyObject *PyFT2Font_fname(PyFT2Font *self, void *closure) +static py::str +PyFT2Font_fname(PyFT2Font *self) { if (self->stream.close) { // Called passed a filename to the constructor. - return PyObject_GetAttrString(self->py_file, "name"); + return self->py_file.attr("name"); } else { - Py_INCREF(self->py_file); - return self->py_file; + return py::cast(self->py_file); } } -static int PyFT2Font_get_buffer(PyFT2Font *self, Py_buffer *buf, int flags) +PYBIND11_MODULE(ft2font, m) { - FT2Image &im = self->x->get_image(); - - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = im.get_buffer(); - buf->len = im.get_width() * im.get_height(); - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 2; - self->shape[0] = im.get_height(); - self->shape[1] = im.get_width(); - buf->shape = self->shape; - self->strides[0] = im.get_width(); - self->strides[1] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} - -static PyTypeObject *PyFT2Font_init_type() -{ - static PyGetSetDef getset[] = { - {(char *)"postscript_name", (getter)PyFT2Font_postscript_name, NULL, NULL, NULL}, - {(char *)"num_faces", (getter)PyFT2Font_num_faces, NULL, NULL, NULL}, - {(char *)"family_name", (getter)PyFT2Font_family_name, NULL, NULL, NULL}, - {(char *)"style_name", (getter)PyFT2Font_style_name, NULL, NULL, NULL}, - {(char *)"face_flags", (getter)PyFT2Font_face_flags, NULL, NULL, NULL}, - {(char *)"style_flags", (getter)PyFT2Font_style_flags, NULL, NULL, NULL}, - {(char *)"num_glyphs", (getter)PyFT2Font_num_glyphs, NULL, NULL, NULL}, - {(char *)"num_fixed_sizes", (getter)PyFT2Font_num_fixed_sizes, NULL, NULL, NULL}, - {(char *)"num_charmaps", (getter)PyFT2Font_num_charmaps, NULL, NULL, NULL}, - {(char *)"scalable", (getter)PyFT2Font_scalable, NULL, NULL, NULL}, - {(char *)"units_per_EM", (getter)PyFT2Font_units_per_EM, NULL, NULL, NULL}, - {(char *)"bbox", (getter)PyFT2Font_get_bbox, NULL, NULL, NULL}, - {(char *)"ascender", (getter)PyFT2Font_ascender, NULL, NULL, NULL}, - {(char *)"descender", (getter)PyFT2Font_descender, NULL, NULL, NULL}, - {(char *)"height", (getter)PyFT2Font_height, NULL, NULL, NULL}, - {(char *)"max_advance_width", (getter)PyFT2Font_max_advance_width, NULL, NULL, NULL}, - {(char *)"max_advance_height", (getter)PyFT2Font_max_advance_height, NULL, NULL, NULL}, - {(char *)"underline_position", (getter)PyFT2Font_underline_position, NULL, NULL, NULL}, - {(char *)"underline_thickness", (getter)PyFT2Font_underline_thickness, NULL, NULL, NULL}, - {(char *)"fname", (getter)PyFT2Font_fname, NULL, NULL, NULL}, - {NULL} - }; - - static PyMethodDef methods[] = { - {"clear", (PyCFunction)PyFT2Font_clear, METH_NOARGS, PyFT2Font_clear__doc__}, - {"set_size", (PyCFunction)PyFT2Font_set_size, METH_VARARGS, PyFT2Font_set_size__doc__}, - {"set_charmap", (PyCFunction)PyFT2Font_set_charmap, METH_VARARGS, PyFT2Font_set_charmap__doc__}, - {"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__}, - {"_get_fontmap", (PyCFunction)PyFT2Font_get_fontmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_get_fontmap__doc__}, - {"get_num_glyphs", (PyCFunction)PyFT2Font_get_num_glyphs, METH_NOARGS, PyFT2Font_get_num_glyphs__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__}, - {"get_bitmap_offset", (PyCFunction)PyFT2Font_get_bitmap_offset, METH_NOARGS, PyFT2Font_get_bitmap_offset__doc__}, - {"get_descent", (PyCFunction)PyFT2Font_get_descent, METH_NOARGS, PyFT2Font_get_descent__doc__}, - {"draw_glyphs_to_bitmap", (PyCFunction)PyFT2Font_draw_glyphs_to_bitmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_draw_glyphs_to_bitmap__doc__}, - {"draw_glyph_to_bitmap", (PyCFunction)PyFT2Font_draw_glyph_to_bitmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_draw_glyph_to_bitmap__doc__}, - {"get_glyph_name", (PyCFunction)PyFT2Font_get_glyph_name, METH_VARARGS, PyFT2Font_get_glyph_name__doc__}, - {"get_charmap", (PyCFunction)PyFT2Font_get_charmap, METH_NOARGS, PyFT2Font_get_charmap__doc__}, - {"get_char_index", (PyCFunction)PyFT2Font_get_char_index, METH_VARARGS, PyFT2Font_get_char_index__doc__}, - {"get_sfnt", (PyCFunction)PyFT2Font_get_sfnt, METH_NOARGS, PyFT2Font_get_sfnt__doc__}, - {"get_name_index", (PyCFunction)PyFT2Font_get_name_index, METH_VARARGS, PyFT2Font_get_name_index__doc__}, - {"get_ps_font_info", (PyCFunction)PyFT2Font_get_ps_font_info, METH_NOARGS, PyFT2Font_get_ps_font_info__doc__}, - {"get_sfnt_table", (PyCFunction)PyFT2Font_get_sfnt_table, METH_VARARGS, PyFT2Font_get_sfnt_table__doc__}, - {"get_path", (PyCFunction)PyFT2Font_get_path, METH_NOARGS, PyFT2Font_get_path__doc__}, - {"get_image", (PyCFunction)PyFT2Font_get_image, METH_NOARGS, PyFT2Font_get_image__doc__}, - {NULL} + auto ia = [m]() -> const void* { + import_array(); + return &m; }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyFT2Font_get_buffer; - - PyFT2FontType.tp_name = "matplotlib.ft2font.FT2Font"; - PyFT2FontType.tp_doc = PyFT2Font_init__doc__; - PyFT2FontType.tp_basicsize = sizeof(PyFT2Font); - PyFT2FontType.tp_dealloc = (destructor)PyFT2Font_dealloc; - PyFT2FontType.tp_flags = Py_TPFLAGS_DEFAULT; - PyFT2FontType.tp_methods = methods; - PyFT2FontType.tp_getset = getset; - PyFT2FontType.tp_new = PyFT2Font_new; - PyFT2FontType.tp_init = (initproc)PyFT2Font_init; - PyFT2FontType.tp_as_buffer = &buffer_procs; - - return &PyFT2FontType; -} - -static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "ft2font" }; - -PyMODINIT_FUNC PyInit_ft2font(void) -{ - import_array(); + if (ia() == NULL) { + throw py::error_already_set(); + } if (FT_Init_FreeType(&_ft2Library)) { // initialize library - return PyErr_Format( - PyExc_RuntimeError, "Could not initialize the freetype2 library"); + throw std::runtime_error("Could not initialize the freetype2 library"); } FT_Int major, minor, patch; char version_string[64]; FT_Library_Version(_ft2Library, &major, &minor, &patch); snprintf(version_string, sizeof(version_string), "%d.%d.%d", major, minor, patch); - PyObject *m; - if (!(m = PyModule_Create(&moduledef)) || - prepare_and_add_type(PyFT2Image_init_type(), m) || - prepare_and_add_type(PyFT2Font_init_type(), m) || - // Glyph is not constructible from Python, thus not added to the module. - PyType_Ready(PyGlyph_init_type()) || - PyModule_AddStringConstant(m, "__freetype_version__", version_string) || - PyModule_AddStringConstant(m, "__freetype_build_type__", FREETYPE_BUILD_TYPE) || - PyModule_AddIntConstant(m, "SCALABLE", FT_FACE_FLAG_SCALABLE) || - PyModule_AddIntConstant(m, "FIXED_SIZES", FT_FACE_FLAG_FIXED_SIZES) || - PyModule_AddIntConstant(m, "FIXED_WIDTH", FT_FACE_FLAG_FIXED_WIDTH) || - PyModule_AddIntConstant(m, "SFNT", FT_FACE_FLAG_SFNT) || - PyModule_AddIntConstant(m, "HORIZONTAL", FT_FACE_FLAG_HORIZONTAL) || - PyModule_AddIntConstant(m, "VERTICAL", FT_FACE_FLAG_VERTICAL) || - PyModule_AddIntConstant(m, "KERNING", FT_FACE_FLAG_KERNING) || - PyModule_AddIntConstant(m, "FAST_GLYPHS", FT_FACE_FLAG_FAST_GLYPHS) || - PyModule_AddIntConstant(m, "MULTIPLE_MASTERS", FT_FACE_FLAG_MULTIPLE_MASTERS) || - PyModule_AddIntConstant(m, "GLYPH_NAMES", FT_FACE_FLAG_GLYPH_NAMES) || - PyModule_AddIntConstant(m, "EXTERNAL_STREAM", FT_FACE_FLAG_EXTERNAL_STREAM) || - PyModule_AddIntConstant(m, "ITALIC", FT_STYLE_FLAG_ITALIC) || - PyModule_AddIntConstant(m, "BOLD", FT_STYLE_FLAG_BOLD) || - PyModule_AddIntConstant(m, "KERNING_DEFAULT", FT_KERNING_DEFAULT) || - PyModule_AddIntConstant(m, "KERNING_UNFITTED", FT_KERNING_UNFITTED) || - PyModule_AddIntConstant(m, "KERNING_UNSCALED", FT_KERNING_UNSCALED) || - PyModule_AddIntConstant(m, "LOAD_DEFAULT", FT_LOAD_DEFAULT) || - PyModule_AddIntConstant(m, "LOAD_NO_SCALE", FT_LOAD_NO_SCALE) || - PyModule_AddIntConstant(m, "LOAD_NO_HINTING", FT_LOAD_NO_HINTING) || - PyModule_AddIntConstant(m, "LOAD_RENDER", FT_LOAD_RENDER) || - PyModule_AddIntConstant(m, "LOAD_NO_BITMAP", FT_LOAD_NO_BITMAP) || - PyModule_AddIntConstant(m, "LOAD_VERTICAL_LAYOUT", FT_LOAD_VERTICAL_LAYOUT) || - PyModule_AddIntConstant(m, "LOAD_FORCE_AUTOHINT", FT_LOAD_FORCE_AUTOHINT) || - PyModule_AddIntConstant(m, "LOAD_CROP_BITMAP", FT_LOAD_CROP_BITMAP) || - PyModule_AddIntConstant(m, "LOAD_PEDANTIC", FT_LOAD_PEDANTIC) || - PyModule_AddIntConstant(m, "LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH", FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH) || - PyModule_AddIntConstant(m, "LOAD_NO_RECURSE", FT_LOAD_NO_RECURSE) || - PyModule_AddIntConstant(m, "LOAD_IGNORE_TRANSFORM", FT_LOAD_IGNORE_TRANSFORM) || - PyModule_AddIntConstant(m, "LOAD_MONOCHROME", FT_LOAD_MONOCHROME) || - PyModule_AddIntConstant(m, "LOAD_LINEAR_DESIGN", FT_LOAD_LINEAR_DESIGN) || - PyModule_AddIntConstant(m, "LOAD_NO_AUTOHINT", (unsigned long)FT_LOAD_NO_AUTOHINT) || - PyModule_AddIntConstant(m, "LOAD_TARGET_NORMAL", (unsigned long)FT_LOAD_TARGET_NORMAL) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LIGHT", (unsigned long)FT_LOAD_TARGET_LIGHT) || - PyModule_AddIntConstant(m, "LOAD_TARGET_MONO", (unsigned long)FT_LOAD_TARGET_MONO) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LCD", (unsigned long)FT_LOAD_TARGET_LCD) || - PyModule_AddIntConstant(m, "LOAD_TARGET_LCD_V", (unsigned long)FT_LOAD_TARGET_LCD_V)) { - FT_Done_FreeType(_ft2Library); - Py_XDECREF(m); - return NULL; - } - - return m; + py::class_(m, "FT2Image", py::is_final(), py::buffer_protocol()) + .def(py::init(), "width"_a, "height"_a) + .def("draw_rect_filled", &PyFT2Image_draw_rect_filled, + "x0"_a, "y0"_a, "x1"_a, "y1"_a, + PyFT2Image_draw_rect_filled__doc__) + .def_buffer([](FT2Image &self) -> py::buffer_info { + std::vector shape { self.get_height(), self.get_width() }; + std::vector strides { self.get_width(), 1 }; + return py::buffer_info(self.get_buffer(), shape, strides); + }); + + py::class_(m, "Glyph", py::is_final()) + .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(py::init(&PyFT2Font_init), + "filename"_a, "hinting_factor"_a=8, py::kw_only(), + "_fallback_list"_a=py::none(), "_kerning_factor"_a=0, + PyFT2Font_init__doc__) + .def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__) + .def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a, + PyFT2Font_set_size__doc__) + .def("set_charmap", &PyFT2Font_set_charmap, "i"_a, + PyFT2Font_set_charmap__doc__) + .def("select_charmap", &PyFT2Font_select_charmap, "i"_a, + PyFT2Font_select_charmap__doc__) + .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, + 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, + PyFT2Font_load_char__doc__) + .def("load_glyph", &PyFT2Font_load_glyph, + "glyph_index"_a, "flags"_a=FT_LOAD_FORCE_AUTOHINT, + PyFT2Font_load_glyph__doc__) + .def("get_width_height", &PyFT2Font_get_width_height, + PyFT2Font_get_width_height__doc__) + .def("get_bitmap_offset", &PyFT2Font_get_bitmap_offset, + PyFT2Font_get_bitmap_offset__doc__) + .def("get_descent", &PyFT2Font_get_descent, PyFT2Font_get_descent__doc__) + .def("draw_glyphs_to_bitmap", &PyFT2Font_draw_glyphs_to_bitmap, + py::kw_only(), "antialiased"_a=true, + PyFT2Font_draw_glyphs_to_bitmap__doc__) + .def("draw_glyph_to_bitmap", &PyFT2Font_draw_glyph_to_bitmap, + "image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true, + PyFT2Font_draw_glyph_to_bitmap__doc__) + .def("get_glyph_name", &PyFT2Font_get_glyph_name, "index"_a, + PyFT2Font_get_glyph_name__doc__) + .def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__) + .def("get_char_index", &PyFT2Font_get_char_index, "codepoint"_a, + PyFT2Font_get_char_index__doc__) + .def("get_sfnt", &PyFT2Font_get_sfnt, PyFT2Font_get_sfnt__doc__) + .def("get_name_index", &PyFT2Font_get_name_index, "name"_a, + PyFT2Font_get_name_index__doc__) + .def("get_ps_font_info", &PyFT2Font_get_ps_font_info, + PyFT2Font_get_ps_font_info__doc__) + .def("get_sfnt_table", &PyFT2Font_get_sfnt_table, "name"_a, + PyFT2Font_get_sfnt_table__doc__) + .def("get_path", &PyFT2Font_get_path, PyFT2Font_get_path__doc__) + .def("get_image", &PyFT2Font_get_image, PyFT2Font_get_image__doc__) + + .def_property_readonly("postscript_name", &PyFT2Font_postscript_name, + "PostScript name of the font.") + .def_property_readonly("num_faces", &PyFT2Font_num_faces, + "Number of faces in file.") + .def_property_readonly("family_name", &PyFT2Font_family_name, + "Face family name.") + .def_property_readonly("style_name", &PyFT2Font_style_name, + "Style name.") + .def_property_readonly("face_flags", &PyFT2Font_face_flags, + "Face flags; see the ft2font constants.") + .def_property_readonly("style_flags", &PyFT2Font_style_flags, + "Style flags; see the ft2font constants.") + .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("scalable", &PyFT2Font_scalable, + "Whether face is scalable; attributes after this one " + "are only defined for scalable faces.") + .def_property_readonly("units_per_EM", &PyFT2Font_units_per_EM, + "Number of font units covered by the EM.") + .def_property_readonly("bbox", &PyFT2Font_get_bbox, + "Face global bounding box (xmin, ymin, xmax, ymax).") + .def_property_readonly("ascender", &PyFT2Font_ascender, + "Ascender in 26.6 units.") + .def_property_readonly("descender", &PyFT2Font_descender, + "Descender in 26.6 units.") + .def_property_readonly("height", &PyFT2Font_height, + "Height in 26.6 units; used to compute a default line " + "spacing (baseline-to-baseline distance).") + .def_property_readonly("max_advance_width", &PyFT2Font_max_advance_width, + "Maximum horizontal cursor advance for all glyphs.") + .def_property_readonly("max_advance_height", &PyFT2Font_max_advance_height, + "Maximum vertical cursor advance for all glyphs.") + .def_property_readonly("underline_position", &PyFT2Font_underline_position, + "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_buffer([](PyFT2Font &self) -> py::buffer_info { + FT2Image &im = self.x->get_image(); + std::vector shape { im.get_height(), im.get_width() }; + std::vector strides { im.get_width(), 1 }; + return py::buffer_info(im.get_buffer(), shape, strides); + }); + + 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; } diff --git a/src/meson.build b/src/meson.build index a046b3306ab8..4edd8451aad2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -94,7 +94,7 @@ extension_data = { 'py_converters.cpp', ), 'dependencies': [ - freetype_dep, numpy_dep, agg_dep.partial_dependency(includes: true), + freetype_dep, pybind11_dep, numpy_dep, agg_dep.partial_dependency(includes: true), ], 'cpp_args': [ '-DFREETYPE_BUILD_TYPE="@0@"'.format( From a9d4633b6f6583042f432df6bf467390f9e9a1fd Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 7 Sep 2024 03:28:58 -0400 Subject: [PATCH 2/5] Simplify FT2Font.get_font Inline `convert_xys_to_array` and modify the arguments to take a C++ container, so we don't need a less-safe pointer, and we don't need to copy another time over. --- src/ft2font.cpp | 10 +++++----- src/ft2font.h | 6 +++--- src/ft2font_wrapper.cpp | 33 +++++++-------------------------- 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 34a602562735..f9700fa2ee7d 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -397,7 +397,7 @@ void FT2Font::set_kerning_factor(int factor) } void FT2Font::set_text( - size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector &xys) + std::u32string_view text, double angle, FT_Int32 flags, std::vector &xys) { FT_Matrix matrix; /* transformation matrix */ @@ -420,7 +420,7 @@ void FT2Font::set_text( FT_UInt previous = 0; FT2Font *previous_ft_object = NULL; - for (size_t n = 0; n < N; n++) { + for (auto codepoint : text) { FT_UInt glyph_index = 0; FT_BBox glyph_bbox; FT_Pos last_advance; @@ -429,14 +429,14 @@ void FT2Font::set_text( std::set glyph_seen_fonts; FT2Font *ft_object_with_glyph = this; bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs, - char_to_font, glyph_to_font, codepoints[n], flags, + char_to_font, glyph_to_font, codepoint, flags, charcode_error, glyph_error, glyph_seen_fonts, false); if (!was_found) { - ft_glyph_warn((FT_ULong)codepoints[n], glyph_seen_fonts); + ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); // render missing glyph tofu // come back to top-most font ft_object_with_glyph = this; - char_to_font[codepoints[n]] = ft_object_with_glyph; + char_to_font[codepoint] = ft_object_with_glyph; glyph_to_font[glyph_index] = ft_object_with_glyph; ft_object_with_glyph->load_glyph(glyph_index, flags, ft_object_with_glyph, false); } diff --git a/src/ft2font.h b/src/ft2font.h index 7891c6050341..79b0e1ccc518 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -6,9 +6,9 @@ #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H -#include #include #include +#include #include #include @@ -77,8 +77,8 @@ class FT2Font void set_size(double ptsize, double dpi); void set_charmap(int i); void select_charmap(unsigned long i); - void set_text( - size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector &xys); + 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); void set_kerning_factor(int factor); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 27ba249ec916..704ffe51c0c0 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -6,7 +6,6 @@ #include "ft2font.h" #include "numpy/arrayobject.h" -#include #include #include #include @@ -14,17 +13,6 @@ namespace py = pybind11; using namespace pybind11::literals; -static py::array_t -convert_xys_to_array(std::vector &xys) -{ - py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; - py::array_t result(dims); - if (xys.size() > 0) { - memcpy(result.mutable_data(), xys.data(), result.nbytes()); - } - return result; -} - /********************************************************************** * FT2Image * */ @@ -346,26 +334,19 @@ const char *PyFT2Font_set_text__doc__ = "A sequence of x,y positions in 26.6 subpixels is returned; divide by 64 for pixels.\n"; static py::array_t -PyFT2Font_set_text(PyFT2Font *self, std::u32string text, double angle = 0.0, +PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0, FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT) { std::vector xys; - std::vector codepoints; - size_t size; - size = text.size(); - codepoints.resize(size); - for (size_t i = 0; i < size; ++i) { - codepoints[i] = text[i]; - } + self->x->set_text(text, angle, flags, xys); - uint32_t* codepoints_array = NULL; - if (size > 0) { - codepoints_array = &codepoints[0]; + py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; + py::array_t result(dims); + if (xys.size() > 0) { + memcpy(result.mutable_data(), xys.data(), result.nbytes()); } - self->x->set_text(size, codepoints_array, angle, flags, xys); - - return convert_xys_to_array(xys); + return result; } const char *PyFT2Font_get_num_glyphs__doc__ = From 1140e149bf3fbb331f4fb5b31e613d1146642d90 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 7 Sep 2024 04:41:09 -0400 Subject: [PATCH 3/5] Use STL type for FT2Font fallback list This allows pybind11 to generate the type hints for us, and it also takes care of checking the list and its contents are the right type. --- lib/matplotlib/tests/test_ft2font.py | 4 ++-- src/ft2font_wrapper.cpp | 22 ++++++---------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 1bfa990bd8f5..ace4cea5865e 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -152,10 +152,10 @@ def test_ft2font_invalid_args(tmp_path): with pytest.raises(ValueError, match='hinting_factor must be greater than 0'): ft2font.FT2Font(file, 0) - with pytest.raises(TypeError, match='Fallback list must be a list'): + with pytest.raises(TypeError, match='incompatible constructor arguments'): # failing to be a list will fail before the 0 ft2font.FT2Font(file, _fallback_list=(0,)) # type: ignore[arg-type] - with pytest.raises(TypeError, match='Fallback fonts must be FT2Font objects.'): + with pytest.raises(TypeError, match='incompatible constructor arguments'): ft2font.FT2Font(file, _fallback_list=[0]) # type: ignore[list-item] # kerning_factor argument. diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 704ffe51c0c0..6021e0a17535 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -172,7 +172,8 @@ const char *PyFT2Font_init__doc__ = static PyFT2Font * PyFT2Font_init(py::object filename, long hinting_factor = 8, - py::object fallback_list_or_none = py::none(), int kerning_factor = 0) + std::optional> fallback_list = std::nullopt, + int kerning_factor = 0) { if (hinting_factor <= 0) { throw py::value_error("hinting_factor must be greater than 0"); @@ -192,24 +193,13 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, open_args.stream = &self->stream; std::vector fallback_fonts; - if (!fallback_list_or_none.is_none()) { - if (!py::isinstance(fallback_list_or_none)) { - throw py::type_error("Fallback list must be a list"); - } - auto fallback_list = fallback_list_or_none.cast(); - - // go through fallbacks once to make sure the types are right - for (auto item : fallback_list) { - if (!py::isinstance(item)) { - throw py::type_error("Fallback fonts must be FT2Font objects."); - } - } - // go through a second time to add them to our lists - for (auto item : fallback_list) { + if (fallback_list) { + // go through fallbacks to add them to our lists + for (auto item : *fallback_list) { self->fallbacks.append(item); // Also (locally) cache the underlying FT2Font objects. As long as // the Python objects are kept alive, these pointer are good. - FT2Font *fback = py::cast(item)->x; + FT2Font *fback = item->x; fallback_fonts.push_back(fback); } } From 6ef1b97c928c4641d5c8ea5da5a1e1c45733c2a5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 9 Sep 2024 23:06:47 -0400 Subject: [PATCH 4/5] Replace std::to_chars with plain snprintf The former is not available on the macOS deployment target we use for wheels. We could revert back to `PyOS_snprintf`, but C++11 contains `snprintf`, and it seems to guarantee the same things. --- src/ft2font.cpp | 15 +++++++++++---- src/ft2font_wrapper.cpp | 5 ----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index f9700fa2ee7d..dc6bc419d43e 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -1,7 +1,7 @@ /* -*- mode: c++; c-basic-offset: 4 -*- */ #include -#include +#include #include #include #include @@ -727,13 +727,20 @@ void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ - buffer.replace(0, 3, "uni"); - std::to_chars(buffer.data() + 3, buffer.data() + buffer.size(), - glyph_number, 16); + auto len = snprintf(buffer.data(), buffer.size(), "uni%08x", glyph_number); + if (len >= 0) { + buffer.resize(len); + } else { + throw std::runtime_error("Failed to convert glyph to standard name"); + } } else { if (FT_Error error = FT_Get_Glyph_Name(face, glyph_number, buffer.data(), buffer.size())) { throw_ft_error("Could not get glyph names", error); } + auto len = buffer.find('\0'); + if (len != buffer.npos) { + buffer.resize(len); + } } } diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6021e0a17535..9791dc7e2e06 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -486,11 +486,6 @@ PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) buffer.resize(128); self->x->get_glyph_name(glyph_number, buffer, fallback); - // pybind11 uses the entire string's size(), so trim all the NULLs off the end. - auto len = buffer.find('\0'); - if (len != buffer.npos) { - buffer.resize(len); - } return buffer; } From a0649e792797e16e5c2d84e267c9a848b9905272 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 10 Sep 2024 01:46:18 -0400 Subject: [PATCH 5/5] DOC: Hide pybind11 base object from inheritance And also ignore the `numpy.float64` reference. The latter seems to be broken since Sphinx tries to auto-link type hints as `py:class`, but it's an alias in NumPy making it a `py:attr` in their inventory. --- doc/conf.py | 15 +++++++++++++++ doc/missing-references.json | 3 +++ 2 files changed, 18 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index d153aead739c..ea6b1a3fa444 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -230,6 +230,20 @@ def tutorials_download_error(record): autodoc_docstring_signature = True autodoc_default_options = {'members': None, 'undoc-members': None} + +def autodoc_process_bases(app, name, obj, options, bases): + """ + Hide pybind11 base object from inheritance tree. + + Note, *bases* must be modified in place. + """ + for cls in bases[:]: + if not isinstance(cls, type): + continue + if cls.__module__ == 'pybind11_builtins' and cls.__name__ == 'pybind11_object': + bases.remove(cls) + + # make sure to ignore warnings that stem from simply inspecting deprecated # class-level attributes warnings.filterwarnings('ignore', category=DeprecationWarning, @@ -847,5 +861,6 @@ def setup(app): bld_type = 'rel' app.add_config_value('skip_sub_dirs', 0, '') app.add_config_value('releaselevel', bld_type, 'env') + app.connect('autodoc-process-bases', autodoc_process_bases) if sphinx.version_info[:2] < (7, 1): app.connect('html-page-context', add_html_cache_busting, priority=1000) diff --git a/doc/missing-references.json b/doc/missing-references.json index a0eb69308eb4..089434172e58 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -268,6 +268,9 @@ ":1", "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:32::1" ], + "numpy.float64": [ + "doc/docstring of matplotlib.ft2font.PyCapsule.set_text:1" + ], "numpy.uint8": [ ":1" ]