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
|