Skip to content

Commit 0feebd5

Browse files
committed
Added outline property to pygame.font.Font
1 parent f572e93 commit 0feebd5

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: Optional[FileLike] = None, size: int = 20) -> None: ...
6165
def render(
6266
self,
@@ -87,6 +91,8 @@ class Font:
8791
def set_direction(self, direction: int) -> None: ...
8892
def get_point_size(self) -> int: ...
8993
def set_point_size(self, val: int, /) -> None: ...
94+
def get_outline(self) -> int: ...
95+
def set_outline(self, val: int, /) -> None: ...
9096

9197
@deprecated("Use `Font` instead (FontType is an old alias)")
9298
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 point size 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 point size 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+
"Outline width in pixels (integer, >= 0)", 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
@@ -685,6 +685,70 @@ def test_point_size_method(self):
685685
self.assertRaises(ValueError, f.set_point_size, -500)
686686
self.assertRaises(TypeError, f.set_point_size, "15")
687687

688+
def test_outline_property(self):
689+
if pygame_font.__name__ == "pygame.ftfont":
690+
return # not a pygame.ftfont feature
691+
692+
pygame_font.init()
693+
font_path = os.path.join(
694+
os.path.split(pygame.__file__)[0], pygame_font.get_default_font()
695+
)
696+
f = pygame_font.Font(pathlib.Path(font_path), 25)
697+
698+
ttf_version = pygame_font.get_sdl_ttf_version()
699+
if ttf_version < (2, 0, 12):
700+
with self.assertRaises(pygame.error):
701+
f.outline = 0
702+
with self.assertRaises(pygame.error):
703+
_ = f.outline
704+
return
705+
706+
# Default outline should be an integer >= 0 (typically 0)
707+
self.assertIsInstance(f.outline, int)
708+
self.assertGreaterEqual(f.outline, 0)
709+
710+
orig = f.outline
711+
f.outline = orig + 1
712+
self.assertEqual(orig + 1, f.outline)
713+
f.outline += 2
714+
self.assertEqual(orig + 3, f.outline)
715+
f.outline -= 1
716+
self.assertEqual(orig + 2, f.outline)
717+
718+
def test_neg():
719+
f.outline = -1
720+
721+
def test_incorrect_type():
722+
f.outline = "2"
723+
724+
self.assertRaises(ValueError, test_neg)
725+
self.assertRaises(TypeError, test_incorrect_type)
726+
727+
def test_outline_method(self):
728+
if pygame_font.__name__ == "pygame.ftfont":
729+
return # not a pygame.ftfont feature
730+
731+
pygame_font.init()
732+
font_path = os.path.join(
733+
os.path.split(pygame.__file__)[0], pygame_font.get_default_font()
734+
)
735+
f = pygame_font.Font(pathlib.Path(font_path), 25)
736+
737+
ttf_version = pygame_font.get_sdl_ttf_version()
738+
if ttf_version < (2, 0, 12):
739+
self.assertRaises(pygame.error, f.get_outline)
740+
self.assertRaises(pygame.error, f.set_outline, 1)
741+
return
742+
743+
val0 = f.get_outline()
744+
self.assertIsInstance(val0, int)
745+
self.assertGreaterEqual(val0, 0)
746+
747+
f.set_outline(5)
748+
self.assertEqual(5, f.get_outline())
749+
self.assertRaises(ValueError, f.set_outline, -1)
750+
self.assertRaises(TypeError, f.set_outline, "2")
751+
688752
def test_font_name(self):
689753
f = pygame_font.Font(None, 20)
690754
self.assertEqual(f.name, "FreeSans")
@@ -933,6 +997,14 @@ def test_font_method_should_raise_exception_after_quit(self):
933997
]
934998
skip_methods = set()
935999
version = pygame.font.get_sdl_ttf_version()
1000+
1001+
if version >= (2, 0, 12):
1002+
methods.append(("get_outline", ()))
1003+
methods.append(("set_outline", (2,)))
1004+
else:
1005+
skip_methods.add("get_outline")
1006+
skip_methods.add("set_outline")
1007+
9361008
if version >= (2, 0, 18):
9371009
methods.append(("get_point_size", ()))
9381010
methods.append(("set_point_size", (34,)))
@@ -1032,6 +1104,11 @@ def test_font_property_should_raise_exception_after_quit(self):
10321104
else:
10331105
skip_properties.add("point_size")
10341106

1107+
if version >= (2, 0, 12):
1108+
properties.append(("outline", 1))
1109+
else:
1110+
skip_properties.add("outline")
1111+
10351112
font = pygame_font.Font(None, 10)
10361113
actual_names = []
10371114

@@ -1096,6 +1173,7 @@ def query(
10961173
underline=False,
10971174
strikethrough=False,
10981175
antialiase=False,
1176+
outline=0
10991177
):
11001178
if self.aborted:
11011179
return False
@@ -1106,7 +1184,7 @@ def query(
11061184
screen = self.screen
11071185
screen.fill((255, 255, 255))
11081186
pygame.display.flip()
1109-
if not (bold or italic or underline or strikethrough or antialiase):
1187+
if not (bold or italic or underline or strikethrough or antialiase or outline):
11101188
text = "normal"
11111189
else:
11121190
modes = []
@@ -1120,18 +1198,22 @@ def query(
11201198
modes.append("strikethrough")
11211199
if antialiase:
11221200
modes.append("antialiased")
1201+
if outline:
1202+
modes.append("outlined")
11231203
text = f"{'-'.join(modes)} (y/n):"
11241204
f.set_bold(bold)
11251205
f.set_italic(italic)
11261206
f.set_underline(underline)
11271207
f.set_strikethrough(strikethrough)
1208+
f.set_outline(outline)
11281209
s = f.render(text, antialiase, (0, 0, 0))
11291210
screen.blit(s, (offset, y))
11301211
y += s.get_size()[1] + spacing
11311212
f.set_bold(False)
11321213
f.set_italic(False)
11331214
f.set_underline(False)
11341215
f.set_strikethrough(False)
1216+
f.set_outline(0)
11351217
s = f.render("(some comparison text)", False, (0, 0, 0))
11361218
screen.blit(s, (offset, y))
11371219
pygame.display.flip()
@@ -1173,6 +1255,9 @@ def test_italic_underline(self):
11731255
def test_bold_strikethrough(self):
11741256
self.assertTrue(self.query(bold=True, strikethrough=True))
11751257

1258+
def test_outline(self):
1259+
self.assertTrue(self.query(outline=1))
1260+
11761261

11771262
if __name__ == "__main__":
11781263
unittest.main()

0 commit comments

Comments
 (0)