Source code for arcade.text_pillow

"""
PIL based text functions
"""

from itertools import chain
from typing import Tuple, Union, cast

import PIL.Image
import PIL.ImageDraw
import PIL.ImageFont

from arcade.arcade_types import RGBA, Color
from arcade.draw_commands import Texture, get_four_byte_color
from arcade.sprite import Sprite

DEFAULT_FONT_NAMES = (
    "arial.ttf",
    "Arial.ttf",
    "NotoSans-Regular.ttf",
    "/usr/share/fonts/truetype/freefont/FreeMono.ttf",
    "/System/Library/Fonts/SFNSDisplay.ttf",
    "/Library/Fonts/Arial.ttf",
)


[docs]def create_text_image( text: str, text_color: Color, font_size: float = 12, width: int = 0, align: str = "left", valign: str = "top", font_name: Union[str, Tuple[str, ...]] = ("calibri", "arial"), background_color: Color = None, height: int = 0, ) -> PIL.Image.Image: """ Create a PIL.Image containing text. .. warning:: This method can be fairly slow. We recommend creating images on initialization or infrequently later on. :param str text: The text to render to the image :param Color text_color: Color of the text :param float font_size: Size of the font :param int width: The width of the image in pixels :param str align: "left" or "right" aligned :param str valign: "top" or "bottom" aligned :param str font_name: The font to use :param Color background_color: The background color of the image :param int height: the height of the image in pixels """ # Scale the font up, so it matches with the sizes of the old code back # when Pyglet drew the text. font_size *= 1.25 # Text isn't anti-aliased, so we'll draw big, and then shrink scale_up = 2 scale_down = 2 font_size *= scale_up # Figure out the font to use font = None # Font was specified with a string if isinstance(font_name, str): font_name = (font_name,) font_names = chain( *[ [font_string_name, f"{font_string_name}.ttf"] for font_string_name in font_name ], DEFAULT_FONT_NAMES, ) font_found = False for font_string_name in font_names: try: font = PIL.ImageFont.truetype(font_string_name, int(font_size)) except OSError: continue else: font_found = True break if not font_found: try: import pyglet.font font_config = pyglet.font.fontconfig.get_fontconfig() result = font_config.find_font("Arial") font = PIL.ImageFont.truetype(result.name, int(font_size)) except Exception: # NOTE: Will catch OSError from loading font and missing fontconfig in pyglet pass else: font_found = True # Final fallback just getting PIL's default font if possible if not font_found: try: font = PIL.ImageFont.load_default() font_found = True except Exception: pass if not font_found: raise RuntimeError( "Unable to find a default font on this system. Please specify an available font." ) # This is stupid. We have to have an image to figure out what size # the text will be when we draw it. Of course, we don't know how big # to make the image. Catch-22. So we just make a small image we'll trash text_image_size = [10, 10] image = PIL.Image.new("RGBA", text_image_size) draw = PIL.ImageDraw.Draw(image) # Get size the text will be text_image_size = draw.multiline_textsize(text, font=font) # Add some extra pixels at the bottom to account for letters that drop below the baseline. text_image_size = [text_image_size[0], text_image_size[1] + int(font_size * 0.25)] # Create image of proper size text_height = text_image_size[1] text_width = text_image_size[0] image_start_x = 0 specified_width = width if specified_width == 0: width = text_image_size[0] else: # Wait! We were given a field width. if align == "center": # Center text on given field width field_width = width * scale_up image_start_x = (field_width // 2) - (text_width // 2) else: image_start_x = 0 # Find y of top-left corner image_start_y = 0 if height and valign == "middle": field_height = height * scale_up image_start_y = (field_height // 2) - (text_height // 2) if height: text_image_size[1] = height * scale_up if specified_width: text_image_size[0] = width * scale_up # Create image image = PIL.Image.new("RGBA", text_image_size, background_color) draw = PIL.ImageDraw.Draw(image) # Convert to tuple if needed, because the multiline_text does not take a # list for a color if isinstance(text_color, list): color = cast(RGBA, tuple(text_color)) draw.multiline_text( (image_start_x, image_start_y), text, text_color, align=align, font=font ) image = image.resize( (max(1, text_image_size[0] // scale_down), text_image_size[1] // scale_down), resample=PIL.Image.LANCZOS, ) return image
[docs]def create_text_sprite( text: str, start_x: float, start_y: float, color: Color, font_size: float = 12, width: int = 0, align: str = "left", font_name: Union[str, Tuple[str, ...]] = ("calibri", "arial"), bold: bool = False, italic: bool = False, anchor_x: str = "left", anchor_y: str = "baseline", rotation: float = 0, ) -> Sprite: """ Creates a sprite with a text texture using :py:func:`~arcade.create_text_image`. Internally this works by creating an image, and using the Pillow library to draw the text to it. Then use that image to create a sprite. We cache the sprite (so we don't have to recreate over and over, which is slow) and use it to draw text to the screen. This implementation does not support bold/italic like the older Pyglet-based implementation of draw_text. However if you specify the 'italic' or 'bold' version of the font via the font name, you will get that font. Just the booleans do not work. :param str text: Text to draw :param float start_x: x coordinate of the lower-left point to start drawing text :param float start_y: y coordinate of the lower-left point to start drawing text :param Color color: Color of the text :param float font_size: Size of the text :param float width: Width of the text-box for the text to go into. Used with alignment. :param str align: Align left, right, center :param Union[str, Tuple[str, ...]] font_name: Font name, or list of font names in order of preference :param bool bold: Bold the font (currently unsupported) :param bool italic: Italicize the font (currently unsupported) :param str anchor_x: Anchor the font location, defaults to 'left' :param str anchor_y: Anchor the font location, defaults to 'baseline' :param float rotation: Rotate the text """ r, g, b, alpha = get_four_byte_color(color) cache_color = f"{r}{g}{b}" key = f"{text}{cache_color}{font_size}{width}{align}{font_name}{bold}{italic}" image = create_text_image( text=text, text_color=color, font_size=font_size, width=width, align=align, font_name=font_name, ) text_sprite = Sprite() text_sprite._texture = Texture(key) text_sprite.texture.image = image text_sprite.width = image.width text_sprite.height = image.height if anchor_x == "left": text_sprite.center_x = start_x + text_sprite.width / 2 elif anchor_x == "center": text_sprite.center_x = start_x elif anchor_x == "right": text_sprite.right = start_x else: raise ValueError( f"anchor_x should be 'left', 'center', or 'right'. Not '{anchor_x}'" ) if anchor_y == "top": text_sprite.center_y = start_y - text_sprite.height / 2 elif anchor_y == "center": text_sprite.center_y = start_y elif anchor_y == "bottom" or anchor_y == "baseline": text_sprite.center_y = start_y + text_sprite.height / 2 else: raise ValueError( f"anchor_y should be 'top', 'center', 'bottom', or 'baseline'. Not '{anchor_y}'" ) text_sprite.angle = rotation text_sprite.alpha = alpha return text_sprite