From fd943696fbc6b756612be23a02ba390149e4d797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Miquel?= Date: Tue, 3 Nov 2026 09:30:50 +0100 Subject: [PATCH] Fix review snapshots without handwriting images --- copienator/review_snapshot.py | 10 ++++++++-- tests/test_finalisation_web.py | 26 +++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/copienator/review_snapshot.py b/copienator/review_snapshot.py index de49793..d0ba8fb 100644 --- a/copienator/review_snapshot.py +++ b/copienator/review_snapshot.py @@ -45,8 +45,11 @@ def stage(output, staging, data, notes, *, engine, bnote=None, selected=None, fo merged_notes.pop(label, None) note = notes.get(label) if note is not None: - filename = '.review-note-' + sha256(label.encode()).hexdigest() + '.png' - note['img'].save(staging / filename) + # 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. @@ -64,6 +67,9 @@ def load(root, output): 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: diff --git a/tests/test_finalisation_web.py b/tests/test_finalisation_web.py index 1689e25..a58336f 100644 --- a/tests/test_finalisation_web.py +++ b/tests/test_finalisation_web.py @@ -177,6 +177,29 @@ class WebTests(unittest.TestCase): class SnapshotTests(unittest.TestCase): + def test_grouped_layout_without_handwriting_replaces_previous_ink(self): + from copienator.review_snapshot import stage, load + from copienator.annotation_layout import legacy_layout + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + seed(root, 'BGnot') + output = root / 'BGnot/Copie01' + data = {label: {'pdf_path': root / f'{label}.pdf', 'result': {'score': 3}} + for label in ('Ex 1', 'Ex 2')} + ink = Image.new('RGBA', (100, 120), 'red') + notes = {label: {'img': ink, 'old_header_h': 20, 'layout': legacy_layout()} + for label in data} + with staged_files(output) as staging: + stage(output, staging, data, notes, engine='grouped') + notes['Ex 1']['img'] = None + for selected in ({'Ex 1'}, None): + with staged_files(output) as staging: + stage(output, staging, data, notes, engine='grouped', selected=selected) + saved, _, restored = load(root, output) + self.assertIsNone(saved['notes']['Ex 1']['file']) + self.assertEqual(restored['Ex 1'], notes['Ex 1']) + self.assertEqual(restored['Ex 2']['img'].getpixel((0, 0)), (255, 0, 0, 255)) + def test_selective_redo_merges_raw_inputs_and_preserves_handwriting(self): from copienator.review_snapshot import stage, load with tempfile.TemporaryDirectory() as temporary: @@ -214,8 +237,9 @@ class SnapshotTests(unittest.TestCase): pdf = root / 'Copies/Copie01/Ex 1.pdf' pdf.write_bytes(b'placeholder') data = {'Ex 1': {'pdf_path': pdf, 'result': {'score': 3, 'feedback': []}, 'coordinates': [0,0]}} + notes = {'Ex 1': {'img': None, 'old_header_h': 20}} if mode == 'BGnot' else {} with staged_files(output) as staging: - stage(output, staging, data, {}, engine='standard' if mode == 'Bnot' else 'grouped', + stage(output, staging, data, notes, engine='standard' if mode == 'Bnot' else 'grouped', bnote={'images':[{'label':'Ex 1','header_height':20}]}) set_remark(root, '01', 'Ex 1', 'Une seule remarque') with patch.object(annotating, 'make_base_image', return_value=(Image.new('RGB',(100,100),'white'),0,0)), \