Commits

Marcus von Appen committed 5c3f489

- added new sdl2.ext.FontManager class for simple font rendering support
- added new sdl2.ext.SpriteFactory.from_text() method for creating text
sprites

Comments (2)

Files changed (8)

sdl2/ext/common.py

 from .. import SDL_Init, SDL_Quit, SDL_QuitSubSystem, SDL_WasInit, \
     SDL_INIT_VIDEO, error, events, timer
 
+_HASSDLTTF = True
+try:
+    from .. import sdlttf
+except ImportError:
+    _HASSDLTTF = False
+_HASSDLIMAGE = True
+try:
+    from .. import sdlimage
+except ImportError:
+    _HASSDLIMAGE = False
+
 __all__ = ["SDLError", "init", "quit", "get_events", "TestEventProcessor"]
 
 
 def quit():
     """Quits the SDL2 video subysystem.
 
-    If no other subsystems are active, this will also call sdl2.SDL_Quit().
+    If no other subsystems are active, this will also call
+    sdl2.SDL_Quit(), sdlttf.TTF_Quit() and sdlimage.IMG_Quit().
     """
     SDL_QuitSubSystem(SDL_INIT_VIDEO)
     if SDL_WasInit(0) != 0:
+        if _HASSDLTTF and sdlttf.TTF_WasInit() == 1:
+            sdlttf.TTF_Quit()
+        if _HASSDLIMAGE:
+            sdlimage.IMG_Quit()
         SDL_Quit()
 
 

sdl2/ext/compat.py

     stringify = lambda x, enc: str(x)
     ISPYTHON2 = True
 else:
-    __all__ += ["long", "unichr", "callable"]
+    __all__ += ["long", "unichr", "callable", "unicode"]
     byteify = bytes
     stringify = lambda x, enc: x.decode(enc)
     long = int
     unichr = chr
     callable = lambda x: isinstance(x, collections.Callable)
     ISPYTHON3 = True
+    unicode = str
 
 isiterable = lambda x: isinstance(x, collections.Iterable)
 
 """Font and text rendering routines."""
 import os
-from .. import surface, rect
+from .. import surface, rect, pixels
+from .compat import *
 from .sprite import SoftwareSprite
+from .color import Color, convert_to_color
 
+_HASSDLTTF = True
+try:
+    from .. import sdlttf
+except ImportError:
+    _HASSDLTTF = False
 
-__all__ = ["BitmapFont"]
+
+__all__ = ["BitmapFont", "FontManager"]
 
 
 class BitmapFont(object):
                 if c not in self.offsets:
                     return False
         return True
+
+
+class FontManager(object):
+    """Manage fonts and rendering of text."""
+    def __init__(self, font_path, alias=None, size=16,
+                 color=Color(255, 255, 255), bg_color=Color(0, 0, 0)):
+        """Initialize the FontManager
+
+        One font path must be given to initialize the FontManager.
+        A list or tuple of font paths can also be given. In this
+        case, the alias is applied to the first entry. The other
+        fonts will have a default alias. The size is given to all
+        fonts.  The default_font will be set to this font.
+        text_color and bg_color will give the FontManager a default
+        color.
+        """
+        if not _HASSDLTTF:
+            raise UnsupportedError("FontManager requires sdlttf support")
+        if sdlttf.TTF_WasInit() == 0 and sdlttf.TTF_Init() != 0:
+            raise SDLError()
+        self.fonts = {}  # fonts = {alias: {size:font_ptr}}
+        self.aliases = {}  # aliases = {alias:font_path}
+        self._textcolor = pixels.SDL_Color(0, 0, 0)
+        self._bgcolor = pixels.SDL_Color(255, 255, 255)
+        self.color = color
+        self.bg_color = bg_color
+        self._default_font = self.add(font_path, alias, size)
+
+    def __del__(self):
+        """Close all opened fonts."""
+        self.close()
+
+    def close(self):
+        """Close all opened fonts."""
+        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=16):
+        """Add a font to the Font Manager.
+
+        alias is by default the font name. But another name can be
+        passed. Returns the font pointer stored in self.fonts.
+        """
+        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)
+        self.aliases[alias] = font_path
+        self.fonts[alias] = {}
+        self.fonts[alias][size] = font
+        return font
+
+    def _load_font(self, font_path, size):
+        """Helper function to open the font.
+
+        Raises an exception if something went wrong.
+        """
+        font = sdlttf.TTF_OpenFont(byteify(font_path, "utf-8"), size)
+        if font is None:
+            raise SDLError()
+        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):
+        """The text color to be used."""
+        return Color(self._textcolor.r, self._textcolor.g, self._textcolor.b,
+                     self._textcolor.a)
+
+    @color.setter
+    def color(self, value):
+        """The text color to be used."""
+        c = convert_to_color(value)
+        self._textcolor = pixels.SDL_Color(c.r, c.g, c.b, c.a)
+
+    @property
+    def bg_color(self):
+        """The background color to be used."""
+        return Color(self._bgcolor.r, self._bgcolor.g, self._bgcolor.b,
+                     self._bgcolor.a)
+
+    @bg_color.setter
+    def bg_color(self, value):
+        """The background color to be used."""
+        c = convert_to_color(value)
+        self._bgcolor = pixels.SDL_Color(c.r, c.g, c.b, c.a)
+
+    @property
+    def default_font(self):
+        """Returns the name and size of the current default_font."""
+        for alias in self.fonts:
+            for size, font in self.fonts[alias].items():
+                if font == self._default_font:
+                    return alias, size
+
+    @default_font.setter
+    def default_font(self, value):
+        """default must be a tuple with a font alias and a size: (alias, size)
+
+        Set the default_font to the given font name alias and size,
+        provided it's loaded in the font manager.
+        """
+        alias, size = value
+        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=16, width=None, color=None,
+               bg_color=None, **kwargs):
+        """Renders the text to a surface.
+
+        This method uses the font designated by the alias or the
+        default_font.  A size can be passed even if the font was not
+        loaded with this size.  A width can be given for line wrapping.
+        If no bg_color or color are given, it will default to the
+        FontManager's bg_color and color.
+        """
+        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)
+            bg_color = pixels.SDL_Color(c.r, c.g, c.b, c.a)
+        if len(self.fonts) == 0:
+            raise TypeError("There are no font selected.")
+        font = self._default_font
+        if alias is not None:
+            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:
+            surface = sdlttf.TTF_RenderUTF8_Blended_Wrapped(font, text,
+                                                            color, width)
+        elif bg_color == pixels.SDL_Color(0, 0, 0):
+            surface = sdlttf.TTF_RenderUTF8_Blended(font, text, color)
+        else:
+            surface = sdlttf.TTF_RenderUTF8_Shaded(font, text, color, bg_color)
+        return surface.contents

sdl2/ext/sprite.py

             raise SDLError()
         return self.from_surface(sf, True)
 
+    def from_text(self, text, **kwargs):
+        """Creates a Sprite from a string of text."""
+        args = self.default_args.copy()
+        args.update(kwargs)
+        fontmanager = args['fontmanager']
+        surface = fontmanager.render(text, **args)
+        return self.from_surface(surface, free=True)
+
     def create_sprite(self, **kwargs):
         """Creates an empty Sprite.
 
         """
         args = self.default_args.copy()
         args.update(kwargs)
-
         if self.sprite_type == TEXTURE:
             return self.create_texture_sprite(**args)
         else:
Add a comment to this file

sdl2/test/resources/tuffy.copy.ttf

Binary file added.

sdl2/test/sdl2ext_font_test.py

 import unittest
 from .. import ext as sdl2ext
 from ..ext.compat import byteify
-from .. import surface
+from .. import surface, sdlttf
 
 RESOURCES = sdl2ext.Resources(__file__, "resources")
 FONTMAP = ["0123456789",
         self.assertTrue(font.can_render("473285435hfsjadfhriuewtrhefd"))
         self.assertFalse(font.can_render("testä"))
 
+    def test_FontManager(self):
+        fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf"),
+                                 bg_color=(100, 0, 0))
+        self.assertIsInstance(fm, sdl2ext.FontManager)
+        self.assertEqual(fm.default_font, ("tuffy", 16))
+        self.assertEqual(fm.bg_color, sdl2ext.Color(100, 0, 0, 0))
+
+    def test_FontManager_default_font(self):
+        fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf"))
+        self.assertEqual(fm.default_font, ("tuffy", 16))
+        with self.assertRaises(ValueError):
+            fm.default_font = "Inexistent Alias", 16
+        fm.add(RESOURCES.get_path("tuffy.copy.ttf"), size = 10)
+        fm.default_font = ("tuffy.copy", 10)
+        self.assertEqual(fm.default_font, ("tuffy.copy", 10))
+        fm.default_font = ("tuffy.copy", 16)
+        self.assertEqual(fm.default_font, ("tuffy.copy", 16))
+
+    def test_FontManager_add(self):
+        fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf"))
+        self.assertIn("tuffy", fm.aliases)
+        self.assertIn("tuffy", fm.fonts)
+        self.assertIn(16, fm.fonts["tuffy"])
+        self.assertIsInstance(fm.fonts["tuffy"][16].contents, sdlttf.TTF_Font)
+
+        # Do some metrics tests
+        font = fm.fonts["tuffy"][16]
+        self.assertEqual(16, sdlttf.TTF_FontAscent(font))
+        fm.add(RESOURCES.get_path("tuffy.ttf"), size=12)
+        font = fm.fonts["tuffy"][12]
+        self.assertEqual(12, sdlttf.TTF_FontAscent(font))
+
+        self.assertRaises(IOError, fm.add, "inexistent.ttf")
+        # I don't find a scenario raising a TTF_Error.
+        # self.assertRaises(sdl2ext.SDLError, fm.add, "resources/tuffy.ttf",
+        #                   size=-1)
+
+        # Close the font manager and add a new font
+        fm.close()
+        fm.add(RESOURCES.get_path("tuffy.ttf"), size=12)
+        self.assertIsInstance(fm.fonts["tuffy"][12].contents, sdlttf.TTF_Font)
+
+    def test_FontManager_close(self):
+        fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf"))
+        fm.add(RESOURCES.get_path("tuffy.ttf"), size=20)
+        fm.add(RESOURCES.get_path("tuffy.ttf"), alias="Foo", size=10)
+        fm.close()
+        self.assertEqual(fm.fonts, {})
+        # How to make sure TTF_CloseFont was called on each loaded font?
+
+    def test_FontManager_render(self):
+        fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf"))
+        text_surf = fm.render("text")
+        self.assertIsInstance(text_surf, surface.SDL_Surface)
+        self.assertTrue(text_surf.w > 1)
+
+        text_surf = fm.render("text", size=10)
+        self.assertIsInstance(text_surf, surface.SDL_Surface)
+
+        text_surf = fm.render("""
+text long enough to have it wrapped at 100 px width.""", size=20, width=100)
+        self.assertIsInstance(text_surf, surface.SDL_Surface)
+        self.assertTrue(text_surf.w > 1)
+        self.assertTrue(text_surf.w == 100)
+        self.assertRaises(KeyError, fm.render, "text", alias="inexistent")
+
 
 if __name__ == '__main__':
     sys.exit(unittest.main())

sdl2/test/sdl2ext_sprite_test.py

             #self.assertRaises((AttributeError, ArgumentError, TypeError),
             #                  factory.from_surface, 1234)
 
+    def test_SpriteFactory_from_text(self):
+        sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE)
+        fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf"))
+
+        # No Fontmanager passed
+        self.assertRaises(KeyError, sfactory.from_text, "Test")
+
+        # Passing various keywords arguments
+        sprite = sfactory.from_text("Test", fontmanager = fm)
+        self.assertIsInstance(sprite, sdl2ext.SoftwareSprite)
+
+        sprite = sfactory.from_text("Test", fontmanager = fm, alias="tuffy")
+        self.assertIsInstance(sprite, sdl2ext.SoftwareSprite)
+
+        # Get text from a texture sprite factory
+        window = sdl2ext.Window("Test", size=(1, 1))
+        renderer = sdl2ext.RenderContext(window)
+        tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE,
+                                         renderer=renderer,
+                                         fontmanager=fm)
+        sprite = tfactory.from_text("Test", alias="tuffy")
+        self.assertIsInstance(sprite, sdl2ext.TextureSprite)
+
     def test_SpriteRenderer(self):
         renderer = sdl2ext.SpriteRenderer()
         self.assertIsInstance(renderer, sdl2ext.SpriteRenderer)

sdl2/test/sdl_test.py

         self.assertEqual(ret, SDL_INIT_JOYSTICK)
         SDL_QuitSubSystem(SDL_INIT_JOYSTICK)
 
+    @unittest.skipIf(sys.platform.startswith("freebsd"),
+                     "FreeBSD des not support haptic input yet")
     def test_SDL_INIT_HAPTIC(self):
         ret = SDL_Init(SDL_INIT_HAPTIC)
         self.assertEqual(ret, 0, SDL_GetError())
         ret = SDL_WasInit(SDL_INIT_HAPTIC)
-        if sys.platform.startswith("freebsd"):
-            # not supported yet
-            self.assertNotEqual(ret, SDL_INIT_HAPTIC)
-        else:
-            self.assertEqual(ret, SDL_INIT_HAPTIC)
+        self.assertEqual(ret, SDL_INIT_HAPTIC)
         SDL_QuitSubSystem(SDL_INIT_HAPTIC)
 
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.