From 97e7a971e60f75f811f17265ba093f6e5c70a1bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 18 Aug 2025 12:14:59 +1000 Subject: [PATCH] Added FreeTypeFont has_characters() --- Tests/test_imagefont.py | 9 +++++++++ src/PIL/ImageFont.py | 10 ++++++++++ src/PIL/_imagingft.pyi | 1 + src/_imagingft.c | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4565d35bab7..8fa0f92742f 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -891,6 +891,15 @@ def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) +def test_has_characters(font: ImageFont.FreeTypeFont) -> None: + assert font.has_characters("") + + assert font.has_characters("Test text") + assert font.has_characters(b"Test text") + + assert not font.has_characters("Test \u0001") + + @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: text = "Bitmap Font" diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index bf3f471f5e3..b801186c6d9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -636,6 +636,16 @@ def fill(width: int, height: int) -> Image.core.ImagingCore: start, ) + def has_characters(self, text: str | bytes) -> bool: + """ + Check if the font has all of the characters in the text. + + :param text: Text to render. + + :return: Boolean. + """ + return self.font.hascharacters(text) + def font_variant( self, font: StrOrBytesPath | BinaryIO | None = None, diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 2136810ba6a..116c98a4c5f 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -54,6 +54,7 @@ class Font: lang: str | None, /, ) -> float: ... + def hascharacters(self, string: str | bytes) -> bool: ... def getvarnames(self) -> list[bytes]: ... def getvaraxes(self) -> list[ImageFont.Axis]: ... def setvarname(self, instance_index: int, /) -> None: ... diff --git a/src/_imagingft.c b/src/_imagingft.c index 29d8e9e7112..5e119262054 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -517,6 +517,44 @@ text_layout( return count; } +static PyObject * +font_hascharacters(FontObject *self, PyObject *args) { + int i; + char *buffer = NULL; + FT_ULong ch; + Py_ssize_t count; + FT_GlyphSlot glyph; + PyObject *string; + + if (!PyArg_ParseTuple(args, "O", &string)) { + return NULL; + } + + if (PyUnicode_Check(string)) { + count = PyUnicode_GET_LENGTH(string); + } else if (PyBytes_Check(string)) { + PyBytes_AsStringAndSize(string, &buffer, &count); + } else { + PyErr_SetString(PyExc_TypeError, "expected string or bytes"); + return 0; + } + if (count == 0) { + return Py_True; + } + + for (i = 0; i < count; i++) { + if (buffer) { + ch = buffer[i]; + } else { + ch = PyUnicode_READ_CHAR(string, i); + } + if (FT_Get_Char_Index(self->face, ch) == 0) { + return Py_False; + } + } + return Py_True; +} + static PyObject * font_getlength(FontObject *self, PyObject *args) { int length; /* length along primary axis, in 26.6 precision */ @@ -1451,6 +1489,7 @@ static PyMethodDef font_methods[] = { {"render", (PyCFunction)font_render, METH_VARARGS}, {"getsize", (PyCFunction)font_getsize, METH_VARARGS}, {"getlength", (PyCFunction)font_getlength, METH_VARARGS}, + {"hascharacters", (PyCFunction)font_hascharacters, METH_VARARGS}, #if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS},