#!/usr/bin/env python3 import argparse import difflib from pathlib import Path from PIL import Image, ImageDraw, ImageFont REPO = Path(__file__).resolve().parents[1] FONT_PATH = REPO / "package/assets/fonts/petaluma/PetalumaScript.otf" OUTPUT_PATH = REPO / "src/vamp_style/font.cpp" FONT_SIZE = 32 FONT_HEIGHT = 36 FONT_MAX_WIDTH = 28 FIRST_CODEPOINT = 32 LAST_CODEPOINT = 126 def cpp_char_literal(value: str) -> str: if value == "'": return "'\\''" if value == "\\": return "'\\\\'" return f"'{value}'" def render_glyph(font: ImageFont.FreeTypeFont, value: str, y_offset: int) -> tuple[list[int], int, int]: image = Image.new("L", (FONT_MAX_WIDTH, FONT_HEIGHT), 0) draw = ImageDraw.Draw(image) bbox = font.getbbox(value) x = max(0, -bbox[0]) draw.text((x, y_offset), value, font=font, fill=255) pixels = [min(15, max(0, round(pixel / 17))) for pixel in image.get_flattened_data()] visible = image.getbbox() width = 0 if visible is None else min(FONT_MAX_WIDTH, visible[2]) advance = min(255, max(0, round(font.getlength(value)))) return pixels, width, advance def render_alpha_rows(values: list[int]) -> list[str]: lines = [] for row in range(FONT_HEIGHT): start = row * FONT_MAX_WIDTH row_values = values[start : start + FONT_MAX_WIDTH] lines.append(" " + ", ".join(f"0x{value:02x}" for value in row_values) + ",") return lines def pair_adjustments(font: ImageFont.FreeTypeFont) -> list[tuple[int, int]]: chars = [chr(value) for value in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1)] adjustments = [] for left in chars: left_width = font.getlength(left) for right in chars: adjustment = round(font.getlength(left + right) - left_width - font.getlength(right)) if adjustment == 0: continue key = (ord(left) << 8) | ord(right) adjustments.append((key, int(adjustment))) return sorted(adjustments) def generate() -> str: font = ImageFont.truetype(str(FONT_PATH), FONT_SIZE) min_top = min(font.getbbox(chr(value))[1] for value in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1)) y_offset = max(0, -min_top) adjustments = pair_adjustments(font) lines = [ "#include ", "", "#include ", "#include ", "#include ", "#include ", "", "namespace {", "", "struct pair_adjustment {", " std::uint16_t key{};", " std::int8_t adjustment{};", "};", "", f"constexpr auto pair_adjustments = std::array{{{{", ] for key, adjustment in adjustments: lines.append(f" {{0x{key:04x}, {adjustment}}},") lines.extend( [ "}};", "", "auto pair_key(std::char_t left, std::char_t right) -> std::uint16_t {", " auto l = static_cast(static_cast(left));", " auto r = static_cast(static_cast(right));", " return static_cast((l << 8U) | r);", "}", "", "auto empty_glyph() -> vamp_style::font_glyph {", " return {", " .alpha = {},", " .width = 0,", " .advance = 8,", " };", "}", "", "}", "", "auto vamp_style::ui_font_glyph(std::char_t value) -> vamp_style::font_glyph {", " switch (value) {", ] ) for codepoint in range(FIRST_CODEPOINT, LAST_CODEPOINT + 1): value = chr(codepoint) alpha, width, advance = render_glyph(font, value, y_offset) lines.extend( [ f" case {cpp_char_literal(value)}:", " return {", " .alpha = {", ] ) lines.extend(render_alpha_rows(alpha)) lines.extend( [ " },", f" .width = {width},", f" .advance = {advance},", " };", ] ) lines.extend( [ " default:", " return empty_glyph();", " }", "}", "", "auto vamp_style::ui_font_pair_adjust(std::char_t left, std::char_t right) -> std::int32_t {", " auto key = pair_key(left, right);", " auto found = std::lower_bound(pair_adjustments.begin(), pair_adjustments.end(), key, [](pair_adjustment const& item, std::uint16_t value) {", " return item.key < value;", " });", " if (found == pair_adjustments.end() || found->key != key) {", " return 0;", " }", " return found->adjustment;", "}", "", "auto vamp_style::ui_text_width(std::string_view text, std::int32_t scale) -> std::int32_t {", " if (text.empty() || scale <= 0) {", " return 0;", " }", " auto width = std::int32_t{};", " auto previous = std::char_t{};", " for (auto ch : text) {", " if (previous != std::char_t{}) {", " width += vamp_style::ui_font_pair_adjust(previous, ch) * scale;", " }", " width += static_cast(vamp_style::ui_font_glyph(ch).advance) * scale;", " previous = ch;", " }", " return std::max(std::int32_t{}, width);", "}", "", ] ) return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--check", action="store_true") args = parser.parse_args() generated = generate() if args.check: current = OUTPUT_PATH.read_text() if current == generated: print("pico font is up to date") return 0 diff = difflib.unified_diff( current.splitlines(), generated.splitlines(), fromfile=str(OUTPUT_PATH), tofile=f"{OUTPUT_PATH} (generated)", lineterm="", ) print("\n".join(diff)) return 1 OUTPUT_PATH.write_text(generated) print(f"generated {OUTPUT_PATH}") return 0 if __name__ == "__main__": raise SystemExit(main())