70 lines
2.8 KiB
Python
70 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import importlib.metadata
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
|
|
from PIL import features
|
|
|
|
def renderer_fingerprint(fonts, style) -> str:
|
|
root = Path(__file__).resolve().parents[1]
|
|
versions = {name: importlib.metadata.version(name) for name in ('Pillow', 'fonttools', 'aggdraw')}
|
|
versions.update({name: features.version(name) for name in features.get_supported()})
|
|
digest = hashlib.sha256(json.dumps([fonts.fingerprint(style), sys.version, versions], sort_keys=True).encode())
|
|
for path in sorted([*root.joinpath('engraving').glob('*.py'), root / 'model.py', root / 'harmony.py', root / 'requirements.txt']):
|
|
digest.update(path.relative_to(root).as_posix().encode())
|
|
digest.update(path.read_bytes())
|
|
return digest.hexdigest()
|
|
|
|
def score_fingerprint(document: dict, renderer: str) -> str:
|
|
music = {key: value for key, value in document.items() if key != 'playback'}
|
|
return hashlib.sha256(json.dumps([renderer, music], sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
|
|
|
def restore_pages(entry: Path, destination: Path) -> list[dict] | None:
|
|
from generator.engraving import check_page
|
|
try:
|
|
if entry.is_symlink():
|
|
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'} or page['path'] != f'{index:04}.png':
|
|
return None
|
|
path = entry / page['path']
|
|
if path.is_symlink():
|
|
return None
|
|
target = destination / page['path']
|
|
shutil.copyfile(path, target)
|
|
if hashlib.sha256(target.read_bytes()).hexdigest() != page['sha256']:
|
|
return None
|
|
check_page(target)
|
|
return pages
|
|
except (OSError, ValueError, TypeError):
|
|
return None
|
|
|
|
def store_pages(entry: Path, destination: Path, pages: list[dict]) -> None:
|
|
entry.parent.mkdir(parents=True, exist_ok=True)
|
|
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:
|
|
staged = Path(temporary)
|
|
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.
|
|
for name in [page['path'] for page in pages] + ['pages.json']:
|
|
target = entry / name
|
|
if target.is_dir() and not target.is_symlink():
|
|
shutil.rmtree(target, ignore_errors=True)
|
|
(staged / name).replace(target)
|