#!/usr/bin/env python3 """Redraws Endless Sky's two bitmap fonts with DejaVu Sans 2.37 (fredygames.com). Endless Sky draws all of its text from two glyph strips, images/font/ubuntu14r.png and ubuntu18r.png: 98 cells side by side (ASCII 32-126, a "missing glyph" box for 127, and the curly quotes U+2018 and U+201C), rendered at twice the on-screen size, white with the coverage in the alpha channel (source/text/Font.cpp). The upstream strips are rasterizations of the Ubuntu font, which is under the Ubuntu Font Licence, so this build replaces them with the same glyphs drawn from DejaVu Sans (Bitstream Vera licence plus public-domain changes; it is the face used for the other redrawn fonts on fredygames.com). The point sizes below were chosen so that a line of text takes the same width as with the upstream strips (the game measures the glyphs pixel by pixel to space them), and the baseline row is the one the upstream strips use, so the menus keep their layout. make-font.py The output keeps the upstream file names (the game looks them up by name). Needs Pillow 12.3.0 with FreeType 2.14.3 (artifacts/tools/venv); other versions may rasterize differently, and the build script checks the SHA-256 of the result. """ import os import sys import PIL from PIL import Image, ImageDraw, ImageFont, features GLYPHS = 98 # name -> (DejaVu Sans pixel size at 2x, baseline row in the cell). The baseline is the upstream one # (row 26 of 32 and row 30 of 36), raised by two rows in the large font so that the DejaVu descenders # (deeper than Ubuntu's) fit in the cell. FONTS = { 'ubuntu14r.png': (26, 26), 'ubuntu18r.png': (33, 28), } LEFT = 2 # empty columns before the ink of every glyph, as upstream def chars(): for i in range(GLYPHS): if i < 95: yield i, chr(32 + i) elif i == 95: yield i, None # DEL: drawn as an empty box, like upstream elif i == 96: yield i, '‘' # left single quotation mark else: yield i, '“' # left double quotation mark def draw(upstream, ttf, size, base): width, height = upstream.size if width % GLYPHS: sys.exit(f'unexpected strip width {width}') cell = width // GLYPHS font = ImageFont.truetype(ttf, size) alpha = Image.new('L', (width, height), 0) for i, ch in chars(): if ch == ' ': continue canvas = Image.new('L', (cell * 3, height * 2), 0) pen = ImageDraw.Draw(canvas) if ch is None: # The box spans the x-height to the baseline, two pixels wide strokes. top = base - round(size * 0.55) pen.rectangle((cell, top + height // 2, cell + round(size * 0.4), base + height // 2 - 1), outline=255, width=2) else: pen.text((cell, base + height // 2), ch, font=font, fill=255, anchor='ls') box = canvas.getbbox() if not box: sys.exit(f'glyph {i} is empty') if box[1] < height // 2 or box[3] > height // 2 + height: sys.exit(f'glyph {i} ({ch!r}) does not fit vertically: {box}') if box[2] - box[0] > cell - LEFT: sys.exit(f'glyph {i} ({ch!r}) is too wide: {box[2] - box[0]} > {cell - LEFT}') glyph = canvas.crop((box[0], height // 2, box[0] + cell - LEFT, height // 2 + height)) alpha.paste(glyph, (i * cell + LEFT, 0)) out = Image.new('RGBA', (width, height), (255, 255, 255, 0)) out.putalpha(alpha) # Fully transparent pixels white too, as upstream (the game premultiplies on load). return out def main(srcdir, ttf, dstdir): if PIL.__version__ != '12.3.0' or features.version('freetype2') != '2.14.3': sys.exit(f'Needs Pillow 12.3.0 / FreeType 2.14.3, found {PIL.__version__} / ' f'{features.version("freetype2")}') os.makedirs(dstdir, exist_ok=True) for name, (size, base) in FONTS.items(): upstream = Image.open(os.path.join(srcdir, name)) draw(upstream, ttf, size, base).save(os.path.join(dstdir, name), optimize=True) if __name__ == '__main__': if len(sys.argv) != 4: sys.exit(__doc__) main(*sys.argv[1:])