1import os
2
3if os.environ.get("SDL_VIDEODRIVER") == "dummy":
4    __tags__ = ("ignore", "subprocess_ignore")
5
6import unittest
7import sys
8import ctypes
9import weakref
10import gc
11import platform
12
13try:
14    import pathlib
15except ImportError:
16    pathlib = None
17
18IS_PYPY = "PyPy" == platform.python_implementation()
19
20
21try:
22    from pygame.tests.test_utils import arrinter
23except NameError:
24    pass
25
26import pygame
27
28try:
29    import pygame.freetype as ft
30except ImportError:
31    ft = None
32from pygame.compat import as_unicode, bytes_, unichr_, unicode_
33
34
35FONTDIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures", "fonts")
36
37
38def nullfont():
39    """return an uninitialized font instance"""
40    return ft.Font.__new__(ft.Font)
41
42
43max_point_size_FX6 = 0x7FFFFFFF
44max_point_size = max_point_size_FX6 >> 6
45max_point_size_f = max_point_size_FX6 * 0.015625
46
47
48def surf_same_image(a, b):
49    """Return True if a's pixel buffer is identical to b's"""
50
51    a_sz = a.get_height() * a.get_pitch()
52    b_sz = b.get_height() * b.get_pitch()
53    if a_sz != b_sz:
54        return False
55    a_bytes = ctypes.string_at(a._pixels_address, a_sz)
56    b_bytes = ctypes.string_at(b._pixels_address, b_sz)
57    return a_bytes == b_bytes
58
59
60class FreeTypeFontTest(unittest.TestCase):
61
62    _fixed_path = os.path.join(FONTDIR, "test_fixed.otf")
63    _sans_path = os.path.join(FONTDIR, "test_sans.ttf")
64    _mono_path = os.path.join(FONTDIR, "PyGameMono.otf")
65    _bmp_8_75dpi_path = os.path.join(FONTDIR, "PyGameMono-8.bdf")
66    _bmp_18_75dpi_path = os.path.join(FONTDIR, "PyGameMono-18-75dpi.bdf")
67    _bmp_18_100dpi_path = os.path.join(FONTDIR, "PyGameMono-18-100dpi.bdf")
68    _TEST_FONTS = {}
69
70    @classmethod
71    def setUpClass(cls):
72        ft.init()
73
74        # Setup the test fonts.
75
76        # Inconsolata is an open-source font designed by Raph Levien.
77        # Licensed under the Open Font License.
78        # http://www.levien.com/type/myfonts/inconsolata.html
79        cls._TEST_FONTS["fixed"] = ft.Font(cls._fixed_path)
80
81        # Liberation Sans is an open-source font designed by Steve Matteson.
82        # Licensed under the GNU GPL.
83        # https://fedorahosted.org/liberation-fonts/
84        cls._TEST_FONTS["sans"] = ft.Font(cls._sans_path)
85
86        # A scalable mono test font made for pygame. It contains only
87        # a few glyphs: '\0', 'A', 'B', 'C', and U+13079.
88        # It also contains two bitmap sizes: 8.0 X 8.0 and 19.0 X 19.0.
89        cls._TEST_FONTS["mono"] = ft.Font(cls._mono_path)
90
91        # A fixed size bitmap mono test font made for pygame.
92        # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079.
93        # The size is 8.0 X 8.0.
94        cls._TEST_FONTS["bmp-8-75dpi"] = ft.Font(cls._bmp_8_75dpi_path)
95
96        # A fixed size bitmap mono test font made for pygame.
97        # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079.
98        # The size is 8.0 X 8.0.
99        cls._TEST_FONTS["bmp-18-75dpi"] = ft.Font(cls._bmp_18_75dpi_path)
100
101        # A fixed size bitmap mono test font made for pygame.
102        # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079.
103        # The size is 8.0 X 8.0.
104        cls._TEST_FONTS["bmp-18-100dpi"] = ft.Font(cls._bmp_18_100dpi_path)
105
106    @classmethod
107    def tearDownClass(cls):
108        ft.quit()
109
110    def test_freetype_defaultfont(self):
111        font = ft.Font(None)
112        self.assertEqual(font.name, "FreeSans")
113
114    def test_freetype_Font_init(self):
115
116        self.assertRaises(IOError, ft.Font, os.path.join(FONTDIR, "nonexistant.ttf"))
117
118        f = self._TEST_FONTS["sans"]
119        self.assertIsInstance(f, ft.Font)
120
121        f = self._TEST_FONTS["fixed"]
122        self.assertIsInstance(f, ft.Font)
123
124        # Test keyword arguments
125        f = ft.Font(size=22, file=None)
126        self.assertEqual(f.size, 22)
127        f = ft.Font(font_index=0, file=None)
128        self.assertNotEqual(ft.get_default_resolution(), 100)
129        f = ft.Font(resolution=100, file=None)
130        self.assertEqual(f.resolution, 100)
131        f = ft.Font(ucs4=True, file=None)
132        self.assertTrue(f.ucs4)
133        self.assertRaises(OverflowError, ft.Font, file=None, size=(max_point_size + 1))
134        self.assertRaises(OverflowError, ft.Font, file=None, size=-1)
135
136        f = ft.Font(None, size=24)
137        self.assertTrue(f.height > 0)
138        self.assertRaises(IOError, f.__init__, os.path.join(FONTDIR, "nonexistant.ttf"))
139
140        # Test attribute preservation during reinitalization
141        f = ft.Font(self._sans_path, size=24, ucs4=True)
142        self.assertEqual(f.name, "Liberation Sans")
143        self.assertTrue(f.scalable)
144        self.assertFalse(f.fixed_width)
145        self.assertTrue(f.antialiased)
146        self.assertFalse(f.oblique)
147        self.assertTrue(f.ucs4)
148        f.antialiased = False
149        f.oblique = True
150        f.__init__(self._mono_path)
151        self.assertEqual(f.name, "PyGameMono")
152        self.assertTrue(f.scalable)
153        self.assertTrue(f.fixed_width)
154        self.assertFalse(f.antialiased)
155        self.assertTrue(f.oblique)
156        self.assertTrue(f.ucs4)
157
158        # For a bitmap font, the size is automatically set to the first
159        # size in the available sizes list.
160        f = ft.Font(self._bmp_8_75dpi_path)
161        sizes = f.get_sizes()
162        self.assertEqual(len(sizes), 1)
163        size_pt, width_px, height_px, x_ppem, y_ppem = sizes[0]
164        self.assertEqual(f.size, (x_ppem, y_ppem))
165        f.__init__(self._bmp_8_75dpi_path, size=12)
166        self.assertEqual(f.size, 12.0)
167
168    @unittest.skipIf(IS_PYPY, "PyPy doesn't use refcounting")
169    def test_freetype_Font_dealloc(self):
170        import sys
171
172        handle = open(self._sans_path, "rb")
173
174        def load_font():
175            tempFont = ft.Font(handle)
176
177        try:
178            load_font()
179
180            self.assertEqual(sys.getrefcount(handle), 2)
181        finally:
182            # Ensures file is closed even if test fails.
183            handle.close()
184
185    def test_freetype_Font_kerning(self):
186        """Ensures get/set works with the kerning property."""
187        ft_font = self._TEST_FONTS["sans"]
188
189        # Test default is disabled.
190        self.assertFalse(ft_font.kerning)
191
192        # Test setting to True.
193        ft_font.kerning = True
194
195        self.assertTrue(ft_font.kerning)
196
197        # Test setting to False.
198        ft_font.kerning = False
199
200        self.assertFalse(ft_font.kerning)
201
202    def test_freetype_Font_kerning__enabled(self):
203        """Ensures exceptions are not raised when calling freetype methods
204        while kerning is enabled.
205
206        Note: This does not test what changes occur to a rendered font by
207              having kerning enabled.
208
209        Related to issue #367.
210        """
211        surface = pygame.Surface((10, 10), 0, 32)
212        TEST_TEXT = "Freetype Font"
213        ft_font = self._TEST_FONTS["bmp-8-75dpi"]
214
215        ft_font.kerning = True
216
217        # Call different methods to ensure they don't raise an exception.
218        metrics = ft_font.get_metrics(TEST_TEXT)
219        self.assertIsInstance(metrics, list)
220
221        rect = ft_font.get_rect(TEST_TEXT)
222        self.assertIsInstance(rect, pygame.Rect)
223
224        font_surf, rect = ft_font.render(TEST_TEXT)
225        self.assertIsInstance(font_surf, pygame.Surface)
226        self.assertIsInstance(rect, pygame.Rect)
227
228        rect = ft_font.render_to(surface, (0, 0), TEST_TEXT)
229        self.assertIsInstance(rect, pygame.Rect)
230
231        buf, size = ft_font.render_raw(TEST_TEXT)
232        self.assertIsInstance(buf, bytes_)
233        self.assertIsInstance(size, tuple)
234
235        rect = ft_font.render_raw_to(surface.get_view("2"), TEST_TEXT)
236        self.assertIsInstance(rect, pygame.Rect)
237
238    def test_freetype_Font_scalable(self):
239
240        f = self._TEST_FONTS["sans"]
241        self.assertTrue(f.scalable)
242
243        self.assertRaises(RuntimeError, lambda: nullfont().scalable)
244
245    def test_freetype_Font_fixed_width(self):
246
247        f = self._TEST_FONTS["sans"]
248        self.assertFalse(f.fixed_width)
249
250        f = self._TEST_FONTS["mono"]
251        self.assertTrue(f.fixed_width)
252
253        self.assertRaises(RuntimeError, lambda: nullfont().fixed_width)
254
255    def test_freetype_Font_fixed_sizes(self):
256
257        f = self._TEST_FONTS["sans"]
258        self.assertEqual(f.fixed_sizes, 0)
259        f = self._TEST_FONTS["bmp-8-75dpi"]
260        self.assertEqual(f.fixed_sizes, 1)
261        f = self._TEST_FONTS["mono"]
262        self.assertEqual(f.fixed_sizes, 2)
263
264    def test_freetype_Font_get_sizes(self):
265        f = self._TEST_FONTS["sans"]
266        szlist = f.get_sizes()
267        self.assertIsInstance(szlist, list)
268        self.assertEqual(len(szlist), 0)
269
270        f = self._TEST_FONTS["bmp-8-75dpi"]
271        szlist = f.get_sizes()
272        self.assertIsInstance(szlist, list)
273        self.assertEqual(len(szlist), 1)
274
275        size8 = szlist[0]
276        self.assertIsInstance(size8[0], int)
277        self.assertEqual(size8[0], 8)
278        self.assertIsInstance(size8[1], int)
279        self.assertIsInstance(size8[2], int)
280        self.assertIsInstance(size8[3], float)
281        self.assertEqual(int(size8[3] * 64.0 + 0.5), 8 * 64)
282        self.assertIsInstance(size8[4], float)
283        self.assertEqual(int(size8[4] * 64.0 + 0.5), 8 * 64)
284
285        f = self._TEST_FONTS["mono"]
286        szlist = f.get_sizes()
287        self.assertIsInstance(szlist, list)
288        self.assertEqual(len(szlist), 2)
289
290        size8 = szlist[0]
291        self.assertEqual(size8[3], 8)
292        self.assertEqual(int(size8[3] * 64.0 + 0.5), 8 * 64)
293        self.assertEqual(int(size8[4] * 64.0 + 0.5), 8 * 64)
294
295        size19 = szlist[1]
296        self.assertEqual(size19[3], 19)
297        self.assertEqual(int(size19[3] * 64.0 + 0.5), 19 * 64)
298        self.assertEqual(int(size19[4] * 64.0 + 0.5), 19 * 64)
299
300    def test_freetype_Font_use_bitmap_strikes(self):
301        f = self._TEST_FONTS["mono"]
302        try:
303            # use_bitmap_strikes == True
304            #
305            self.assertTrue(f.use_bitmap_strikes)
306
307            # bitmap compatible properties
308            s_strike, sz = f.render_raw("A", size=19)
309            try:
310                f.vertical = True
311                s_strike_vert, sz = f.render_raw("A", size=19)
312            finally:
313                f.vertical = False
314            try:
315                f.wide = True
316                s_strike_wide, sz = f.render_raw("A", size=19)
317            finally:
318                f.wide = False
319            try:
320                f.underline = True
321                s_strike_underline, sz = f.render_raw("A", size=19)
322            finally:
323                f.underline = False
324
325            # bitmap incompatible properties
326            s_strike_rot45, sz = f.render_raw("A", size=19, rotation=45)
327            try:
328                f.strong = True
329                s_strike_strong, sz = f.render_raw("A", size=19)
330            finally:
331                f.strong = False
332            try:
333                f.oblique = True
334                s_strike_oblique, sz = f.render_raw("A", size=19)
335            finally:
336                f.oblique = False
337
338            # compare with use_bitmap_strikes == False
339            #
340            f.use_bitmap_strikes = False
341            self.assertFalse(f.use_bitmap_strikes)
342
343            # bitmap compatible properties
344            s_outline, sz = f.render_raw("A", size=19)
345            self.assertNotEqual(s_outline, s_strike)
346            try:
347                f.vertical = True
348                s_outline, sz = f.render_raw("A", size=19)
349                self.assertNotEqual(s_outline, s_strike_vert)
350            finally:
351                f.vertical = False
352            try:
353                f.wide = True
354                s_outline, sz = f.render_raw("A", size=19)
355                self.assertNotEqual(s_outline, s_strike_wide)
356            finally:
357                f.wide = False
358            try:
359                f.underline = True
360                s_outline, sz = f.render_raw("A", size=19)
361                self.assertNotEqual(s_outline, s_strike_underline)
362            finally:
363                f.underline = False
364
365            # bitmap incompatible properties
366            s_outline, sz = f.render_raw("A", size=19, rotation=45)
367            self.assertEqual(s_outline, s_strike_rot45)
368            try:
369                f.strong = True
370                s_outline, sz = f.render_raw("A", size=19)
371                self.assertEqual(s_outline, s_strike_strong)
372            finally:
373                f.strong = False
374            try:
375                f.oblique = True
376                s_outline, sz = f.render_raw("A", size=19)
377                self.assertEqual(s_outline, s_strike_oblique)
378            finally:
379                f.oblique = False
380        finally:
381            f.use_bitmap_strikes = True
382
383    def test_freetype_Font_bitmap_files(self):
384        """Ensure bitmap file restrictions are caught"""
385        f = self._TEST_FONTS["bmp-8-75dpi"]
386        f_null = nullfont()
387        s = pygame.Surface((10, 10), 0, 32)
388        a = s.get_view("3")
389
390        exception = AttributeError
391        self.assertRaises(exception, setattr, f, "strong", True)
392        self.assertRaises(exception, setattr, f, "oblique", True)
393        self.assertRaises(exception, setattr, f, "style", ft.STYLE_STRONG)
394        self.assertRaises(exception, setattr, f, "style", ft.STYLE_OBLIQUE)
395        exception = RuntimeError
396        self.assertRaises(exception, setattr, f_null, "strong", True)
397        self.assertRaises(exception, setattr, f_null, "oblique", True)
398        self.assertRaises(exception, setattr, f_null, "style", ft.STYLE_STRONG)
399        self.assertRaises(exception, setattr, f_null, "style", ft.STYLE_OBLIQUE)
400        exception = ValueError
401        self.assertRaises(exception, f.render, "A", (0, 0, 0), size=8, rotation=1)
402        self.assertRaises(
403            exception, f.render, "A", (0, 0, 0), size=8, style=ft.STYLE_OBLIQUE
404        )
405        self.assertRaises(
406            exception, f.render, "A", (0, 0, 0), size=8, style=ft.STYLE_STRONG
407        )
408        self.assertRaises(exception, f.render_raw, "A", size=8, rotation=1)
409        self.assertRaises(exception, f.render_raw, "A", size=8, style=ft.STYLE_OBLIQUE)
410        self.assertRaises(exception, f.render_raw, "A", size=8, style=ft.STYLE_STRONG)
411        self.assertRaises(
412            exception, f.render_to, s, (0, 0), "A", (0, 0, 0), size=8, rotation=1
413        )
414        self.assertRaises(
415            exception,
416            f.render_to,
417            s,
418            (0, 0),
419            "A",
420            (0, 0, 0),
421            size=8,
422            style=ft.STYLE_OBLIQUE,
423        )
424        self.assertRaises(
425            exception,
426            f.render_to,
427            s,
428            (0, 0),
429            "A",
430            (0, 0, 0),
431            size=8,
432            style=ft.STYLE_STRONG,
433        )
434        self.assertRaises(exception, f.render_raw_to, a, "A", size=8, rotation=1)
435        self.assertRaises(
436            exception, f.render_raw_to, a, "A", size=8, style=ft.STYLE_OBLIQUE
437        )
438        self.assertRaises(
439            exception, f.render_raw_to, a, "A", size=8, style=ft.STYLE_STRONG
440        )
441        self.assertRaises(exception, f.get_rect, "A", size=8, rotation=1)
442        self.assertRaises(exception, f.get_rect, "A", size=8, style=ft.STYLE_OBLIQUE)
443        self.assertRaises(exception, f.get_rect, "A", size=8, style=ft.STYLE_STRONG)
444
445        # Unsupported point size
446        exception = pygame.error
447        self.assertRaises(exception, f.get_rect, "A", size=42)
448        self.assertRaises(exception, f.get_metrics, "A", size=42)
449        self.assertRaises(exception, f.get_sized_ascender, 42)
450        self.assertRaises(exception, f.get_sized_descender, 42)
451        self.assertRaises(exception, f.get_sized_height, 42)
452        self.assertRaises(exception, f.get_sized_glyph_height, 42)
453
454    def test_freetype_Font_get_metrics(self):
455
456        font = self._TEST_FONTS["sans"]
457
458        metrics = font.get_metrics("ABCD", size=24)
459        self.assertEqual(len(metrics), len("ABCD"))
460        self.assertIsInstance(metrics, list)
461
462        for metrics_tuple in metrics:
463            self.assertIsInstance(metrics_tuple, tuple, metrics_tuple)
464            self.assertEqual(len(metrics_tuple), 6)
465
466            for m in metrics_tuple[:4]:
467                self.assertIsInstance(m, int)
468
469            for m in metrics_tuple[4:]:
470                self.assertIsInstance(m, float)
471
472        # test for empty string
473        metrics = font.get_metrics("", size=24)
474        self.assertEqual(metrics, [])
475
476        # test for invalid string
477        self.assertRaises(TypeError, font.get_metrics, 24, 24)
478
479        # raises exception when uninitalized
480        self.assertRaises(RuntimeError, nullfont().get_metrics, "a", size=24)
481
482    def test_freetype_Font_get_rect(self):
483
484        font = self._TEST_FONTS["sans"]
485
486        def test_rect(r):
487            self.assertIsInstance(r, pygame.Rect)
488
489        rect_default = font.get_rect("ABCDabcd", size=24)
490        test_rect(rect_default)
491        self.assertTrue(rect_default.size > (0, 0))
492        self.assertTrue(rect_default.width > rect_default.height)
493
494        rect_bigger = font.get_rect("ABCDabcd", size=32)
495        test_rect(rect_bigger)
496        self.assertTrue(rect_bigger.size > rect_default.size)
497
498        rect_strong = font.get_rect("ABCDabcd", size=24, style=ft.STYLE_STRONG)
499        test_rect(rect_strong)
500        self.assertTrue(rect_strong.size > rect_default.size)
501
502        font.vertical = True
503        rect_vert = font.get_rect("ABCDabcd", size=24)
504        test_rect(rect_vert)
505        self.assertTrue(rect_vert.width < rect_vert.height)
506        font.vertical = False
507
508        rect_oblique = font.get_rect("ABCDabcd", size=24, style=ft.STYLE_OBLIQUE)
509        test_rect(rect_oblique)
510        self.assertTrue(rect_oblique.width > rect_default.width)
511        self.assertTrue(rect_oblique.height == rect_default.height)
512
513        rect_under = font.get_rect("ABCDabcd", size=24, style=ft.STYLE_UNDERLINE)
514        test_rect(rect_under)
515        self.assertTrue(rect_under.width == rect_default.width)
516        self.assertTrue(rect_under.height > rect_default.height)
517
518        # Rect size should change if UTF surrogate pairs are treated as
519        # one code point or two.
520        ufont = self._TEST_FONTS["mono"]
521        rect_utf32 = ufont.get_rect(as_unicode(r"\U00013079"), size=24)
522        rect_utf16 = ufont.get_rect(as_unicode(r"\uD80C\uDC79"), size=24)
523        self.assertEqual(rect_utf16, rect_utf32)
524        ufont.ucs4 = True
525        try:
526            rect_utf16 = ufont.get_rect(as_unicode(r"\uD80C\uDC79"), size=24)
527        finally:
528            ufont.ucs4 = False
529        self.assertNotEqual(rect_utf16, rect_utf32)
530
531        self.assertRaises(RuntimeError, nullfont().get_rect, "a", size=24)
532
533        # text stretching
534        rect12 = font.get_rect("A", size=12.0)
535        rect24 = font.get_rect("A", size=24.0)
536        rect_x = font.get_rect("A", size=(24.0, 12.0))
537        self.assertEqual(rect_x.width, rect24.width)
538        self.assertEqual(rect_x.height, rect12.height)
539        rect_y = font.get_rect("A", size=(12.0, 24.0))
540        self.assertEqual(rect_y.width, rect12.width)
541        self.assertEqual(rect_y.height, rect24.height)
542
543    def test_freetype_Font_height(self):
544
545        f = self._TEST_FONTS["sans"]
546        self.assertEqual(f.height, 2355)
547
548        f = self._TEST_FONTS["fixed"]
549        self.assertEqual(f.height, 1100)
550
551        self.assertRaises(RuntimeError, lambda: nullfont().height)
552
553    def test_freetype_Font_name(self):
554
555        f = self._TEST_FONTS["sans"]
556        self.assertEqual(f.name, "Liberation Sans")
557
558        f = self._TEST_FONTS["fixed"]
559        self.assertEqual(f.name, "Inconsolata")
560
561        nf = nullfont()
562        self.assertEqual(nf.name, repr(nf))
563
564    def test_freetype_Font_size(self):
565
566        f = ft.Font(None, size=12)
567        self.assertEqual(f.size, 12)
568        f.size = 22
569        self.assertEqual(f.size, 22)
570        f.size = 0
571        self.assertEqual(f.size, 0)
572        f.size = max_point_size
573        self.assertEqual(f.size, max_point_size)
574        f.size = 6.5
575        self.assertEqual(f.size, 6.5)
576        f.size = max_point_size_f
577        self.assertEqual(f.size, max_point_size_f)
578        self.assertRaises(OverflowError, setattr, f, "size", -1)
579        self.assertRaises(OverflowError, setattr, f, "size", (max_point_size + 1))
580
581        f.size = 24.0, 0
582        size = f.size
583        self.assertIsInstance(size, float)
584        self.assertEqual(size, 24.0)
585
586        f.size = 16, 16
587        size = f.size
588        self.assertIsInstance(size, tuple)
589        self.assertEqual(len(size), 2)
590
591        x, y = size
592        self.assertIsInstance(x, float)
593        self.assertEqual(x, 16.0)
594        self.assertIsInstance(y, float)
595        self.assertEqual(y, 16.0)
596
597        f.size = 20.5, 22.25
598        x, y = f.size
599        self.assertEqual(x, 20.5)
600        self.assertEqual(y, 22.25)
601
602        f.size = 0, 0
603        size = f.size
604        self.assertIsInstance(size, float)
605        self.assertEqual(size, 0.0)
606        self.assertRaises(ValueError, setattr, f, "size", (0, 24.0))
607        self.assertRaises(TypeError, setattr, f, "size", (24.0,))
608        self.assertRaises(TypeError, setattr, f, "size", (24.0, 0, 0))
609        self.assertRaises(TypeError, setattr, f, "size", (24.0j, 24.0))
610        self.assertRaises(TypeError, setattr, f, "size", (24.0, 24.0j))
611        self.assertRaises(OverflowError, setattr, f, "size", (-1, 16))
612        self.assertRaises(OverflowError, setattr, f, "size", (max_point_size + 1, 16))
613        self.assertRaises(OverflowError, setattr, f, "size", (16, -1))
614        self.assertRaises(OverflowError, setattr, f, "size", (16, max_point_size + 1))
615
616        # bitmap files with identical point size but differing ppems.
617        f75 = self._TEST_FONTS["bmp-18-75dpi"]
618        sizes = f75.get_sizes()
619        self.assertEqual(len(sizes), 1)
620        size_pt, width_px, height_px, x_ppem, y_ppem = sizes[0]
621        self.assertEqual(size_pt, 18)
622        self.assertEqual(x_ppem, 19.0)
623        self.assertEqual(y_ppem, 19.0)
624        rect = f75.get_rect("A", size=18)
625        rect = f75.get_rect("A", size=19)
626        rect = f75.get_rect("A", size=(19.0, 19.0))
627        self.assertRaises(pygame.error, f75.get_rect, "A", size=17)
628        f100 = self._TEST_FONTS["bmp-18-100dpi"]
629        sizes = f100.get_sizes()
630        self.assertEqual(len(sizes), 1)
631        size_pt, width_px, height_px, x_ppem, y_ppem = sizes[0]
632        self.assertEqual(size_pt, 18)
633        self.assertEqual(x_ppem, 25.0)
634        self.assertEqual(y_ppem, 25.0)
635        rect = f100.get_rect("A", size=18)
636        rect = f100.get_rect("A", size=25)
637        rect = f100.get_rect("A", size=(25.0, 25.0))
638        self.assertRaises(pygame.error, f100.get_rect, "A", size=17)
639
640    def test_freetype_Font_rotation(self):
641
642        test_angles = [
643            (30, 30),
644            (360, 0),
645            (390, 30),
646            (720, 0),
647            (764, 44),
648            (-30, 330),
649            (-360, 0),
650            (-390, 330),
651            (-720, 0),
652            (-764, 316),
653        ]
654
655        f = ft.Font(None)
656        self.assertEqual(f.rotation, 0)
657        for r, r_reduced in test_angles:
658            f.rotation = r
659            self.assertEqual(
660                f.rotation,
661                r_reduced,
662                "for angle %d: %d != %d" % (r, f.rotation, r_reduced),
663            )
664        self.assertRaises(TypeError, setattr, f, "rotation", "12")
665
666    def test_freetype_Font_render_to(self):
667        # Rendering to an existing target surface is equivalent to
668        # blitting a surface returned by Font.render with the target.
669        font = self._TEST_FONTS["sans"]
670
671        surf = pygame.Surface((800, 600))
672        color = pygame.Color(0, 0, 0)
673
674        rrect = font.render_to(surf, (32, 32), "FoobarBaz", color, None, size=24)
675        self.assertIsInstance(rrect, pygame.Rect)
676        self.assertEqual(rrect.topleft, (32, 32))
677        self.assertNotEqual(rrect.bottomright, (32, 32))
678
679        rcopy = rrect.copy()
680        rcopy.topleft = (32, 32)
681        self.assertTrue(surf.get_rect().contains(rcopy))
682
683        rect = pygame.Rect(20, 20, 2, 2)
684        rrect = font.render_to(surf, rect, "FoobarBax", color, None, size=24)
685        self.assertEqual(rect.topleft, rrect.topleft)
686        self.assertNotEqual(rrect.size, rect.size)
687        rrect = font.render_to(surf, (20.1, 18.9), "FoobarBax", color, None, size=24)
688
689        rrect = font.render_to(surf, rect, "", color, None, size=24)
690        self.assertFalse(rrect)
691        self.assertEqual(rrect.height, font.get_sized_height(24))
692
693        # invalid surf test
694        self.assertRaises(TypeError, font.render_to, "not a surface", "text", color)
695        self.assertRaises(TypeError, font.render_to, pygame.Surface, "text", color)
696
697        # invalid dest test
698        for dest in [
699            None,
700            0,
701            "a",
702            "ab",
703            (),
704            (1,),
705            ("a", 2),
706            (1, "a"),
707            (1 + 2j, 2),
708            (1, 1 + 2j),
709            (1, int),
710            (int, 1),
711        ]:
712            self.assertRaises(
713                TypeError, font.render_to, surf, dest, "foobar", color, size=24
714            )
715
716        # misc parameter test
717        self.assertRaises(ValueError, font.render_to, surf, (0, 0), "foobar", color)
718        self.assertRaises(
719            TypeError, font.render_to, surf, (0, 0), "foobar", color, 2.3, size=24
720        )
721        self.assertRaises(
722            ValueError,
723            font.render_to,
724            surf,
725            (0, 0),
726            "foobar",
727            color,
728            None,
729            style=42,
730            size=24,
731        )
732        self.assertRaises(
733            TypeError,
734            font.render_to,
735            surf,
736            (0, 0),
737            "foobar",
738            color,
739            None,
740            style=None,
741            size=24,
742        )
743        self.assertRaises(
744            ValueError,
745            font.render_to,
746            surf,
747            (0, 0),
748            "foobar",
749            color,
750            None,
751            style=97,
752            size=24,
753        )
754
755    def test_freetype_Font_render(self):
756
757        font = self._TEST_FONTS["sans"]
758
759        surf = pygame.Surface((800, 600))
760        color = pygame.Color(0, 0, 0)
761
762        rend = font.render("FoobarBaz", pygame.Color(0, 0, 0), None, size=24)
763        self.assertIsInstance(rend, tuple)
764        self.assertEqual(len(rend), 2)
765        self.assertIsInstance(rend[0], pygame.Surface)
766        self.assertIsInstance(rend[1], pygame.Rect)
767        self.assertEqual(rend[0].get_rect().size, rend[1].size)
768
769        s, r = font.render("", pygame.Color(0, 0, 0), None, size=24)
770        self.assertEqual(r.width, 0)
771        self.assertEqual(r.height, font.get_sized_height(24))
772        self.assertEqual(s.get_size(), r.size)
773        self.assertEqual(s.get_bitsize(), 32)
774
775        # misc parameter test
776        self.assertRaises(ValueError, font.render, "foobar", color)
777        self.assertRaises(TypeError, font.render, "foobar", color, 2.3, size=24)
778        self.assertRaises(
779            ValueError, font.render, "foobar", color, None, style=42, size=24
780        )
781        self.assertRaises(
782            TypeError, font.render, "foobar", color, None, style=None, size=24
783        )
784        self.assertRaises(
785            ValueError, font.render, "foobar", color, None, style=97, size=24
786        )
787
788        # valid surrogate pairs
789        font2 = self._TEST_FONTS["mono"]
790        ucs4 = font2.ucs4
791        try:
792            font2.ucs4 = False
793            rend1 = font2.render(as_unicode(r"\uD80C\uDC79"), color, size=24)
794            rend2 = font2.render(as_unicode(r"\U00013079"), color, size=24)
795            self.assertEqual(rend1[1], rend2[1])
796            font2.ucs4 = True
797            rend1 = font2.render(as_unicode(r"\uD80C\uDC79"), color, size=24)
798            self.assertNotEqual(rend1[1], rend2[1])
799        finally:
800            font2.ucs4 = ucs4
801
802        # malformed surrogate pairs
803        self.assertRaises(
804            UnicodeEncodeError, font.render, as_unicode(r"\uD80C"), color, size=24
805        )
806        self.assertRaises(
807            UnicodeEncodeError, font.render, as_unicode(r"\uDCA7"), color, size=24
808        )
809        self.assertRaises(
810            UnicodeEncodeError, font.render, as_unicode(r"\uD7FF\uDCA7"), color, size=24
811        )
812        self.assertRaises(
813            UnicodeEncodeError, font.render, as_unicode(r"\uDC00\uDCA7"), color, size=24
814        )
815        self.assertRaises(
816            UnicodeEncodeError, font.render, as_unicode(r"\uD80C\uDBFF"), color, size=24
817        )
818        self.assertRaises(
819            UnicodeEncodeError, font.render, as_unicode(r"\uD80C\uE000"), color, size=24
820        )
821
822        # raises exception when uninitalized
823        self.assertRaises(RuntimeError, nullfont().render, "a", (0, 0, 0), size=24)
824
825        # Confirm the correct glpyhs are returned for a couple of
826        # unicode code points, 'A' and '\U00023079'. For each code point
827        # the rendered glyph is compared with an image of glyph bitmap
828        # as exported by FontForge.
829        path = os.path.join(FONTDIR, "A_PyGameMono-8.png")
830        A = pygame.image.load(path)
831        path = os.path.join(FONTDIR, "u13079_PyGameMono-8.png")
832        u13079 = pygame.image.load(path)
833
834        font = self._TEST_FONTS["mono"]
835        font.ucs4 = False
836        A_rendered, r = font.render("A", bgcolor=pygame.Color("white"), size=8)
837        u13079_rendered, r = font.render(
838            as_unicode(r"\U00013079"), bgcolor=pygame.Color("white"), size=8
839        )
840
841        ## before comparing the surfaces, make sure they are the same
842        ## pixel format. Use 32-bit SRCALPHA to avoid row padding and
843        ## undefined bytes (the alpha byte will be set to 255.)
844        bitmap = pygame.Surface(A.get_size(), pygame.SRCALPHA, 32)
845        bitmap.blit(A, (0, 0))
846        rendering = pygame.Surface(A_rendered.get_size(), pygame.SRCALPHA, 32)
847        rendering.blit(A_rendered, (0, 0))
848        self.assertTrue(surf_same_image(rendering, bitmap))
849        bitmap = pygame.Surface(u13079.get_size(), pygame.SRCALPHA, 32)
850        bitmap.blit(u13079, (0, 0))
851        rendering = pygame.Surface(u13079_rendered.get_size(), pygame.SRCALPHA, 32)
852        rendering.blit(u13079_rendered, (0, 0))
853        self.assertTrue(surf_same_image(rendering, bitmap))
854
855    def test_freetype_Font_render_mono(self):
856        font = self._TEST_FONTS["sans"]
857        color = pygame.Color("black")
858        colorkey = pygame.Color("white")
859        text = "."
860
861        save_antialiased = font.antialiased
862        font.antialiased = False
863        try:
864            surf, r = font.render(text, color, size=24)
865            self.assertEqual(surf.get_bitsize(), 8)
866            flags = surf.get_flags()
867            self.assertTrue(flags & pygame.SRCCOLORKEY)
868            self.assertFalse(flags & (pygame.SRCALPHA | pygame.HWSURFACE))
869            self.assertEqual(surf.get_colorkey(), colorkey)
870            self.assertIsNone(surf.get_alpha())
871
872            translucent_color = pygame.Color(*color)
873            translucent_color.a = 55
874            surf, r = font.render(text, translucent_color, size=24)
875            self.assertEqual(surf.get_bitsize(), 8)
876            flags = surf.get_flags()
877            self.assertTrue(flags & (pygame.SRCCOLORKEY | pygame.SRCALPHA))
878            self.assertFalse(flags & pygame.HWSURFACE)
879            self.assertEqual(surf.get_colorkey(), colorkey)
880            self.assertEqual(surf.get_alpha(), translucent_color.a)
881
882            surf, r = font.render(text, color, colorkey, size=24)
883            self.assertEqual(surf.get_bitsize(), 32)
884        finally:
885            font.antialiased = save_antialiased
886
887    def test_freetype_Font_render_to_mono(self):
888        # Blitting is done in two stages. First the target is alpha filled
889        # with the background color, if any. Second, the foreground
890        # color is alpha blitted to the background.
891        font = self._TEST_FONTS["sans"]
892        text = " ."
893        rect = font.get_rect(text, size=24)
894        size = rect.size
895        fg = pygame.Surface((1, 1), pygame.SRCALPHA, 32)
896        bg = pygame.Surface((1, 1), pygame.SRCALPHA, 32)
897        surrogate = pygame.Surface((1, 1), pygame.SRCALPHA, 32)
898        surfaces = [
899            pygame.Surface(size, 0, 8),
900            pygame.Surface(size, 0, 16),
901            pygame.Surface(size, pygame.SRCALPHA, 16),
902            pygame.Surface(size, 0, 24),
903            pygame.Surface(size, 0, 32),
904            pygame.Surface(size, pygame.SRCALPHA, 32),
905        ]
906        fg_colors = [
907            surfaces[0].get_palette_at(2),
908            surfaces[1].unmap_rgb(surfaces[1].map_rgb((128, 64, 200))),
909            surfaces[2].unmap_rgb(surfaces[2].map_rgb((99, 0, 100, 64))),
910            (128, 97, 213),
911            (128, 97, 213),
912            (128, 97, 213, 60),
913        ]
914        fg_colors = [pygame.Color(*c) for c in fg_colors]
915        self.assertEqual(len(surfaces), len(fg_colors))  # integrity check
916        bg_colors = [
917            surfaces[0].get_palette_at(4),
918            surfaces[1].unmap_rgb(surfaces[1].map_rgb((220, 20, 99))),
919            surfaces[2].unmap_rgb(surfaces[2].map_rgb((55, 200, 0, 86))),
920            (255, 120, 13),
921            (255, 120, 13),
922            (255, 120, 13, 180),
923        ]
924        bg_colors = [pygame.Color(*c) for c in bg_colors]
925        self.assertEqual(len(surfaces), len(bg_colors))  # integrity check
926
927        save_antialiased = font.antialiased
928        font.antialiased = False
929        try:
930            fill_color = pygame.Color("black")
931            for i, surf in enumerate(surfaces):
932                surf.fill(fill_color)
933                fg_color = fg_colors[i]
934                fg.set_at((0, 0), fg_color)
935                surf.blit(fg, (0, 0))
936                r_fg_color = surf.get_at((0, 0))
937                surf.set_at((0, 0), fill_color)
938                rrect = font.render_to(surf, (0, 0), text, fg_color, size=24)
939                bottomleft = 0, rrect.height - 1
940                self.assertEqual(
941                    surf.get_at(bottomleft),
942                    fill_color,
943                    "Position: {}. Depth: {}."
944                    " fg_color: {}.".format(bottomleft, surf.get_bitsize(), fg_color),
945                )
946                bottomright = rrect.width - 1, rrect.height - 1
947                self.assertEqual(
948                    surf.get_at(bottomright),
949                    r_fg_color,
950                    "Position: {}. Depth: {}."
951                    " fg_color: {}.".format(bottomright, surf.get_bitsize(), fg_color),
952                )
953            for i, surf in enumerate(surfaces):
954                surf.fill(fill_color)
955                fg_color = fg_colors[i]
956                bg_color = bg_colors[i]
957                bg.set_at((0, 0), bg_color)
958                fg.set_at((0, 0), fg_color)
959                if surf.get_bitsize() == 24:
960                    # For a 24 bit target surface test against Pygame's alpha
961                    # blit as there appears to be a problem with SDL's alpha
962                    # blit:
963                    #
964                    # self.assertEqual(surf.get_at(bottomright), r_fg_color)
965                    #
966                    # raises
967                    #
968                    # AssertionError: (128, 97, 213, 255) != (129, 98, 213, 255)
969                    #
970                    surrogate.set_at((0, 0), fill_color)
971                    surrogate.blit(bg, (0, 0))
972                    r_bg_color = surrogate.get_at((0, 0))
973                    surrogate.blit(fg, (0, 0))
974                    r_fg_color = surrogate.get_at((0, 0))
975                else:
976                    # Surface blit values for comparison.
977                    surf.blit(bg, (0, 0))
978                    r_bg_color = surf.get_at((0, 0))
979                    surf.blit(fg, (0, 0))
980                    r_fg_color = surf.get_at((0, 0))
981                    surf.set_at((0, 0), fill_color)
982                rrect = font.render_to(surf, (0, 0), text, fg_color, bg_color, size=24)
983                bottomleft = 0, rrect.height - 1
984                self.assertEqual(surf.get_at(bottomleft), r_bg_color)
985                bottomright = rrect.width - 1, rrect.height - 1
986                self.assertEqual(surf.get_at(bottomright), r_fg_color)
987        finally:
988            font.antialiased = save_antialiased
989
990    def test_freetype_Font_render_raw(self):
991
992        font = self._TEST_FONTS["sans"]
993
994        text = "abc"
995        size = font.get_rect(text, size=24).size
996        rend = font.render_raw(text, size=24)
997        self.assertIsInstance(rend, tuple)
998        self.assertEqual(len(rend), 2)
999
1000        r, s = rend
1001        self.assertIsInstance(r, bytes_)
1002        self.assertIsInstance(s, tuple)
1003        self.assertTrue(len(s), 2)
1004
1005        w, h = s
1006        self.assertIsInstance(w, int)
1007        self.assertIsInstance(h, int)
1008        self.assertEqual(s, size)
1009        self.assertEqual(len(r), w * h)
1010
1011        r, (w, h) = font.render_raw("", size=24)
1012        self.assertEqual(w, 0)
1013        self.assertEqual(h, font.height)
1014        self.assertEqual(len(r), 0)
1015
1016        # bug with decenders: this would crash
1017        rend = font.render_raw("render_raw", size=24)
1018
1019        # bug with non-printable characters: this would cause a crash
1020        # because the text length was not adjusted for skipped characters.
1021        text = unicode_("").join([unichr_(i) for i in range(31, 64)])
1022        rend = font.render_raw(text, size=10)
1023
1024    def test_freetype_Font_render_raw_to(self):
1025
1026        # This only checks that blits do not crash. It needs to check:
1027        # - int values
1028        # - invert option
1029        #
1030
1031        font = self._TEST_FONTS["sans"]
1032        text = "abc"
1033
1034        # No frills antialiased render to int1 (__render_glyph_INT)
1035        srect = font.get_rect(text, size=24)
1036        surf = pygame.Surface(srect.size, 0, 8)
1037        rrect = font.render_raw_to(surf.get_view("2"), text, size=24)
1038        self.assertEqual(rrect, srect)
1039
1040        for bpp in [24, 32]:
1041            surf = pygame.Surface(srect.size, 0, bpp)
1042            rrect = font.render_raw_to(surf.get_view("r"), text, size=24)
1043            self.assertEqual(rrect, srect)
1044
1045        # Underlining to int1 (__fill_glyph_INT)
1046        srect = font.get_rect(text, size=24, style=ft.STYLE_UNDERLINE)
1047        surf = pygame.Surface(srect.size, 0, 8)
1048        rrect = font.render_raw_to(
1049            surf.get_view("2"), text, size=24, style=ft.STYLE_UNDERLINE
1050        )
1051        self.assertEqual(rrect, srect)
1052
1053        for bpp in [24, 32]:
1054            surf = pygame.Surface(srect.size, 0, bpp)
1055            rrect = font.render_raw_to(
1056                surf.get_view("r"), text, size=24, style=ft.STYLE_UNDERLINE
1057            )
1058            self.assertEqual(rrect, srect)
1059
1060        # Unaliased (mono) rendering to int1 (__render_glyph_MONO_as_INT)
1061        font.antialiased = False
1062        try:
1063            srect = font.get_rect(text, size=24)
1064            surf = pygame.Surface(srect.size, 0, 8)
1065            rrect = font.render_raw_to(surf.get_view("2"), text, size=24)
1066            self.assertEqual(rrect, srect)
1067
1068            for bpp in [24, 32]:
1069                surf = pygame.Surface(srect.size, 0, bpp)
1070                rrect = font.render_raw_to(surf.get_view("r"), text, size=24)
1071                self.assertEqual(rrect, srect)
1072        finally:
1073            font.antialiased = True
1074
1075        # Antialiased render to ints sized greater than 1 byte
1076        # (__render_glyph_INT)
1077        srect = font.get_rect(text, size=24)
1078
1079        for bpp in [16, 24, 32]:
1080            surf = pygame.Surface(srect.size, 0, bpp)
1081            rrect = font.render_raw_to(surf.get_view("2"), text, size=24)
1082            self.assertEqual(rrect, srect)
1083
1084        # Underline render to ints sized greater than 1 byte
1085        # (__fill_glyph_INT)
1086        srect = font.get_rect(text, size=24, style=ft.STYLE_UNDERLINE)
1087
1088        for bpp in [16, 24, 32]:
1089            surf = pygame.Surface(srect.size, 0, bpp)
1090            rrect = font.render_raw_to(
1091                surf.get_view("2"), text, size=24, style=ft.STYLE_UNDERLINE
1092            )
1093            self.assertEqual(rrect, srect)
1094
1095        # Unaliased (mono) rendering to ints greater than 1 byte
1096        # (__render_glyph_MONO_as_INT)
1097        font.antialiased = False
1098        try:
1099            srect = font.get_rect(text, size=24)
1100
1101            for bpp in [16, 24, 32]:
1102                surf = pygame.Surface(srect.size, 0, bpp)
1103                rrect = font.render_raw_to(surf.get_view("2"), text, size=24)
1104                self.assertEqual(rrect, srect)
1105        finally:
1106            font.antialiased = True
1107
1108        # Invalid dest parameter test.
1109        srect = font.get_rect(text, size=24)
1110        surf_buf = pygame.Surface(srect.size, 0, 32).get_view("2")
1111
1112        for dest in [
1113            0,
1114            "a",
1115            "ab",
1116            (),
1117            (1,),
1118            ("a", 2),
1119            (1, "a"),
1120            (1 + 2j, 2),
1121            (1, 1 + 2j),
1122            (1, int),
1123            (int, 1),
1124        ]:
1125            self.assertRaises(
1126                TypeError, font.render_raw_to, surf_buf, text, dest, size=24
1127            )
1128
1129    def test_freetype_Font_text_is_None(self):
1130        f = ft.Font(self._sans_path, 36)
1131        f.style = ft.STYLE_NORMAL
1132        f.rotation = 0
1133        text = "ABCD"
1134
1135        # reference values
1136        get_rect = f.get_rect(text)
1137        f.vertical = True
1138        get_rect_vert = f.get_rect(text)
1139
1140        self.assertTrue(get_rect_vert.width < get_rect.width)
1141        self.assertTrue(get_rect_vert.height > get_rect.height)
1142        f.vertical = False
1143        render_to_surf = pygame.Surface(get_rect.size, pygame.SRCALPHA, 32)
1144
1145        if IS_PYPY:
1146            return
1147
1148        arr = arrinter.Array(get_rect.size, "u", 1)
1149        render = f.render(text, (0, 0, 0))
1150        render_to = f.render_to(render_to_surf, (0, 0), text, (0, 0, 0))
1151        render_raw = f.render_raw(text)
1152        render_raw_to = f.render_raw_to(arr, text)
1153
1154        # comparisons
1155        surf = pygame.Surface(get_rect.size, pygame.SRCALPHA, 32)
1156        self.assertEqual(f.get_rect(None), get_rect)
1157        s, r = f.render(None, (0, 0, 0))
1158        self.assertEqual(r, render[1])
1159        self.assertTrue(surf_same_image(s, render[0]))
1160        r = f.render_to(surf, (0, 0), None, (0, 0, 0))
1161        self.assertEqual(r, render_to)
1162        self.assertTrue(surf_same_image(surf, render_to_surf))
1163        px, sz = f.render_raw(None)
1164        self.assertEqual(sz, render_raw[1])
1165        self.assertEqual(px, render_raw[0])
1166        sz = f.render_raw_to(arr, None)
1167        self.assertEqual(sz, render_raw_to)
1168
1169    def test_freetype_Font_text_is_None(self):
1170        f = ft.Font(self._sans_path, 36)
1171        f.style = ft.STYLE_NORMAL
1172        f.rotation = 0
1173        text = "ABCD"
1174
1175        # reference values
1176        get_rect = f.get_rect(text)
1177        f.vertical = True
1178        get_rect_vert = f.get_rect(text)
1179
1180        # vertical: trigger glyph positioning.
1181        f.vertical = True
1182        r = f.get_rect(None)
1183        self.assertEqual(r, get_rect_vert)
1184        f.vertical = False
1185
1186        # wide style: trigger glyph reload
1187        r = f.get_rect(None, style=ft.STYLE_WIDE)
1188        self.assertEqual(r.height, get_rect.height)
1189        self.assertTrue(r.width > get_rect.width)
1190        r = f.get_rect(None)
1191        self.assertEqual(r, get_rect)
1192
1193        # rotated: trigger glyph reload
1194        r = f.get_rect(None, rotation=90)
1195        self.assertEqual(r.width, get_rect.height)
1196        self.assertEqual(r.height, get_rect.width)
1197
1198        # this method will not support None text
1199        self.assertRaises(TypeError, f.get_metrics, None)
1200
1201    def test_freetype_Font_fgcolor(self):
1202        f = ft.Font(self._bmp_8_75dpi_path)
1203        notdef = "\0"  # the PyGameMono .notdef glyph has a pixel at (0, 0)
1204        f.origin = False
1205        f.pad = False
1206        black = pygame.Color("black")  # initial color
1207        green = pygame.Color("green")
1208        alpha128 = pygame.Color(10, 20, 30, 128)
1209
1210        c = f.fgcolor
1211        self.assertIsInstance(c, pygame.Color)
1212        self.assertEqual(c, black)
1213
1214        s, r = f.render(notdef)
1215        self.assertEqual(s.get_at((0, 0)), black)
1216
1217        f.fgcolor = green
1218        self.assertEqual(f.fgcolor, green)
1219
1220        s, r = f.render(notdef)
1221        self.assertEqual(s.get_at((0, 0)), green)
1222
1223        f.fgcolor = alpha128
1224        s, r = f.render(notdef)
1225        self.assertEqual(s.get_at((0, 0)), alpha128)
1226
1227        surf = pygame.Surface(f.get_rect(notdef).size, pygame.SRCALPHA, 32)
1228        f.render_to(surf, (0, 0), None)
1229        self.assertEqual(surf.get_at((0, 0)), alpha128)
1230
1231        self.assertRaises(AttributeError, setattr, f, "fgcolor", None)
1232
1233    def test_freetype_Font_bgcolor(self):
1234        f = ft.Font(None, 32)
1235        zero = "0"  # the default font 0 glyph does not have a pixel at (0, 0)
1236        f.origin = False
1237        f.pad = False
1238
1239        transparent_black = pygame.Color(0, 0, 0, 0)  # initial color
1240        green = pygame.Color("green")
1241        alpha128 = pygame.Color(10, 20, 30, 128)
1242
1243        c = f.bgcolor
1244        self.assertIsInstance(c, pygame.Color)
1245        self.assertEqual(c, transparent_black)
1246
1247        s, r = f.render(zero, pygame.Color(255, 255, 255))
1248        self.assertEqual(s.get_at((0, 0)), transparent_black)
1249
1250        f.bgcolor = green
1251        self.assertEqual(f.bgcolor, green)
1252
1253        s, r = f.render(zero)
1254        self.assertEqual(s.get_at((0, 0)), green)
1255
1256        f.bgcolor = alpha128
1257        s, r = f.render(zero)
1258        self.assertEqual(s.get_at((0, 0)), alpha128)
1259
1260        surf = pygame.Surface(f.get_rect(zero).size, pygame.SRCALPHA, 32)
1261        f.render_to(surf, (0, 0), None)
1262        self.assertEqual(surf.get_at((0, 0)), alpha128)
1263
1264        self.assertRaises(AttributeError, setattr, f, "bgcolor", None)
1265
1266    @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented")
1267    @unittest.skipIf(IS_PYPY, "pypy2 no likey")
1268    def test_newbuf(self):
1269        from pygame.tests.test_utils import buftools
1270
1271        Exporter = buftools.Exporter
1272        font = self._TEST_FONTS["sans"]
1273        srect = font.get_rect("Hi", size=12)
1274        for format in [
1275            "b",
1276            "B",
1277            "h",
1278            "H",
1279            "i",
1280            "I",
1281            "l",
1282            "L",
1283            "q",
1284            "Q",
1285            "x",
1286            "1x",
1287            "2x",
1288            "3x",
1289            "4x",
1290            "5x",
1291            "6x",
1292            "7x",
1293            "8x",
1294            "9x",
1295            "<h",
1296            ">h",
1297            "=h",
1298            "@h",
1299            "!h",
1300            "1h",
1301            "=1h",
1302        ]:
1303            newbuf = Exporter(srect.size, format=format)
1304            rrect = font.render_raw_to(newbuf, "Hi", size=12)
1305            self.assertEqual(rrect, srect)
1306        # Some unsupported formats
1307        for format in ["f", "d", "2h", "?", "hh"]:
1308            newbuf = Exporter(srect.size, format=format, itemsize=4)
1309            self.assertRaises(ValueError, font.render_raw_to, newbuf, "Hi", size=12)
1310
1311    def test_freetype_Font_style(self):
1312
1313        font = self._TEST_FONTS["sans"]
1314
1315        # make sure STYLE_NORMAL is the default value
1316        self.assertEqual(ft.STYLE_NORMAL, font.style)
1317
1318        # make sure we check for style type
1319        with self.assertRaises(TypeError):
1320            font.style = "None"
1321        with self.assertRaises(TypeError):
1322            font.style = None
1323
1324        # make sure we only accept valid constants
1325        with self.assertRaises(ValueError):
1326            font.style = 112
1327
1328        # make assure no assignments happened
1329        self.assertEqual(ft.STYLE_NORMAL, font.style)
1330
1331        # test assignement
1332        font.style = ft.STYLE_UNDERLINE
1333        self.assertEqual(ft.STYLE_UNDERLINE, font.style)
1334
1335        # test complex styles
1336        st = ft.STYLE_STRONG | ft.STYLE_UNDERLINE | ft.STYLE_OBLIQUE
1337
1338        font.style = st
1339        self.assertEqual(st, font.style)
1340
1341        # and that STYLE_DEFAULT has no effect (continued from above)
1342        self.assertNotEqual(st, ft.STYLE_DEFAULT)
1343        font.style = ft.STYLE_DEFAULT
1344        self.assertEqual(st, font.style)
1345
1346        # revert changes
1347        font.style = ft.STYLE_NORMAL
1348        self.assertEqual(ft.STYLE_NORMAL, font.style)
1349
1350    def test_freetype_Font_resolution(self):
1351        text = "|"  # Differs in width and height
1352        resolution = ft.get_default_resolution()
1353        new_font = ft.Font(self._sans_path, resolution=2 * resolution)
1354        self.assertEqual(new_font.resolution, 2 * resolution)
1355        size_normal = self._TEST_FONTS["sans"].get_rect(text, size=24).size
1356        size_scaled = new_font.get_rect(text, size=24).size
1357        size_by_2 = size_normal[0] * 2
1358        self.assertTrue(
1359            size_by_2 + 2 >= size_scaled[0] >= size_by_2 - 2,
1360            "%i not equal %i" % (size_scaled[1], size_by_2),
1361        )
1362        size_by_2 = size_normal[1] * 2
1363        self.assertTrue(
1364            size_by_2 + 2 >= size_scaled[1] >= size_by_2 - 2,
1365            "%i not equal %i" % (size_scaled[1], size_by_2),
1366        )
1367        new_resolution = resolution + 10
1368        ft.set_default_resolution(new_resolution)
1369        try:
1370            new_font = ft.Font(self._sans_path, resolution=0)
1371            self.assertEqual(new_font.resolution, new_resolution)
1372        finally:
1373            ft.set_default_resolution()
1374
1375    def test_freetype_Font_path(self):
1376        self.assertEqual(self._TEST_FONTS["sans"].path, self._sans_path)
1377        self.assertRaises(AttributeError, getattr, nullfont(), "path")
1378
1379    # This Font cache test is conditional on freetype being built by a debug
1380    # version of Python or with the C macro PGFT_DEBUG_CACHE defined.
1381    def test_freetype_Font_cache(self):
1382        glyphs = "abcde"
1383        glen = len(glyphs)
1384        other_glyphs = "123"
1385        oglen = len(other_glyphs)
1386        uempty = unicode_("")
1387        ##        many_glyphs = (uempty.join([unichr_(i) for i in range(32,127)] +
1388        ##                                   [unichr_(i) for i in range(161,172)] +
1389        ##                                   [unichr_(i) for i in range(174,239)]))
1390        many_glyphs = uempty.join([unichr_(i) for i in range(32, 127)])
1391        mglen = len(many_glyphs)
1392
1393        count = 0
1394        access = 0
1395        hit = 0
1396        miss = 0
1397
1398        f = ft.Font(None, size=24, font_index=0, resolution=72, ucs4=False)
1399        f.style = ft.STYLE_NORMAL
1400        f.antialiased = True
1401
1402        # Ensure debug counters are zero
1403        self.assertEqual(f._debug_cache_stats, (0, 0, 0, 0, 0))
1404        # Load some basic glyphs
1405        count = access = miss = glen
1406        f.render_raw(glyphs)
1407        self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss))
1408        # Vertical should not affect the cache
1409        access += glen
1410        hit += glen
1411        f.vertical = True
1412        f.render_raw(glyphs)
1413        f.vertical = False
1414        self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss))
1415        # New glyphs will
1416        count += oglen
1417        access += oglen
1418        miss += oglen
1419        f.render_raw(other_glyphs)
1420        self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss))
1421        # Point size does
1422        count += glen
1423        access += glen
1424        miss += glen
1425        f.render_raw(glyphs, size=12)
1426        self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss))
1427        # Underline style does not
1428        access += oglen
1429        hit += oglen
1430        f.underline = True
1431        f.render_raw(other_glyphs)
1432        f.underline = False
1433        self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss))
1434        # Oblique style does
1435        count += glen
1436        access += glen
1437        miss += glen
1438        f.oblique = True
1439        f.render_raw(glyphs)
1440        f.oblique = False
1441        self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss))
1442        # Strong style does; by this point cache clears can happen
1443        count += glen
1444        access += glen
1445        miss += glen
1446        f.strong = True
1447        f.render_raw(glyphs)
1448        f.strong = False
1449        ccount, cdelete_count, caccess, chit, cmiss = f._debug_cache_stats
1450        self.assertEqual(
1451            (ccount + cdelete_count, caccess, chit, cmiss), (count, access, hit, miss)
1452        )
1453        # Rotation does
1454        count += glen
1455        access += glen
1456        miss += glen
1457        f.render_raw(glyphs, rotation=10)
1458        ccount, cdelete_count, caccess, chit, cmiss = f._debug_cache_stats
1459        self.assertEqual(
1460            (ccount + cdelete_count, caccess, chit, cmiss), (count, access, hit, miss)
1461        )
1462        # aliased (mono) glyphs do
1463        count += oglen
1464        access += oglen
1465        miss += oglen
1466        f.antialiased = False
1467        f.render_raw(other_glyphs)
1468        f.antialiased = True
1469        ccount, cdelete_count, caccess, chit, cmiss = f._debug_cache_stats
1470        self.assertEqual(
1471            (ccount + cdelete_count, caccess, chit, cmiss), (count, access, hit, miss)
1472        )
1473        # Trigger a cleanup for sure.
1474        count += 2 * mglen
1475        access += 2 * mglen
1476        miss += 2 * mglen
1477        f.get_metrics(many_glyphs, size=8)
1478        f.get_metrics(many_glyphs, size=10)
1479        ccount, cdelete_count, caccess, chit, cmiss = f._debug_cache_stats
1480        self.assertTrue(ccount < count)
1481        self.assertEqual(
1482            (ccount + cdelete_count, caccess, chit, cmiss), (count, access, hit, miss)
1483        )
1484
1485    try:
1486        ft.Font._debug_cache_stats
1487    except AttributeError:
1488        del test_freetype_Font_cache
1489
1490    def test_undefined_character_code(self):
1491        # To be consistent with pygame.font.Font, undefined codes
1492        # are rendered as the undefined character, and has metrics
1493        # of None.
1494        font = self._TEST_FONTS["sans"]
1495
1496        img, size1 = font.render(unichr_(1), (0, 0, 0), size=24)
1497        img, size0 = font.render("", (0, 0, 0), size=24)
1498        self.assertTrue(size1.width > size0.width)
1499
1500        metrics = font.get_metrics(unichr_(1) + unichr_(48), size=24)
1501        self.assertEqual(len(metrics), 2)
1502        self.assertIsNone(metrics[0])
1503        self.assertIsInstance(metrics[1], tuple)
1504
1505    @unittest.skipIf(
1506        pygame.get_sdl_version()[0] == 2, "SDL2 surfaces are only limited by memory"
1507    )
1508    def test_issue_144(self):
1509        """Issue #144: unable to render text"""
1510
1511        # The bug came in two parts. The first was a convertion bug from
1512        # FT_Fixed to integer in for an Intel x86_64 Pygame build. The second
1513        # was to have the raised exception disappear before Font.render
1514        # returned to Python level.
1515        #
1516        font = ft.Font(None, size=64)
1517        s = "M" * 100000  # Way too long for an SDL surface
1518        self.assertRaises(pygame.error, font.render, s, (0, 0, 0))
1519
1520    def test_issue_242(self):
1521        """Issue #242: get_rect() uses 0 as default style"""
1522
1523        # Issue #242: freetype.Font.get_rect() ignores style defaults when
1524        #             the style argument is not given
1525        #
1526        # The text boundary rectangle returned by freetype.Font.get_rect()
1527        # should match the boundary of the same text rendered directly to a
1528        # surface. This permits accurate text positioning. To work properly,
1529        # get_rect() should calculate the text boundary to reflect text style,
1530        # such as underline. Instead, it ignores the style settings for the
1531        # Font object when the style argument is omitted.
1532        #
1533        # When the style argument is not given, freetype.get_rect() uses
1534        # unstyled text when calculating the boundary rectangle. This is
1535        # because _ftfont_getrect(), in _freetype.c, set the default
1536        # style to 0 rather than FT_STYLE_DEFAULT.
1537        #
1538        font = self._TEST_FONTS["sans"]
1539
1540        # Try wide style on a wide character.
1541        prev_style = font.wide
1542        font.wide = True
1543        try:
1544            rect = font.get_rect("M", size=64)
1545            surf, rrect = font.render(None, size=64)
1546            self.assertEqual(rect, rrect)
1547        finally:
1548            font.wide = prev_style
1549
1550        # Try strong style on several wide characters.
1551        prev_style = font.strong
1552        font.strong = True
1553        try:
1554            rect = font.get_rect("Mm_", size=64)
1555            surf, rrect = font.render(None, size=64)
1556            self.assertEqual(rect, rrect)
1557        finally:
1558            font.strong = prev_style
1559
1560        # Try oblique style on a tall, narrow character.
1561        prev_style = font.oblique
1562        font.oblique = True
1563        try:
1564            rect = font.get_rect("|", size=64)
1565            surf, rrect = font.render(None, size=64)
1566            self.assertEqual(rect, rrect)
1567        finally:
1568            font.oblique = prev_style
1569
1570        # Try underline style on a glyphless character.
1571        prev_style = font.underline
1572        font.underline = True
1573        try:
1574            rect = font.get_rect(" ", size=64)
1575            surf, rrect = font.render(None, size=64)
1576            self.assertEqual(rect, rrect)
1577        finally:
1578            font.underline = prev_style
1579
1580    def test_issue_237(self):
1581        """Issue #237: Memory overrun when rendered with underlining"""
1582
1583        # Issue #237: Memory overrun when text without descenders is rendered
1584        #             with underlining
1585        #
1586        # The bug crashes the Python interpreter. The bug is caught with C
1587        # assertions in ft_render_cb.c when the Pygame module is compiled
1588        # for debugging. So far it is only known to affect Times New Roman.
1589        #
1590        name = "Times New Roman"
1591        font = ft.SysFont(name, 19)
1592        if font.name != name:
1593            # The font is unavailable, so skip the test.
1594            return
1595        font.underline = True
1596        s, r = font.render("Amazon", size=19)
1597
1598        # Some other checks to make sure nothing else broke.
1599        for adj in [-2, -1.9, -1, 0, 1.9, 2]:
1600            font.underline_adjustment = adj
1601            s, r = font.render("Amazon", size=19)
1602
1603    def test_issue_243(self):
1604        """Issue Y: trailing space ignored in boundary calculation"""
1605
1606        # Issue #243: For a string with trailing spaces, freetype ignores the
1607        # last space in boundary calculations
1608        #
1609        font = self._TEST_FONTS["fixed"]
1610        r1 = font.get_rect(" ", size=64)
1611        self.assertTrue(r1.width > 1)
1612        r2 = font.get_rect("  ", size=64)
1613        self.assertEqual(r2.width, 2 * r1.width)
1614
1615    def test_garbage_collection(self):
1616        """Check reference counting on returned new references"""
1617
1618        def ref_items(seq):
1619            return [weakref.ref(o) for o in seq]
1620
1621        font = self._TEST_FONTS["bmp-8-75dpi"]
1622        font.size = font.get_sizes()[0][0]
1623        text = "A"
1624        rect = font.get_rect(text)
1625        surf = pygame.Surface(rect.size, pygame.SRCALPHA, 32)
1626        refs = []
1627        refs.extend(ref_items(font.render(text, (0, 0, 0))))
1628        refs.append(weakref.ref(font.render_to(surf, (0, 0), text, (0, 0, 0))))
1629        refs.append(weakref.ref(font.get_rect(text)))
1630
1631        n = len(refs)
1632        self.assertTrue(n > 0)
1633
1634        # for pypy we garbage collection twice.
1635        for i in range(2):
1636            gc.collect()
1637
1638        for i in range(n):
1639            self.assertIsNone(refs[i](), "ref %d not collected" % i)
1640
1641        try:
1642            from sys import getrefcount
1643        except ImportError:
1644            pass
1645        else:
1646            array = arrinter.Array(rect.size, "u", 1)
1647            o = font.render_raw(text)
1648            self.assertEqual(getrefcount(o), 2)
1649            self.assertEqual(getrefcount(o[0]), 2)
1650            self.assertEqual(getrefcount(o[1]), 2)
1651            self.assertEqual(getrefcount(font.render_raw_to(array, text)), 1)
1652            o = font.get_metrics("AB")
1653            self.assertEqual(getrefcount(o), 2)
1654            for i in range(len(o)):
1655                 self.assertEqual(getrefcount(o[i]), 2, "refcount fail for item %d" % i)
1656            o = font.get_sizes()
1657            self.assertEqual(getrefcount(o), 2)
1658            for i in range(len(o)):
1659                self.assertEqual(getrefcount(o[i]), 2, "refcount fail for item %d" % i)
1660
1661    def test_display_surface_quit(self):
1662        """Font.render_to() on a closed display surface"""
1663
1664        # The Font.render_to() method checks that PySurfaceObject.surf is NULL
1665        # and raise a exception if it is. This fixes a bug in Pygame revision
1666        # 0600ea4f1cfb and earlier where Pygame segfaults instead.
1667        null_surface = pygame.Surface.__new__(pygame.Surface)
1668        f = self._TEST_FONTS["sans"]
1669        self.assertRaises(
1670            pygame.error, f.render_to, null_surface, (0, 0), "Crash!", size=12
1671        )
1672
1673    def test_issue_565(self):
1674        """get_metrics supporting rotation/styles/size"""
1675
1676        tests = [
1677            {"method": "size", "value": 36, "msg": "metrics same for size"},
1678            {"method": "rotation", "value": 90, "msg": "metrics same for rotation"},
1679            {"method": "oblique", "value": True, "msg": "metrics same for oblique"},
1680        ]
1681        text = "|"
1682
1683        def run_test(method, value, msg):
1684            font = ft.Font(self._sans_path, size=24)
1685            before = font.get_metrics(text)
1686            font.__setattr__(method, value)
1687            after = font.get_metrics(text)
1688            self.assertNotEqual(before, after, msg)
1689
1690        for test in tests:
1691            run_test(test["method"], test["value"], test["msg"])
1692
1693    def test_freetype_SysFont_name(self):
1694        """that SysFont accepts names of various types"""
1695        fonts = pygame.font.get_fonts()
1696        size = 12
1697
1698        # Check single name string:
1699        font_name = ft.SysFont(fonts[0], size).name
1700        self.assertFalse(font_name is None)
1701
1702        # Check string of comma-separated names.
1703        names = ",".join(fonts)
1704        font_name_2 = ft.SysFont(names, size).name
1705        self.assertEqual(font_name_2, font_name)
1706
1707        # Check list of names.
1708        font_name_2 = ft.SysFont(fonts, size).name
1709        self.assertEqual(font_name_2, font_name)
1710
1711        # Check generator:
1712        names = (name for name in fonts)
1713        font_name_2 = ft.SysFont(names, size).name
1714        self.assertEqual(font_name_2, font_name)
1715
1716        fonts_b = [f.encode() for f in fonts]
1717
1718        # Check single name bytes.
1719        font_name_2 = ft.SysFont(fonts_b[0], size).name
1720        self.assertEqual(font_name_2, font_name)
1721
1722        # Check comma-separated bytes.
1723        names = b",".join(fonts_b)
1724        font_name_2 = ft.SysFont(names, size).name
1725        self.assertEqual(font_name_2, font_name)
1726
1727        # Check list of bytes.
1728        font_name_2 = ft.SysFont(fonts_b, size).name
1729        self.assertEqual(font_name_2, font_name)
1730
1731        # Check mixed list of bytes and string.
1732        names = [fonts[0], fonts_b[1], fonts[2], fonts_b[3]]
1733        font_name_2 = ft.SysFont(names, size).name
1734        self.assertEqual(font_name_2, font_name)
1735
1736    @unittest.skipIf(pathlib is None, "no pathlib")
1737    def test_pathlib(self):
1738        f = ft.Font(pathlib.Path(self._fixed_path), 20)
1739
1740
1741class FreeTypeTest(unittest.TestCase):
1742    def setUp(self):
1743        ft.init()
1744
1745    def tearDown(self):
1746        ft.quit()
1747
1748    def test_resolution(self):
1749        try:
1750            ft.set_default_resolution()
1751            resolution = ft.get_default_resolution()
1752            self.assertEqual(resolution, 72)
1753            new_resolution = resolution + 10
1754            ft.set_default_resolution(new_resolution)
1755            self.assertEqual(ft.get_default_resolution(), new_resolution)
1756            ft.init(resolution=resolution + 20)
1757            self.assertEqual(ft.get_default_resolution(), new_resolution)
1758        finally:
1759            ft.set_default_resolution()
1760
1761    def test_autoinit_and_autoquit(self):
1762        pygame.init()
1763        self.assertTrue(ft.get_init())
1764        pygame.quit()
1765        self.assertFalse(ft.get_init())
1766
1767        # Ensure autoquit is replaced at init time
1768        pygame.init()
1769        self.assertTrue(ft.get_init())
1770        pygame.quit()
1771        self.assertFalse(ft.get_init())
1772
1773    def test_init(self):
1774        # Test if module initialized after calling init().
1775        ft.quit()
1776        ft.init()
1777
1778        self.assertTrue(ft.get_init())
1779
1780    def test_init__multiple(self):
1781        # Test if module initialized after multiple init() calls.
1782        ft.init()
1783        ft.init()
1784
1785        self.assertTrue(ft.get_init())
1786
1787    def test_quit(self):
1788        # Test if module uninitialized after calling quit().
1789        ft.quit()
1790
1791        self.assertFalse(ft.get_init())
1792
1793    def test_quit__multiple(self):
1794        # Test if module initialized after multiple quit() calls.
1795        ft.quit()
1796        ft.quit()
1797
1798        self.assertFalse(ft.get_init())
1799
1800    def test_get_init(self):
1801        # Test if get_init() gets the init state.
1802        self.assertTrue(ft.get_init())
1803
1804    def test_cache_size(self):
1805        DEFAULT_CACHE_SIZE = 64
1806        self.assertEqual(ft.get_cache_size(), DEFAULT_CACHE_SIZE)
1807        ft.quit()
1808        self.assertEqual(ft.get_cache_size(), 0)
1809        new_cache_size = DEFAULT_CACHE_SIZE * 2
1810        ft.init(cache_size=new_cache_size)
1811        self.assertEqual(ft.get_cache_size(), new_cache_size)
1812
1813    def test_get_error(self):
1814        """Ensures get_error() is initially empty (None)."""
1815        error_msg = ft.get_error()
1816
1817        self.assertIsNone(error_msg)
1818
1819
1820if __name__ == "__main__":
1821    unittest.main()
1822