Package
ci / checks (push) Successful in 48m34s

This commit is contained in:
khodowany
2026-09-30 19:46:37 -05:00
parent 2a1aa2c67a
commit 09656a9d60
6621 changed files with 698 additions and 774 deletions
+3 -3
View File
@@ -31,7 +31,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
raise generator.engraving.types.EngravingError('music systems overlap their page rows')
audit_primitives(page.primitives, page.width, page.height, fonts)
header = generator.engraving.types.bounds([p for p in page.primitives if p.role in ('title', 'subtitle')])
footer = generator.engraving.types.bounds([p for p in page.primitives if p.role == 'page-number'])
footer = generator.engraving.types.bounds([p for p in page.primitives if p.role == 'page_number'])
if page.systems[0].box.top < header.bottom + layout.style.page_clearance - 0.01 or page.systems[-1].box.bottom > footer.top - layout.style.page_clearance + 0.01:
raise generator.engraving.types.EngravingError('music overlaps the page header or footer')
for item in page.primitives:
@@ -40,7 +40,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
for system in page.systems:
if system.scale != scale:
raise generator.engraving.types.EngravingError('notation size must remain consistent throughout a part')
for role in ('staff', 'tab-staff'):
for role in ('staff', 'tab_staff'):
lines = [p for p in system.primitives if p.role == role]
if lines:
left = min(x for p in lines for x, _ in p.points)
@@ -68,7 +68,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
if index.staff.get('tablature') and anchor.fragment.kind == 'tone':
tone = index.tones[anchor.source][1]
tab = tone['tablature']
tab_baseline = max(p.points[0][1] for p in system.primitives if p.role == 'tab-staff')
tab_baseline = max(p.points[0][1] for p in system.primitives if p.role == 'tab_staff')
expected_y = tab_baseline + (4 - (6 - tab['string']) * layout.style.tab_space) * system.scale
expected_x = min(a.x for a in system.anchors if a.event == anchor.event and a.time == anchor.time)
tabs = [p for p in system.primitives if p.role == 'tab' and anchor.source in p.sources]
+5 -4
View File
@@ -16,12 +16,13 @@ import PIL.ImageChops
import PIL.features
import generator.engraving.validation
import generator.theme
def renderer_identity(fonts, style) -> dict:
root = pathlib.Path(__file__).resolve().parents[1]
versions = {name: importlib.metadata.version(name) for name in ('Pillow', 'fonttools', 'aggdraw')}
versions.update({name: PIL.features.version(name) for name in PIL.features.get_supported()})
sources = [*root.joinpath('engraving').glob('*.py'), root / 'model.py', root / 'harmony.py', root / 'requirements.txt']
sources = [*root.joinpath('engraving').glob('*.py'), root / 'model.py', root / 'harmony.py', root / 'theme.py', root / 'palette.json']
return {'fonts_style': fonts.fingerprint(style), 'python': {'implementation': platform.python_implementation(), 'version': platform.python_version()}, 'dependencies': versions,
'sources': {path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(sources)}}
@@ -81,7 +82,7 @@ def store_pages(entry: pathlib.Path, destination: pathlib.Path, pages: list[dict
if entry.is_symlink() or entry.is_file():
entry.unlink(missing_ok=True)
entry.mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix='.engraving-', dir=entry.parent) as temporary:
with tempfile.TemporaryDirectory(prefix='.engraving_', dir=entry.parent) as temporary:
staged = pathlib.Path(temporary)
for page in pages:
shutil.copyfile(destination / page['path'], staged / page['path'])
@@ -113,7 +114,7 @@ def restore_band(entry: pathlib.Path, key: str, width: int, height: int) -> PIL.
if image.format != 'PNG' or image.size != (width, height) or image.mode != 'RGB':
return None
image.load()
ink = PIL.ImageChops.difference(image, PIL.Image.new('RGB', image.size, 'white')).getbbox()
ink = PIL.ImageChops.difference(image, PIL.Image.new('RGB', image.size, generator.theme.rgb('background'))).getbbox()
if not ink or ink[0] < 3 or ink[1] < 3 or ink[2] > width - 3 or ink[3] > height - 3:
return None
return image.copy()
@@ -127,7 +128,7 @@ def store_band(entry: pathlib.Path, key: str, image: PIL.Image.Image) -> None:
if entry.is_symlink() or entry.is_file():
entry.unlink()
entry.mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix='.engraving-', dir=entry.parent) as temporary:
with tempfile.TemporaryDirectory(prefix='.engraving_', dir=entry.parent) as temporary:
staged = pathlib.Path(temporary)
image.save(staged / 'band.png', format='PNG')
metadata = {'context': key, 'width': image.width, 'height': image.height, 'sha256': hashlib.sha256((staged / 'band.png').read_bytes()).hexdigest()}
+2 -2
View File
@@ -57,7 +57,7 @@ def column(index: generator.engraving.notation.ScoreIndex, time: fractions.Fract
items.append(fonts.text(generator.harmony.symbol(item), 0, 0, 18 if index.staff.get('tablature') else 21, 'harmony', (item['id'],)))
items.extend(generator.engraving.scales.column(index, time, start, fonts))
if time == measure_start:
items.append(fonts.text(str(measure['number']), 0, -style.space * (index.state(time)['staff_details']['lines'] + 0.5), 12, 'measure-number', (measure['id'],)))
items.append(fonts.text(str(measure['number']), 0, -style.space * (index.state(time)['staff_details']['lines'] + 0.5), 12, 'measure_number', (measure['id'],)))
if label := measure.get('section_label'):
items.append(fonts.text(label, 0, 0, 16 if index.staff.get('tablature') else 19, 'section', (measure['id'] + ':section',)))
return items, anchors, fragments
@@ -219,7 +219,7 @@ def paginate(systems: list[generator.engraving.types.System], document: dict, fo
index = index or generator.engraving.notation.ScoreIndex(document)
header = part_header(document, fonts, style)
top = generator.engraving.types.bounds(header).bottom + style.page_clearance
footers = [fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page-number', (), True) for number in range(1, (len(systems) + 1) // 2 + 1)]
footers = [fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page_number', (), True) for number in range(1, (len(systems) + 1) // 2 + 1)]
bottom = min(p.box.top for p in footers) - style.page_clearance
if bottom - top <= style.system_gap:
raise generator.engraving.types.EngravingError('page header leaves no room for two music systems')
+2 -2
View File
@@ -104,7 +104,7 @@ def draw_event(index: generator.engraving.notation.ScoreIndex, event: generator.
head = fonts.glyph(glyph, x, y, tone_size, 'head', sources)
group_items.append(head)
if tone.get('notehead', {}).get('parentheses') == 'yes':
group_items.extend((fonts.text('(', head.box.left - 6, y + 5, 18, 'head-parentheses', sources), fonts.text(')', head.box.right + 2, y + 5, 18, 'head-parentheses', sources)))
group_items.extend((fonts.text('(', head.box.left - 6, y + 5, 18, 'head_parentheses', sources), fonts.text(')', head.box.right + 2, y + 5, 18, 'head_parentheses', sources)))
ledger = -2
while position <= ledger:
group_items.append(generator.engraving.types.line('ledger', sources, ((head.box.left - 5, -ledger * style.space / 2), (head.box.right + 5, -ledger * style.space / 2)), 1.8))
@@ -149,7 +149,7 @@ def draw_event(index: generator.engraving.notation.ScoreIndex, event: generator.
flag = 'flag' + ('8th' if written['type'] == 'eighth' else written['type']) + ('Up' if direction == -1 else 'Down')
group_items.append(fonts.glyph(flag, anchor.stem_x, stem_y, size, 'flag', references))
if note.get('grace', {}).get('slash'):
group_items.append(generator.engraving.types.line('grace-slash', references, ((anchor.stem_x - 5, stem_y + 14), (anchor.stem_x + 9, stem_y + 4)), 1.5))
group_items.append(generator.engraving.types.line('grace_slash', references, ((anchor.stem_x - 5, stem_y + 14), (anchor.stem_x + 9, stem_y + 4)), 1.5))
if note.get('tremolo', {}).get('type') == 'single':
for mark in range(note['tremolo']['marks']):
ty = stem_y - direction * (10 + mark * 5)
+2 -2
View File
@@ -116,7 +116,7 @@ def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, prog
scores = {}
audited = set()
result = GroupResult()
with tempfile.TemporaryDirectory(prefix='vamp-notation-') as temporary:
with tempfile.TemporaryDirectory(prefix='vamp_notation_') as temporary:
bodies = BodyBands(pathlib.Path(temporary), cache_dir, result)
try:
for path, score_key in paths:
@@ -147,7 +147,7 @@ def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, prog
audited.add(placement)
else:
for page in pages_layout:
decoration = tuple(p for p in page.primitives if p.role in ('title', 'subtitle', 'page-number'))
decoration = tuple(p for p in page.primitives if p.role in ('title', 'subtitle', 'page_number'))
generator.engraving.audit.audit_primitives(decoration, page.width, page.height, family)
result.timings['pagination'] += time.monotonic() - started
pages = []
+7 -6
View File
@@ -10,6 +10,7 @@ import fontTools.pens.basePen
import generator.engraving.fonts
import generator.engraving.types
import generator.theme
class OutlinePen(fontTools.pens.basePen.BasePen):
def __init__(self, glyphs, scale: float, x: float, y: float):
@@ -34,7 +35,7 @@ def rasterize_page(page: generator.engraving.types.Page, fonts: generator.engrav
def rasterize_band(page: generator.engraving.types.Page, primitives: tuple[generator.engraving.types.Primitive, ...], top: int, bottom: int, fonts: generator.engraving.fonts.Fonts) -> PIL.Image.Image:
scale = page.supersampling
image = PIL.Image.new('RGB', (page.width * scale, (bottom - top) * scale), 'white')
image = PIL.Image.new('RGB', (page.width * scale, (bottom - top) * scale), generator.theme.rgb('background'))
context = aggdraw.Draw(image)
for item in primitives:
points = [(x * scale, y * scale - top * scale) for x, y in item.points]
@@ -55,15 +56,15 @@ def rasterize_band(page: generator.engraving.types.Page, primitives: tuple[gener
if item.kind == 'polygon':
path.close()
if item.kind in ('glyph', 'polygon'):
context.path(path, None, aggdraw.Brush('black'))
context.path(path, None, aggdraw.Brush(generator.theme.rgb('ink')))
else:
context.path(path, aggdraw.Pen('black', item.thickness * scale))
context.path(path, aggdraw.Pen(generator.theme.rgb('ink'), item.thickness * scale))
context.flush()
drawing = PIL.ImageDraw.Draw(image)
for item in primitives:
if item.kind == 'text':
x, y = item.points[0]
drawing.text((x * scale, y * scale - top * scale), item.value, font=fonts.text_font(item.size * scale), anchor='ls', fill='black')
drawing.text((x * scale, y * scale - top * scale), item.value, font=fonts.text_font(item.size * scale), anchor='ls', fill=generator.theme.rgb('ink'))
return image.resize((page.width, bottom - top), PIL.Image.Resampling.LANCZOS)
@dataclasses.dataclass(frozen=True)
@@ -75,7 +76,7 @@ class Band:
def page_bands(page: generator.engraving.types.Page) -> tuple[Band, Band, Band] | None:
body = tuple(p for system in page.systems for p in system.primitives)
header = tuple(p for p in page.primitives if p.role in ('title', 'subtitle'))
footer = tuple(p for p in page.primitives if p.role == 'page-number')
footer = tuple(p for p in page.primitives if p.role == 'page_number')
if not body or not header or not footer or page.primitives != header + body + footer:
return None
bands = []
@@ -91,7 +92,7 @@ def page_bands(page: generator.engraving.types.Page) -> tuple[Band, Band, Band]
return tuple(bands)
def compose_page(page: generator.engraving.types.Page, fonts: generator.engraving.fonts.Fonts, bands: tuple[Band, Band, Band], body: PIL.Image.Image) -> PIL.Image.Image:
image = PIL.Image.new('RGB', (page.width, page.height), 'white')
image = PIL.Image.new('RGB', (page.width, page.height), generator.theme.rgb('background'))
if body.mode != 'RGB' or body.size != (page.width, bands[1].bottom - bands[1].top):
raise generator.engraving.types.EngravingError('notation band has incompatible geometry')
image.paste(body, (0, bands[1].top))
+3 -3
View File
@@ -65,7 +65,7 @@ def draw_beams(index: generator.engraving.notation.ScoreIndex, anchors: list[gen
x1 = left if index.by_id[group[0]].start < start else first.stem_x
x2 = right if index.by_id[group[-1]].start >= end else last.stem_x
for anchor in selected:
additions.append(generator.engraving.types.line('beam-stem', (anchor.event,), ((anchor.stem_x, anchor.stem_y), (anchor.stem_x, beam_y)), 1.4))
additions.append(generator.engraving.types.line('beam_stem', (anchor.event,), ((anchor.stem_x, anchor.stem_y), (anchor.stem_x, beam_y)), 1.4))
for level in range(1, max(a.flags for a in selected) + 1):
y = beam_y - direction * (level - 1) * style.space * 0.75
for i, anchor in enumerate(selected):
@@ -262,9 +262,9 @@ def draw_lyric_spans(index: generator.engraving.notation.ScoreIndex, items: list
if x2 <= x1:
continue
if extend in ('start', 'continue'):
result.append(generator.engraving.types.line('lyric-extension', (identifier,), ((x1, y + 3), (x2, y + 3)), 1))
result.append(generator.engraving.types.line('lyric_extension', (identifier,), ((x1, y + 3), (x2, y + 3)), 1))
elif following:
count = max(1, int((x2 - x1) / 90))
for dash in range(count):
result.append(fonts.text('-', x1 + (x2 - x1) * (dash + 1) / (count + 1), y, 18, 'lyric-hyphen', (identifier,), True))
result.append(fonts.text('-', x1 + (x2 - x1) * (dash + 1) / (count + 1), y, 18, 'lyric_hyphen', (identifier,), True))
return result
+5 -5
View File
@@ -10,7 +10,7 @@ def column(index, event, anchors, fonts, style):
sources = (event.value['id'],)
if not event.value['pitches']:
anchor = anchors[0]
return [fonts.glyph('noteheadSlashHorizontalEnds', x, -2 * style.tab_space, style.space, 'tab-slash', sources)] if anchor.fragment.kind == 'slash' else []
return [fonts.glyph('noteheadSlashHorizontalEnds', x, -2 * style.tab_space, style.space, 'tab_slash', sources)] if anchor.fragment.kind == 'slash' else []
result = []
for anchor in anchors:
tone = index.tones[anchor.source][1]
@@ -27,23 +27,23 @@ def staff(index, items, start, end, left, right, fonts, style):
offset = generator.engraving.types.bounds(notation).bottom + style.tab_gap + 5 * style.tab_space
numbers = [p.moved(0, offset) for p in numbers]
drawing = list(numbers)
drawing.append(fonts.text('TAB', 0, offset - 2 * style.tab_space, style.tab_font_size, 'tab-label', ()))
drawing.append(fonts.text('TAB', 0, offset - 2 * style.tab_space, style.tab_font_size, 'tab_label', ()))
for string in range(1, 7):
y = offset - (6 - string) * style.tab_space
cuts = sorted((max(0, p.box.left - 2), min(right, p.box.right + 2)) for p in numbers if p.box.top <= y <= p.box.bottom)
cursor = 0
for a, b in cuts + [(right, right)]:
if a > cursor:
drawing.append(generator.engraving.types.line('tab-staff', (), ((cursor, y), (a, y)), style.space * 0.13))
drawing.append(generator.engraving.types.line('tab_staff', (), ((cursor, y), (a, y)), style.space * 0.13))
cursor = max(cursor, b)
for bar in (p for p in notation if p.role == 'barline'):
drawing.append(generator.engraving.types.line('tab-barline', bar.sources, tuple((x, offset + y * 5 * style.tab_space / (4 * style.space)) for x, y in bar.points), bar.thickness))
drawing.append(generator.engraving.types.line('tab_barline', bar.sources, tuple((x, offset + y * 5 * style.tab_space / (4 * style.space)) for x, y in bar.points), bar.thickness))
by_source = {source: p for p in numbers if p.role == 'tab' for source in p.sources[1:]}
def tie(identifier, tone, x1, x2):
y = offset - (6 - tone['tablature']['string']) * style.tab_space + 6
if x2 > x1:
drawing.append(generator.engraving.types.curve('tab-tie', (identifier,), ((x1, y), (x1 + (x2 - x1) / 3, y + 5), (x2 - (x2 - x1) / 3, y + 5), (x2, y))))
drawing.append(generator.engraving.types.curve('tab_tie', (identifier,), ((x1, y), (x1 + (x2 - x1) / 3, y + 5), (x2 - (x2 - x1) / 3, y + 5), (x2, y))))
for identifier, (event, tone) in index.tones.items():
first = by_source.get(identifier)
+5 -3
View File
@@ -9,6 +9,7 @@ import PIL.Image
import PIL.ImageChops
import generator.engraving.types
import generator.theme
ENGINE_NAME = 'Vamp Python'
PAGE_WIDTH = 976
@@ -19,9 +20,10 @@ def check_content(data: bytes, name: str) -> None:
if image.format != 'PNG' or image.size != (PAGE_WIDTH, PAGE_HEIGHT):
raise generator.engraving.types.EngravingError(f'{name}: expected a {PAGE_WIDTH}x{PAGE_HEIGHT} PNG page')
image.load()
white = PIL.Image.new('RGBA', image.size, 'white')
white.alpha_composite(image.convert('RGBA'))
ink = PIL.ImageChops.difference(white.convert('RGB'), PIL.Image.new('RGB', image.size, 'white')).convert('L').point(lambda x: 255 if x > 32 else 0)
background = PIL.Image.new('RGB', image.size, generator.theme.rgb('background'))
composited = background.convert('RGBA')
composited.alpha_composite(image.convert('RGBA'))
ink = PIL.ImageChops.difference(composited.convert('RGB'), background).convert('L').point(lambda x: 255 if x > 32 else 0)
bounds = ink.getbbox()
if not bounds:
raise generator.engraving.types.EngravingError(f'{name}: engraver produced a blank page')