From b3e3784b8ebf4fc8c29d9a28d1987fb21ec57c65 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Jun 2024 16:05:58 +1000 Subject: [PATCH] Added byte support to FreeTypeFont --- Tests/test_imagefont.py | 17 +++++++++++ src/PIL/ImageFont.py | 8 +++--- src/PIL/_imagingft.pyi | 4 +-- src/_imagingft.c | 64 +++++++++++++++++++---------------------- 4 files changed, 53 insertions(+), 40 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4398f8a3055..4ca882aab03 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1096,6 +1096,23 @@ def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: imagefont.getmask("A" * 1_000_001) +def test_bytes(font: ImageFont.FreeTypeFont) -> None: + assert font.getlength(b"test") == font.getlength("test") + + assert font.getbbox(b"test") == font.getbbox("test") + + assert_image_equal( + Image.Image()._new(font.getmask(b"test")), + Image.Image()._new(font.getmask("test")), + ) + + assert_image_equal( + Image.Image()._new(font.getmask2(b"test")[0]), + Image.Image()._new(font.getmask2("test")[0]), + ) + assert font.getmask2(b"test")[1] == font.getmask2("test")[1] + + @pytest.mark.parametrize( "test_file", [ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index fa5608e6cd8..ac85f48bedf 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -279,7 +279,7 @@ def getmetrics(self) -> tuple[int, int]: return self.font.ascent, self.font.descent def getlength( - self, text: str, mode="", direction=None, features=None, language=None + self, text: str | bytes, mode="", direction=None, features=None, language=None ) -> float: """ Returns length (in pixels with 1/64 precision) of given text when rendered @@ -354,7 +354,7 @@ def getlength( def getbbox( self, - text: str, + text: str | bytes, mode: str = "", direction: str | None = None, features: list[str] | None = None, @@ -511,7 +511,7 @@ def getmask( def getmask2( self, - text: str, + text: str | bytes, mode="", direction=None, features=None, @@ -730,7 +730,7 @@ def getbbox(self, text, *args, **kwargs): return 0, 0, height, width return 0, 0, width, height - def getlength(self, text: str, *args, **kwargs) -> float: + def getlength(self, text: str | bytes, *args, **kwargs) -> float: if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 6e0ddd2f165..5e97b40b2e0 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -27,7 +27,7 @@ class Font: def glyphs(self) -> int: ... def render( self, - string: str, + string: str | bytes, fill, mode=..., dir=..., @@ -51,7 +51,7 @@ class Font: /, ) -> tuple[tuple[int, int], tuple[int, int]]: ... def getlength( - self, string: str, mode=..., dir=..., features=..., lang=..., / + self, string: str | bytes, mode=..., dir=..., features=..., lang=..., / ) -> float: ... def getvarnames(self) -> list[bytes]: ... def getvaraxes(self) -> list[_Axis] | None: ... diff --git a/src/_imagingft.c b/src/_imagingft.c index e83ddfec122..ba36cc72c23 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -233,18 +233,6 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { return (PyObject *)self; } -static int -font_getchar(PyObject *string, int index, FT_ULong *char_out) { - if (PyUnicode_Check(string)) { - if (index >= PyUnicode_GET_LENGTH(string)) { - return 0; - } - *char_out = PyUnicode_READ_CHAR(string, index); - return 1; - } - return 0; -} - #ifdef HAVE_RAQM static size_t @@ -266,28 +254,34 @@ text_layout_raqm( goto failed; } + Py_ssize_t size; + int set_text; if (PyUnicode_Check(string)) { Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); - Py_ssize_t size = PyUnicode_GET_LENGTH(string); + size = PyUnicode_GET_LENGTH(string); if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ goto failed; } - int set_text = raqm_set_text(rq, text, size); + set_text = raqm_set_text(rq, text, size); PyMem_Free(text); - if (!set_text) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + } else { + char *buffer; + PyBytes_AsStringAndSize(string, &buffer, &size); + if (!buffer || !size) { + /* return 0 and clean up, no glyphs==no size, + and raqm fails with empty strings */ goto failed; } - if (lang) { - if (!raqm_set_language(rq, lang, start, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); - goto failed; - } - } - } else { - PyErr_SetString(PyExc_TypeError, "expected string"); + set_text = raqm_set_text_utf8(rq, buffer, size); + } + if (!set_text) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + goto failed; + } + if (lang && !raqm_set_language(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); goto failed; } @@ -405,13 +399,13 @@ text_layout_fallback( GlyphInfo **glyph_info, int mask, int color) { - int error, load_flags; + int error, load_flags, i; + char *buffer = NULL; FT_ULong ch; Py_ssize_t count; FT_GlyphSlot glyph; FT_Bool kerning = FT_HAS_KERNING(self->face); FT_UInt last_index = 0; - int i; if (features != Py_None || dir != NULL || lang != NULL) { PyErr_SetString( @@ -419,14 +413,11 @@ text_layout_fallback( "setting text direction, language or font features is not supported " "without libraqm"); } - if (!PyUnicode_Check(string)) { - PyErr_SetString(PyExc_TypeError, "expected string"); - return 0; - } - count = 0; - while (font_getchar(string, count, &ch)) { - count++; + if (PyUnicode_Check(string)) { + count = PyUnicode_GET_LENGTH(string); + } else { + PyBytes_AsStringAndSize(string, &buffer, &count); } if (count == 0) { return 0; @@ -445,7 +436,12 @@ text_layout_fallback( if (color) { load_flags |= FT_LOAD_COLOR; } - for (i = 0; font_getchar(string, i, &ch); i++) { + for (i = 0; i < count; i++) { + if (buffer) { + ch = buffer[i]; + } else { + ch = PyUnicode_READ_CHAR(string, i); + } (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); if (error) {