Skip to content

Commit 731f43e

Browse files
committed
Added outline property to pygame.font.Font
1 parent aa236dd commit 731f43e

File tree

5 files changed

+231
-1
lines changed

5 files changed

+231
-1
lines changed

buildconfig/stubs/pygame/font.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class Font:
5757
def point_size(self) -> int: ...
5858
@point_size.setter
5959
def point_size(self, value: int) -> None: ...
60+
@property
61+
def outline(self) -> int: ...
62+
@outline.setter
63+
def outline(self, value: int) -> None: ...
6064
def __init__(self, filename: FileLike | None = None, size: int = 20) -> None: ...
6165
def render(
6266
self,
@@ -85,6 +89,8 @@ class Font:
8589
def set_direction(self, direction: int) -> None: ...
8690
def get_point_size(self) -> int: ...
8791
def set_point_size(self, val: int, /) -> None: ...
92+
def get_outline(self) -> int: ...
93+
def set_outline(self, val: int, /) -> None: ...
8894

8995
@deprecated("Use `Font` instead (FontType is an old alias)")
9096
class FontType(Font): ...

docs/reST/ref/font.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,22 @@ solves no longer exists, it will likely be removed in the future.
306306

307307
.. ## Font.point_size ##
308308
309+
310+
.. attribute:: outline
311+
312+
| :sl:`Gets or sets the font's outline value`
313+
| :sg:`outline -> int`
314+
315+
The outline value of the font.
316+
317+
When set to 0, the font will be drawn normally. When positive,
318+
the text will be drawn as a hollow outline. This can be drawn
319+
underneath unoutlined text to create a text outline effect.
320+
321+
.. versionadded:: 2.5.6
322+
323+
.. ## Font.outline ##
324+
309325
.. method:: render
310326

311327
| :sl:`draw text on a new Surface`
@@ -562,6 +578,32 @@ solves no longer exists, it will likely be removed in the future.
562578

563579
.. ## Font.get_point_size ##
564580
581+
.. method:: set_outline
582+
583+
| :sl:`set the outline value of the font`
584+
| :sg:`set_outline(size, /) -> None`
585+
586+
Sets the outline value of the font.
587+
588+
.. note:: This is the same as the :attr:`outline` attribute.
589+
590+
.. versionadded:: 2.5.6
591+
592+
.. ## Font.set_outline ##
593+
594+
.. method:: get_outline
595+
596+
| :sl:`get the outline value of the font`
597+
| :sg:`get_outline() -> int`
598+
599+
Returns the outline value of the font.
600+
601+
.. note:: This is the same as the :attr:`outline` attribute.
602+
603+
.. versionadded:: 2.5.6
604+
605+
.. ## Font.get_outline ##
606+
565607
.. method:: get_ascent
566608

567609
| :sl:`get the ascent of the font`

src_c/doc/font_doc.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#define DOC_FONT_FONT_STRIKETHROUGH "strikethrough -> bool\nGets or sets whether the font should be rendered with a strikethrough."
1818
#define DOC_FONT_FONT_ALIGN "align -> int\nSet how rendered text is aligned when given a wrap length."
1919
#define DOC_FONT_FONT_POINTSIZE "point_size -> int\nGets or sets the font's point size"
20+
#define DOC_FONT_FONT_OUTLINE "outline -> int\nGets or sets the font's outline value"
2021
#define DOC_FONT_FONT_RENDER "render(text, antialias, color, bgcolor=None, wraplength=0) -> Surface\ndraw text on a new Surface"
2122
#define DOC_FONT_FONT_SIZE "size(text, /) -> (width, height)\ndetermine the amount of space needed to render text"
2223
#define DOC_FONT_FONT_SETUNDERLINE "set_underline(bool, /) -> None\ncontrol if text is rendered with an underline"
@@ -33,6 +34,8 @@
3334
#define DOC_FONT_FONT_GETHEIGHT "get_height() -> int\nget the height of the font"
3435
#define DOC_FONT_FONT_SETPOINTSIZE "set_point_size(size, /) -> None\nset the point size of the font"
3536
#define DOC_FONT_FONT_GETPOINTSIZE "get_point_size() -> int\nget the point size of the font"
37+
#define DOC_FONT_FONT_SETOUTLINE "set_outline(outline, /) -> None\nset The outline value of the font"
38+
#define DOC_FONT_FONT_GETOUTLINE "get_outline() -> int\nget the outline value of the font"
3639
#define DOC_FONT_FONT_GETASCENT "get_ascent() -> int\nget the ascent of the font"
3740
#define DOC_FONT_FONT_GETDESCENT "get_descent() -> int\nget the descent of the font"
3841
#define DOC_FONT_FONT_SETSCRIPT "set_script(str, /) -> None\nset the script code for text shaping"

src_c/font.c

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,96 @@ font_set_ptsize(PyObject *self, PyObject *arg)
901901
#endif
902902
}
903903

904+
static PyObject *
905+
font_getter_outline(PyObject *self, void *closure)
906+
{
907+
if (!PgFont_GenerationCheck(self)) {
908+
return RAISE_FONT_QUIT_ERROR();
909+
}
910+
911+
#if SDL_TTF_VERSION_ATLEAST(2, 0, 12)
912+
TTF_Font *font = PyFont_AsFont(self);
913+
return PyLong_FromLong(TTF_GetFontOutline(font));
914+
#else
915+
return RAISE(pgExc_SDLError,
916+
"pygame.font not compiled with a new enough SDL_ttf version. "
917+
"Needs SDL_ttf 2.0.12 or above.");
918+
#endif
919+
}
920+
921+
static int
922+
font_setter_outline(PyObject *self, PyObject *value, void *closure)
923+
{
924+
if (!PgFont_GenerationCheck(self)) {
925+
RAISE_FONT_QUIT_ERROR_RETURN(-1);
926+
}
927+
#if SDL_TTF_VERSION_ATLEAST(2, 0, 12)
928+
TTF_Font *font = PyFont_AsFont(self);
929+
930+
DEL_ATTR_NOT_SUPPORTED_CHECK("outline", value);
931+
932+
if (!PyLong_Check(value)) {
933+
PyErr_SetString(PyExc_TypeError, "outline must be an integer");
934+
return -1;
935+
}
936+
long val = PyLong_AsLong(value);
937+
if (val == -1 && PyErr_Occurred()) {
938+
return -1;
939+
}
940+
if (val < 0) {
941+
PyErr_SetString(PyExc_ValueError, "outline must be >= 0");
942+
return -1;
943+
}
944+
TTF_SetFontOutline(font, (int)val);
945+
return 0;
946+
#else
947+
RAISE(pgExc_SDLError,
948+
"pygame.font not compiled with a new enough SDL_ttf version. Needs SDL_ttf 2.0.12 or above.");
949+
return -1;
950+
#endif
951+
}
952+
953+
static PyObject *
954+
font_get_outline(PyObject *self, PyObject *_null)
955+
{
956+
if (!PgFont_GenerationCheck(self)) {
957+
return RAISE_FONT_QUIT_ERROR();
958+
}
959+
#if SDL_TTF_VERSION_ATLEAST(2, 0, 12)
960+
TTF_Font *font = PyFont_AsFont(self);
961+
return PyLong_FromLong(TTF_GetFontOutline(font));
962+
#else
963+
return RAISE(pgExc_SDLError,
964+
"pygame.font not compiled with a new enough SDL_ttf version. "
965+
"Needs SDL_ttf 2.0.12 or above.");
966+
#endif
967+
}
968+
969+
static PyObject *
970+
font_set_outline(PyObject *self, PyObject *arg)
971+
{
972+
if (!PgFont_GenerationCheck(self)) {
973+
return RAISE_FONT_QUIT_ERROR();
974+
}
975+
#if SDL_TTF_VERSION_ATLEAST(2, 0, 12)
976+
TTF_Font *font = PyFont_AsFont(self);
977+
long val = PyLong_AsLong(arg);
978+
if (val == -1 && PyErr_Occurred()) {
979+
return NULL;
980+
}
981+
if (val < 0) {
982+
return RAISE(PyExc_ValueError, "outline must be >= 0");
983+
}
984+
TTF_SetFontOutline(font, (int)val);
985+
Py_RETURN_NONE;
986+
#else
987+
return RAISE(pgExc_SDLError,
988+
"pygame.font not compiled with a new enough SDL_ttf version. "
989+
"Needs SDL_ttf 2.0.12 or above.");
990+
#endif
991+
}
992+
993+
904994
static PyObject *
905995
font_getter_name(PyObject *self, void *closure)
906996
{
@@ -1168,6 +1258,8 @@ static PyGetSetDef font_getsets[] = {
11681258
DOC_FONT_FONT_UNDERLINE, NULL},
11691259
{"strikethrough", (getter)font_getter_strikethrough,
11701260
(setter)font_setter_strikethrough, DOC_FONT_FONT_STRIKETHROUGH, NULL},
1261+
{"outline", (getter)font_getter_outline, (setter)font_setter_outline,
1262+
DOC_FONT_FONT_OUTLINE, NULL},
11711263
{"align", (getter)font_getter_align, (setter)font_setter_align,
11721264
DOC_FONT_FONT_ALIGN, NULL},
11731265
{"point_size", (getter)font_getter_point_size,
@@ -1192,6 +1284,8 @@ static PyMethodDef font_methods[] = {
11921284
DOC_FONT_FONT_GETSTRIKETHROUGH},
11931285
{"set_strikethrough", font_set_strikethrough, METH_O,
11941286
DOC_FONT_FONT_SETSTRIKETHROUGH},
1287+
{"get_outline", font_get_outline, METH_NOARGS, DOC_FONT_FONT_GETOUTLINE},
1288+
{"set_outline", font_set_outline, METH_O, DOC_FONT_FONT_SETOUTLINE},
11951289
{"get_point_size", font_get_ptsize, METH_NOARGS,
11961290
DOC_FONT_FONT_GETPOINTSIZE},
11971291
{"set_point_size", font_set_ptsize, METH_O, DOC_FONT_FONT_SETPOINTSIZE},

test/font_test.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,70 @@ def test_point_size_method(self):
688688
self.assertRaises(ValueError, f.set_point_size, -500)
689689
self.assertRaises(TypeError, f.set_point_size, "15")
690690

691+
def test_outline_property(self):
692+
if pygame_font.__name__ == "pygame.ftfont":
693+
return # not a pygame.ftfont feature
694+
695+
pygame_font.init()
696+
font_path = os.path.join(
697+
os.path.split(pygame.__file__)[0], pygame_font.get_default_font()
698+
)
699+
f = pygame_font.Font(pathlib.Path(font_path), 25)
700+
701+
ttf_version = pygame_font.get_sdl_ttf_version()
702+
if ttf_version < (2, 0, 12):
703+
with self.assertRaises(pygame.error):
704+
f.outline = 0
705+
with self.assertRaises(pygame.error):
706+
_ = f.outline
707+
return
708+
709+
# Default outline should be an integer >= 0 (typically 0)
710+
self.assertIsInstance(f.outline, int)
711+
self.assertGreaterEqual(f.outline, 0)
712+
713+
orig = f.outline
714+
f.outline = orig + 1
715+
self.assertEqual(orig + 1, f.outline)
716+
f.outline += 2
717+
self.assertEqual(orig + 3, f.outline)
718+
f.outline -= 1
719+
self.assertEqual(orig + 2, f.outline)
720+
721+
def test_neg():
722+
f.outline = -1
723+
724+
def test_incorrect_type():
725+
f.outline = "2"
726+
727+
self.assertRaises(ValueError, test_neg)
728+
self.assertRaises(TypeError, test_incorrect_type)
729+
730+
def test_outline_method(self):
731+
if pygame_font.__name__ == "pygame.ftfont":
732+
return # not a pygame.ftfont feature
733+
734+
pygame_font.init()
735+
font_path = os.path.join(
736+
os.path.split(pygame.__file__)[0], pygame_font.get_default_font()
737+
)
738+
f = pygame_font.Font(pathlib.Path(font_path), 25)
739+
740+
ttf_version = pygame_font.get_sdl_ttf_version()
741+
if ttf_version < (2, 0, 12):
742+
self.assertRaises(pygame.error, f.get_outline)
743+
self.assertRaises(pygame.error, f.set_outline, 1)
744+
return
745+
746+
val0 = f.get_outline()
747+
self.assertIsInstance(val0, int)
748+
self.assertGreaterEqual(val0, 0)
749+
750+
f.set_outline(5)
751+
self.assertEqual(5, f.get_outline())
752+
self.assertRaises(ValueError, f.set_outline, -1)
753+
self.assertRaises(TypeError, f.set_outline, "2")
754+
691755
def test_font_name(self):
692756
f = pygame_font.Font(None, 20)
693757
self.assertEqual(f.name, "FreeSans")
@@ -936,6 +1000,14 @@ def test_font_method_should_raise_exception_after_quit(self):
9361000
]
9371001
skip_methods = set()
9381002
version = pygame.font.get_sdl_ttf_version()
1003+
1004+
if version >= (2, 0, 12):
1005+
methods.append(("get_outline", ()))
1006+
methods.append(("set_outline", (2,)))
1007+
else:
1008+
skip_methods.add("get_outline")
1009+
skip_methods.add("set_outline")
1010+
9391011
if version >= (2, 0, 18):
9401012
methods.append(("get_point_size", ()))
9411013
methods.append(("set_point_size", (34,)))
@@ -1035,6 +1107,11 @@ def test_font_property_should_raise_exception_after_quit(self):
10351107
else:
10361108
skip_properties.add("point_size")
10371109

1110+
if version >= (2, 0, 12):
1111+
properties.append(("outline", 1))
1112+
else:
1113+
skip_properties.add("outline")
1114+
10381115
font = pygame_font.Font(None, 10)
10391116
actual_names = []
10401117

@@ -1099,6 +1176,7 @@ def query(
10991176
underline=False,
11001177
strikethrough=False,
11011178
antialiase=False,
1179+
outline=0
11021180
):
11031181
if self.aborted:
11041182
return False
@@ -1109,7 +1187,7 @@ def query(
11091187
screen = self.screen
11101188
screen.fill((255, 255, 255))
11111189
pygame.display.flip()
1112-
if not (bold or italic or underline or strikethrough or antialiase):
1190+
if not (bold or italic or underline or strikethrough or antialiase or outline):
11131191
text = "normal"
11141192
else:
11151193
modes = []
@@ -1123,18 +1201,22 @@ def query(
11231201
modes.append("strikethrough")
11241202
if antialiase:
11251203
modes.append("antialiased")
1204+
if outline:
1205+
modes.append("outlined")
11261206
text = f"{'-'.join(modes)} (y/n):"
11271207
f.set_bold(bold)
11281208
f.set_italic(italic)
11291209
f.set_underline(underline)
11301210
f.set_strikethrough(strikethrough)
1211+
f.set_outline(outline)
11311212
s = f.render(text, antialiase, (0, 0, 0))
11321213
screen.blit(s, (offset, y))
11331214
y += s.get_size()[1] + spacing
11341215
f.set_bold(False)
11351216
f.set_italic(False)
11361217
f.set_underline(False)
11371218
f.set_strikethrough(False)
1219+
f.set_outline(0)
11381220
s = f.render("(some comparison text)", False, (0, 0, 0))
11391221
screen.blit(s, (offset, y))
11401222
pygame.display.flip()
@@ -1176,6 +1258,9 @@ def test_italic_underline(self):
11761258
def test_bold_strikethrough(self):
11771259
self.assertTrue(self.query(bold=True, strikethrough=True))
11781260

1261+
def test_outline(self):
1262+
self.assertTrue(self.query(outline=1))
1263+
11791264

11801265
if __name__ == "__main__":
11811266
unittest.main()

0 commit comments

Comments
 (0)