12345678910111213141516from __future__ import annotations1718import math19import sys20from pathlib import Path21from typing import Iterable2223from fontTools.fontBuilder import FontBuilder24from fontTools.pens.t2CharStringPen import T2CharStringPen25262728293031UPM = 100032ADVANCE = 56033ASCENT = 75034DESCENT = -20035LINE_GAP = 036STROKE = 90 373839B = 0 40T = 700 41M = 350 42LO = 175 43HI = 525 444546L = 100 47LL = 180 48C = 280 49RR = 380 50R = 460 515253Stroke = tuple[float, float, float, float]54555657585960def stroke_rect(x1: float, y1: float, x2: float, y2: float, w: float = STROKE) -> list[tuple[float, float]]:61 626364656667 dx, dy = x2 - x1, y2 - y168 length = math.hypot(dx, dy)69 if length < 0.5:70 return []71 nx = -dy / length * (w / 2)72 ny = dx / length * (w / 2)73 return [74 (x1 + nx, y1 + ny),75 (x2 + nx, y2 + ny),76 (x2 - nx, y2 - ny),77 (x1 - nx, y1 - ny),78 ]798081def draw(strokes: Iterable[Stroke]):82 83 pen = T2CharStringPen(ADVANCE, None)84 for s in strokes:85 rect = stroke_rect(*s)86 if not rect:87 continue88 pen.moveTo(rect[0])89 pen.lineTo(rect[1])90 pen.lineTo(rect[2])91 pen.lineTo(rect[3])92 pen.closePath()93 return pen.getCharString()949596979899100101102103104ELDER_FUTHARK: list[list[Stroke]] = [105 106 [107 (C, B, C, T),108 (C, 560, R - 20, T),109 (C, 380, R - 20, 540),110 ],111 112 [113 (LL, B, LL, T),114 (LL, T, RR, T),115 (RR, T, RR + 20, 60),116 ],117 118 [119 (LL, B, LL, T),120 (LL, HI + 30, R - 20, M + 20),121 (LL, M - 70, R - 20, M + 20),122 ],123 124 [125 (LL, B, LL, T),126 (LL, T - 40, R - 20, T - 200),127 (LL, T - 200, R - 20, T - 360),128 ],129 130 [131 (LL, B, LL, T),132 (LL, T, R - 20, T - 80),133 (R - 20, T - 80, LL, M + 30),134 (LL, M + 30, R - 20, B),135 ],136 137 [138 (L + 20, T, R - 20, M),139 (L + 20, B, R - 20, M),140 ],141 142 [143 (L, B, R, T),144 (L, T, R, B),145 ],146 147 [148 (LL, B, LL, T),149 (LL, T, R - 20, T - 80),150 (R - 20, T - 80, LL, T - 220),151 ],152 153 [154 (LL, B, LL, T),155 (RR + 20, B, RR + 20, T),156 (LL, HI - 20, RR + 20, LO + 60),157 ],158 159 [160 (C, B, C, T),161 (C - 160, M - 120, C + 160, M + 120),162 ],163 164 [165 (C, B, C, T),166 ],167 168 [169 (L + 20, T, C, HI - 30),170 (C, HI - 30, L + 20, M),171 (R - 20, M, C, LO + 30),172 (C, LO + 30, R - 20, B),173 ],174 175 [176 (LL + 20, B, RR - 20, T),177 (RR - 20, T, R + 20, T - 120),178 (LL + 20, B, L - 20, 120),179 ],180 181 [182 (LL, B, LL, T),183 (LL, T, R - 20, T - 100),184 (R - 20, T - 100, R - 20, 100),185 (R - 20, 100, LL, B),186 ],187 188 [189 (C, B, C, M + 50),190 (C, M + 50, L + 20, T),191 (C, M + 50, R - 20, T),192 ],193 194 [195 (R - 20, T, L + 60, HI - 20),196 (L + 60, HI - 20, R - 60, M - 20),197 (R - 60, M - 20, L + 20, B),198 ],199 200 [201 (C, B, C, T),202 (C, T, L + 40, T - 160),203 (C, T, R - 40, T - 160),204 ],205 206 [207 (LL, B, LL, T),208 (LL, T, R - 20, T - 130),209 (R - 20, T - 130, LL, M + 40),210 (LL, M + 40, R - 20, M - 130),211 (R - 20, M - 130, LL, B + 30),212 ],213 214 [215 (LL, B, LL, T),216 (RR + 20, B, RR + 20, T),217 (LL, T, RR + 20, M),218 (RR + 20, T, LL, M),219 ],220 221 [222 (LL, B, LL, T),223 (RR + 20, B, RR + 20, T),224 (LL, T, RR + 20, M - 40),225 (RR + 20, T, LL, M - 40),226 ],227 228 [229 (LL, B, LL, T),230 (LL, T, R - 60, T - 200),231 ],232 233 [234 (LL, M, C, T - 60),235 (C, T - 60, RR + 20, M),236 (RR + 20, M, C, B + 60),237 (C, B + 60, LL, M),238 ],239 240 [241 (LL, B, LL, T),242 (RR + 20, B, RR + 20, T),243 (LL, B, RR + 20, T),244 (LL, T, RR + 20, B),245 ],246 247 [248 (LL, M + 50, C, T - 30),249 (C, T - 30, RR + 20, M + 50),250 (RR + 20, M + 50, C, M - 100),251 (C, M - 100, LL, M + 50),252 (LL, M + 50, L - 30, B),253 (RR + 20, M + 50, R + 30, B),254 ],255]256257258259260261262263264265ANGLO_SAXON: list[list[Stroke]] = [266 267 [268 (LL, B, LL, T),269 (LL, M + 100, R - 20, M),270 (LL, M, R - 20, M - 100),271 ],272 273 [274 (LL, B, LL, T),275 (LL, 240, R - 20, 100),276 (LL, 100, R - 20, B - 30),277 ],278 279 [280 (C, B, C, T),281 (C, T - 40, L + 20, T - 200),282 (C, T - 200, L + 20, T - 360),283 (C, T - 40, R - 20, T - 200),284 (C, T - 200, R - 20, T - 360),285 ],286 287 [288 (C, B - 100, C, M + 50),289 (C, M + 50, L + 20, T - 80),290 (C, M + 50, R - 20, T - 80),291 (L + 60, T, R - 60, T),292 ],293 294 [295 (L + 40, T - 80, R - 40, T - 80),296 (C, T - 80, C, B + 80),297 (L + 40, B + 80, R - 40, B + 80),298 ],299 300 [301 (LL, B, LL, T),302 (LL, T - 40, R - 20, T - 200),303 (LL, T - 200, R - 20, T - 360),304 (LL, T - 360, R - 20, T - 520),305 ],306 307 [308 (LL, B, LL, T),309 (LL, T, R - 20, T - 100),310 (R - 20, T - 100, R - 20, 100),311 (R - 20, 100, LL, B),312 (LL, M, R - 20, M),313 ],314 315 [316 (C, B, C, M),317 (C, M, L, T),318 (C, M, R, T),319 (L, T, L + 60, T - 80),320 (R, T, R - 60, T - 80),321 ],322 323 [324 (C, B, C, T),325 (C, T, L + 20, T - 140),326 (C, T, R - 20, T - 140),327 (C, M + 80, L + 60, M - 60),328 (C, M + 80, R - 60, M - 60),329 ],330 331 [332 (L, B, R, T),333 (L, T, R, B),334 (LL, M, C, T - 60),335 (C, T - 60, RR + 20, M),336 (RR + 20, M, C, B + 60),337 (C, B + 60, LL, M),338 ],339 340 [341 (C, B, C, T),342 (C, M + 100, L + 60, T),343 (C, M + 100, R - 60, T),344 ],345 346 [347 (C, B, C, T),348 (L + 20, M, C, M + 140),349 (C, M - 140, R - 20, M),350 ],351 352 [353 (C, B, C, T),354 (C, T, L + 40, T - 160),355 (C, T, R - 40, T - 160),356 (L + 40, B, R - 40, B),357 ],358 359 [360 (LL, B, LL, T),361 (RR + 20, B, RR + 20, T),362 (LL, B, C, M),363 (C, M, RR + 20, T),364 (LL, T, C, M),365 (C, M, RR + 20, B),366 ],367 368 [369 (LL, B, LL, T),370 (RR + 20, B, RR + 20, T),371 (LL, HI, RR + 20, HI),372 (LL, LO, RR + 20, LO),373 ],374]375376377378379380381382383384YOUNGER_FUTHARK: list[list[Stroke]] = [385 386 [387 (C, B, C, T),388 (C, T - 60, R - 40, T),389 ],390 391 [392 (C, B, C, T),393 (C, T - 30, R - 40, T - 180),394 ],395 396 [397 (LL, B, LL, T),398 (LL, M + 80, R - 80, M),399 (LL, M - 80, R - 80, M),400 ],401 402 [403 (LL, B, LL, T),404 (LL, T - 40, R - 20, T - 200),405 ],406 407 [408 (LL, B, LL, T),409 (LL, T, R - 20, T - 80),410 (R - 20, T - 80, LL, M + 30),411 ],412 413 [414 (C, B, C, T),415 (C, M + 50, R - 60, T - 40),416 ],417 418 [419 (C, B, C, T),420 (L + 30, M, R - 30, M),421 (L + 50, M - 160, R - 50, M + 160),422 ],423 424 [425 (C, B, C, T),426 (L + 80, M - 60, R - 80, M + 60),427 ],428 429 [430 (RR + 20, B, RR + 20, T),431 (RR + 20, T - 40, L + 20, T - 200),432 ],433 434 [435 (R - 40, T, L + 40, B),436 ],437 438 [439 (C, B, C, T),440 (C, T, L + 80, T - 100),441 (C, T, R - 80, T - 100),442 ],443 444 [445 (C, B, C, T),446 (C, T - 100, R - 60, T),447 ],448 449 [450 (C, T, C, M - 50),451 (C, M - 50, L + 20, B),452 (C, M - 50, R - 20, B),453 ],454]455456457458459460461MEDIEVAL: list[list[Stroke]] = [462 463 [464 (L + 20, T, R - 20, M),465 (L + 20, B, R - 20, M),466 (R - 60, M, R + 20, M),467 ],468 469 [470 (LL, B, LL, T),471 (RR + 20, B, RR + 20, T),472 (LL, M + 80, RR + 20, M + 80),473 (LL, M - 80, RR + 20, M - 80),474 (LL, T, RR + 20, B),475 ],476 477 [478 (LL, B, LL, T),479 (RR + 20, B, RR + 20, T),480 (LL, T, RR + 20, M),481 (LL, M, RR + 20, T),482 ],483 484 [485 (LL, B, LL, T),486 (RR + 20, B, RR + 20, T),487 (LL, T - 80, C, T),488 (C, T, RR + 20, T - 80),489 (LL, B + 80, C, B),490 (C, B, RR + 20, B + 80),491 ],492 493 [494 (C, B, C, T),495 (L + 30, T, R - 30, T),496 (L + 30, M, R - 30, M),497 (L + 30, B, R - 30, B),498 ],499 500 [501 (C, B, C, M + 50),502 (C, M + 50, L + 20, T),503 (C, M + 50, R - 20, T),504 (L + 20, T, R - 20, T),505 ],506 507 [508 (R - 20, T, L + 60, HI - 20),509 (L + 60, HI - 20, R - 60, M - 20),510 (R - 60, M - 20, L + 20, B),511 (C - 30, M - 20, C + 30, M - 20),512 ],513 514 [515 (L + 40, T, C, T - 200),516 (C, T - 200, R - 40, T),517 (L + 40, T - 320, C, T - 520),518 (C, T - 520, R - 40, T - 320),519 ],520 521 [522 (LL, B, LL, T),523 (RR + 20, B, RR + 20, T),524 (LL, T, RR + 20, M),525 (RR + 20, T, LL, M),526 (C, M - 80, C, M + 20),527 ],528 529 [530 (LL, M + 50, C, T - 80),531 (C, T - 80, RR + 20, M + 50),532 (RR + 20, M + 50, C, M - 60),533 (C, M - 60, LL, M + 50),534 ],535 536 [537 (LL, B, LL, T),538 (RR + 20, B, RR + 20, T),539 (LL, HI, RR + 20, LO + 80),540 (LL, LO + 80, RR + 20, HI),541 ],542 543 [544 (C, B, C, T),545 (C, T - 40, R - 60, T - 40),546 ],547 548 [549 (C, B, C, T),550 (C, T - 40, R - 60, T - 40),551 (C, T - 200, R - 60, T - 200),552 (C, T - 360, R - 60, T - 360),553 ],554 555 [556 (C, B, C, T),557 (C, T, R - 60, T - 80),558 (R - 60, T - 80, C, T - 160),559 (C, T - 160, R - 60, T - 240),560 ],561 562 [563 (LL, B, LL, T),564 (LL, M, R - 20, T),565 (LL, M, R - 20, B),566 ],567 568 [569 (L + 20, B, L + 20, T),570 (R - 20, B, R - 20, T),571 (L + 20, M + 100, R - 20, M),572 (L + 20, M, R - 20, M - 100),573 ],574 575 [576 (R - 20, T, L + 60, HI - 20),577 (L + 60, HI - 20, R - 60, M - 20),578 (R - 60, M - 20, L + 20, B),579 (R - 20, T, R + 30, T - 60),580 ],581 582 [583 (C, B, C, M),584 (C, M, L, M + 200),585 (C, M, R, M + 200),586 (L, M + 200, L + 60, T),587 (R, M + 200, R - 60, T),588 ],589 590 [591 (C, B, C, T),592 (C, T - 80, L + 30, T),593 (C, T - 80, R - 30, T),594 (C, M + 80, L + 80, M + 200),595 (C, M + 80, R - 80, M + 200),596 ],597 598 [599 (L + 20, T, C, T - 200),600 (C, T - 200, R - 20, T),601 (L + 80, T - 80, C, T - 240),602 (C, T - 240, R - 80, T - 80),603 (C, T - 240, C, B),604 ],605 606 [607 (LL, B + 80, LL, T - 80),608 (RR + 20, B + 80, RR + 20, T - 80),609 (LL, B + 80, RR + 20, B + 80),610 (LL, M, RR + 20, M),611 (LL, T - 80, RR + 20, T - 80),612 ],613 614 [615 (L + 40, T, R - 40, T - 140),616 (R - 40, T - 140, L + 40, T - 280),617 (L + 40, T - 280, R - 40, T - 420),618 (R - 40, T - 420, L + 40, T - 560),619 ],620 621 [622 (LL, M, C, T - 30),623 (C, T - 30, RR + 20, M),624 (RR + 20, M, C, B + 30),625 (C, B + 30, LL, M),626 (C, M + 200, C, M - 200),627 ],628]629630631LAYER_1: list[list[Stroke]] = (632 ELDER_FUTHARK + ANGLO_SAXON + YOUNGER_FUTHARK + MEDIEVAL633)634assert len(LAYER_1) == 75, f"Layer 1 expected 75, got {len(LAYER_1)}"635636637638639640641def mirror(strokes: list[Stroke]) -> list[Stroke]:642 643 cx = ADVANCE / 2644 return [(2 * cx - x1, y1, 2 * cx - x2, y2) for x1, y1, x2, y2 in strokes]645646647def rotate_180(strokes: list[Stroke]) -> list[Stroke]:648 649 cx, cy = ADVANCE / 2, T / 2650 return [(2 * cx - x1, 2 * cy - y1, 2 * cx - x2, 2 * cy - y2) for x1, y1, x2, y2 in strokes]651652653def _key(strokes: list[Stroke]) -> tuple:654 return tuple(sorted(tuple(round(v, 1) for v in s) for s in strokes))655656657658659660661662663664665666LAYER_2: list[list[Stroke]] = []667for _i in range(75):668 _base = LAYER_1[_i]669 _src_key = _key(_base)670 _candidate = mirror(_base)671 if _key(_candidate) == _src_key:672 _candidate = rotate_180(_base)673 if _key(_candidate) == _src_key:674 continue675 LAYER_2.append(_candidate)676 if len(LAYER_2) >= 70:677 break678assert len(LAYER_2) == 70, f"Layer 2 expected 70, got {len(LAYER_2)}"679680681682683684685686687688689690BIND_PAIRS: list[tuple[int, int]] = [691 692 (2, 3), 693 (4, 5), 694 (16, 10), 695 (17, 18), 696 (19, 20), 697 (6, 7), 698 (0, 16), 699 (3, 14), 700 (16, 0), 701 (4, 0), 702 703 (44, 0), 704 (47, 39), 705 (49, 16), 706 (50, 47), 707 (39, 40), 708 709 (24, 25), 710 (29, 30), 711 (31, 14), 712 (32, 16), 713 (33, 21), 714 715]716717718719720BIND_FILLER: list[tuple[int, int]] = []721for a in range(24):722 for b in range(a + 1, 24):723 if (a, b) in BIND_PAIRS or (b, a) in BIND_PAIRS:724 continue725 if a in {6, 10, 21, 22} or b in {6, 10, 21, 22}:726 continue727 BIND_FILLER.append((a, b))728 if len(BIND_PAIRS) + len(BIND_FILLER) >= 60:729 break730 if len(BIND_PAIRS) + len(BIND_FILLER) >= 60:731 break732BIND_PAIRS = BIND_PAIRS + BIND_FILLER733BIND_PAIRS = BIND_PAIRS[:60]734735736def is_central_stave(s: Stroke) -> bool:737 738 x1, y1, x2, y2 = s739 return abs(x1 - C) < 30 and abs(x2 - C) < 30 and abs(y2 - y1) > 200740741742def to_central(strokes: list[Stroke]) -> list[Stroke]:743 744 if not strokes:745 return strokes746 747 if any(is_central_stave(s) for s in strokes):748 return strokes749 750 dx = C - LL751 return [(x1 + dx, y1, x2 + dx, y2) for x1, y1, x2, y2 in strokes]752753754def bind(a: list[Stroke], b: list[Stroke]) -> list[Stroke]:755 a2 = to_central(a)756 b2 = to_central(b)757 758 759 out = list(a2)760 for s in b2:761 if is_central_stave(s):762 continue763 764 x1, y1, x2, y2 = s765 out.append((2 * C - x1, y1, 2 * C - x2, y2))766 return out767768769LAYER_3: list[list[Stroke]] = [bind(LAYER_1[a], LAYER_1[b]) for a, b in BIND_PAIRS]770assert len(LAYER_3) == 60, f"Layer 3 expected 60, got {len(LAYER_3)}"771772773774775776777def remove_stave(strokes: list[Stroke]) -> list[Stroke]:778 779 return [s for s in strokes if not is_central_stave(s) and not _is_left_stave(s)]780781782def _is_left_stave(s: Stroke) -> bool:783 x1, y1, x2, y2 = s784 return abs(x1 - LL) < 30 and abs(x2 - LL) < 30 and abs(y2 - y1) > 200785786787788789790STAVELESS_CANDIDATES = [791 i for i in range(75)792 if i not in {10, 6, 21, 22, 38, 56, 60, 61, 62, 67, 72}793]794795796def staveless_or_rotate(idx: int) -> list[Stroke]:797 base = LAYER_1[idx]798 stripped = remove_stave(base)799 if len(stripped) >= 2:800 return stripped801 return rotate_180(base)802803804LAYER_4: list[list[Stroke]] = [staveless_or_rotate(i) for i in STAVELESS_CANDIDATES[:51]]805assert len(LAYER_4) == 51, f"Layer 4 expected 51, got {len(LAYER_4)}"806807808809810811812ALL_GLYPHS: list[list[Stroke]] = LAYER_1 + LAYER_2 + LAYER_3 + LAYER_4813assert len(ALL_GLYPHS) == 256, f"Total expected 256, got {len(ALL_GLYPHS)}"814815816817818819820821822823824825826OVERRIDES: dict[int, list[Stroke]] = {827 828 187: [829 (C, B, C, T),830 (C, T * 2 // 3, L + (R - L) // 3, T // 3),831 (C, T * 2 // 3, L + (R - L) * 2 // 3, T // 3),832 ],833 834 835 836 188: [837 (C, B, C, T),838 (C, T * 3 // 4, R - 60, T),839 (C, T // 4, R - 60, B),840 ],841 842 192: [843 (C, B, C, T),844 (C, B, L, T // 3),845 (L, M, L + (R - L) // 2, M),846 ],847 848 205: [849 (L, T // 4, R, T * 3 // 4),850 (L, T * 3 // 4, R, T // 4),851 ],852 853 206: [854 (L, B, R, T),855 (L + (R - L) // 3, T, R, T // 4),856 ],857 858 207: [859 (L, T // 3, C, T),860 (C, T, R, T // 3),861 ],862 863 213: [864 (L, T // 4, L + (R - L) * 2 // 3, T // 2),865 (L + (R - L) * 2 // 3, T // 2, L, T * 3 // 4),866 (L + (R - L) * 2 // 3, T // 4, L + (R - L) * 2 // 3, T * 3 // 4),867 ],868 869 214: [870 (L, B, C, M),871 (C, M, R, B),872 (L, T, C, M),873 (C, M, R, T),874 ],875 876 225: [877 (L, B, L, T),878 (R, B, R, T),879 (L, T, C, M),880 (C, M, R, T),881 (L, T, R, M),882 ],883 884 226: [885 (C, B, C, T),886 (C, M + 20, C - (R - L) // 3, M - 20),887 (C, M + 20, C + (R - L) // 3, M - 20),888 ],889 890 228: [891 (C, B, C, T),892 (C, M, C + (R - L) // 2, M),893 (C + (R - L) // 2, M, C, T // 4),894 (C + (R - L) // 2, M, C, T * 3 // 4),895 ],896 897 230: [898 (C, B, C, T),899 (C, T // 6, C + (R - L) // 3, T // 6),900 (C, T * 5 // 6, C + (R - L) // 3, T * 5 // 6),901 ],902 903 231: [904 (L, T * 3 // 4, R, T // 4),905 (L, T // 4 + 30, R, T * 3 // 4 - 30),906 ],907 908 240: [909 (L, M, R, M),910 (L, M, L + (R - L) // 3, B),911 (L, M, L + (R - L) // 3, T),912 (R, M, L + (R - L) * 2 // 3, B),913 (R, M, L + (R - L) * 2 // 3, T),914 ],915 916 241: [917 (L + (R - L) // 3, B, L + (R - L) // 3, T),918 (L + (R - L) * 2 // 3, B, L + (R - L) * 2 // 3, T),919 (L, T // 3, R, T * 2 // 3),920 (L, T * 2 // 3, R, T // 3),921 ],922}923924for _idx, _strokes in OVERRIDES.items():925 ALL_GLYPHS[_idx] = _strokes926927928929930931932def build() -> None:933 repo_root = Path(__file__).resolve().parent.parent934 out_path = repo_root / "public" / "fonts" / "PeachScript-Regular.otf"935936 glyph_names = [".notdef"] + [f"ps{i:03d}" for i in range(256)]937 cmap = {0xE000 + i: f"ps{i:03d}" for i in range(256)}938939 fb = FontBuilder(UPM, isTTF=False)940 fb.setupGlyphOrder(glyph_names)941 fb.setupCharacterMap(cmap)942943 charstrings = {".notdef": draw([(C, B, C, T)])} 944 for i, strokes in enumerate(ALL_GLYPHS):945 charstrings[f"ps{i:03d}"] = draw(strokes)946947 private = {948 "BlueValues": [-12, 0, T, T + 12],949 "OtherBlues": [DESCENT - 12, DESCENT],950 "BlueScale": 0.039625,951 "BlueShift": 7,952 "BlueFuzz": 1,953 "StdHW": STROKE,954 "StdVW": STROKE,955 "ForceBold": 0,956 "LanguageGroup": 0,957 "ExpansionFactor": 0.06,958 "initialRandomSeed": 0,959 "defaultWidthX": ADVANCE,960 "nominalWidthX": ADVANCE,961 }962963 fb.setupCFF(964 psName="PeachScript-Regular",965 fontInfo={966 "FullName": "PeachScript Regular",967 "FamilyName": "PeachScript",968 "Weight": "Regular",969 "version": "2.0",970 },971 privateDict=private,972 charStringsDict=charstrings,973 )974975 metrics = {name: (ADVANCE, 0) for name in glyph_names}976 fb.setupHorizontalMetrics(metrics)977 fb.setupHorizontalHeader(ascent=ASCENT, descent=DESCENT, lineGap=LINE_GAP)978 fb.setupNameTable({979 "familyName": "PeachScript",980 "styleName": "Regular",981 "uniqueFontIdentifier": "PeachScript-Regular;v2.0",982 "fullName": "PeachScript Regular",983 "psName": "PeachScript-Regular",984 "version": "Version 2.000",985 })986 fb.setupOS2(987 sTypoAscender=ASCENT,988 sTypoDescender=DESCENT,989 sTypoLineGap=LINE_GAP,990 usWinAscent=ASCENT,991 usWinDescent=-DESCENT,992 sxHeight=int(M),993 sCapHeight=T,994 achVendID="PEAC",995 fsType=0,996 )997 fb.setupPost()998999 out_path.parent.mkdir(parents=True, exist_ok=True)1000 fb.save(out_path)1001 print(f"wrote {out_path} ({out_path.stat().st_size} bytes)")100210031004if __name__ == "__main__":1005 build()