import io
import threading
from sdl2 import rw_from_object
from sdl2 import (
SDL_FreeSurface, # https://wiki.libsdl.org/SDL_FreeSurface
SDL_Color,
)
from sdl2.sdlttf import (
TTF_Init, TTF_Quit, # https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_6.html#SEC6
TTF_OpenFontRW, # https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_15.html
TTF_OpenFontIndexRW, # https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_17.html
TTF_CloseFont, # https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_18.html
TTF_FontFaceIsFixedWidth, # https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_34.html
TTF_FontFaceFamilyName, # https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_35.html
TTF_FontFaceStyleName, # https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_36.html
TTF_RenderUTF8_Blended, # https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_52.html
)
from ppb.assetlib import Asset, ChainingMixin, AbstractAsset, FreeingMixin
from ppb.systems.sdl_utils import ttf_call
# From https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html:
# [Since 2.5.6] In multi-threaded applications it is easiest to use one
# FT_Library object per thread. In case this is too cumbersome, a single
# FT_Library object across threads is possible also, as long as a mutex lock is
# used around FT_New_Face and FT_Done_Face.
#
# I assume this translates to TTF_OpenFont* and TTF_CloseFont
# SDL_ttf manages a single global FT_Library, so we need to use the lock
# for threaded calls into it, like in Asset._background.
_freetype_lock = threading.RLock()
[docs]
class Font(ChainingMixin, FreeingMixin, AbstractAsset):
"""
A TrueType/OpenType Font
"""
def __init__(self, name, *, size, index=None):
"""
:param name: the filename to load
:param size: the size in points
:param index: the index of the font in a multi-font file (rare)
"""
# We do it this way so that the raw data can be cached between multiple
# invocations, even though we have to reparse it every time.
self._data = Asset(name)
self.size = size
self.index = index
self._start(self._data)
def _background(self):
self._file = rw_from_object(io.BytesIO(self._data.load()))
# We have to keep the file around because freetype doesn't load
# everything at once, resulting in segfaults.
with _freetype_lock:
# Doing this so that we "refcount" the FT_Library internal to SDL_ttf
# (TTF_CloseFont is often called after system cleanup)
ttf_call(TTF_Init, _check_error=lambda rv: rv == -1)
if self.index is None:
return ttf_call(
TTF_OpenFontRW, self._file, False, self.size,
_check_error=lambda rv: not rv
)
else:
return ttf_call(
TTF_OpenFontIndexRW, self._file, False, self.size, self.index,
_check_error=lambda rv: not rv
)
def free(self, data, _TTF_CloseFont=TTF_CloseFont, _lock=_freetype_lock,
_TTF_Quit=TTF_Quit):
# ^^^ is a way to keep required functions during interpreter cleanup
with _lock:
_TTF_CloseFont(data) # Can't fail
_TTF_Quit()
def __repr__(self):
return f"<{type(self).__name__} name={self.name!r} size={self.size!r}{' loaded' if self.is_loaded() else ''} at {id(self):x}>"
@property
def name(self):
return self._data.name
def resize(self, size):
"""
Returns a new copy of this font in a different size
"""
return type(self)(self._data.name, size=size, index=self._index)
@property
def _is_fixed_width(self):
return bool(TTF_FontFaceIsFixedWidth(self.load()))
@property
def _family_name(self):
return TTF_FontFaceFamilyName(self.load())
@property
def _style_name(self):
return TTF_FontFaceStyleName(self.load())
[docs]
class Text(ChainingMixin, FreeingMixin, AbstractAsset):
"""
A bit of rendered text.
"""
def __init__(self, txt, *, font, color=(0, 0, 0)):
"""
:param txt: The text to display.
:param font: The font to use (a :py:class:`ppb.Font`)
:param color: The color to use.
"""
self.txt = txt
self.font = font
self.color = color
self._start(self.font)
def __repr__(self):
return f"<{type(self).__name__} txt={self.txt!r} font={self.font!r} color={self.color!r}{' loaded' if self.is_loaded() else ''} at 0x{id(self):x}>"
def _background(self):
with _freetype_lock:
return ttf_call(
TTF_RenderUTF8_Blended, self.font.load(), self.txt.encode('utf-8'),
SDL_Color(*self.color),
_check_error=lambda rv: not rv
)
def free(self, object, _SDL_FreeSurface=SDL_FreeSurface):
# ^^^ is a way to keep required functions during interpreter cleanup
_SDL_FreeSurface(object) # Can't fail