from __future__ import absolute_import, division, print_function, unicode_literals
import ctypes
import numpy as np
import itertools
import os.path
import sys
if sys.version_info[0] == 3:
unichr = chr
# NB PIL must be available to use Font. Otherwise use Pngfont
from PIL import Image, ImageDraw, ImageFont
from pi3d.Texture import Texture
MAX_SIZE = 1920
def _strengthen(x):
# used if shadow required to slightly harden edges
F = 0.5
return int(x * (1 + F - F * x / 256))
[docs]class Font(Texture):
"""
A Font contains a TrueType font ready to be rendered in OpenGL.
A font is just a mapping from codepoints (single Unicode characters) to glyphs
(graphical representations of those characters).
Font packs one whole font into a single Texture using PIL.ImageFont,
then creates a table mapping codepoints to subrectangles of that Texture."""
def __init__(self, font, color=(255,255,255,255), codepoints=None,
add_codepoints=None, font_size=None, image_size=1024,
italic_adjustment=1.1, background_color=None,
shadow=(0,0,0,255), shadow_radius=0, spacing=None,
mipmap=True, filter=None, grid_size=16):
"""Arguments:
*font*:
File path/name to a TrueType font file.
*color*:
Color in format '#RRGGBB', (255,0,0,255), 'orange' etc (as accepted
by PIL.ImageDraw) default (255, 255, 255, 255) i.e. white 100% alpha
*font_size*:
Point size for drawing the letters on the internal Texture
*codepoints*:
Iterable list of characters. All these formats will work:
'ABCDEabcde '
[65, 66, 67, 68, 69, 97, 98, 99, 100, 101, 145, 148, 172, 32]
[c for c in range(65, 173)]
Note that Font will ONLY use the codepoints in this list - if you
forget to list a codepoint or character here, it won't be displayed.
If you just want to add a few missing codepoints, you're probably better
off using the *add_codepoints* parameter.
If the string version is used then the program file might need to
have the coding defined at the top: # -*- coding: utf-8 -*-
The default is *codepoints*=range(256).
*add_codepoints*:
If you are only wanting to add a few codepoints that are missing, you
should use the *add_codepoints* parameter, which just adds codepoints or
characters to the default list of codepoints (range(256). All the other
comments for the *codepoints* parameter still apply.
*image_size*:
Width and height of the Texture that backs the image.
Since the introduction of PointText using Point drawing image_size is
no longer used - all Font Textures are 1024.
*italic_adjustment*:
Adjusts the bounding width to take italics into account. The default
value is 1.1; you can get a tighter bounding if you set this down
closer to 1, but italics might get cut off at the right. Since PointText
this isn't used.
*background_color*:
filled background in ImageDraw format as above. default None i.e.
transparent.
*shadow*:
Color of shadow, default black.
*shadow_radius*:
Gaussian blur radius applied to shadow layer, default 0 (no shadow)
*spacing*:
Extra spacing between letters to allow for shadow. The default value
None will add spacing equal to the shadow_radius, this will be overridden
by any value supplied.
*mipmap*:
Resulting texture mipmap option, default true
*filter*:
Resulting texture filter option, default None
*grid_size*
number rows and cols to divide 1024 pixels. For high res fonts this can be
changed 4 -> 16chars, 8 -> 64chars, 10 -> 100chars etc.
"""
super(Font, self).__init__(font, mipmap=mipmap, filter=filter)
self.font = font
if font_size is None:
font_size = int(672 / grid_size) # i.e. 16x16 has font size 42
try:
imgfont = ImageFont.truetype(font, font_size)
except IOError:
abspath = os.path.abspath(font)
msg = "Couldn't find font file '%s'" % font
if font != abspath:
msg = "%s - absolute path is '%s'" % (msg, abspath)
raise Exception(msg)
ascent, descent = imgfont.getmetrics()
if spacing is None:
spacing = shadow_radius
self.height = ascent + descent + spacing # allow extra pixels if shadow or for certain fonts
self.grid_size = grid_size
num_char = self.grid_size ** 2
image_size = 1024 # fixed value despite having as argument! TODO allow variable size now grid_size variable
self.spacing = image_size / self.grid_size
if codepoints is not None:
codepoints = list(codepoints)[:num_char]
else:
codepoints = list(range(num_char))
if add_codepoints is not None:
add_codepoints = list(add_codepoints)
if (len(codepoints) + len(add_codepoints)) > num_char: # make room at end
codepoints = codepoints[:(num_char - len(add_codepoints))]
codepoints += add_codepoints
is_draw_shadows = shadow_radius > 0
is_text_transparent = is_draw_shadows or (background_color == None)
self.im = Image.new("RGBA", (image_size, image_size), (0, 0, 0, 0) if is_text_transparent else background_color)
if is_draw_shadows:
shadow_img = Image.new("RGBA", (image_size, image_size), background_color)
shadow_draw = ImageDraw.Draw(shadow_img)
self.ix, self.iy = image_size, image_size
self.glyph_table = {}
draw = ImageDraw.Draw(self.im)
curX = 0.0
curY = 0.0
yindex = 0
xindex = 0
characters = []
for i in itertools.chain([0], codepoints):
try:
ch = unichr(i)
except TypeError:
ch = i
chwidth, chheight = imgfont.getsize(ch)
curX = xindex * self.spacing
curY = yindex * self.spacing
offset = (self.spacing - chwidth) / 2.0
draw.text((curX + offset, curY), ch, font=imgfont, fill=color)
if is_draw_shadows:
shadow_draw.text((curX + offset, curY), ch, font=imgfont, fill=shadow)
chwidth += spacing * 2 # make a little more room (for w for instance)
offset -= spacing
x = float(curX + offset) / self.ix
y = float(curY + self.height) / self.iy
tw = float(chwidth) / self.ix
th = float(self.height) / self.iy
table_entry = [
chwidth,
chheight,
[[x + tw, y - th], [x, y - th], [x, y], [x + tw, y]], # UV texture coordinates
[[chwidth, 0, 0], [0, 0, 0], [0, -self.height, 0], [chwidth, -self.height, 0]], # xyz vertex coordinates of corners
float(curX) / self.ix,
float(curY) / self.iy
]
self.glyph_table[ch] = table_entry
xindex += 1
if xindex >= self.grid_size:
xindex = 0
yindex += 1
if is_text_transparent:
self.im = self._force_color(self.im, color)
if is_draw_shadows:
from PIL import ImageFilter
if background_color == None:
shadow_img = self._force_color(shadow_img, shadow)
shadow_img = shadow_img.filter(ImageFilter.GaussianBlur(radius=shadow_radius))
shadow_img = Image.eval(shadow_img, _strengthen) # slightly sharpen edge of blur - see func def at top
self.im = Image.alpha_composite(shadow_img, self.im)
self.image = np.array(self.im)
self._tex = ctypes.c_uint()
def _force_color(self, img, color):
"""
Overwrite color of all pixels as PIL renders text incorrectly when drawing on transparent backgrounds
http://nedbatchelder.com/blog/200801/truly_transparent_text_with_pil.html
"""
img = np.array(img)
if isinstance(color, str):
from PIL import ImageColor
color = ImageColor.getrgb(color)
img[:,:,:3] = color[:3]
try: # numpy not quite working fully in pypy so have to convert tobytes
return Image.fromarray(img)
except:
h, w, c = img.shape
rgb = 'RGB' if c == 3 else 'RGBA'
return Image.frombytes(rgb, (w, h), img.tobytes())
def _load_disk(self):
"""
we need to stop the normal file loading by overriding this method
"""