diff --git a/buildconfig/stubs/pygame/font.pyi b/buildconfig/stubs/pygame/font.pyi index 683a28ef2b..27e94ae0c4 100644 --- a/buildconfig/stubs/pygame/font.pyi +++ b/buildconfig/stubs/pygame/font.pyi @@ -81,6 +81,7 @@ class Font: def get_height(self) -> int: ... def get_ascent(self) -> int: ... def get_descent(self) -> int: ... + def is_char_defined(self, char: Union[str, bytes], /) -> bool: ... def set_script(self, script_code: str, /) -> None: ... def set_direction(self, direction: int) -> None: ... def get_point_size(self) -> int: ... diff --git a/buildconfig/stubs/pygame/freetype.pyi b/buildconfig/stubs/pygame/freetype.pyi index 54751f8385..48d0d1207c 100644 --- a/buildconfig/stubs/pygame/freetype.pyi +++ b/buildconfig/stubs/pygame/freetype.pyi @@ -150,6 +150,7 @@ class Font: ) -> List[Tuple[int, int, int, int, float, float]]: ... def get_sized_ascender(self, size: float, /) -> int: ... def get_sized_descender(self, size: float, /) -> int: ... + def is_char_defined(self, char: Union[str, bytes], /) -> bool: ... def get_sized_height(self, size: float, /) -> int: ... def get_sized_glyph_height(self, size: float, /) -> int: ... def get_sizes(self) -> List[Tuple[int, int, int, float, float]]: ... diff --git a/docs/reST/ref/font.rst b/docs/reST/ref/font.rst index 644a3b52b2..3e23bf94ed 100644 --- a/docs/reST/ref/font.rst +++ b/docs/reST/ref/font.rst @@ -306,6 +306,17 @@ solves no longer exists, it will likely be removed in the future. .. ## Font.point_size ## + .. method:: is_char_defined + + | :sl:`Check if a char is defined in the font.` + | :sg:`is_char_defined(char) -> bool` + + This checks if the char is defined in this font. + + .. versionadded:: 2.4.0 + + .. ## Font.is_char_defined ## + .. method:: render | :sl:`draw text on a new Surface` diff --git a/docs/reST/ref/freetype.rst b/docs/reST/ref/freetype.rst index bf6a74609e..c02c53b19e 100644 --- a/docs/reST/ref/freetype.rst +++ b/docs/reST/ref/freetype.rst @@ -395,6 +395,18 @@ loaded. This module must be imported explicitly to be used. :: width in pixels, horizontal ppem (nominal width) in fractional pixels, and vertical ppem (nominal height) in fractional pixels. + + .. method:: is_char_defined + + | :sl:`Check if a char is defined in the font.` + | :sg:`is_char_defined(char) -> bool` + + This checks if the char is defined in this font. + + .. versionadded:: 2.4.0 + + .. ## Font.is_char_defined ## + .. method:: render | :sl:`Return rendered text as a surface` diff --git a/src_c/_freetype.c b/src_c/_freetype.c index f24673f588..f03bfc6886 100644 --- a/src_c/_freetype.c +++ b/src_c/_freetype.c @@ -81,6 +81,8 @@ _ftfont_getrect(pgFontObject *, PyObject *, PyObject *); static PyObject * _ftfont_getmetrics(pgFontObject *, PyObject *, PyObject *); static PyObject * +_ftfont_is_char_defined(pgFontObject *self, PyObject *textobj); +static PyObject * _ftfont_render(pgFontObject *, PyObject *, PyObject *); static PyObject * _ftfont_render_to(pgFontObject *, PyObject *, PyObject *); @@ -533,6 +535,8 @@ static PyMethodDef _ftfont_methods[] = { DOC_FREETYPE_FONT_GETRECT}, {"get_metrics", (PyCFunction)_ftfont_getmetrics, METH_VARARGS | METH_KEYWORDS, DOC_FREETYPE_FONT_GETMETRICS}, + {"is_char_defined", (PyCFunction)_ftfont_is_char_defined, METH_O, + DOC_FREETYPE_FONT_ISCHARDEFINED}, {"get_sizes", (PyCFunction)_ftfont_getsizes, METH_NOARGS, DOC_FREETYPE_FONT_GETSIZES}, {"render", (PyCFunction)_ftfont_render, METH_VARARGS | METH_KEYWORDS, @@ -1570,6 +1574,40 @@ _ftfont_getmetrics(pgFontObject *self, PyObject *args, PyObject *kwds) return 0; } +static PyObject * +_ftfont_is_char_defined(pgFontObject *self, PyObject *textobj) +{ + PGFT_String *text = 0; + int glyph_index; + FT_Face face; + + /* Encode text */ + text = _PGFT_EncodePyString(textobj, self->render_flags & FT_RFLAG_UCS4); + if (!text) + goto error; + + if (text->length > 1) { + free_string(text); + return RAISE(PyExc_ValueError, "Too long, only 1 char supported."); + } + + ASSERT_SELF_IS_ALIVE(self); + + face = _PGFT_GetFont(self->freetype, self); + PGFT_char charcode = text->data[0]; + glyph_index = FT_Get_Char_Index(face, charcode); + if (glyph_index > 0) { + free_string(text); + Py_RETURN_TRUE; + } + + goto error; + +error: + free_string(text); + Py_RETURN_FALSE; +} + static PyObject * _ftfont_getsizedascender(pgFontObject *self, PyObject *args) { diff --git a/src_c/doc/font_doc.h b/src_c/doc/font_doc.h index 4ac4af922c..f7c230fe4b 100644 --- a/src_c/doc/font_doc.h +++ b/src_c/doc/font_doc.h @@ -17,6 +17,7 @@ #define DOC_FONT_FONT_STRIKETHROUGH "strikethrough -> bool\nGets or sets whether the font should be rendered with a strikethrough." #define DOC_FONT_FONT_ALIGN "align -> int\nSet how rendered text is aligned when given a wrap length." #define DOC_FONT_FONT_POINTSIZE "point_size -> int\nGets or sets the font's point size" +#define DOC_FONT_FONT_ISCHARDEFINED "is_char_defined(char) -> bool\nCheck if a char is defined in the font." #define DOC_FONT_FONT_RENDER "render(text, antialias, color, bgcolor=None, wraplength=0) -> Surface\ndraw text on a new Surface" #define DOC_FONT_FONT_SIZE "size(text, /) -> (width, height)\ndetermine the amount of space needed to render text" #define DOC_FONT_FONT_SETUNDERLINE "set_underline(bool, /) -> None\ncontrol if text is rendered with an underline" diff --git a/src_c/doc/freetype_doc.h b/src_c/doc/freetype_doc.h index 0c49a5ee0a..099f1411e8 100644 --- a/src_c/doc/freetype_doc.h +++ b/src_c/doc/freetype_doc.h @@ -26,6 +26,7 @@ #define DOC_FREETYPE_FONT_GETSIZEDHEIGHT "get_sized_height(size=0, /) -> int\nThe scaled height of the font in pixels" #define DOC_FREETYPE_FONT_GETSIZEDGLYPHHEIGHT "get_sized_glyph_height(size=0, /) -> int\nThe scaled bounding box height of the font in pixels" #define DOC_FREETYPE_FONT_GETSIZES "get_sizes() -> [(int, int, int, float, float), ...]\nget_sizes() -> []\nreturn the available sizes of embedded bitmaps" +#define DOC_FREETYPE_FONT_ISCHARDEFINED "is_char_defined(char) -> bool\nCheck if a char is defined in the font." #define DOC_FREETYPE_FONT_RENDER "render(text, fgcolor=None, bgcolor=None, style=STYLE_DEFAULT, rotation=0, size=0) -> (Surface, Rect)\nReturn rendered text as a surface" #define DOC_FREETYPE_FONT_RENDERTO "render_to(surf, dest, text, fgcolor=None, bgcolor=None, style=STYLE_DEFAULT, rotation=0, size=0) -> Rect\nRender text onto an existing surface" #define DOC_FREETYPE_FONT_RENDERRAW "render_raw(text, style=STYLE_DEFAULT, rotation=0, size=0, invert=False) -> (bytes, (int, int))\nReturn rendered text as a string of bytes" diff --git a/src_c/font.c b/src_c/font.c index 94b50da68a..7bf35e3b07 100644 --- a/src_c/font.c +++ b/src_c/font.c @@ -535,6 +535,61 @@ font_set_strikethrough(PyObject *self, PyObject *arg) Py_RETURN_NONE; } +static PyObject * +font_is_char_provided(PyObject *self, PyObject *textobj) +{ + TTF_Font *font = PyFont_AsFont(self); + PyObject *temp; + PyObject *obj; + Uint16 *buffer; + Py_ssize_t length; + Uint16 ch; + + if (PyUnicode_Check(textobj)) { + obj = textobj; + Py_INCREF(obj); + } + else if (PyBytes_Check(textobj)) { + obj = PyUnicode_FromEncodedObject(textobj, "UTF-8", NULL); + if (!obj) { + return NULL; + } + } + else { + return RAISE_TEXT_TYPE_ERROR(); + } + temp = PyUnicode_AsUTF16String(obj); + Py_DECREF(obj); + if (!temp) + return NULL; + obj = temp; + +#if !SDL_TTF_VERSION_ATLEAST(2, 0, 15) + if (utf_8_needs_UCS_4(astring)) { + Py_DECREF(obj); + return RAISE(PyExc_UnicodeError, + "a Unicode character above '\\uFFFF' was found;" + " not supported with SDL_ttf version below 2.0.15"); + } +#endif + + buffer = (Uint16 *)PyBytes_AS_STRING(obj); + length = PyBytes_GET_SIZE(obj) / sizeof(Uint16); + + if (length > 2) { + Py_DECREF(obj); + return RAISE(PyExc_ValueError, "Too long, only 1 char supported."); + } + + ch = buffer[1]; /* skip BOM */ + int index = TTF_GlyphIsProvided(font, ch); + Py_DECREF(obj); + if (!index) { + Py_RETURN_FALSE; + } + Py_RETURN_TRUE; +} + static PyObject * font_render(PyObject *self, PyObject *args, PyObject *kwds) { @@ -1080,6 +1135,8 @@ static PyMethodDef font_methods[] = { {"metrics", font_metrics, METH_O, DOC_FONT_FONT_METRICS}, {"render", (PyCFunction)font_render, METH_VARARGS | METH_KEYWORDS, DOC_FONT_FONT_RENDER}, + {"is_char_defined", font_is_char_provided, METH_O, + DOC_FONT_FONT_ISCHARDEFINED}, {"size", font_size, METH_O, DOC_FONT_FONT_SIZE}, {"set_script", font_set_script, METH_O, DOC_FONT_FONT_SETSCRIPT}, {"set_direction", (PyCFunction)font_set_direction, diff --git a/test/font_test.py b/test/font_test.py index 258506c281..ba23ccbf02 100644 --- a/test/font_test.py +++ b/test/font_test.py @@ -655,6 +655,30 @@ def _set_style_name(): self.assertRaises(AttributeError, _set_style_name) + def test_font_is_char_defined_true(self): + f = pygame_font.Font(None, 20) + self.assertTrue(f.is_char_defined("a")) + + def test_font_is_char_defined_true_unicode(self): + f = pygame_font.Font(None, 20) + self.assertTrue(f.is_char_defined("\u003F")) + + def test_font_is_char_defined_raises_value_error_if_too_long(self): + f = pygame_font.Font(None, 20) + self.assertRaises(ValueError, f.is_char_defined, "ab") + + def test_font_is_char_defined_false_is_empty(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.is_char_defined("")) + + def test_font_is_char_defined_false(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.is_char_defined("❤")) + + def test_font_is_char_defined_false_unicode(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.is_char_defined("\uFF00")) + def test_font_file_not_found(self): # A per BUG reported by Bo Jangeborg on pygame-user mailing list, # http://www.mail-archive.com/pygame-users@seul.org/msg11675.html