Skip to content
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

Add outline option for rendering fonts #2828

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

narilee2006
Copy link
Contributor

Hello! I tried doing #2452. Let me know if there are any issues.

@narilee2006 narilee2006 requested a review from a team as a code owner April 26, 2024 21:11
src_c/font.c Outdated Show resolved Hide resolved
buildconfig/stubs/pygame/font.pyi Outdated Show resolved Hide resolved
src_c/font.c Outdated Show resolved Hide resolved
test/font_test.py Outdated Show resolved Hide resolved
Copy link
Member

@oddbookworm oddbookworm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just a couple comments

test/font_test.py Outdated Show resolved Hide resolved
test/font_test.py Outdated Show resolved Hide resolved
Copy link
Member

@oddbookworm oddbookworm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Member

@Starbuck5 Starbuck5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The challenge of #2452 is to design a good API for this, and this PR doesn’t accomplish that in my opinion.

  • Outline color isn’t supported
  • Looking at my notes from before (haven’t run this yet), doesn’t this PR mean that people have to call font.render twice? Once for the text, then once for the outline. That seems inconvenient for users.

@narilee2006
Copy link
Contributor Author

Outline color isn’t supported

It actually is, you need to call it in the font.render() method.

Looking at my notes from before (haven’t run this yet), doesn’t this PR mean that people have to call font.render twice? Once for the text, then once for the outline. That seems inconvenient for users.

Yep, that's how SDL2 implemented it.

@narilee2006
Copy link
Contributor Author

Perhaps outline as a kwarg so the developer doesn't need to use two lines to create the font?

@oddbookworm
Copy link
Member

Maybe perhaps

pygame.font.Font(filename=None, size=20, outline=0)

for the font constructor and

pygame.font.Font.render(text, antialias, color, bgcolor=None, wraplength=0, outline_color=None)

for the render call?

But I'm not sure what to do if outline_color defaults when outline was set to something nonzero

@Starbuck5
Copy link
Member

Starbuck5 commented Apr 27, 2024

Perhaps outline as a kwarg so the developer doesn't need to use two lines to create the font?

Well then how do you do the color? Another kwarg? Kwargs are expensive to parse in functions that get called all the time, too.

I think my second proposal in the issue would work well (minus the render outline only thing, I think that’s unnecessary now)

font.outline_width (attribute, get/set with TTF_ outline)
font.outline_color (attribute, defaults to black)

Render with an outline:
font.render

@narilee2006
Copy link
Contributor Author

narilee2006 commented Apr 27, 2024

You can do the outline color by specifying it in the render() method with no modifications to the existing code.

I'm personally against the outline_color parameter as existing draw methods have the width parameter to determine their border size; if the width parameter is greater than 0 it won't fill the shape and only draw the borders.

https://pyga.me/docs/ref/draw.html

@narilee2006
Copy link
Contributor Author

Well then how do you do the color? Another kwarg? Kwargs are expensive to parse in functions that get called all the time, too.

@Starbuck5, sorry I meant the outline kwarg in the constructor of the Font object.

@MyreMylar
Copy link
Member

I agree with @Starbuck that this should be implemented as a styling attribute to Font - similar to the current styling attributes bold, italic & underline. The current .render() function is already overlong with parameters like the 'antialias' parameter that definitely should have been an attribute defaulting to the most common usage (true).

Proposed test program:

import pygame
from pygame import Color, Font, QUIT

pygame.init()
window_surf = pygame.display.set_mode((400, 200))

impact_font = Font(filename="fonts/impact.ttf", size=64)

impact_font.outline_width = 2  # defaults to 0
impact_font.outline_color = Color("red")  # defaults to black

text_with_outline = impact_font.render(
    text="Hello World", antialias=True, color=Color("white")
)

running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False

    window_surf.fill(Color("black"))

    window_surf.blit(text_with_outline, (50, 50))

    pygame.display.update()

font.render can just check if outline_width is greater than 0 or not - and if so draw the outline before or after the main font rendering code - whichever looks better.

As an aside maybe we could make color and antialias into arguments with default values. We almost certainly would have already converted 'antialias' if it was on the other side of 'color' in the argument list. At least that way you could do:

font.render("Hello World", color=Color("white"))

Which isn't a typing saving over:

font.render("Hello World", True, Color("white"))

But probably clearer to read.

Might also open up:

font.default_color = Color("red")

hw_text_surf = font.render("Hello World in red")
other_text_surf = font.render("Other text, also in red")

Copy link
Member

@MyreMylar MyreMylar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 80% of the time when people use this they are going to want to draw an outline around actual visible text.

A possible upgrade to font.render() that might be nice in concert with this is to allow it to set alpha pixels on the produced surface. I believe at the minute text surfaces come out of render in a slightly odd format that looks like normal 32 bit 'ARGB' - but is subtly not that. You can see this by doing:

text_surf  = font.render("Hello World", True, Color("white"))
# text_surf = text_surf.convert_alpha()
text_surf = text_surf.premul_alpha() 

running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False

    window_surf.fill(Color("grey"))

    window_surf.blit(text_surf, (50, 50), special_flags=BLEND_PREMULTIPLIED)

    pygame.display.update()

and seeing the difference after uncommenting the .convert_alpha() line. I expect at some point it was marginally cheaper to blit whatever, not-quite-full-transparency format comes out of font.render() but I doubt it still is. Anyway, if we could render semi- and full transparency text then you would get the benefits of outline only text without having to change the API.

@narilee2006
Copy link
Contributor Author

Okay... I'll get to work on this.

@narilee2006 narilee2006 changed the title Outlineee Add outline option for rendering fonts Apr 29, 2024
docs/reST/ref/font.rst Outdated Show resolved Hide resolved
docs/reST/ref/font.rst Outdated Show resolved Hide resolved
| :sl:`Gets or sets the font's outline color`
| :sg:`outline_size -> RGB`

Returns the color of the outline.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto this doesn't just return it, it can be set here too. What does this default to?

Plus one of these attributes should have a small example of how to use this, with an image of what the output looks like.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example as in the examples directory? Or an example as an example image in the documentation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example as in a code block in the docs showing how it works, with a picture of the rendered output.

src_c/font.c Outdated Show resolved Hide resolved
src_c/font.c Outdated Show resolved Hide resolved
src_c/font.c Outdated
Comment on lines 548 to 568
if (!PyUnicode_Check(text) && !PyBytes_Check(text) && text != Py_None) {
return RAISE_TEXT_TYPE_ERROR();
PyErr_Format(PyExc_TypeError, "text must be a unicode or bytes");
return 0;
}

if (wraplength < 0) {
return RAISE(PyExc_ValueError,
"wraplength parameter must be positive");
PyErr_Format(PyExc_ValueError,
"The wraplength parameter must be positive.");
return 0;
}

if (PyUnicode_Check(text)) {
Py_ssize_t _size = -1;
astring = PyUnicode_AsUTF8AndSize(text, &_size);
if (astring == NULL) { /* exception already set */
return NULL;
if (astring == NULL) {
return 0; // exception already set.
}
if (strlen(astring) != (size_t)_size) {
return RAISE(PyExc_ValueError,
"A null character was found in the text");
PyErr_Format(PyExc_ValueError,
"A null character was found in the text.");
return 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these changes exist?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type is no longer a PyObject *.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RAISE doesn't return a PyObject, it returns NULL. And NULL is 0, so this is the same code?

Comment on lines -1046 to +1055
self.f = pygame_font.Font(None, 32)
self.f = pygame_font.Font(None, 96)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard to see the outline when the font is small.

src_c/font.c Outdated Show resolved Hide resolved
docs/reST/ref/font.rst Outdated Show resolved Hide resolved
}

SDL_Color foreg = {rgba[0], rgba[1], rgba[2], SDL_ALPHA_OPAQUE};
SDL_Color backg = {0, 0, 0, SDL_ALPHA_OPAQUE};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we warn here when outline colour is the same as background or foreground?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that'll be necessary as printing content to the console is already a controversial topic.

pygame/pygame#1468

src_c/font.c Outdated
static char *kwlist[] = {"text", "antialias", "color",
"bgcolor", "wraplength", NULL};

if (!PyArg_ParseTupleAndKeywords(args, kwds, "OpO|Oi", kwlist, &text,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having foreground colour and background colour as params, but outline colour as a property feels inconsistent. I would like to request some feedback from the maintainers on this. Maybe we should fix this now, maybe we need to make an issue for this after merge.

return 0; // exception already set
}

SDL_Color foreg = {rgba[0], rgba[1], rgba[2], SDL_ALPHA_OPAQUE};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the API here, shouldn't the background be transparent by default? I know this matches the old behaviour, but this probably warrants a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I learned from my professor that you cannot assign NULL to typedef structs. Correct me if she's wrong.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I meant I don't understand why the alpha isn't 0. It was like that before, but this part of the code is unclear to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src_c/font.c Outdated Show resolved Hide resolved
@robertpfeiffer
Copy link
Contributor

Overall, this is promising.

@narilee2006 narilee2006 force-pushed the outlineee branch 5 times, most recently from 00660ec to 0a2094a Compare May 4, 2024 09:43
docs/reST/ref/font.rst Outdated Show resolved Hide resolved
}

static int
font_setter_outline_width(PyFontObject *self, PyObject *value, void *closure)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I try to set a float it gives me this:

C:\Users\charl\Desktop\testr.py:8: DeprecationWarning: an integer is required (got type float).  Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python.
  arial.outline_width = 1.5

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not happen on my machine.

======================================================================
ERROR: test_set_outline_width_as_float (__main__.FontTypeTest.test_set_outline_width_as_float)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/nari/Documents/pygame-ce/test/font_test.py", line 392, in test_set_outline_width_as_float
    f.outline_width = 20.06
    ^^^^^^^^^^^^^^^
TypeError: 'float' object cannot be interpreted as an integer

----------------------------------------------------------------------
Ran 72 tests in 13.958s

@yunline yunline added New API This pull request may need extra debate as it adds a new class or function to pygame font pygame.font labels May 9, 2024
@@ -306,6 +306,30 @@ solves no longer exists, it will likely be removed in the future.

.. ## Font.point_size ##

.. attribute:: outline_width
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outline_radius is a better name if you're already going to mention radius in the docs description.

.. attribute:: outline_color

| :sl:`Gets or sets the font's outline color`
| :sg:`outline_width -> RGB`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You say (0, 0, 0, 255), but that's RGBA.

src_c/font.c Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
font pygame.font New API This pull request may need extra debate as it adds a new class or function to pygame
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants