Skip to content

font: add is_char_defined method in Font and Freetype #2178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions buildconfig/stubs/pygame/font.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down
1 change: 1 addition & 0 deletions buildconfig/stubs/pygame/freetype.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]]: ...
Expand Down
11 changes: 11 additions & 0 deletions docs/reST/ref/font.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
12 changes: 12 additions & 0 deletions docs/reST/ref/freetype.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
38 changes: 38 additions & 0 deletions src_c/_freetype.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
{
Expand Down
1 change: 1 addition & 0 deletions src_c/doc/font_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src_c/doc/freetype_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
57 changes: 57 additions & 0 deletions src_c/font.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions test/font_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]/msg11675.html
Expand Down