Files
khodowany bc7baea7c4
ci / checks (push) Failing after 23m22s
Cleanup
2026-10-08 02:13:45 -05:00

117 lines
5.7 KiB
Python

from __future__ import annotations
import dataclasses
import hashlib
import importlib.metadata
import io
import json
import pathlib
import platform
import re
import shutil
import PIL.Image
import PIL.ImageChops
import PIL.features
import generator.cache
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 = generator.cache.source_identity(('generator.engraving.pipeline', 'generator.printing'))
sources['palette.json'] = hashlib.sha256((root / 'palette.json').read_bytes()).hexdigest()
return {'fonts_style': fonts.fingerprint(style), 'python': {'implementation': platform.python_implementation(), 'version': platform.python_version()}, 'dependencies': versions,
'sources': sources}
def renderer_fingerprint(fonts, style) -> str:
return hashlib.sha256(json.dumps(renderer_identity(fonts, style), sort_keys=True).encode()).hexdigest()
def renderer_namespace(cache: pathlib.Path, renderer: str) -> pathlib.Path:
namespace = cache / renderer
if namespace.parent.is_symlink() or namespace.is_symlink():
raise ValueError('engraving cache namespaces must not be symlinks')
return namespace
def score_fingerprint(document: dict, renderer: str) -> str:
music = document
return hashlib.sha256(json.dumps([renderer, music], sort_keys=True, ensure_ascii=False).encode()).hexdigest()
def notation_fingerprint(document: dict, renderer: str) -> str:
return hashlib.sha256(json.dumps([renderer, document['staves']], sort_keys=True, ensure_ascii=False).encode()).hexdigest()
def band_fingerprint(notation: str, page, band) -> str:
context = [notation, page.width, page.height, page.supersampling, dataclasses.asdict(band)]
return hashlib.sha256(json.dumps(context, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
def restore_pages(entry: pathlib.Path, destination: pathlib.Path, validated_pages: set[str] | None = None) -> list[dict] | None:
try:
if generator.cache.has_symlink(entry) or generator.cache.has_symlink(destination):
return None
manifest = entry / 'pages.json'
if manifest.is_symlink():
return None
pages = json.loads(manifest.read_text(encoding='utf-8'))
if not isinstance(pages, list) or not 0 < len(pages) <= 10000:
return None
destination.mkdir(parents=True, exist_ok=True)
for index, page in enumerate(pages, 1):
if not isinstance(page, dict) or set(page) != {'path', 'sha256', 'first_measure', 'measure_count', 'start_beat', 'end_beat', 'systems', 'playback'} or type(page['playback']) is not bool or not isinstance(page['path'], str) or not re.fullmatch(r'(?:playback-)?[0-9]{4,5}\.png', page['path']) or not isinstance(page['sha256'], str) or not re.fullmatch('[0-9a-f]{64}', page['sha256']):
return None
path = entry / page['path']
if path.is_symlink() or not path.is_file():
return None
target = destination / page['path']
if target.is_symlink():
return None
shutil.copyfile(path, target)
generator.engraving.validation.check_page(target, validated_pages, expected_sha256=page['sha256'])
return pages
except (OSError, ValueError, TypeError):
return None
def store_pages(entry: pathlib.Path, destination: pathlib.Path, pages: list[dict]) -> None:
with generator.cache.stage_entry(entry, '.engraving_') as staged:
for page in pages:
shutil.copyfile(destination / page['path'], staged / page['path'])
(staged / 'pages.json').write_text(json.dumps(pages), encoding='utf-8')
# Identical keys have identical pages; publish complete files, then the manifest.
generator.cache.publish_files(entry, staged, list(dict.fromkeys(page['path'] for page in pages)) + ['pages.json'])
def restore_band(entry: pathlib.Path, key: str, width: int, height: int) -> PIL.Image.Image | None:
try:
manifest, asset = entry / 'band.json', entry / 'band.png'
if generator.cache.has_symlink(entry) or manifest.is_symlink() or asset.is_symlink():
return None
metadata = json.loads(manifest.read_text(encoding='utf-8'))
if not isinstance(metadata, dict) or set(metadata) != {'context', 'width', 'height', 'sha256'}:
return None
if metadata['context'] != key or type(metadata['width']) is not int or type(metadata['height']) is not int or (metadata['width'], metadata['height']) != (width, height):
return None
digest = metadata['sha256']
if not isinstance(digest, str) or not re.fullmatch('[0-9a-f]{64}', digest):
return None
data = asset.read_bytes()
if hashlib.sha256(data).hexdigest() != digest:
return None
with PIL.Image.open(io.BytesIO(data)) as image:
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, 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()
except (OSError, ValueError, TypeError):
return None
def store_band(entry: pathlib.Path, key: str, image: PIL.Image.Image) -> None:
with generator.cache.stage_entry(entry, '.engraving_') as staged:
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()}
(staged / 'band.json').write_text(json.dumps(metadata), encoding='utf-8')
generator.cache.publish_files(entry, staged, ('band.png', 'band.json'))