File: ttf.py

package info (click to toggle)
pysdl2 0.9.17%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,328 kB
  • sloc: python: 24,685; makefile: 36; sh: 8
file content (783 lines) | stat: -rw-r--r-- 32,637 bytes parent folder | download
1
2
3
4
5
6
7
8
9
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
import os
import re
from ctypes import c_int, byref
from .. import surface, pixels, rwops, rect
from .compat import byteify, stringify, utf8, _is_text
from .err import raise_sdl_err
from .color import Color, convert_to_color
from .draw import prepare_color
from .resources import _validate_path
from .surface import _create_surface

_HASSDLTTF = True
try:
    from .. import sdlttf
except ImportError:
    _HASSDLTTF = False

__all__ = ["FontTTF", "FontManager"]


# This module variable keeps track of the number of separate times SDL_ttf has
# been initialized. This is important to keep track of, since de-initializing
# the library frees all associated font data, so we want to make sure this
# hasn't changed during the lifetime of an object to avoid segfaults.
_ttf_init_count = 0


def _ttf_init():
    global _ttf_init_count
    if not _HASSDLTTF:
        raise RuntimeError("SDL_ttf is required, but is not installed.")

    # Check if TTF already initialized, return immediately if it was
    if sdlttf.TTF_WasInit() > 0:
        return _ttf_init_count

    # Handle a weirdness in how TTF_Init and TTF_Quit work: TTF_Init
    # blindly increments TTF_WasInit every time it's called and TTF_Quit
    # blindly decrements it, but TTF_Quit only *actually* quits when 
    # TTF_WasInit - 1 == 0. Here, we try to ensure we're starting at 0.
    while sdlttf.TTF_WasInit() < 1:
        ret = sdlttf.TTF_Init()
        if ret != 0:
            raise_sdl_err("initializing the SDL_ttf library")

    # If initialized successfully, update and return the global init count
    _ttf_init_count += 1
    return _ttf_init_count


def _ttf_quit():
    if not _HASSDLTTF:
        raise RuntimeError("SDL_ttf is required, but is not installed.")

    # Make sure WasInit is non-negative before trying to quit
    while sdlttf.TTF_WasInit() < 1:
        ret = sdlttf.TTF_Init()
        if ret != 0:
            raise_sdl_err("initializing the SDL_ttf library")

    # Actually quit the library (won't really quit until TTF_WasInit == 0)
    while sdlttf.TTF_WasInit > 0:
        sdlttf.TTF_Quit()


def _is_whitespace(s):
    ws_chars = [" ", "\n", "\t", "\r"]
    return len(s) == 0 or all([c in ws_chars for c in s])


def _split_on_whitespace(line):
    # Splits a string on whitespace starts without removing the whitespace
    words = []
    current_word = ""
    for s in re.split(r'(\s+)', line):
        current_word += s
        if not _is_whitespace(s):
            words.append(current_word)
            current_word = ""
    return words



class FontTTF(object):
    """A class for rendering text with a given TrueType font.

    This class loads a TrueType or OpenType font using the **SDL_ttf** library
    and allows the user to render text with it in various sizes and colors.

    To simplify rendering text with different font sizes and colors, this class
    allows users to define font styles (e.g. 'instructions', 'error', 'title',
    etc.) using :meth:`add_style`, which can then be used by name with the
    :meth:`render_text` method.

    By default, the :class:`FontTTF` class defines font sizes in points (pt), a
    common unit of font size used by many libraries and text editors. However,
    you can also define font sizes in units of *pixel height* (px), i.e. the
    ascent height of the font's tallest alphanumeric ASCII character. This is
    done by calculating each font's px-to-pt scaling factor on import, meaning
    that some rounding error may occur. If using a font that does not include
    all alphanumeric ASCII characters (A-Z, a-z, 0-9) or wanting to scale the
    maximum pixel height to a different subset of characters (e.g. just 0-9
    digits), you will need to specifiy a custom 'height_chars' string when
    creating the font object.

    .. note::
       If loading a font from an SDL RWops file object, you must not free the
       file object until you are done with the font. Otherwise, SDL_ttf will
       try to render with a closed font and hard-crash Python. 
    
    Args:
        font (str or :obj:`SDL_RWops`): The relative (or absolute) path to the
            font to load, or an SDL file object containing the font data.
        size (int or str): The default size for the font, either as an integer
            (assumed to be in pt) or as a string specifying the unit of size
            (e.g. '12pt' or '22px').
        color (~sdl2.ext.Color or tuple): The default color to use for
            rendering text. Defaults to white if not specified.
        index (int, optional): The index of the font face to load if the font
            file contains multiple faces. Defaults to 0 (first face in the file)
            if not specified.
        height_chars (str, optional): The set of font characters to use for
            calculating the maximum height (in pixels) of the font for 
    
    """
    def __init__(self, font, size, color, index=0, height_chars=None):
        # Load the font, either from a file or an SDL file object
        if isinstance(font, rwops.SDL_RWops):
            fname = None
            self._font_file = None
            self._font_rw = font
        elif _is_text(font):
            fullpath, fname = _validate_path(font, "a font")
            fullpath = byteify(fullpath)
            self._font_file = open(fullpath, "rb")
            self._font_rw = rwops.rw_from_object(self._font_file)
        else:
            e = "'font' must be a path string or an SDL file object (got {0})."
            raise ValueError(e.format(type(font).__str__))
    
        # Initialize the TTF library (if not already initialized)
        self._ttf_init_count = _ttf_init()
        self._index = index

        # Get the px-to-pt scaling factor for the font
        if not height_chars:
            caps = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            height_chars = caps + caps.lower() + "0123456789"
        self._scale_factor = self._get_scale_factor(height_chars, fname)

        # Initialize font styles and add the default style    
        self._styles = {}
        self.add_style("default", size, color)

    def _check_if_closed(self):
        # Makes sure the font hasn't been closed, raising an exception if it has
        ttf_was_closed = _ttf_init_count > self._ttf_init_count
        if self._font_rw == None:
            e = "The font has been closed and can no longer be used."
            raise RuntimeError(e)
        elif sdlttf.TTF_WasInit() < 1 or ttf_was_closed:
            e = "The SDL_ttf library has been de-initialized since creating the "
            e += "font. The font can no longer be used."
            raise RuntimeError(e)

    def _get_scale_factor(self, testchars, fname):
        # Calculate the pt -> px conversion factor for the current font.
        tmpsize = 40  # Font size in pt at which to calculate pt -> px scale factor
        max_ascent = 0
        minX, maxX, minY, maxY = c_int(0), c_int(0), c_int(0), c_int(0)
        advance = c_int(0)
        tmpfont = sdlttf.TTF_OpenFontIndexRW(self._font_rw, 0, tmpsize, self._index)
        if not tmpfont:
            e = "initializing the font"
            if fname:
                e += " '{0}'".format(fname)
            raise_sdl_err(e)
        for char in testchars:
            if sdlttf.TTF_GlyphIsProvided(tmpfont, ord(char)) == 0:
                e = "The current font does not provide a glyph for character "
                e += "'{0}'. Please initialize the font with a different "
                e += "'height_chars' string."
                sdlttf.TTF_CloseFont(tmpfont)
                raise RuntimeError(e.format(char.encode('utf-8')))
            sdlttf.TTF_GlyphMetrics(
                tmpfont, ord(char), byref(minX), byref(maxX),
                byref(minY), byref(maxY), byref(advance)
            )
            if maxY.value > max_ascent:
                max_ascent = maxY.value
        sdlttf.TTF_CloseFont(tmpfont)
        return tmpsize / float(max_ascent)

    def _parse_size(self, size):
        # Parse and validate a font size, which can be an int or a string
        if isinstance(size, str):
            if '-' in size or '.' in size:
                e = "Font size must be a positive whole number (got '{0}')."
                raise ValueError(e.format(size))
            sz = "".join([i for i in size if i.isdigit()])
            unit = size[len(sz):]
            sz = int(sz)
            if unit == "px":
                sz = int(sz * self._scale_factor)
            elif unit not in ["pt", "px"]:
                e = "Font size units must be either 'pt' or 'px' (got '{0}')."
                raise ValueError(e.format(unit))
        else:
            sz = int(size)
        return sz

    def _parse_line_height(self, height, default_skip):
        if height is None:
            h = default_skip
        elif isinstance(height, str):
            # If line height specified as a percentage of the default line skip
            if height[-1] == "%":
                pct = float(height[:-1])
                if not pct > 0:
                    e = "Relative line heights must be greater than 0% (got '{0}')."
                    raise ValueError(e.format(height))
                h = int(default_skip * (pct / 100.0))
            # If line height specified explicitly in pixels
            elif height[-2:] == "px":
                if '-' in height or '.' in height:
                    e = "Line height must be a positive whole number (got '{0}')."
                    raise ValueError(e.format(height))
                h = int("".join([i for i in height if i.isdigit()]))
            else:
                e = "Line height units must be either 'px' or '%' (got '{0}')."
                raise ValueError(e.format(height))
        else:
            # If numeric, assume pixels
            if height != int(height) or height < 1:
                e = "Line height must be a positive integer (got {0})."
                raise ValueError(e.format(height))
            h = int(height)
        return max(h, 1)

    def _get_line_size(self, line, style):
        # Get the height and width of a given line of text in a given style
        font = self._styles[style]['font']
        if len(line) == 0:
            return (0, sdlttf.TTF_FontHeight(font))
        lw, lh = c_int(0), c_int(0)
        ret = sdlttf.TTF_SizeUTF8(font, byteify(line), byref(lw), byref(lh))
        if ret != 0:
            raise_sdl_err("rendering text with the '{0}' style".format(style))
        return (lw.value, lh.value)

    def _wrap_line(self, line, style, line_width, width):
        # Splits a single line of text into separate lines in order to fit
        # within a given width with a given text style
        lines = []
        while line_width > width:

            # First, estimate the position of the wrap word and move to the
            # end of it
            pos = int((width / float(line_width)) * len(line))
            words = _split_on_whitespace(line)
            for i in range(len(words)):
                split_word = i + 1
                segment = "".join(words[:split_word])
                if len(segment) >= pos and len(segment) < len(line):
                    pos = len(segment)
                    break

            # Then, work backwards until the line fits within the width
            segment_w = self._get_line_size(segment, style)[0]
            while segment_w > width:
                if split_word > 1:
                    split_word -= 1
                    segment = "".join(words[:split_word])
                    pos = len(segment)
                elif pos > 1:
                    pos -= 1
                    segment = segment[:pos]
                else:
                    e = "Character '{0}' exceeds maximum width of text"
                    raise RuntimeError(e.format(line[0]))
                segment_w = self._get_line_size(segment, style)[0]

            # Append the trimmed line & remove the text from the line, then
            # recalculate the width of the remaining text to see if it fits
            lines.append(segment)
            line = line[pos:].lstrip()
            line_width = self._get_line_size(line, style)[0]

        lines.append(line)
        return lines

    def _split_lines(self, text, style, width=None):
        # Splits one or more lines of text to ensure they all fit within a given
        # width (in pixels) when rendered with a given text style
        unwrapped_lines = text.split("\n")
        if width is None:
            return unwrapped_lines

        lines = []
        for line in unwrapped_lines:
            lw = self._get_line_size(line, style)[0]
            if lw > width:
                # If line is too long and contains trailing whitespace, try w/o
                if len(line) < len(line.rstrip()):
                    line = line.rstrip()
                    lw = self._get_line_size(line, style)[0]
                # If line width exceeds wrap width, we break it into smaller lines
                lines += self._wrap_line(line, style, lw, width)
            else:
                lines.append(line)

        return lines

    def _render_lines(self, lines, style, line_h, width, align):
        # Renders one or more lines of text to an SDL surface, optionally with
        # a custom line height, wrap width, and/or text alignment
        font = self._styles[style]['font']
        color = self._styles[style]['color']
        bg_col = self._styles[style]['bg']
        if not bg_col:
            c = color
            bg_col = (c.r, c.g, c.b, 0)  # Default bg = font color w/ full transparency

        # Actually render the text
        rendered = []
        for line in lines:
            if len(line) == 0:
                rendered.append(None)
                continue
            fontsf = sdlttf.TTF_RenderUTF8_Blended(font, byteify(line), color)
            if not fontsf:
                raise_sdl_err("rendering text with the '{0}' style".format(style))
            rendered.append(fontsf)

        # If single line of text w/ no background fill or custom width, return as-is
        if len(rendered) == 1 and width == None and bg_col is None:
            return rendered[0]

        # Determine height and width of background surface
        font_height = sdlttf.TTF_FontHeight(font)
        default_skip = sdlttf.TTF_FontLineSkip(font)
        line_h = self._parse_line_height(line_h, default_skip)
        height = line_h * (len(lines) - 1) + font_height
        if width == None:
            width = max([line.contents.w for line in rendered if line])

        # Create background surface for the text and render lines to it
        sf = _create_surface((width, height), bg_col, errname="background")
        line_y = 0
        for line in rendered:
            if line:
                lw, lh = (line.contents.w, line.contents.h)
                if align == "left":
                    line_x = 0
                elif align == "center":
                    line_x = int((width - lw) / 2)
                elif align == "right":
                    line_x = width - lw
                line_rect = rect.SDL_Rect(line_x, line_y, lw, lh)
                surface.SDL_BlitSurface(line, None, sf, line_rect)
                surface.SDL_FreeSurface(line)
            line_y += line_h

        return sf

    def get_ttf_font(self, style='default'):
        """Returns the base :obj:`~sdl2.sdlttf.TTF_Font` for a given font style.

        This method provides access to the base ctypes object for each font
        style so that they can be used with the full set of :mod:`~sdl2.sdlttf`
        functions.

        Args:
            style (str, optional): The font style for which the base ctypes font
                object will be retrieved. Defaults to the 'default' style if not
                specified.

        Returns:
            :obj:`~sdl2.sdlttf.TTF_Font`: The base ctypes font for the style.

        """
        self._check_if_closed()
        if style not in self._styles.keys():
            e = "The '{0}' style is not defined for the current font."
            raise ValueError(e.format(style))
        return self._styles[style]['font'].contents

    def add_style(self, name, size, color, bg_color=None):
        """Defines a named font style for the current font.

        Currently, a font style defines a font size, color, and background color
        combination to use for rendering text. Additional style attributes may
        be added in future releases.

        Args:
            name (str): The name of the new font style (e.g. 'title').
            size (int or str): The font size for the style, either as an integer
                (assumed to be in pt) or as a string specifying the unit of size
                (e.g. '12pt' or '22px').
            color (~sdl2.ext.Color): The font color for the style.
            bg_color (~sdl2.ext.Color, optional): The background surface color
                for the style. Defaults to a fully-transparent background if not
                specified.
    
        """
        # Preprocess and validate style values
        self._check_if_closed()
        if name in self._styles.keys():
            e = "A style named '{0}' is already defined for the current font."
            raise ValueError(e.format(name))
        size_pt = self._parse_size(size)
        if size_pt < 1:
            e = "Font size must be at least 1pt, got {0} (specified as '{1}')"
            raise ValueError(e.format(size_pt, size))
        c = convert_to_color(color)
        if bg_color is not None:
            bg_color = convert_to_color(bg_color)

        # Actually create font object for style
        rwops.SDL_RWseek(self._font_rw, 0, rwops.RW_SEEK_SET)
        font = sdlttf.TTF_OpenFontIndexRW(self._font_rw, 0, size_pt, self._index)
        if not font:
            raise_sdl_err("initializing the '{0}' font style".format(name))
        
        self._styles[name] = {
            'font': font,
            'size': size_pt,
            'color': pixels.SDL_Color(c.r, c.g, c.b, c.a),
            'bg': bg_color
        }

    def render_text(self, text, style='default', line_h=None, width=None, align='left'):
        """Renders a string of text to a new surface.

        If a newline character (``\\n``) is encountered in the string, it will
        be rendered as a line break in the rendered text. Additionally, if a
        surface width is specified, any lines of text that exceed the width of
        the surface will be wrapped. Multi-line text can be left-aligned
        (the default), right-aligned, or centered, and the spacing between lines
        can be modified using the `line_h` argument.

        Line heights can be specified in pixels (e.g. ``20`` or ``'20px'``) or
        as percentages of the TTF-suggested line spacing for the font (e.g.
        ``'150%'``).

        Args:
            text (str): The string of text to render to the target surface.
            style (str, optional): The font style with which to render the
                given string. Defaults to the 'default' style if not specified.
            line_h (int or str, optional): The line height to use for each
                line of the rendered text, either in pixels or as a percentage
                of the font's suggested line height. If not specified, the
                suggested line height for the font will be used.
            width (int, optional): The width (in pixels) of the output surface.
                If a line of text exceeds this value, it will be automatically
                wrapped to fit within the specified width. Defaults to ``None``.
            align (str, optional): The alignment of lines of text for multi-line
                strings. Can be 'left' (left-aligned), 'right' (right-aligned),
                or 'center' (centered). Defaults to 'left'.

        Returns:
            :obj:`~sdl2.SDL_Surface`: A 32-bit ARGB surface containing the
            rendered text.

        """
        self._check_if_closed()
        if len(text) == 0:
            raise ValueError("Cannot render an empty string.")
        if style not in self._styles.keys():
            e = "The '{0}' style is not defined for the current font."
            raise ValueError(e.format(style))
        if align not in ["left", "right", "center"]:
            e = "Text alignment mode must be 'left', 'right', or 'center'."
            raise ValueError(e)

        # Actually render the text
        lines = self._split_lines(utf8(text), style, width)
        sf = self._render_lines(lines, style, line_h, width, align)
        return sf.contents

    def close(self):
        """Cleanly closes the font and frees all associated memory.

        This method should be called when you are finished with the font to free
        the resources taken up by the font and its styles. Once this method has
        been called, the font can no longer be used.

        """
        ttf_was_closed = _ttf_init_count > self._ttf_init_count
        if self._font_rw != None:
            if sdlttf.TTF_WasInit() > 0 and not ttf_was_closed:
                for name, style in self._styles.items():
                    sdlttf.TTF_CloseFont(style['font'])
            self._styles = None
            if self._font_file:
                rwops.SDL_RWclose(self._font_rw)
            self._font_rw = None
            self._font_file = None

    def contains(self, c):
        """Checks whether a given character exists within the font.
        
        Args:
            c (str): The glpyh (i.e. character) to check for within the font.

        Returns:
            bool: ``True`` if the font contains the glpyh, otherwise ``False``.
    
        """
        self._check_if_closed()
        font = self._styles['default']['font']
        return sdlttf.TTF_GlyphIsProvided(font, ord(c)) != 0

    @property
    def family_name(self):
        """str: The family name (e.g. "Helvetica") of the loaded font.
        
        """
        self._check_if_closed()
        name = sdlttf.TTF_FontFaceFamilyName(self._styles['default']['font'])
        return stringify(name) if name else None

    @property
    def style_name(self):
        """str: The style name (e.g. "Bold") of the loaded font.
        
        """
        self._check_if_closed()
        name = sdlttf.TTF_FontFaceStyleName(self._styles['default']['font'])
        return stringify(name) if name else None

    @property
    def is_fixed_width(self):
        """bool: Whether the current font face is fixed-width (i.e. monospaced).

        If ``True``, all characters in the current font have the same width.

        """
        self._check_if_closed()
        font = self._styles['default']['font']
        return sdlttf.TTF_FontFaceIsFixedWidth(font) != 0



class FontManager(object):
    """A class for managing and rendering TrueType fonts.

    .. note:: 
       This class is has been deprecated in favor of the more flexible
       :class:`~sdl2.ext.FontTTF` class.
    
    This class provides a basic wrapper around the SDL_ttf library. One font
    path must be given to initialise the FontManager.
    
    The first face is always at index 0. It can be used for TTC (TrueType Font
    Collection) fonts.

    Args:
        font_path (str): The relative (or absolute) path to the font
            to load.
        alias (str, optional): The name to give the font within the
            FontManager. Defaults to the font filename if not specified.
        size (int, optional): The size (in pt) at which to load the default
            font. Defaults to 16pt if not specified.
        color (~sdl2.ext.Color): The default font rendering color. Defaults
            to opaque white if not specified.
        bg_color (~sdl2.ext.Color, optional): The default background surface
            color. Defaults to a fully-transparent background if not specified.
        index (int, optional): The index of the font face to load if the
            font file contains multiple faces. Defaults to 0 (first face in
            the file) if not specified.

    Attributes:
        size (int): The default font size in pt.

    """
    def __init__(self, font_path, alias=None, size=16,
                 color=Color(255, 255, 255), bg_color=Color(0, 0, 0), index=0):
        self._ttf_init_count = 0
        self._ttf_init_count = _ttf_init()
        self.fonts = {}  # fonts = {alias: {size:font_ptr}}
        self.aliases = {}  # aliases = {alias:font_path}
        self._textcolor = None
        self._bgcolor = None
        self.color = color
        self.bg_color = bg_color
        self.size = size
        self._default_font = self.add(font_path, alias, size, index)

    def __del__(self):
        """Close all opened fonts."""
        self.close()

    def close(self):
        """Closes all fonts opened by the class."""
        ttf_was_closed = _ttf_init_count > self._ttf_init_count
        if sdlttf.TTF_WasInit() > 0 and not ttf_was_closed:
            for alias, fonts in self.fonts.items():
                for size, font in fonts.items():
                    if font:
                        sdlttf.TTF_CloseFont(font)
        self.fonts = {}
        self.aliases = {}

    def add(self, font_path, alias=None, size=None, index=0):
        """Adds a font to the :class:`FontManager`.
        
        Args:
            font_path (str): The relative (or absolute) path to the font
                to load.
            alias (str, optional): The name to give the font within the
                FontManager. Defaults to the font filename if not specified.
            size (int, optional): The size (in pt) at which to load the font.
                Defaults to the FontManager's default size if not specified.
            index (int, optional): The index of the font face to load if the
                font file contains multiple faces. Defaults to 0 (first face in
                the file) if not specified.

        Returns:
            :obj:`~sdl2.sdlttf.TTF_Font`: A pointer to the ctypes font object
            for the added font.

        """
        size = size or self.size
        if alias is None:
            # If no alias given, take the font name as alias
            basename = os.path.basename(font_path)
            alias = os.path.splitext(basename)[0]
            if alias in self.fonts:
                if size in self.fonts[alias] and self.fonts[alias]:
                    # font with selected size already opened
                    return
                else:
                    self._change_font_size(alias, size)
                    return
            else:
                if not os.path.isfile(font_path):
                    raise IOError("Cannot find %s" % font_path)

        font = self._load_font(font_path, size, index)
        self.aliases[alias] = font_path
        self.fonts[alias] = {}
        self.fonts[alias][size] = font
        return font

    def _load_font(self, font_path, size, index=0):
        """Helper function to open the font.

        Raises an exception if something went wrong.
        """
        fullpath, fname = _validate_path(font_path, "a font")
        fullpath = byteify(fullpath)
        font = sdlttf.TTF_OpenFontIndex(fullpath, size, index)
        if not font:
            raise_sdl_err("opening the font '{0}'".format(fname))
        return font

    def _change_font_size(self, alias, size):
        """Loads an already opened font in another size."""
        if alias not in self.fonts:
            raise KeyError("Font %s not loaded in FontManager" % alias)
        font = self._load_font(self.aliases[alias], size)
        self.fonts[alias][size] = font

    @property
    def color(self):
        """:obj:`~sdl2.ext.Color`: The color to use for rendering text."""
        c = self._textcolor
        return Color(c.r, c.g, c.b, c.a)

    @color.setter
    def color(self, value):
        c = convert_to_color(value)
        self._textcolor = pixels.SDL_Color(c.r, c.g, c.b, c.a)

    @property
    def bg_color(self):
        """:obj:`~sdl2.ext.Color`: The background color to use for rendering."""
        c = self._bgcolor
        return Color(c.r, c.g, c.b, c.a)

    @bg_color.setter
    def bg_color(self, value):
        c = convert_to_color(value)
        self._bgcolor = pixels.SDL_Color(c.r, c.g, c.b, c.a)

    @property
    def default_font(self):
        """str: The name of the default font. Must be set to the alias of a
        currently-loaded font.

        """
        for alias in self.fonts:
            for size, font in self.fonts[alias].items():
                if font == self._default_font:
                    return alias

    @default_font.setter
    def default_font(self, value):
        alias = value
        size = self.size
        if alias not in self.fonts:
            raise ValueError("Font %s not loaded in FontManager" % alias)
        # Check if size is already loaded, otherwise do it.
        if size not in self.fonts[alias]:
            self._change_font_size(alias, size)
            size = list(self.fonts[alias].keys())[0]
        self._default_font = self.fonts[alias][size]

    def render(self, text, alias=None, size=None, width=None, color=None,
               bg_color=None, **kwargs):
        """Renders text to a surface.

        Args:
            text (str): The text to render.
            alias (str, optional): The alias of the font to use for rendering
                the text. Defaults to the FontManager's default font if not
                specified.
            size (int, optional): The size (in pt) at which to render the font.
                Defaults to the FontManager's default size if not specified.
            width (int, optional): The width (in pixels) of the output surface.
                If a line of text exceeds this value, it will be automatically
                wrapped to fit within the specified width. Defaults to ``None``.
            color (~sdl2.ext.Color): The font rendering color. Defaults to the
                FontManager's default color if not specified.
            bg_color (~sdl2.ext.Color, optional): The background surface color.
                Defaults to the FontManager's default background color if not
                specified.

        Returns:
            :obj:`~sdl2.SDL_Surface`: A 32-bit ARGB surface containing the
            rendered text.

        """
        alias = alias or self.default_font
        size = size or self.size
        if bg_color is None:
            bg_color = self._bgcolor
        elif not isinstance(bg_color, pixels.SDL_Color):
            c = convert_to_color(bg_color)
            bg_color = pixels.SDL_Color(c.r, c.g, c.b, c.a)
        if color is None:
            color = self._textcolor
        elif not isinstance(color, pixels.SDL_Color):
            c = convert_to_color(color)
            color = pixels.SDL_Color(c.r, c.g, c.b, c.a)
        if len(self.fonts) == 0:
            raise TypeError("There are no fonts selected.")
        font = self._default_font
        if alias not in self.aliases:
            raise KeyError("Font %s not loaded" % font)
        elif size not in self.fonts[alias]:
            self._change_font_size(alias, size)
        font = self.fonts[alias][size]
        text = byteify(text, "utf-8")
        if width:
            fontsf = sdlttf.TTF_RenderUTF8_Blended_Wrapped(font, text, color,
                                                           width)
            if not fontsf:
                raise_sdl_err("rendering the text")
            if bg_color != pixels.SDL_Color(0, 0, 0):
                fontsf = fontsf.contents
                w, h = fontsf.w, fontsf.h
                bpp = fontsf.format.contents.BitsPerPixel
                fmt = fontsf.format.contents.format
                bgsf = surface.SDL_CreateRGBSurfaceWithFormat(0, w, h, bpp, fmt)
                if not bgsf:
                    surface.SDL_FreeSurface(fontsf)
                    raise_sdl_err("creating the background surface")
                bg_color = prepare_color(bg_color, bgsf.contents)
                surface.SDL_FillRect(bgsf, None, bg_color)
                surface.SDL_BlitSurface(fontsf, None, bgsf, None)
                return bgsf.contents
            return fontsf.contents
        sf = None
        if bg_color == pixels.SDL_Color(0, 0, 0):
            sf = sdlttf.TTF_RenderUTF8_Blended(font, text, color)
        else:
            sf = sdlttf.TTF_RenderUTF8_Shaded(font, text, color,
                                              bg_color)
        if not sf:
            raise_sdl_err("rendering the text")
        return sf.contents