From 97e7a971e60f75f811f17265ba093f6e5c70a1bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 18 Aug 2025 12:14:59 +1000 Subject: [PATCH 1/2] 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}, From acc481625a160144fdaa10850903af4bb2708a39 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jan 2026 21:02:03 +1100 Subject: [PATCH 2/2] Added release notes --- docs/releasenotes/12.2.0.rst | 64 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 65 insertions(+) create mode 100644 docs/releasenotes/12.2.0.rst diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst new file mode 100644 index 00000000000..9092af1ea95 --- /dev/null +++ b/docs/releasenotes/12.2.0.rst @@ -0,0 +1,64 @@ +12.2.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +FreeTypeFont.has_characters +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFont.FreeTypeFont`` has a new method for checking whether characters are present +in the font or not. :py:meth:`.ImageFont.FreeTypeFont.has_characters` will return +``True`` if all of the characters in the given string are available:: + + from PIL import ImageFont + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + font.has_characters("Test") # True + font.has_characters(b"Test") # True + font.has_characters("\u2022") # True + font.has_characters("\u0000") # False + +Other changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 4b25bb6a2d1..0f684501538 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,6 +15,7 @@ expected to be backported to earlier versions. :maxdepth: 2 versioning + 12.2.0 12.1.0 12.0.0 11.3.0