202 lines
5.7 KiB
Python
202 lines
5.7 KiB
Python
#!/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 <vamp_style/font.hpp>",
|
|
"",
|
|
"#include <algorithm>",
|
|
"#include <array>",
|
|
"#include <cstdint>",
|
|
"#include <string_view>",
|
|
"",
|
|
"namespace {",
|
|
"",
|
|
"struct pair_adjustment {",
|
|
" std::uint16_t key{};",
|
|
" std::int8_t adjustment{};",
|
|
"};",
|
|
"",
|
|
f"constexpr auto pair_adjustments = std::array<pair_adjustment, {len(adjustments)}>{{{{",
|
|
]
|
|
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<std::uint16_t>(static_cast<std::uint8_t>(left));",
|
|
" auto r = static_cast<std::uint16_t>(static_cast<std::uint8_t>(right));",
|
|
" return static_cast<std::uint16_t>((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<std::int32_t>(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())
|