Rhythm Selection & Cleanup
ci / checks (push) Successful in 1h16m10s

This commit is contained in:
khodowany
2026-09-26 02:50:22 -05:00
parent 4d6348d354
commit 9fd5abd075
91 changed files with 4692 additions and 2484 deletions
+26 -26
View File
@@ -1,27 +1,27 @@
from __future__ import annotations
from dataclasses import asdict
import dataclasses
import hashlib
import json
from pathlib import Path
import pathlib
from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.recordingPen import DecomposingRecordingPen
from fontTools.ttLib import TTFont
from PIL import ImageFont
import PIL.ImageFont
import fontTools.pens.boundsPen
import fontTools.pens.recordingPen
import fontTools.ttLib
from generator.engraving.types import Box, EngravingError, Primitive, Style
import generator.engraving.types
class Fonts:
def __init__(self, root: Path):
def __init__(self, root: pathlib.Path):
self.root = root
try:
self.music = TTFont(root / 'Petaluma.otf')
self.script = TTFont(root / 'PetalumaScript.otf')
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 EngravingError(f'cannot load bundled engraving fonts: {error}') from 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()
@@ -30,37 +30,37 @@ class Fonts:
self.metrics = {}
self.outlines = {}
self.text_fonts = {}
def fingerprint(self, style: Style) -> str:
digest = hashlib.sha256(json.dumps(asdict(style), sort_keys=True).encode())
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 EngravingError(f'unknown SMuFL glyph {name!r}')
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 EngravingError(f'Petaluma is missing requested SMuFL glyph {name!r}')
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) -> Box:
def glyph_box(self, name: str, size: float) -> generator.engraving.types.Box:
if name not in self.metrics:
pen = BoundsPen(self.glyphs)
pen = fontTools.pens.boundsPen.BoundsPen(self.glyphs)
self.glyphs[self.glyph_name(name)].draw(pen)
if pen.bounds is None:
raise EngravingError(f'empty SMuFL glyph {name!r}')
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 Box(left * scale, -top * scale, right * scale, -bottom * scale)
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 = DecomposingRecordingPen(self.glyphs)
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, ...]) -> Primitive:
return Primitive('glyph', role, sources, self.glyph_box(name, size).moved(x, y), ((x, y),), name, size)
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:
@@ -70,14 +70,14 @@ class Fonts:
def text_font(self, size: float):
pixels = max(1, round(size))
if pixels not in self.text_fonts:
self.text_fonts[pixels] = ImageFont.truetype(str(self.root / 'PetalumaScript.otf'), pixels)
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) -> Primitive:
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 EngravingError(f'Petaluma Script is missing text glyph {missing[0]!r}')
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 Primitive('text', role, sources, Box(x + left / 4, y + top / 4, x + right / 4, y + bottom / 4), ((x, y),), value, size)
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)