Source

py-sdl2 / sdl2 / ext / font.py

Full commit
"""Font and text rendering routines."""
import os
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", "FontManager"]


class BitmapFont(object):
    """A bitmap graphics to character mapping.

    The BitmapFont class uses an image surface to find and render font
    character glyphs for text. It requires a mapping table, which
    denotes the characters available on the image.

    The mapping table is a list of strings, where each string reflects a
    'line' of characters on the image. Each character within each line
    has the same size as specified by the size argument.

    A typical mapping table might look like

      [ '0123456789',
        'ABCDEFGHIJ',
        'KLMNOPQRST',
        'UVWXYZ    ',
        'abcdefghij',
        'klmnopqrst',
        'uvwxyz    ',
        ',;.:!?+-()' ]
    """

    DEFAULTMAP = ["0123456789",
                  "ABCDEFGHIJ",
                  "KLMNOPQRST",
                  "UVWXYZ    ",
                  "abcdefghij",
                  "klmnopqrst",
                  "uvwxyz    ",
                  ",;.:!?+-()"
                  ]

    def __init__(self, imgsurface, size, mapping=None):
        """Creates a new BitmapFont instance from the passed image.

        Each character is expected to be of the same size (a 2-value tuple
        denoting the width and height) and to be in order of the passed
        mapping.
        """
        if mapping is None:
            self.mapping = list(BitmapFont.DEFAULTMAP)
        else:
            self.mapping = mapping
        self.offsets = {}
        if isinstance(imgsurface, SoftwareSprite):
            self.surface = imgsurface.surface
        #elif isinstance(surface, sprite.Sprite):
        #    TODO
        elif isinstance(imgsurface, surface.SDL_Surface):
            self.surface = imgsurface
        self.size = size[0], size[1]
        self._calculate_offsets()

    def _calculate_offsets(self):
        """Calculates the internal character offsets for each line."""
        self.offsets = {}
        offsets = self.offsets
        x, y = 0, 0
        w, h = self.size
        for line in self.mapping:
            x = 0
            for c in line:
                offsets[c] = rect.SDL_Rect(x, y, w, h)
                x += w
            y += h

    def render(self, text, bpp=None):
        """Renders the passed text on a new Sprite and returns it."""
        x, y = 0, 0
        tw, th = 0, 0
        w, h = self.size
        # TODO
        lines = text.split(os.linesep)
        for line in lines:
            tw = max(tw, sum([w for c in line]))
            th += h

        if bpp is None:
            bpp = self.surface.format.BitsPerPixel
        imgsurface = SoftwareSprite(tw, th, bpp)
        target = imgsurface.surface
        blit_surface = surface.SDL_BlitSurface
        fontsf = self.surface
        offsets = self.offsets

        srcr = rect.SDL_Rect(0, 0, 0, 0)
        for line in lines:
            for c in line:
                srcr.x = x
                srcr.y = y
                blit_surface(target, srcr, fontsf, offsets[c])
                x += w
            y += h
        return imgsurface

    def render_on(self, imgsurface, text, offset=(0, 0)):
        """Renders a text on the passed sprite, starting at a specific
        offset.

        The top-left start position of the text will be the passed offset and
        4-value tuple with the changed area will be returned.
        """
        x, y = offset
        w, h = self.size

        target = None
        if isinstance(imgsurface, SoftwareSprite):
            target = imgsurface.surface
        #elif isinstance(surface, sprite.Sprite):
        #    TODO
        elif isinstance(imgsurface, surface.SDL_Surface):
            target = imgsurface
        else:
            raise TypeError("unsupported surface type")

        lines = text.split(os.linesep)
        blit_surface = surface.SDL_BlitSurface
        fontsf = self.surface
        offsets = self.offsets

        srcr = rect.SDL_Rect(0, 0, 0, 0)
        for line in lines:
            for c in line:
                srcr.x = x
                srcr.y = y
                blit_surface(target, srcr, fontsf, offsets[c])
                x += w
            y += h
        return (offset[0], offset[1], x + w, y + h)

    def contains(self, c):
        """Checks, whether a certain character exists in the font."""
        return c in self.offsets

    def can_render(self, text):
        """Checks, whether all characters in the passed text can be rendered.
        """
        lines = text.split(os.linesep)
        for line in lines:
            for c in line:
                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. The
        default_font will be set to this font. color and bg_color
        will give the FontManager a default color. size is the default
        font size in pixels.
        """
        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.size = size
        self._default_font = self.add(font_path, alias)

    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=None):
        """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.
        """
        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)
        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 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

    @default_font.setter
    def default_font(self, value):
        """value must be a font alias

        Set the default_font to the given font name alias,
        provided it's loaded in the font manager.
        """
        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.

        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.
        """
        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)
            bg_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:
            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