Source code for pi3d.util.FixedString

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 FixedString. Otherwise use Pngfont
from PIL import Image, ImageDraw, ImageFont

from pi3d.Texture import Texture, WIDTHS
from pi3d.shape.Sprite import Sprite

[docs]class FixedString(Texture): """ A texture containing a simple string drawn using ImageDraw. The advantage over a standard String is that it only requires a simple Sprite shape for drawing so the gpu has to only draw two triangles rather than two triangles for each letter.""" def __init__(self, font, string, camera=None, color=(255,255,255,255), shadow=(0,0,0,255), shadow_radius=0, font_size=24, margin=5.0, justify='C', background_color=None, shader=None, f_type='', mipmap=True, width=None): """Arguments: *font*: File path/name to a TrueType font file. *string*: String to write. *camera*: Camera object passed on to constructor of sprite *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 *shadow*: Color of shadow, default black. *shadow_radius*: Gaussian blur radius applied to shadow layer, default 0 (no shadow) *font_size*: Point size for drawing the letters on the internal Texture. default 24 *margin*: Offsets from the top left corner for the text and space on right and bottom. default 5.0 *justify*: L(eft), C(entre), R(ight) default C *background_color*: filled background in ImageDraw format as above. default None i.e. transparent. *shader*: can be passed to init otherwise needs to be set in set_shader or draw. default None *f_type*: filter type. BUMP will generate a normal map (indented by default, +BUMP or BUMP+ will make it stick out), EMBOSS, CONTOUR, BLUR and SMOOTH do what they sound like they will do. """ super(FixedString, self).__init__(font, mipmap=mipmap) self.font = font 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) justify = justify.upper() f_type = f_type.upper() ascent, descent = imgfont.getmetrics() height = ascent + descent lines = string.split('\n') new_lines = [] maxwid = 0 for l in lines: line_wid = imgfont.getsize(l)[0] if width is not None and line_wid > width: new_line = "" space = "" words = l.split(" ") for word in words: check_line = "{}{}{}".format(new_line, space, word) if imgfont.getsize(check_line)[0] <= width: new_line = check_line else: # wrap before this word TODO cope with lines with no spaces if "-" in word: # TODO make this a function to split first on " " then "-" then on split_word = word.split("-") pre = split_word[0] post = "-".join(split_word[1:]) check_line = "{} {}-".format(new_line, pre) if imgfont.getsize(check_line)[0] <= width: new_line = check_line word = post new_lines.append(new_line) new_line = word space = " " new_lines.append(new_line) else: new_lines.append(l) lines = new_lines for l in lines: line_wid = imgfont.getsize(l)[0] if line_wid > maxwid: maxwid = line_wid maxwid += 2.0 * margin texture_wid = min(int(maxwid / 4) * 4, 2048) nlines = len(lines) texture_hgt = int(nlines * height + 2 * margin) self.im = Image.new("RGBA", (texture_wid, texture_hgt), background_color) self.ix, self.iy = texture_wid, texture_hgt draw = ImageDraw.Draw(self.im) if shadow_radius > 0: from PIL import ImageFilter self._render_text(lines, justify, margin, imgfont, maxwid, height, shadow, draw) self.im = self.im.filter(ImageFilter.GaussianBlur(radius=shadow_radius)) if background_color == None: im_arr = self._force_color(np.array(self.im), shadow) try: # numpy not quite working fully in pypy so have to convert tobytes self.im = Image.fromarray(im_arr) except: h, w, c = im_arr.shape rgb = 'RGB' if c == 3 else 'RGBA' self.im = Image.frombytes(rgb, (w, h), im_arr.tobytes()) draw = ImageDraw.Draw(self.im) self._render_text(lines, justify, margin, imgfont, maxwid, height, color, draw) force_color = background_color is None and shadow_radius == 0 if f_type == '': self.image = np.array(self.im) elif 'BUMP' in f_type: amount = -1.0 if '+' in f_type else 1.0 self.image = self._normal_map(np.array(self.im, dtype=np.uint8), amount) force_color = False else: from PIL import ImageFilter if f_type == 'EMBOSS': self.im = self.im.filter(ImageFilter.EMBOSS) elif f_type == 'CONTOUR': self.im = self.im.filter(ImageFilter.CONTOUR) elif f_type == 'BLUR': self.im = self.im.filter(ImageFilter.BLUR) elif f_type == 'SMOOTH': self.im = self.im.filter(ImageFilter.SMOOTH_MORE) self.image = np.array(self.im) if force_color: self.image = self._force_color(self.image, color) self._tex = ctypes.c_uint() bmedge = nlines * height + 2.0 * margin self.sprite = Sprite(camera=camera, w=maxwid, h=bmedge) buf = self.sprite.buf[0] #convenience alias buf.textures = [self] if shader != None: self.sprite.shader = shader buf.shader = shader buf.unib[6] = float(maxwid / texture_wid) #scale to fit buf.unib[7] = float(bmedge / texture_hgt) def _render_text(self, lines, justify, margin, imgfont, maxwid, height, color, draw): for i, line in enumerate(lines): line_len = imgfont.getsize(line)[0] if justify == "C": xoff = (maxwid - line_len) / 2.0 elif justify == "L": xoff = margin else: xoff = maxwid - line_len draw.text((xoff, margin + i * height), line, font=imgfont, fill=color)
[docs] def set_shader(self, shader): ''' wrapper for Shape.set_shader''' self.sprite.set_shader(shader)
[docs] def draw(self, shader=None, txtrs=None, ntl=None, shny=None, camera=None): '''wrapper for Shape.draw()''' self.sprite.draw(shader, txtrs, ntl, shny, camera)
def _force_color(self, im_array, 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 """ if isinstance(color, str): from PIL import ImageColor color = ImageColor.getrgb(color) im_array[:,:,:3] = color[:3] return im_array def _load_disk(self): """ we need to stop the normal file loading by overriding this method """