Font

Handles loading custom fonts from a spritesheet or image sequence, and rendering text onto a surface.

Source code in robingame/text/font.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
class Font:
    """
    Handles loading custom fonts from a spritesheet or image sequence, and rendering text onto a
    surface.
    """

    letters: dict[str:Surface]
    image_size: tuple[int, int]
    xpad: int
    ypad: int

    def __init__(
        self,
        images: list[Surface],
        letters: str,
        xpad: int = 0,
        ypad: int = 0,
        trim: bool = False,
        space_width: int = None,
    ):
        """
        Create a Font from a list of images.
        Usually you'll want to use `from_spritesheet` or `from_image_sequence` instead.

        Args:
            images: a list of images representing characters
            letters: a string of characters in the same order as the images
            xpad: extra x space between characters (in pixels). Can be negative.
            ypad: extra y space between characters (in pixels). Can be negative.
            trim: if `True`, trim any empty x space from the characters so that the width of
                each character depends on the letter ("l" will be narrower than "m"). If `False`,
                leave the characters equal width (results in a monospaced font)
            space_width: desired width of the space character (in pixels). If omitted, the space
                will be made as wide as a character
        """
        try:
            self.image_size = width, height = images[0].get_size()
        except IndexError:
            raise TextError(f"{self.__class__.__name__}.__init__ received no images!")

        self.xpad = xpad
        self.ypad = ypad
        self.letters = dict()
        self.not_found = Surface(self.image_size)
        self.not_found.fill(Color("red"))
        if trim:
            images = self._trim_images(images)
        self.letters.update({letter: image for letter, image in zip(letters, images)})
        if space_width:
            self.letters[" "] = empty_image((space_width or width, height))

    @classmethod
    def from_spritesheet(
        cls,
        filename: str | Path,
        image_size: tuple[int, int],
        letters: str,
        xpad: int = 0,
        ypad: int = 0,
        trim: bool = False,
        space_width: int = None,
        **kwargs: dict,
    ) -> "Font":
        """
        Loads the font from a spritesheet using `load_spritesheet`.

        Args:
            filename: path to the spritesheet of letters
            image_size: the xy size of a character image in the spritesheet (we assume the
                characters are evenly spaced in the spritesheet)
            letters: see `__init__`
            xpad: see `__init__`
            ypad: see `__init__`
            trim: see `__init__`
            space_width: see `__init__`
            kwargs: passed to `load_spritesheet`

        Example:
            ```
            test_font = Font.from_spritesheet(
                filename="test_font.png",
                image_size=(16, 16),
                letters=(
                    string.ascii_uppercase
                    + string.ascii_lowercase
                    + r"1234567890-=!@#$%^&*()_+[];',./{}|:<>?~`"
                ),
                trim=True,
                xpad=1,
                space_width=8,
            )
            ```

        """
        return cls(
            images=load_spritesheet(filename, image_size=image_size, **kwargs),
            letters=letters,
            xpad=xpad,
            ypad=ypad,
            trim=trim,
            space_width=space_width,
        )

    @classmethod
    def from_image_sequence(
        cls,
        pattern: str | Path,
        letters: str,
        xpad: int = 0,
        ypad: int = 0,
        trim: bool = False,
        space_width: int = None,
        **kwargs: dict,
    ) -> "Font":
        """
        Loads the font from a sequence of images using `load_image_sequence`.

        Args:
            pattern: file pattern used to glob the images
            letters: see `__init__`
            xpad: see `__init__`
            ypad: see `__init__`
            trim: see `__init__`
            space_width: see `__init__`
            kwargs: passed to `load_image_sequence`

        Example:
            ```
            test_font = Font.from_image_sequence(
                pattern="font*.png",  # matches font1.png, font2.png, etc.
                letters=(
                    string.ascii_uppercase
                    + string.ascii_lowercase
                    + r"1234567890-=!@#$%^&*()_+[];',./{}|:<>?~`"
                ),
                trim=True,
                xpad=1,
                space_width=8,
            )
            ```

        """
        return cls(
            images=load_image_sequence(pattern=pattern, **kwargs),
            letters=letters,
            xpad=xpad,
            ypad=ypad,
            trim=trim,
            space_width=space_width,
        )

    def render(
        self,
        surf: Surface,
        text: str,
        x: int = 0,
        y: int = 0,
        scale: int = 1,
        wrap: int = 0,
        align: int = None,
    ) -> int:
        """
        Render text onto a surface.

        Args:
            surf: surface on which to render the text
            text: the string of characters to render in this font
            x: x-position on the surface
            y: y-position on the surface
            scale: factor by which to scale the text (1 = no scaling)
            wrap: x width at which to wrap text
            align: -1=left, 0=center, 1=right

        Example:
            ```
            test_font.render(
                surface,
                text="Hello world!",
                scale=2,
                wrap=50,
                x=10,
                y=20,
                align=-1,
            )
            ```
        """
        _, ysize = self.image_size
        cursor = x
        for line in text.splitlines():
            wrapped_lines = self._wrap_words(line, wrap, x, scale) if wrap else [line]
            for line in wrapped_lines:
                cursor = self._align_cursor(line, x, align, scale, wrap)
                for letter in line:
                    image = self.get(letter)
                    image = scale_image(image, scale)
                    surf.blit(image, (cursor, y))
                    w = image.get_width()
                    cursor += w + self.xpad * scale
                y += (ysize + self.ypad) * scale
        return cursor

    def get(self, letter: str) -> Surface:
        """
        Get the image associated with a letter.

        If this font does not have a character for the letter, return the error image ( usually a
        red rectangle)

        Args:
            letter:
        """
        try:
            return self.letters[letter]
        except KeyError:
            return self.not_found

    def _align_cursor(self, line: str, x: int, align: int, scale: int, wrap: int) -> int:
        """
        Used for left/right/centered text alignmnent
        """
        match align:
            case -1 | None:
                cursor = x
            case 0:
                if not wrap:
                    raise TextError("Can't center text without specifying a wrap width.")
                line_width = self._printed_width(line, scale)
                slack = wrap - line_width
                cursor = x + slack // 2
            case 1:
                line_width = self._printed_width(line, scale)
                cursor = x + wrap - line_width
            case _:
                raise TextError(f"Bad alignment value: {align}")
        return cursor

    def _wrap_words(self, text: str, wrap: int, x: int = 0, scale: int = 1) -> list[str]:
        """
        Break one long line into multiple lines based on the wrap width.
        """
        lines = []
        line = ""
        for word in text.split(" "):
            new_line = f"{line} {word}" if line else word
            if self._printed_width(new_line, scale) <= wrap:
                line = new_line
            else:
                lines.append(line)
                line = word
        lines.append(line)  # last line
        return lines

    def _printed_width(self, text: str, scale: int) -> int:
        """
        Calculate how wide a string of text will be when rendered.
        """
        return sum((self.get(letter).get_width() + self.xpad) * scale for letter in text)

    def _trim_images(self, images: list[Surface]) -> list[Surface]:
        """
        Make a monospaced font non-monospaced
        """
        trimmed = []
        for image in images:
            x, _, w, _ = image.get_bounding_rect()  # trim x to bounding rect
            _, y, _, h = image.get_rect()  # maintain original y position of character
            new = image.subsurface((x, y, w, h))
            trimmed.append(new)
        return trimmed

__init__(images, letters, xpad=0, ypad=0, trim=False, space_width=None)

Create a Font from a list of images. Usually you'll want to use from_spritesheet or from_image_sequence instead.

Parameters:
  • images (list[Surface]) –

    a list of images representing characters

  • letters (str) –

    a string of characters in the same order as the images

  • xpad (int) –

    extra x space between characters (in pixels). Can be negative.

  • ypad (int) –

    extra y space between characters (in pixels). Can be negative.

  • trim (bool) –

    if True, trim any empty x space from the characters so that the width of each character depends on the letter ("l" will be narrower than "m"). If False, leave the characters equal width (results in a monospaced font)

  • space_width (int) –

    desired width of the space character (in pixels). If omitted, the space will be made as wide as a character

robingame/text/font.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def __init__(
    self,
    images: list[Surface],
    letters: str,
    xpad: int = 0,
    ypad: int = 0,
    trim: bool = False,
    space_width: int = None,
):
    """
    Create a Font from a list of images.
    Usually you'll want to use `from_spritesheet` or `from_image_sequence` instead.

    Args:
        images: a list of images representing characters
        letters: a string of characters in the same order as the images
        xpad: extra x space between characters (in pixels). Can be negative.
        ypad: extra y space between characters (in pixels). Can be negative.
        trim: if `True`, trim any empty x space from the characters so that the width of
            each character depends on the letter ("l" will be narrower than "m"). If `False`,
            leave the characters equal width (results in a monospaced font)
        space_width: desired width of the space character (in pixels). If omitted, the space
            will be made as wide as a character
    """
    try:
        self.image_size = width, height = images[0].get_size()
    except IndexError:
        raise TextError(f"{self.__class__.__name__}.__init__ received no images!")

    self.xpad = xpad
    self.ypad = ypad
    self.letters = dict()
    self.not_found = Surface(self.image_size)
    self.not_found.fill(Color("red"))
    if trim:
        images = self._trim_images(images)
    self.letters.update({letter: image for letter, image in zip(letters, images)})
    if space_width:
        self.letters[" "] = empty_image((space_width or width, height))

from_image_sequence(pattern, letters, xpad=0, ypad=0, trim=False, space_width=None, **kwargs) classmethod

Loads the font from a sequence of images using load_image_sequence.

Parameters:
  • pattern (str | Path) –

    file pattern used to glob the images

  • letters (str) –

    see __init__

  • xpad (int) –

    see __init__

  • ypad (int) –

    see __init__

  • trim (bool) –

    see __init__

  • space_width (int) –

    see __init__

  • kwargs (dict) –

    passed to load_image_sequence

Example
test_font = Font.from_image_sequence(
    pattern="font*.png",  # matches font1.png, font2.png, etc.
    letters=(
        string.ascii_uppercase
        + string.ascii_lowercase
        + r"1234567890-=!@#$%^&*()_+[];',./{}|:<>?~`"
    ),
    trim=True,
    xpad=1,
    space_width=8,
)
robingame/text/font.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@classmethod
def from_image_sequence(
    cls,
    pattern: str | Path,
    letters: str,
    xpad: int = 0,
    ypad: int = 0,
    trim: bool = False,
    space_width: int = None,
    **kwargs: dict,
) -> "Font":
    """
    Loads the font from a sequence of images using `load_image_sequence`.

    Args:
        pattern: file pattern used to glob the images
        letters: see `__init__`
        xpad: see `__init__`
        ypad: see `__init__`
        trim: see `__init__`
        space_width: see `__init__`
        kwargs: passed to `load_image_sequence`

    Example:
        ```
        test_font = Font.from_image_sequence(
            pattern="font*.png",  # matches font1.png, font2.png, etc.
            letters=(
                string.ascii_uppercase
                + string.ascii_lowercase
                + r"1234567890-=!@#$%^&*()_+[];',./{}|:<>?~`"
            ),
            trim=True,
            xpad=1,
            space_width=8,
        )
        ```

    """
    return cls(
        images=load_image_sequence(pattern=pattern, **kwargs),
        letters=letters,
        xpad=xpad,
        ypad=ypad,
        trim=trim,
        space_width=space_width,
    )

from_spritesheet(filename, image_size, letters, xpad=0, ypad=0, trim=False, space_width=None, **kwargs) classmethod

Loads the font from a spritesheet using load_spritesheet.

Parameters:
  • filename (str | Path) –

    path to the spritesheet of letters

  • image_size (tuple[int, int]) –

    the xy size of a character image in the spritesheet (we assume the characters are evenly spaced in the spritesheet)

  • letters (str) –

    see __init__

  • xpad (int) –

    see __init__

  • ypad (int) –

    see __init__

  • trim (bool) –

    see __init__

  • space_width (int) –

    see __init__

  • kwargs (dict) –

    passed to load_spritesheet

Example
test_font = Font.from_spritesheet(
    filename="test_font.png",
    image_size=(16, 16),
    letters=(
        string.ascii_uppercase
        + string.ascii_lowercase
        + r"1234567890-=!@#$%^&*()_+[];',./{}|:<>?~`"
    ),
    trim=True,
    xpad=1,
    space_width=8,
)
robingame/text/font.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
@classmethod
def from_spritesheet(
    cls,
    filename: str | Path,
    image_size: tuple[int, int],
    letters: str,
    xpad: int = 0,
    ypad: int = 0,
    trim: bool = False,
    space_width: int = None,
    **kwargs: dict,
) -> "Font":
    """
    Loads the font from a spritesheet using `load_spritesheet`.

    Args:
        filename: path to the spritesheet of letters
        image_size: the xy size of a character image in the spritesheet (we assume the
            characters are evenly spaced in the spritesheet)
        letters: see `__init__`
        xpad: see `__init__`
        ypad: see `__init__`
        trim: see `__init__`
        space_width: see `__init__`
        kwargs: passed to `load_spritesheet`

    Example:
        ```
        test_font = Font.from_spritesheet(
            filename="test_font.png",
            image_size=(16, 16),
            letters=(
                string.ascii_uppercase
                + string.ascii_lowercase
                + r"1234567890-=!@#$%^&*()_+[];',./{}|:<>?~`"
            ),
            trim=True,
            xpad=1,
            space_width=8,
        )
        ```

    """
    return cls(
        images=load_spritesheet(filename, image_size=image_size, **kwargs),
        letters=letters,
        xpad=xpad,
        ypad=ypad,
        trim=trim,
        space_width=space_width,
    )

get(letter)

Get the image associated with a letter.

If this font does not have a character for the letter, return the error image ( usually a red rectangle)

Parameters:
  • letter (str) –
robingame/text/font.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def get(self, letter: str) -> Surface:
    """
    Get the image associated with a letter.

    If this font does not have a character for the letter, return the error image ( usually a
    red rectangle)

    Args:
        letter:
    """
    try:
        return self.letters[letter]
    except KeyError:
        return self.not_found

render(surf, text, x=0, y=0, scale=1, wrap=0, align=None)

Render text onto a surface.

Parameters:
  • surf (Surface) –

    surface on which to render the text

  • text (str) –

    the string of characters to render in this font

  • x (int) –

    x-position on the surface

  • y (int) –

    y-position on the surface

  • scale (int) –

    factor by which to scale the text (1 = no scaling)

  • wrap (int) –

    x width at which to wrap text

  • align (int) –

    -1=left, 0=center, 1=right

Example
test_font.render(
    surface,
    text="Hello world!",
    scale=2,
    wrap=50,
    x=10,
    y=20,
    align=-1,
)
robingame/text/font.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def render(
    self,
    surf: Surface,
    text: str,
    x: int = 0,
    y: int = 0,
    scale: int = 1,
    wrap: int = 0,
    align: int = None,
) -> int:
    """
    Render text onto a surface.

    Args:
        surf: surface on which to render the text
        text: the string of characters to render in this font
        x: x-position on the surface
        y: y-position on the surface
        scale: factor by which to scale the text (1 = no scaling)
        wrap: x width at which to wrap text
        align: -1=left, 0=center, 1=right

    Example:
        ```
        test_font.render(
            surface,
            text="Hello world!",
            scale=2,
            wrap=50,
            x=10,
            y=20,
            align=-1,
        )
        ```
    """
    _, ysize = self.image_size
    cursor = x
    for line in text.splitlines():
        wrapped_lines = self._wrap_words(line, wrap, x, scale) if wrap else [line]
        for line in wrapped_lines:
            cursor = self._align_cursor(line, x, align, scale, wrap)
            for letter in line:
                image = self.get(letter)
                image = scale_image(image, scale)
                surf.blit(image, (cursor, y))
                w = image.get_width()
                cursor += w + self.xpad * scale
            y += (ysize + self.ypad) * scale
    return cursor