from PIL import Image, ImageFont, ImageDraw import scipy.ndimage as nd import numpy as np from . import d2 # TODO: add support for newlines? PIXELS = 2**22 def _load_image(thing): if isinstance(thing, str): return Image.open(thing) elif isinstance(thing, (np.ndarray, np.generic)): return Image.fromarray(thing) return Image.fromarray(np.array(thing)) def measure_text(name, text, width=None, height=None): font = ImageFont.truetype(name, 96) x0, y0, x1, y1 = font.getbbox(text) aspect = (x1 - x0) / (y1 - y0) if width is None and height is None: height = 1 if width is None: width = height * aspect if height is None: height = width / aspect return (width, height) def measure_image(thing, width=None, height=None): im = _load_image(thing) w, h = im.size aspect = w / h if width is None and height is None: height = 1 if width is None: width = height * aspect if height is None: height = width / aspect return (width, height) @d2.sdf2 def text(font_name, text, width=None, height=None, pixels=PIXELS, points=512): # load font file font = ImageFont.truetype(font_name, points) # compute texture bounds p = 0.2 x0, y0, x1, y1 = font.getbbox(text) px = int((x1 - x0) * p) py = int((y1 - y0) * p) tw = x1 - x0 + 1 + px * 2 th = y1 - y0 + 1 + py * 2 # render text to image im = Image.new("L", (tw, th)) draw = ImageDraw.Draw(im) draw.text((px - x0, py - y0), text, font=font, fill=255) return _sdf(width, height, pixels, px, py, im) @d2.sdf2 def image(thing, width=None, height=None, pixels=PIXELS): im = _load_image(thing).convert("L") return _sdf(width, height, pixels, 0, 0, im) def _sdf(width, height, pixels, px, py, im): tw, th = im.size # downscale image if necessary factor = (pixels / (tw * th)) ** 0.5 if factor < 1: tw, th = int(round(tw * factor)), int(round(th * factor)) px, py = int(round(px * factor)), int(round(py * factor)) im = im.resize((tw, th)) # convert to numpy array and apply distance transform im = im.convert("1") a = np.array(im) inside = -nd.distance_transform_edt(a) outside = nd.distance_transform_edt(~a) texture = np.zeros(a.shape) texture[a] = inside[a] texture[~a] = outside[~a] # save debug image # a = np.abs(texture) # lo, hi = a.min(), a.max() # a = (a - lo) / (hi - lo) * 255 # im = Image.fromarray(a.astype('uint8')) # im.save('debug.png') # compute world bounds pw = tw - px * 2 ph = th - py * 2 aspect = pw / ph if width is None and height is None: height = 1 if width is None: width = height * aspect if height is None: height = width / aspect x0 = -width / 2 y0 = -height / 2 x1 = width / 2 y1 = height / 2 # scale texture distances scale = width / tw texture *= scale # prepare fallback rectangle # TODO: reduce size based on mesh resolution instead of dividing by 2 rectangle = d2.rectangle((width / 2, height / 2)) def f(p): x = p[:, 0] y = p[:, 1] u = (x - x0) / (x1 - x0) v = (y - y0) / (y1 - y0) v = 1 - v i = u * pw + px j = v * ph + py d = _bilinear_interpolate(texture, i, j) q = rectangle(p).reshape(-1) outside = (i < 0) | (i >= tw - 1) | (j < 0) | (j >= th - 1) d[outside] = q[outside] return d return f def _bilinear_interpolate(a, x, y): x0 = np.floor(x).astype(int) x1 = x0 + 1 y0 = np.floor(y).astype(int) y1 = y0 + 1 x0 = np.clip(x0, 0, a.shape[1] - 1) x1 = np.clip(x1, 0, a.shape[1] - 1) y0 = np.clip(y0, 0, a.shape[0] - 1) y1 = np.clip(y1, 0, a.shape[0] - 1) pa = a[y0, x0] pb = a[y1, x0] pc = a[y0, x1] pd = a[y1, x1] wa = (x1 - x) * (y1 - y) wb = (x1 - x) * (y - y0) wc = (x - x0) * (y1 - y) wd = (x - x0) * (y - y0) return wa * pa + wb * pb + wc * pc + wd * pd