Files
Copies/copienator/review_snapshot.py

78 lines
3.6 KiB
Python

"""Rendering inputs captured after handwriting interpretation, before user overrides.
Optional for the legacy readers. The web renderer consumes this state without
rescanning annotation PDFs. A selective redo merges only the selected labels.
"""
from copy import deepcopy
from hashlib import sha256
import json
from pathlib import Path
from PIL import Image
from copienator import atomic_write_json, read_json
FILENAME = 'review_snapshot.json'
def stage(output, staging, data, notes, *, engine, bnote=None, selected=None, force_filtered=()):
previous = read_json(output / FILENAME, default=None)
if selected and previous is None:
# Old preserved images cannot tell us their original feedback and ink.
return
if previous and previous['engine'] == 'standard' and engine == 'grouped':
geometry = {item['label']: item for item in previous['bnote']['images']}
for label, note in previous['notes'].items():
note['old_header_h'] = geometry.get(label, {}).get('header_height', 0)
if 'layout' in geometry.get(label, {}):
note['layout'] = geometry[label]['layout']
retained = set(force_filtered)
if selected:
base = read_json(output / 'annotation_baseline.json', default={})
retained.update(set(base.get('filtered', [])) - set(selected))
selected = set(selected or data)
if previous:
# Preserve raw inputs for the labels not re-read in a selective redo.
merged_data = previous['data'] if selected != set(data) else {}
merged_notes = previous['notes'] if selected != set(data) else {}
else:
merged_data, merged_notes = {}, {}
for label in selected:
if label in data:
content = deepcopy(data[label])
path = Path(content['pdf_path']).resolve()
root = output.parent.parent.resolve()
content['pdf_path'] = str(path.relative_to(root)) if path.is_relative_to(root) else str(path)
merged_data[label] = content
merged_notes.pop(label, None)
note = notes.get(label)
if note is not None:
# Grouped scans retain layout metadata even without handwriting.
filename = None
if note['img'] is not None:
filename = '.review-note-' + sha256(label.encode()).hexdigest() + '.png'
note['img'].save(staging / filename)
merged_notes[label] = {k: v for k, v in note.items() if k != 'img'} | {'file': filename}
payload = {'version': 1, 'engine': engine, 'data': merged_data, 'notes': merged_notes, 'bnote': bnote, 'force_filtered': sorted(retained)}
# Roundtrip normalizes tuples while rejecting unsupported rendering data.
atomic_write_json(staging / FILENAME, json.loads(json.dumps(payload)))
def load(root, output):
saved = read_json(output / FILENAME)
if saved.get('version') != 1:
raise ValueError('Version du résultat de lecture inconnue. Relisez les annotations.')
data = saved['data']
for content in data.values():
path = Path(content['pdf_path'])
content['pdf_path'] = path if path.is_absolute() else root / path
notes = {}
for label, metadata in saved['notes'].items():
filename = metadata['file']
if filename is None:
notes[label] = {k: v for k, v in metadata.items() if k != 'file'} | {'img': None}
continue
if Path(filename).name != filename:
raise ValueError('Image manuscrite invalide.')
with Image.open(output / filename) as image:
notes[label] = {k: v for k, v in metadata.items() if k != 'file'} | {'img': image.copy()}
return saved, data, notes