Files
Arch-R/scripts/generate-logo-paths.py
Douglas Teles 06dc2d16f3 Add system scripts, services, and boot splash
System daemons: hotkeys (volume/brightness), automount, bluetooth agent,
memory manager, sleep/suspend, USB gadget mode, save-config persistence.

Boot splash: initramfs SVG renderer (fbsplash + svg_parser) for 0.7s splash.

Panel tools: generate-panel-dtbos.sh rewrite, convert-panel.py for ROCKNIX
panel data extraction, archr-dtbo.py for runtime overlay management.

Input: archr-gptokeyb.c gamepad-to-keyboard mapper via uinput.

Launch wrappers: emulationstation.sh and retroarch-launch.sh updated for
KMS/DRM + Mesa 26 Panfrost environment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:22:30 -03:00

452 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Arch R — Generate SVG path data from Quantico font for splash logo.
Reads Quantico-Regular.ttf and extracts glyph outlines for "ARCH" + "R",
outputting scripts/splash/archr-logo.h with C-embeddable SVG path strings.
This script runs OFFLINE (not part of the build pipeline).
The generated archr-logo.h is committed to the repo.
Usage:
python3 scripts/generate-logo-paths.py
# or with venv:
/tmp/fonttools-venv/bin/python3 scripts/generate-logo-paths.py
"""
import os
import sys
try:
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
except ImportError:
print("ERROR: fonttools not installed. Install with: pip install fonttools", file=sys.stderr)
sys.exit(1)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
FONT_PATH = os.path.join(PROJECT_DIR, "assets", "fonts", "Quantico-Regular.ttf")
OUTPUT_PATH = os.path.join(SCRIPT_DIR, "splash", "archr-logo.h")
# Logo layout: "ARCH" in blue + " R" in white
ARCH_GLYPHS = ["A", "R", "C", "H"]
R_GLYPH = "R"
# Colors
ARCH_COLOR = "rgb(23,147,209)" # Arch Linux blue #1793D1
R_COLOR = "rgb(255,255,255)" # White
# Spacing between ARCH and R (in font units)
WORD_SPACE_FACTOR = 0.25 # 25% of font UPM as space between words
def extract_glyph_path(font, glyph_name, x_offset=0, y_offset=0):
"""Extract SVG path string for a glyph with optional offset."""
glyf = font['glyf']
glyph = glyf[glyph_name]
if glyph.numberOfContours == 0:
return None, 0
# Get advance width for positioning
hmtx = font['hmtx']
advance_width = hmtx[glyph_name][0]
pen = SVGPathPen(font.getGlyphSet())
# We need to draw with offset
gs = font.getGlyphSet()
gs[glyph_name].draw(pen)
path_str = pen.getCommands()
if not path_str:
return None, advance_width
# Apply offset by modifying the path commands
if x_offset != 0 or y_offset != 0:
path_str = offset_svg_path(path_str, x_offset, y_offset)
return path_str, advance_width
def offset_svg_path(path_str, dx, dy):
"""Offset all coordinates in an SVG path string by (dx, dy)."""
import re
result = []
i = 0
s = path_str
while i < len(s):
if s[i] in 'MmLlHhVvCcSsQqTtAaZz':
cmd = s[i]
result.append(cmd)
i += 1
if cmd in 'Zz':
continue
# Parse numbers after command
while i < len(s) and s[i] not in 'MmLlHhVvCcSsQqTtAaZz':
# Skip whitespace and commas
while i < len(s) and s[i] in ' ,\t\n':
result.append(s[i])
i += 1
if i >= len(s) or s[i] in 'MmLlHhVvCcSsQqTtAaZz':
break
# Parse a number
num_start = i
if s[i] == '-':
i += 1
while i < len(s) and (s[i].isdigit() or s[i] == '.'):
i += 1
# Handle scientific notation
if i < len(s) and s[i] in 'eE':
i += 1
if i < len(s) and s[i] in '+-':
i += 1
while i < len(s) and s[i].isdigit():
i += 1
num_str = s[num_start:i]
if num_str:
result.append(num_str)
else:
result.append(s[i])
i += 1
return ''.join(result)
def extract_glyph_path_with_offset(font, char, x_offset, y_offset=0):
"""Extract glyph path and apply coordinate offset at path level."""
cmap = font.getBestCmap()
glyph_name = cmap.get(ord(char))
if not glyph_name:
print(f"WARNING: Glyph not found for '{char}'", file=sys.stderr)
return None, 0
glyf = font['glyf']
hmtx = font['hmtx']
advance_width = hmtx[glyph_name][0]
# Use a recording pen to get the path with offset
gs = font.getGlyphSet()
pen = SVGPathPen(gs)
gs[glyph_name].draw(pen)
path_str = pen.getCommands()
if not path_str:
return None, advance_width
# Parse and rebuild path with offsets applied to absolute coordinates
path_str = apply_offset_to_path(path_str, x_offset, y_offset)
return path_str, advance_width
def apply_offset_to_path(path_data, dx, dy):
"""Apply coordinate offset to SVG path data (absolute commands only)."""
import re
tokens = []
i = 0
while i < len(path_data):
if path_data[i] in 'MmLlHhVvCcSsQqTtAaZz':
tokens.append(path_data[i])
i += 1
elif path_data[i] in ' ,\t\n':
i += 1
elif path_data[i] == '-' or path_data[i].isdigit() or path_data[i] == '.':
j = i
if path_data[i] == '-':
i += 1
while i < len(path_data) and (path_data[i].isdigit() or path_data[i] == '.'):
i += 1
if i < len(path_data) and path_data[i] in 'eE':
i += 1
if i < len(path_data) and path_data[i] in '+-':
i += 1
while i < len(path_data) and path_data[i].isdigit():
i += 1
tokens.append(float(path_data[j:i]))
else:
i += 1
# Process tokens: apply offset to absolute coordinate commands
result_parts = []
ti = 0
while ti < len(tokens):
tok = tokens[ti]
if isinstance(tok, str):
cmd = tok
ti += 1
if cmd == 'Z' or cmd == 'z':
result_parts.append(cmd)
continue
# Determine how many coordinate pairs per command
if cmd == 'M' or cmd == 'L':
# Pairs of (x, y) — offset both
while ti < len(tokens) and isinstance(tokens[ti], float):
x = tokens[ti] + dx
y = tokens[ti + 1] + dy
result_parts.append(f"{cmd} {x:.3f} {y:.3f}")
ti += 2
cmd = 'L' if cmd == 'M' else cmd # After M, implicit L
elif cmd == 'H':
while ti < len(tokens) and isinstance(tokens[ti], float):
x = tokens[ti] + dx
result_parts.append(f"H {x:.3f}")
ti += 1
elif cmd == 'V':
while ti < len(tokens) and isinstance(tokens[ti], float):
y = tokens[ti] + dy
result_parts.append(f"V {y:.3f}")
ti += 1
elif cmd == 'C':
while ti + 5 < len(tokens) and isinstance(tokens[ti], float):
x1 = tokens[ti] + dx
y1 = tokens[ti + 1] + dy
x2 = tokens[ti + 2] + dx
y2 = tokens[ti + 3] + dy
x3 = tokens[ti + 4] + dx
y3 = tokens[ti + 5] + dy
result_parts.append(f"C {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x3:.3f} {y3:.3f}")
ti += 6
elif cmd == 'Q':
while ti + 3 < len(tokens) and isinstance(tokens[ti], float):
x1 = tokens[ti] + dx
y1 = tokens[ti + 1] + dy
x2 = tokens[ti + 2] + dx
y2 = tokens[ti + 3] + dy
result_parts.append(f"Q {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f}")
ti += 4
else:
# Unknown command, pass through
result_parts.append(cmd)
else:
ti += 1
return ' '.join(result_parts)
def main():
if not os.path.exists(FONT_PATH):
print(f"ERROR: Font not found: {FONT_PATH}", file=sys.stderr)
sys.exit(1)
font = TTFont(FONT_PATH)
cmap = font.getBestCmap()
hmtx = font['hmtx']
os2 = font['OS/2']
upm = font['head'].unitsPerEm
# Font metrics for vertical positioning
ascender = os2.sTypoAscender
descender = os2.sTypoDescender
total_height = ascender - descender
# SVG coordinate system: Y increases downward
# Font coordinate system: Y increases upward
# We need to flip Y: svg_y = ascender - font_y
print(f"Font: Quantico-Regular")
print(f"UPM: {upm}, Ascender: {ascender}, Descender: {descender}")
# Calculate total width for "ARCH R" layout
paths = []
colors = []
x_cursor = 0
# Extract "ARCH" glyphs
for char in ARCH_GLYPHS:
glyph_name = cmap.get(ord(char))
if not glyph_name:
print(f"ERROR: No glyph for '{char}'", file=sys.stderr)
sys.exit(1)
path_str, advance = extract_glyph_path_with_offset(font, char, x_cursor, 0)
if path_str:
paths.append(path_str)
colors.append(ARCH_COLOR)
print(f" {char}: advance={advance}, offset={x_cursor}")
x_cursor += advance
# Add word space
space_glyph = cmap.get(ord(' '))
if space_glyph:
space_advance = hmtx[space_glyph][0]
else:
space_advance = int(upm * WORD_SPACE_FACTOR)
x_cursor += space_advance
print(f" Space: {space_advance}")
# Extract "R" glyph (white)
path_str, advance = extract_glyph_path_with_offset(font, R_GLYPH, x_cursor, 0)
if path_str:
paths.append(path_str)
colors.append(R_COLOR)
print(f" R (white): advance={advance}, offset={x_cursor}")
x_cursor += advance
total_width = x_cursor
# The Y coordinate in font is bottom-up, SVG is top-down
# Flip all Y coordinates: new_y = ascender - old_y
flipped_paths = []
for p in paths:
flipped = flip_y_coordinates(p, ascender)
flipped_paths.append(flipped)
# Bounding box
svg_width = total_width
svg_height = total_height
print(f"\nBounding box: {svg_width} x {svg_height}")
print(f"Paths: {len(flipped_paths)}")
# Generate C header
generate_header(flipped_paths, colors, svg_width, svg_height)
font.close()
print(f"\nGenerated: {OUTPUT_PATH}")
def flip_y_coordinates(path_data, ascender):
"""Flip Y coordinates for SVG (font Y-up → SVG Y-down)."""
import re
tokens = []
i = 0
while i < len(path_data):
if path_data[i] in 'MmLlHhVvCcSsQqTtAaZz':
tokens.append(path_data[i])
i += 1
elif path_data[i] in ' ,\t\n':
i += 1
elif path_data[i] == '-' or path_data[i].isdigit() or path_data[i] == '.':
j = i
if path_data[i] == '-':
i += 1
while i < len(path_data) and (path_data[i].isdigit() or path_data[i] == '.'):
i += 1
if i < len(path_data) and path_data[i] in 'eE':
i += 1
if i < len(path_data) and path_data[i] in '+-':
i += 1
while i < len(path_data) and path_data[i].isdigit():
i += 1
tokens.append(float(path_data[j:i]))
else:
i += 1
result_parts = []
ti = 0
while ti < len(tokens):
tok = tokens[ti]
if isinstance(tok, str):
cmd = tok
ti += 1
if cmd == 'Z' or cmd == 'z':
result_parts.append(cmd)
continue
if cmd in ('M', 'L'):
while ti + 1 < len(tokens) and isinstance(tokens[ti], float):
x = tokens[ti]
y = ascender - tokens[ti + 1]
result_parts.append(f"{cmd} {x:.3f} {y:.3f}")
ti += 2
if cmd == 'M':
cmd = 'L'
elif cmd == 'H':
while ti < len(tokens) and isinstance(tokens[ti], float):
result_parts.append(f"H {tokens[ti]:.3f}")
ti += 1
elif cmd == 'V':
while ti < len(tokens) and isinstance(tokens[ti], float):
y = ascender - tokens[ti]
result_parts.append(f"V {y:.3f}")
ti += 1
elif cmd == 'C':
while ti + 5 < len(tokens) and isinstance(tokens[ti], float):
x1 = tokens[ti]
y1 = ascender - tokens[ti + 1]
x2 = tokens[ti + 2]
y2 = ascender - tokens[ti + 3]
x3 = tokens[ti + 4]
y3 = ascender - tokens[ti + 5]
result_parts.append(f"C {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x3:.3f} {y3:.3f}")
ti += 6
elif cmd == 'Q':
while ti + 3 < len(tokens) and isinstance(tokens[ti], float):
x1 = tokens[ti]
y1 = ascender - tokens[ti + 1]
x2 = tokens[ti + 2]
y2 = ascender - tokens[ti + 3]
result_parts.append(f"Q {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f}")
ti += 4
else:
result_parts.append(cmd)
else:
ti += 1
return ' '.join(result_parts)
def generate_header(paths, colors, svg_width, svg_height):
"""Generate archr-logo.h C header file."""
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
lines = []
lines.append("/*")
lines.append(" * Arch R — Logo SVG path data (auto-generated)")
lines.append(" * Generated by: scripts/generate-logo-paths.py")
lines.append(" * Font: Quantico-Regular.ttf")
lines.append(" *")
lines.append(" * DO NOT EDIT MANUALLY — regenerate with:")
lines.append(" * /tmp/fonttools-venv/bin/python3 scripts/generate-logo-paths.py")
lines.append(" */")
lines.append("")
lines.append("#ifndef ARCHR_LOGO_H")
lines.append("#define ARCHR_LOGO_H")
lines.append("")
lines.append(f"#define ARCHR_SVG_WIDTH {svg_width:.1f}f")
lines.append(f"#define ARCHR_SVG_HEIGHT {svg_height:.1f}f")
lines.append(f"#define ARCHR_NUM_PATHS {len(paths)}")
lines.append("")
# Path data
lines.append("static const char *archr_svg_paths[] = {")
for i, path in enumerate(paths):
# Split long paths for readability
comma = "," if i < len(paths) - 1 else ""
# Escape any quotes in path data (shouldn't happen, but safety)
escaped = path.replace('"', '\\"')
lines.append(f' "{escaped}"{comma}')
lines.append("};")
lines.append("")
# Colors
lines.append("static const char *archr_svg_colors[] = {")
for i, color in enumerate(colors):
comma = "," if i < len(colors) - 1 else ""
lines.append(f' "{color}"{comma}')
lines.append("};")
lines.append("")
lines.append("#endif")
lines.append("")
with open(OUTPUT_PATH, 'w') as f:
f.write('\n'.join(lines))
if __name__ == '__main__':
main()