Skip to content
Open
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
4 changes: 4 additions & 0 deletions buildconfig/stubs/pygame/font.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class Font:
def point_size(self) -> int: ...
@point_size.setter
def point_size(self, value: int) -> None: ...
@property
def outline(self) -> int: ...
@outline.setter
def outline(self, value: int) -> None: ...
def __init__(self, filename: Optional[FileLike] = None, size: int = 20) -> None: ...
def render(
self,
Expand Down
38 changes: 38 additions & 0 deletions docs/reST/ref/font.rst
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,44 @@ solves no longer exists, it will likely be removed in the future.

.. ## Font.point_size ##


.. attribute:: outline

| :sl:`Gets or sets the font's outline thickness (pixels)`
| :sg:`outline -> int`

The outline value of the font.

When set to 0, the font will be drawn normally. When positive,
the text will be drawn as a hollow outline. The outline grows in all
directions a number of pixels equal to the value set. Negative values
are not allowed.

This can be drawn underneath unoutlined text to create a text outline
effect. For example: ::

def render_outlined(
font: pygame.Font,
text: str,
text_color: pygame.typing.ColorLike,
outline_color: pygame.typing.ColorLike,
outline_width: int,
) -> pygame.Surface:
old_outline = font.outline
if old_outline != 0:
font.outline = 0
base_text_surf = font.render(text, True, text_color)
font.outline = outline_width
outlined_text_surf = font.render(text, True, outline_color)

outlined_text_surf.blit(base_text_surf, (outline_width, outline_width))
font.outline = old_outline
return outlined_text_surf

.. versionadded:: 2.5.6

.. ## Font.outline ##

.. method:: render

| :sl:`draw text on a new Surface`
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_OUTLINE "outline -> int\nGets or sets the font's outline thickness (pixels)"
#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
43 changes: 43 additions & 0 deletions src_c/font.c
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,47 @@ font_set_ptsize(PyObject *self, PyObject *arg)
#endif
}

static PyObject *
font_getter_outline(PyObject *self, void *closure)
{
if (!PgFont_GenerationCheck(self)) {
return RAISE_FONT_QUIT_ERROR();
}

TTF_Font *font = PyFont_AsFont(self);
return PyLong_FromLong(TTF_GetFontOutline(font));
}

static int
font_setter_outline(PyObject *self, PyObject *value, void *closure)
{
if (!PgFont_GenerationCheck(self)) {
RAISE_FONT_QUIT_ERROR_RETURN(-1);
}
TTF_Font *font = PyFont_AsFont(self);

DEL_ATTR_NOT_SUPPORTED_CHECK("outline", value);

long val = PyLong_AsLong(value);
if (val == -1 && PyErr_Occurred()) {
return -1;
}
if (val < 0) {
PyErr_SetString(PyExc_ValueError, "outline must be >= 0");
return -1;
}

#if SDL_TTF_VERSION_ATLEAST(3, 0, 0)
if (!TTF_SetFontOutline(font, (int)val)) {
PyErr_SetString(pgExc_SDLError, SDL_GetError());
return -1;
}
#else
TTF_SetFontOutline(font, (int)val);
#endif
return 0;
}

static PyObject *
font_getter_name(PyObject *self, void *closure)
{
Expand Down Expand Up @@ -1168,6 +1209,8 @@ static PyGetSetDef font_getsets[] = {
DOC_FONT_FONT_UNDERLINE, NULL},
{"strikethrough", (getter)font_getter_strikethrough,
(setter)font_setter_strikethrough, DOC_FONT_FONT_STRIKETHROUGH, NULL},
{"outline", (getter)font_getter_outline, (setter)font_setter_outline,
DOC_FONT_FONT_OUTLINE, NULL},
{"align", (getter)font_getter_align, (setter)font_setter_align,
DOC_FONT_FONT_ALIGN, NULL},
{"point_size", (getter)font_getter_point_size,
Expand Down
43 changes: 42 additions & 1 deletion test/font_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,37 @@ def test_point_size_method(self):
self.assertRaises(ValueError, f.set_point_size, -500)
self.assertRaises(TypeError, f.set_point_size, "15")

def test_outline_property(self):
if pygame_font.__name__ == "pygame.ftfont":
return # not a pygame.ftfont feature

pygame_font.init()
font_path = os.path.join(
os.path.split(pygame.__file__)[0], pygame_font.get_default_font()
)
f = pygame_font.Font(pathlib.Path(font_path), 25)

# Default outline should be an integer >= 0 (typically 0)
self.assertIsInstance(f.outline, int)
self.assertGreaterEqual(f.outline, 0)

orig = f.outline
f.outline = orig + 1
self.assertEqual(orig + 1, f.outline)
f.outline += 2
self.assertEqual(orig + 3, f.outline)
f.outline -= 1
self.assertEqual(orig + 2, f.outline)

def test_neg():
f.outline = -1

def test_incorrect_type():
f.outline = "2"

self.assertRaises(ValueError, test_neg)
self.assertRaises(TypeError, test_incorrect_type)

def test_font_name(self):
f = pygame_font.Font(None, 20)
self.assertEqual(f.name, "FreeSans")
Expand Down Expand Up @@ -933,6 +964,7 @@ def test_font_method_should_raise_exception_after_quit(self):
]
skip_methods = set()
version = pygame.font.get_sdl_ttf_version()

if version >= (2, 0, 18):
methods.append(("get_point_size", ()))
methods.append(("set_point_size", (34,)))
Expand Down Expand Up @@ -1020,6 +1052,7 @@ def test_font_property_should_raise_exception_after_quit(self):
("italic", True),
("underline", True),
("strikethrough", True),
("outline", 1),
]
skip_properties = set()
version = pygame.font.get_sdl_ttf_version()
Expand Down Expand Up @@ -1096,6 +1129,7 @@ def query(
underline=False,
strikethrough=False,
antialiase=False,
outline=0,
):
if self.aborted:
return False
Expand All @@ -1106,7 +1140,7 @@ def query(
screen = self.screen
screen.fill((255, 255, 255))
pygame.display.flip()
if not (bold or italic or underline or strikethrough or antialiase):
if not (bold or italic or underline or strikethrough or antialiase or outline):
text = "normal"
else:
modes = []
Expand All @@ -1120,18 +1154,22 @@ def query(
modes.append("strikethrough")
if antialiase:
modes.append("antialiased")
if outline:
modes.append("outlined")
text = f"{'-'.join(modes)} (y/n):"
f.set_bold(bold)
f.set_italic(italic)
f.set_underline(underline)
f.set_strikethrough(strikethrough)
f.outline = outline
s = f.render(text, antialiase, (0, 0, 0))
screen.blit(s, (offset, y))
y += s.get_size()[1] + spacing
f.set_bold(False)
f.set_italic(False)
f.set_underline(False)
f.set_strikethrough(False)
f.outline = 0
s = f.render("(some comparison text)", False, (0, 0, 0))
screen.blit(s, (offset, y))
pygame.display.flip()
Expand Down Expand Up @@ -1173,6 +1211,9 @@ def test_italic_underline(self):
def test_bold_strikethrough(self):
self.assertTrue(self.query(bold=True, strikethrough=True))

def test_outline(self):
self.assertTrue(self.query(outline=1))


if __name__ == "__main__":
unittest.main()
Loading