"""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