@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
from fontTools.pens.recordingPen import DecomposingRecordingPen
|
||||
from fontTools.ttLib import TTFont
|
||||
from PIL import ImageFont
|
||||
|
||||
from generator.engraving.types import Box, EngravingError, Primitive, Style
|
||||
|
||||
class Fonts:
|
||||
def __init__(self, root: Path):
|
||||
self.root = root
|
||||
try:
|
||||
self.music = TTFont(root / 'Petaluma.otf')
|
||||
self.script = 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
|
||||
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: Style) -> str:
|
||||
digest = hashlib.sha256(json.dumps(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}')
|
||||
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}')
|
||||
return self.cmap[code]
|
||||
def glyph_box(self, name: str, size: float) -> Box:
|
||||
if name not in self.metrics:
|
||||
pen = BoundsPen(self.glyphs)
|
||||
self.glyphs[self.glyph_name(name)].draw(pen)
|
||||
if pen.bounds is None:
|
||||
raise 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)
|
||||
def outline(self, name: str):
|
||||
if name not in self.outlines:
|
||||
pen = 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 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] = 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:
|
||||
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}')
|
||||
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)
|
||||
Reference in New Issue
Block a user