Files
product-vamp/generator/engraving/fonts.py
T
khodowany 9fd5abd075
ci / checks (push) Successful in 1h16m10s
Rhythm Selection & Cleanup
2026-09-26 02:50:22 -05:00

84 lines
4.2 KiB
Python

from __future__ import annotations
import dataclasses
import hashlib
import json
import pathlib
import PIL.ImageFont
import fontTools.pens.boundsPen
import fontTools.pens.recordingPen
import fontTools.ttLib
import generator.engraving.types
class Fonts:
def __init__(self, root: pathlib.Path):
self.root = root
try:
self.music = fontTools.ttLib.TTFont(root / 'Petaluma.otf')
self.script = fontTools.ttLib.TTFont(root / 'PetalumaScript.otf')
self.metadata = json.loads((root / 'petaluma_metadata.json').read_text())
self.names = json.loads((root / 'smufl_glyphnames.json').read_text())
except (OSError, ValueError) as error:
raise generator.engraving.types.EngravingError(f'cannot load bundled engraving fonts: {error}') from error
self.names.update(self.metadata.get('optionalGlyphs', {}))
self.cmap = self.music.getBestCmap()
self.text_cmap = self.script.getBestCmap()
self.glyphs = self.music.getGlyphSet()
self.units = self.music['head'].unitsPerEm / 4
self.metrics = {}
self.outlines = {}
self.text_fonts = {}
def fingerprint(self, style: generator.engraving.types.Style) -> str:
digest = hashlib.sha256(json.dumps(dataclasses.asdict(style), sort_keys=True).encode())
for name in ('Petaluma.otf', 'PetalumaScript.otf', 'petaluma_metadata.json', 'smufl_glyphnames.json'):
digest.update(name.encode())
digest.update((self.root / name).read_bytes())
return digest.hexdigest()
def glyph_name(self, name: str) -> str:
if name not in self.names:
raise generator.engraving.types.EngravingError(f'unknown SMuFL glyph {name!r}')
code = int(self.names[name]['codepoint'].removeprefix('U+'), 16)
if code not in self.cmap:
raise generator.engraving.types.EngravingError(f'Petaluma is missing requested SMuFL glyph {name!r}')
return self.cmap[code]
def glyph_box(self, name: str, size: float) -> generator.engraving.types.Box:
if name not in self.metrics:
pen = fontTools.pens.boundsPen.BoundsPen(self.glyphs)
self.glyphs[self.glyph_name(name)].draw(pen)
if pen.bounds is None:
raise generator.engraving.types.EngravingError(f'empty SMuFL glyph {name!r}')
self.metrics[name] = pen.bounds
left, bottom, right, top = self.metrics[name]
scale = size / self.units
return generator.engraving.types.Box(left * scale, -top * scale, right * scale, -bottom * scale)
def outline(self, name: str):
if name not in self.outlines:
pen = fontTools.pens.recordingPen.DecomposingRecordingPen(self.glyphs)
self.glyphs[self.glyph_name(name)].draw(pen)
self.outlines[name] = pen
return self.outlines[name]
def glyph(self, name: str, x: float, y: float, size: float, role: str, sources: tuple[str, ...]) -> generator.engraving.types.Primitive:
return generator.engraving.types.Primitive('glyph', role, sources, self.glyph_box(name, size).moved(x, y), ((x, y),), name, size)
def anchor(self, name: str, anchor: str, size: float) -> tuple[float, float]:
value = self.metadata.get('glyphsWithAnchors', {}).get(name, {}).get(anchor)
if value is None:
box = self.glyph_box(name, size)
return (box.right, 0) if 'Up' in anchor else (box.left, 0)
return value[0] * size, -value[1] * size
def text_font(self, size: float):
pixels = max(1, round(size))
if pixels not in self.text_fonts:
self.text_fonts[pixels] = PIL.ImageFont.truetype(str(self.root / 'PetalumaScript.otf'), pixels)
return self.text_fonts[pixels]
def text(self, value: str, x: float, y: float, size: float, role: str, sources: tuple[str, ...], center: bool = False) -> generator.engraving.types.Primitive:
missing = [c for c in value if ord(c) not in self.text_cmap and not c.isspace()]
if missing:
raise generator.engraving.types.EngravingError(f'Petaluma Script is missing text glyph {missing[0]!r}')
font = self.text_font(size * 4)
left, top, right, bottom = font.getbbox(value, anchor='ls')
offset = (left + right) / 8 if center else 0
x -= offset
return generator.engraving.types.Primitive('text', role, sources, generator.engraving.types.Box(x + left / 4, y + top / 4, x + right / 4, y + bottom / 4), ((x, y),), value, size)