@@ -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]
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user