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 a2bf9ccf92b..9bc21586994 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 c9938fd3eac..7978bb40fde 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 */ @@ -1448,6 +1486,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}, {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS},