Files
2026-10-16 08:31:15 +02:00

87 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""UI atlas for the on-screen panels: glyphs of the logo's font (URW Gothic Demi) + DS4 button icons.
python3 tools/make_ui_atlas.py FONT.otf (FONT: URW Gothic Demi; not shipped)
Writes assets/ui/ui_atlas.bin ('UIA1', u32 width, u32 height, RGBA8 rows top-down, sRGB colours,
straight alpha: glyphs white with coverage alpha, icons in colour) and src/ui_atlas.h (glyph rects,
bearings and advances for ASCII 32..126 plus the multiplication sign at index 127; icon rects).
Icons: tools/ui_icons/ (from the DS4 icon pack), resampled with premultiplied alpha."""
import os, sys
import numpy as np
from PIL import Image, ImageDraw, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__)); ROOT = os.path.dirname(HERE)
FONT_PX = 19 # cap height 14 px: the PS4 debug overlay's text size at 1080p
ICON_PX = 30 # scaled with the font (44 x 19 / 28), fits the 38 px line pitch
ICONS = ["Cross_Color", "Circle_Color", "Square_Color", "Triangle_Color", "L1", "R1", "L2", "R2",
"Dpad_UP", "Dpad_Down", "Dpad_Left", "Dpad_Right", "Options", "L_2D", "R_2D",
"Left_Stick_Click", "Right_Stick_Click"]
W = 1024
def resize_premul(im, size):
"""Crop to the visible content (alpha > 0), centre on a square canvas, resample to size."""
im = im.convert("RGBA"); bb = im.getchannel("A").getbbox()
if bb:
im = im.crop(bb); side = max(im.size); sq = Image.new("RGBA", (side, side), (0, 0, 0, 0))
sq.paste(im, ((side - im.size[0]) // 2, (side - im.size[1]) // 2)); im = sq
a = np.asarray(im, np.float64) / 255.0
lin = np.where(a[..., :3] <= 0.04045, a[..., :3] / 12.92, ((a[..., :3] + 0.055) / 1.055) ** 2.4) * a[..., 3:]
ch = [Image.fromarray(np.float32(c), "F").resize((size, size), Image.LANCZOS) for c in np.moveaxis(np.concatenate([lin, a[..., 3:]], 2), 2, 0)]
r = np.stack([np.asarray(c, np.float64) for c in ch], 2); al = np.clip(r[..., 3:], 0, 1)
rgb = np.clip(r[..., :3] / np.maximum(al, 1e-6), 0, 1)
rgb = np.where(rgb <= 0.0031308, 12.92 * rgb, 1.055 * rgb ** (1 / 2.4) - 0.055)
return np.round(np.concatenate([rgb, al], 2) * 255).astype(np.uint8)
def main():
font = ImageFont.truetype(sys.argv[1], FONT_PX)
chars = [chr(c) for c in range(32, 127)] + ["\u00d7"]
ascent, descent = font.getmetrics()
cap = -font.getbbox("H", anchor="ls")[1] # cap height above the baseline
glyphs = []; x = y = 0; row_h = 0; tiles = []
for ch in chars:
l, t, r, b = font.getbbox(ch, anchor="ls") # relative to the baseline
w, h = max(r - l, 0), max(b - t, 0)
if x + w + 2 > W: x = 0; y += row_h + 2; row_h = 0
glyphs.append(dict(x=x, y=y, w=w, h=h, xoff=l, yoff=t, adv=int(round(font.getlength(ch)))))
if w and h:
im = Image.new("L", (w, h), 0); ImageDraw.Draw(im).text((-l, -t), ch, font=font, fill=255, anchor="ls")
tiles.append((x, y, im))
x += w + 2; row_h = max(row_h, h)
y += row_h + 4; x = 0; icons = []
for n in ICONS:
if x + ICON_PX > W: x = 0; y += ICON_PX + 2
icons.append(dict(x=x, y=y, w=ICON_PX, h=ICON_PX, name=n))
x += ICON_PX + 2
H = y + ICON_PX + 2
atlas = np.zeros((H, W, 4), np.uint8)
for (gx, gy, im) in tiles:
a = np.asarray(im); atlas[gy:gy + a.shape[0], gx:gx + a.shape[1], :3] = 255; atlas[gy:gy + a.shape[0], gx:gx + a.shape[1], 3] = a
for ic in icons:
atlas[ic["y"]:ic["y"] + ICON_PX, ic["x"]:ic["x"] + ICON_PX] = resize_premul(Image.open(os.path.join(HERE, "ui_icons", "T_P4_%s.png" % ic["name"])), ICON_PX)
os.makedirs(os.path.join(ROOT, "assets", "ui"), exist_ok=True)
with open(os.path.join(ROOT, "assets", "ui", "ui_atlas.bin"), "wb") as f:
f.write(b"UIA1" + np.uint32(W).tobytes() + np.uint32(H).tobytes() + atlas.tobytes())
L = ["/* Generated by tools/make_ui_atlas.py - do not edit. UI atlas (assets/ui/ui_atlas.bin):",
" URW Gothic Demi %d px (cap %d, ascent %d, descent %d) glyphs for ASCII 32..126 + the" % (FONT_PX, cap, ascent, descent),
" multiplication sign at index 127, and DS4 icons %d px. yoff = top relative to the baseline. */" % ICON_PX,
"#pragma once", "#include <stdint.h>", "#define UI_ATLAS_W %d" % W, "#define UI_ATLAS_H %d" % H,
"#define UI_FONT_PX %d" % FONT_PX, "#define UI_FONT_CAP %d" % cap, "#define UI_FONT_ASCENT %d" % ascent, "#define UI_FONT_DESCENT %d" % descent,
"#define UI_ICON_PX %d" % ICON_PX, "#define UI_CHAR_TIMES 127",
"typedef struct { int16_t x, y, w, h, xoff, yoff, adv; } UiGlyph;",
"static const UiGlyph ui_glyph[96] = {"]
for g, ch in zip(glyphs, chars):
L.append(" {%d, %d, %d, %d, %d, %d, %d}, /* %s */" % (g["x"], g["y"], g["w"], g["h"], g["xoff"], g["yoff"], g["adv"],
"U+00D7" if ch == "\u00d7" else ("'%s'" % ch if ch not in "\\*/" else "0x%02x" % ord(ch))))
L.append("};")
L.append("enum { " + ", ".join("UI_ICON_%s" % n.upper().replace("_COLOR", "") for n in ICONS) + ", UI_ICON_COUNT };")
L.append("static const int16_t ui_icon_rect[UI_ICON_COUNT][2] = {" + ", ".join("{%d, %d}" % (i["x"], i["y"]) for i in icons) + "};")
open(os.path.join(ROOT, "src", "ui_atlas.h"), "w").write("\n".join(L) + "\n")
print("ui_atlas.bin %dx%d (%d glyphs, %d icons); src/ui_atlas.h" % (W, H, len(glyphs), len(icons)))
if __name__ == "__main__":
main()