Files
StarPilot/roadscore/tools/preview_overlay.py
T

84 lines
3.6 KiB
Python

"""Render synthetic overlay states with the repo's font atlas, entirely on CPU.
Requires Pillow. No device, network, audio, replay, worker, or window is opened.
"""
import argparse
import re
import sys
from pathlib import Path
from types import SimpleNamespace
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'roadscore/prototype'))
from overlay_view import draw_panel
class Canvas:
WHITE = (255, 255, 255, 255)
Color = staticmethod(lambda *rgba: rgba)
Rectangle = staticmethod(lambda x, y, w, h: (x, y, x + w, y + h))
Vector2 = staticmethod(lambda x, y: SimpleNamespace(x=x, y=y))
def __init__(self, image, font_path):
self.image = image
self.atlas = Image.open(font_path.with_suffix('.png')).convert('RGBA')
lines = font_path.read_text().splitlines()
self.base = abs(int(re.search(r'size=(-?\d+)', lines[0])[1]))
self.glyphs = {}
for line in lines:
if line.startswith('char '):
glyph = {key: int(value) for key, value in re.findall(r'(\w+)=(-?\d+)', line)}
self.glyphs[glyph['id']] = glyph
def measure_text_ex(self, font, text, size, spacing):
return SimpleNamespace(x=sum(self.glyphs[ord(c)]['xadvance'] for c in text) * size / self.base)
def draw_text_ex(self, font, text, pos, size, spacing, color):
scale = size / self.base
for char in text:
g = self.glyphs[ord(char)]
if g['width'] and g['height']:
mask = self.atlas.crop((g['x'], g['y'], g['x'] + g['width'], g['y'] + g['height'])).getchannel('A')
mask = mask.resize((max(1, round(g['width'] * scale)), max(1, round(g['height'] * scale))))
ink = Image.new('RGBA', mask.size, color)
self.image.paste(ink, (round(pos.x + g['xoffset'] * scale), round(pos.y + g['yoffset'] * scale)), mask)
pos.x += g['xadvance'] * scale
def draw_rectangle_rounded(self, rect, roundness, segments, color):
layer = Image.new('RGBA', self.image.size)
ImageDraw.Draw(layer).rounded_rectangle(rect, radius=6, fill=color)
self.image.alpha_composite(layer)
def draw_circle(self, x, y, radius, color):
ImageDraw.Draw(self.image).ellipse((x-radius, y-radius, x+radius, y+radius), fill=color)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
base = dict(composer='ace', style='Prism', readiness='READY', section='VERSE / CONTINUOUS',
next_section='PRECHORUS', form_labels_are_intent=True, buffered=64)
cases = [{}, base, dict(base, job_inflight=True, generation_elapsed_seconds=23.4, turn_signal_music=True),
dict(base, readiness='DEGRADED', holding_accepted_music=True, job_inflight=True, buffered=18),
dict(base, worker_failed=True, buffered=0),
dict(readiness='READY', style='Stored score', section='ARCHIVED SCORE', compute='none', buffered=92)]
# Match the comma four UI's logical canvas, not a cropped panel.
width, height = 536, 240
board = Image.new('RGBA', (width * 2, height * 3), '#080e16')
for index, state in enumerate(cases):
panel = Image.new('RGBA', (width, height), '#080e16')
canvas = Canvas(panel, ROOT / 'selfdrive/assets/fonts/Inter-Medium.fnt')
canvas.draw_text_ex(None, 'SYNTHETIC UI / 536 x 240', canvas.Vector2(12, 12), 11, 0, (120, 135, 150, 255))
draw_panel(canvas, None, state, width, height)
board.paste(panel, ((index % 2) * width, (index // 2) * height))
args.output.parent.mkdir(parents=True, exist_ok=True)
board.convert('RGB').save(args.output)
print(args.output)
if __name__ == '__main__':
main()