Skip to content

Commit 6fea16a

Browse files
authored
Added outline property to pygame.font.Font (#3597)
* Added outline property to pygame.font.Font * Updated font outline formatting. * Improved version checks when in font outline tests. * Expanded font outline documentation to better explain effects of values. * Deduplicated font outline getter and setter logic. * Font outline code formatting. * Changed return Py_None to Py_RETURN_NONE. * Changed use of RAISE to PyErr_SetString for consistency. * font.c formatting. * Removed Font outline getters/setters. Preferring properties for new values. * font test formatting fix. * Removed checks for SDL_TTF < 2.0.12 in outline code. We depend on SDL_TTF >= 2.0.15, so this is not needed. * Improved font outline docs. Specify that the thickness is in pixels, and added example to render outlined text. * Updated version note for Font.outline to 2.5.7. 2.5.6 released while this was still in review.
1 parent 4b7f812 commit 6fea16a

File tree

5 files changed

+128
-1
lines changed

5 files changed

+128
-1
lines changed

buildconfig/stubs/pygame/font.pyi

Lines changed: 4 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,

docs/reST/ref/font.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,44 @@ 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 thickness (pixels)`
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. The outline grows in all
319+
directions a number of pixels equal to the value set. Negative values
320+
are not allowed.
321+
322+
This can be drawn underneath unoutlined text to create a text outline
323+
effect. For example: ::
324+
325+
def render_outlined(
326+
font: pygame.Font,
327+
text: str,
328+
text_color: pygame.typing.ColorLike,
329+
outline_color: pygame.typing.ColorLike,
330+
outline_width: int,
331+
) -> pygame.Surface:
332+
old_outline = font.outline
333+
if old_outline != 0:
334+
font.outline = 0
335+
base_text_surf = font.render(text, True, text_color)
336+
font.outline = outline_width
337+
outlined_text_surf = font.render(text, True, outline_color)
338+
339+
outlined_text_surf.blit(base_text_surf, (outline_width, outline_width))
340+
font.outline = old_outline
341+
return outlined_text_surf
342+
343+
.. versionadded:: 2.5.7
344+
345+
.. ## Font.outline ##
346+
309347
.. method:: render
310348

311349
| :sl:`draw text on a new Surface`

src_c/doc/font_doc.h

Lines changed: 1 addition & 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 thickness (pixels)"
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"

src_c/font.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,47 @@ 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+
TTF_Font *font = PyFont_AsFont(self);
912+
return PyLong_FromLong(TTF_GetFontOutline(font));
913+
}
914+
915+
static int
916+
font_setter_outline(PyObject *self, PyObject *value, void *closure)
917+
{
918+
if (!PgFont_GenerationCheck(self)) {
919+
RAISE_FONT_QUIT_ERROR_RETURN(-1);
920+
}
921+
TTF_Font *font = PyFont_AsFont(self);
922+
923+
DEL_ATTR_NOT_SUPPORTED_CHECK("outline", value);
924+
925+
long val = PyLong_AsLong(value);
926+
if (val == -1 && PyErr_Occurred()) {
927+
return -1;
928+
}
929+
if (val < 0) {
930+
PyErr_SetString(PyExc_ValueError, "outline must be >= 0");
931+
return -1;
932+
}
933+
934+
#if SDL_TTF_VERSION_ATLEAST(3, 0, 0)
935+
if (!TTF_SetFontOutline(font, (int)val)) {
936+
PyErr_SetString(pgExc_SDLError, SDL_GetError());
937+
return -1;
938+
}
939+
#else
940+
TTF_SetFontOutline(font, (int)val);
941+
#endif
942+
return 0;
943+
}
944+
904945
static PyObject *
905946
font_getter_name(PyObject *self, void *closure)
906947
{
@@ -1168,6 +1209,8 @@ static PyGetSetDef font_getsets[] = {
11681209
DOC_FONT_FONT_UNDERLINE, NULL},
11691210
{"strikethrough", (getter)font_getter_strikethrough,
11701211
(setter)font_setter_strikethrough, DOC_FONT_FONT_STRIKETHROUGH, NULL},
1212+
{"outline", (getter)font_getter_outline, (setter)font_setter_outline,
1213+
DOC_FONT_FONT_OUTLINE, NULL},
11711214
{"align", (getter)font_getter_align, (setter)font_setter_align,
11721215
DOC_FONT_FONT_ALIGN, NULL},
11731216
{"point_size", (getter)font_getter_point_size,

test/font_test.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,37 @@ 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+
# Default outline should be an integer >= 0 (typically 0)
702+
self.assertIsInstance(f.outline, int)
703+
self.assertGreaterEqual(f.outline, 0)
704+
705+
orig = f.outline
706+
f.outline = orig + 1
707+
self.assertEqual(orig + 1, f.outline)
708+
f.outline += 2
709+
self.assertEqual(orig + 3, f.outline)
710+
f.outline -= 1
711+
self.assertEqual(orig + 2, f.outline)
712+
713+
def test_neg():
714+
f.outline = -1
715+
716+
def test_incorrect_type():
717+
f.outline = "2"
718+
719+
self.assertRaises(ValueError, test_neg)
720+
self.assertRaises(TypeError, test_incorrect_type)
721+
691722
def test_font_name(self):
692723
f = pygame_font.Font(None, 20)
693724
self.assertEqual(f.name, "FreeSans")
@@ -936,6 +967,7 @@ def test_font_method_should_raise_exception_after_quit(self):
936967
]
937968
skip_methods = set()
938969
version = pygame.font.get_sdl_ttf_version()
970+
939971
if version >= (2, 0, 18):
940972
methods.append(("get_point_size", ()))
941973
methods.append(("set_point_size", (34,)))
@@ -1023,6 +1055,7 @@ def test_font_property_should_raise_exception_after_quit(self):
10231055
("italic", True),
10241056
("underline", True),
10251057
("strikethrough", True),
1058+
("outline", 1),
10261059
]
10271060
skip_properties = set()
10281061
version = pygame.font.get_sdl_ttf_version()
@@ -1099,6 +1132,7 @@ def query(
10991132
underline=False,
11001133
strikethrough=False,
11011134
antialiase=False,
1135+
outline=0,
11021136
):
11031137
if self.aborted:
11041138
return False
@@ -1109,7 +1143,7 @@ def query(
11091143
screen = self.screen
11101144
screen.fill((255, 255, 255))
11111145
pygame.display.flip()
1112-
if not (bold or italic or underline or strikethrough or antialiase):
1146+
if not (bold or italic or underline or strikethrough or antialiase or outline):
11131147
text = "normal"
11141148
else:
11151149
modes = []
@@ -1123,18 +1157,22 @@ def query(
11231157
modes.append("strikethrough")
11241158
if antialiase:
11251159
modes.append("antialiased")
1160+
if outline:
1161+
modes.append("outlined")
11261162
text = f"{'-'.join(modes)} (y/n):"
11271163
f.set_bold(bold)
11281164
f.set_italic(italic)
11291165
f.set_underline(underline)
11301166
f.set_strikethrough(strikethrough)
1167+
f.outline = outline
11311168
s = f.render(text, antialiase, (0, 0, 0))
11321169
screen.blit(s, (offset, y))
11331170
y += s.get_size()[1] + spacing
11341171
f.set_bold(False)
11351172
f.set_italic(False)
11361173
f.set_underline(False)
11371174
f.set_strikethrough(False)
1175+
f.outline = 0
11381176
s = f.render("(some comparison text)", False, (0, 0, 0))
11391177
screen.blit(s, (offset, y))
11401178
pygame.display.flip()
@@ -1176,6 +1214,9 @@ def test_italic_underline(self):
11761214
def test_bold_strikethrough(self):
11771215
self.assertTrue(self.query(bold=True, strikethrough=True))
11781216

1217+
def test_outline(self):
1218+
self.assertTrue(self.query(outline=1))
1219+
11791220

11801221
if __name__ == "__main__":
11811222
unittest.main()

0 commit comments

Comments
 (0)