#!/usr/bin/env python3
"""Redraws the text glyphs of Brogue CE's tiles.png with DejaVu fonts (fredygames.com).

The text rows of the upstream tiles.png (glyphs 0x20-0xFF: ASCII, Latin-1 and the symbols Brogue
draws in text mode) were rasterized from Apple's Monaco, with a few glyphs from Consolas and Noto
Sans (BrogueCE pull requests #133 and #158). This script replaces every one of those glyphs with
DejaVu Sans Mono 2.37 (DejaVu Sans for the symbols that the monospaced face lacks), so the page
does not ship pictures of proprietary fonts. The graphic tiles (rows 16-23, CC BY-SA 4.0 like the
whole file) are copied unchanged. The result stays under CC BY-SA 4.0 (modified: text glyphs
redrawn).

    make-tiles.py <upstream tiles.png> <dejavu ttf dir> <output tiles.png>

Needs Pillow 12.3.0 with FreeType 2.14.3 and fontTools (artifacts/tools/venv); other versions may rasterize
differently, and the build script checks the SHA-256 of the result.
"""
import sys

import PIL
from fontTools.ttLib import TTFont
from PIL import Image, ImageDraw, ImageFont, features

TILE_W, TILE_H = 128, 232
X_HEIGHT, BASELINE = 100, 46  # tiles.c: TEXT_X_HEIGHT and TEXT_BASELINE
BASE_Y = TILE_H - BASELINE    # baseline row inside a tile

# Glyph index -> code point for the non-Latin-1 slots (sdl2-platform.c: fontIndex()). 0x88 and
# 0x8B are unused by the game; they keep an up arrow and a filled circle, as upstream drew them.
SPECIAL = {
    0x80: 0x00B7, 0x81: 0x2237, 0x82: 0x25C7, 0x83: 0x22CF, 0x84: 0x2648, 0x85: 0x2640,
    0x86: 0x266A, 0x87: 0x26AA, 0x88: 0x2191, 0x89: 0x25CF, 0x8A: 0x26B2, 0x8B: 0x25CF,
    0x8C: 0x29F2, 0x8D: 0x29F3, 0x8E: 0x25C0,  # U+1F780 is not in DejaVu; U+25C0 is the same shape
    0x90: 0x2191, 0x91: 0x2193, 0x92: 0x2190, 0x93: 0x2192, 0x94: 0x0394, 0x95: 0x2207,
    0x96: 0x03A9, 0x97: 0x03B8, 0x98: 0x03BB, 0x99: 0x03DF, 0x9A: 0x25C6, 0x9B: 0x00D7,
}
# Not in DejaVu either (ERROR-BARRED WHITE / BLACK CIRCLE): drawn here as a circle with bars.
ERROR_BARRED = {0x29F2: 'white', 0x29F3: 'filled'}


def main(src, fontdir, dst):
    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")}')

    upstream = Image.open(src)
    gray = upstream.convert('L')
    if upstream.convert('RGB').tobytes() != gray.convert('RGB').tobytes():
        sys.exit('tiles.png is not grayscale')

    # Size the fonts so that the "x" is exactly X_HEIGHT pixels tall.
    probe = ImageFont.truetype(f'{fontdir}/DejaVuSansMono.ttf', 1000)
    x_top, x_bottom = probe.getbbox('x', anchor='ls')[1], probe.getbbox('x', anchor='ls')[3]
    size = round(1000 * X_HEIGHT / (x_bottom - x_top))
    mono = ImageFont.truetype(f'{fontdir}/DejaVuSansMono.ttf', size)
    sans = ImageFont.truetype(f'{fontdir}/DejaVuSans.ttf', size)
    has_mono = set(TTFont(f'{fontdir}/DejaVuSansMono.ttf').getBestCmap())

    out = gray.copy()
    for index in range(0x20, 0x100):
        row, col = divmod(index, 16)
        box = (col * TILE_W, row * TILE_H, (col + 1) * TILE_W, (row + 1) * TILE_H)
        # Only redraw the slots that upstream draws (blank slots must stay blank: tiles.c skips
        # them).
        if gray.crop(box).getbbox() is None:
            continue
        code = SPECIAL.get(index, index)
        if index == 0xAD:  # soft hyphen, drawn as a hyphen like upstream
            code = 0x2D

        tile = Image.new('L', (TILE_W, TILE_H), 0)
        draw = ImageDraw.Draw(tile)
        if code in ERROR_BARRED:
            # Circle on the middle of the x-height, bars with caps above and below it.
            cx, cy, r, stroke = TILE_W / 2, BASE_Y - X_HEIGHT / 2, 34, 9
            ring = (cx - r, cy - r, cx + r, cy + r)
            if ERROR_BARRED[code] == 'filled':
                draw.ellipse(ring, fill=255)
            else:
                draw.ellipse(ring, outline=255, width=stroke)
            for y0, y1 in ((cy - r - 40, cy - r), (cy + r, cy + r + 40)):
                draw.rectangle((cx - stroke / 2, y0, cx + stroke / 2, y1), fill=255)
            for y in (cy - r - 40, cy + r + 40 - stroke):
                draw.rectangle((cx - 24, y, cx + 24, y + stroke), fill=255)
        else:
            font = mono if code in has_mono else sans
            draw.text((TILE_W / 2, BASE_Y), chr(code), fill=255, font=font, anchor='ms')
        out.paste(tile, box)

    # 8-bit grayscale (the antialiased glyphs need more than upstream's 35-color palette);
    # SDL_image converts it to ARGB8888 like the original.
    out.info = {}  # no metadata (upstream's RGB ICC profile does not fit a grayscale PNG)
    out.save(dst, optimize=False, compress_level=9)


if __name__ == '__main__':
    main(*sys.argv[1:4])
