When no margin detected, reserve a bit more space for comments
This commit is contained in:
1 parent
e7094d850e
commit
78a7b7679c
17 files changed
+357
-28
No files matched your search
@@ -0,0 +1,139 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pymupdf
|
||||
from PIL import Image
|
||||
|
||||
from copienator import atomic_write_json, configuration, read_json
|
||||
from copienator.annotation_layout import (
|
||||
answer_parts, copy_answer_pdf, fingerprint, layout_for, legacy_layout,
|
||||
move_answer_pdf, save_parts,
|
||||
)
|
||||
from copienator.commands import annotating, annotating_by_label, splitting_int
|
||||
from copienator.commands import reading_grouped_annotations as reader
|
||||
from copienator.left_margin import analyse_margins
|
||||
from copienator.pdf_cut import split_pdf
|
||||
|
||||
|
||||
class AnnotationLayoutTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.pdf = self.root / 'Copies' / 'Copie01.pdf'
|
||||
self.pdf.parent.mkdir()
|
||||
with pymupdf.open() as document:
|
||||
for _ in range(2):
|
||||
page = document.new_page(width=600, height=800)
|
||||
page.insert_text((100, 100), 'Answer')
|
||||
document.save(self.pdf)
|
||||
|
||||
def test_all_pages_measured_after_early_no_margin_run(self):
|
||||
images = [Image.new('RGB', (600, 100)) for _ in range(6)]
|
||||
values = [10, 10, 40, None, 19.9, 20]
|
||||
estimates = {id(image): value for image, value in zip(images, values)}
|
||||
def estimate(image, dpi):
|
||||
edge = estimates[id(image)]
|
||||
return {'edge_mm': edge, 'bands': 0 if edge is None else 8,
|
||||
'band_starts_mm': [] if edge is None else [edge] * 8}
|
||||
with patch('copienator.left_margin.estimate_page', side_effect=estimate):
|
||||
result = analyse_margins(images, 200, 600)
|
||||
self.assertEqual([p['status'] for p in result['pages']],
|
||||
['low', 'low', 'normal', 'unknown', 'low', 'normal'])
|
||||
|
||||
def test_split_records_pages_and_rejects_stale_measurements(self):
|
||||
schema_path = self.root / 'Cutleft' / 'Copie01_schema.json'
|
||||
atomic_write_json(schema_path, {
|
||||
'source_sha256': fingerprint(self.pdf),
|
||||
'auto_margin': {'pages': [{'page': 1, 'edge_mm': 40}, {'page': 2, 'edge_mm': 10}]},
|
||||
})
|
||||
staging = self.root / 'split'
|
||||
staging.mkdir()
|
||||
splitting_int._render_split_outputs(self.pdf, [('Ex 1', 0, 0, 10, 0, 10)], staging)
|
||||
answer = staging / 'Ex 1.pdf'
|
||||
self.assertEqual([p['page'] for p in answer_parts(answer)], [1, 2])
|
||||
self.assertEqual(layout_for(answer)['margin_left'], 400)
|
||||
atomic_write_json(schema_path, {'source_sha256': 'stale', 'auto_margin': {
|
||||
'pages': [{'page': 1, 'edge_mm': 10}]}})
|
||||
splitting_int._render_split_outputs(self.pdf, [('Ex 1', 0, 0, 10, 0, 10)], staging)
|
||||
self.assertEqual(layout_for(answer)['margin_left'], 300)
|
||||
|
||||
def test_configurable_margin_unknown_fallback_and_stale_sidecar(self):
|
||||
self.assertEqual(layout_for(self.pdf)['margin_left'], 300)
|
||||
save_parts(self.pdf, [{'margin_status': 'normal'}, {'margin_status': 'low'}])
|
||||
with patch.object(configuration, 'ANNOTATION_LOW_MARGIN_LEFT', 450):
|
||||
self.assertEqual(layout_for(self.pdf)['margin_left'], 450)
|
||||
self.pdf.write_bytes(self.pdf.read_bytes() + b'\n')
|
||||
self.assertEqual(layout_for(self.pdf)['margin_left'], 300)
|
||||
|
||||
def test_horizontal_answer_crop_can_remove_an_original_margin(self):
|
||||
atomic_write_json(self.root / 'Cutleft' / 'Copie01_schema.json', {
|
||||
'source_sha256': fingerprint(self.pdf),
|
||||
'auto_margin': {'pages': [{'page': 1, 'edge_mm': 40}, {'page': 2, 'edge_mm': 40}]},
|
||||
})
|
||||
staging = self.root / 'horizontal'
|
||||
staging.mkdir()
|
||||
# 20% of a 600-point page removes 42.3 mm from the left.
|
||||
splitting_int._render_split_outputs(self.pdf, [('|Ex 1', 0, 0, 10, 100, 110)], staging)
|
||||
answer = staging / 'Ex 1.pdf'
|
||||
self.assertEqual(layout_for(answer)['margin_left'], 400)
|
||||
self.assertEqual(answer_parts(answer)[0]['margin_mm'], 0)
|
||||
|
||||
def test_copy_move_and_vertical_cut_preserve_page_classification(self):
|
||||
save_parts(self.pdf, [{'margin_status': 'normal'}, {'margin_status': 'low'}])
|
||||
copied = self.root / 'copied.pdf'
|
||||
moved = self.root / 'moved.pdf'
|
||||
copy_answer_pdf(self.pdf, copied)
|
||||
move_answer_pdf(copied, moved)
|
||||
self.assertEqual(layout_for(moved)['margin_left'], 400)
|
||||
first, second = self.root / 'first.pdf', self.root / 'second.pdf'
|
||||
split_pdf(moved, 50, first, second)
|
||||
self.assertEqual(layout_for(first)['margin_left'], 300)
|
||||
self.assertEqual(layout_for(second)['margin_left'], 400)
|
||||
|
||||
def render(self, layout):
|
||||
callback = Mock()
|
||||
result = {'score': 2, 'feedback': [{'text': 'Feedback', 'box_2d': [10, 50, 30, 100]}]}
|
||||
with patch.object(annotating, 'render_score_text', return_value=Image.new('RGBA', (200, 40))):
|
||||
image, header = annotating.compose_label_image(
|
||||
Image.new('RGB', (1654, 100), 'blue'), 'Ex 1', result, 0,
|
||||
render_fn=lambda *args, **kwargs: Image.new('RGBA', (200, 10)),
|
||||
draw_callback=callback, layout=layout)
|
||||
box = next(call.args[2]['box'] for call in callback.call_args_list if call.args[0] == 'local_rect')
|
||||
return image, header, box
|
||||
|
||||
def test_wider_layout_translates_copy_and_feedback_without_scaling(self):
|
||||
normal, height, box = self.render(legacy_layout())
|
||||
low, low_height, low_box = self.render({**legacy_layout(), 'margin_left': 400})
|
||||
self.assertEqual(low.width, normal.width + 100)
|
||||
self.assertEqual(height, low_height)
|
||||
self.assertEqual(low_box, [box[0] + 100, box[1], box[2] + 100, box[3]])
|
||||
self.assertEqual(low.getpixel((400, height + 80)), (0, 0, 255))
|
||||
self.assertEqual(low.getpixel((399, height + 80)), (255, 255, 255))
|
||||
|
||||
def test_mixed_bgnot_widths_and_saved_geometry_survive_config_change(self):
|
||||
normal, height, _ = self.render(legacy_layout())
|
||||
low, _, _ = self.render({**legacy_layout(), 'margin_left': 400})
|
||||
annotating_by_label.save_batch([
|
||||
('01', 'Ex 1', normal, height, []), ('02', 'Ex 1', low, height, []),
|
||||
], 'Ex 1', 1, self.root)
|
||||
output = self.root / 'Ex 1 G1'
|
||||
meta = read_json(output / 'bnote.json')
|
||||
self.assertEqual(meta['width'], low.width)
|
||||
with Image.open(output / 'Reference.jpg') as reference:
|
||||
self.assertGreater(min(reference.getpixel((low.width - 10, 80))), 245)
|
||||
with patch.object(reader, 'detect_checks_and_notes', return_value=([], Image.new('RGBA', (low.width, low.height * 2)))), patch.object(configuration, 'ANNOTATION_LOW_MARGIN_LEFT', 500):
|
||||
_, notes = reader._scan_annotation_directory(output)
|
||||
self.assertEqual(notes['02']['Ex 1']['layout']['margin_left'], 400)
|
||||
rerender, _, _ = self.render(notes['02']['Ex 1']['layout'])
|
||||
self.assertEqual(rerender.width, low.width)
|
||||
with patch.object(reader, 'detect_checks_and_notes', return_value=([], None)):
|
||||
_, notes = reader._scan_annotation_directory(output)
|
||||
self.assertEqual(notes['02']['Ex 1']['layout']['margin_left'], 400)
|
||||
self.assertIsNone(notes['02']['Ex 1']['img'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -9,11 +9,29 @@ import pymupdf
|
||||
from copienator import CliError, EvaluationWorkspace, atomic_write_json, read_json
|
||||
from copienator.commands import correction, resolve_manual
|
||||
from copienator.answer_origins import origins_for, save_origins
|
||||
from copienator.annotation_layout import save_parts, answer_parts, layout_for, page_parts
|
||||
from copienator.annotation_data import load_annotation_data
|
||||
from copienator_gui.app import has_manual_conflicts
|
||||
|
||||
|
||||
class ManualResolutionStateTests(unittest.TestCase):
|
||||
def test_merge_preserves_low_margin_and_page_order(self):
|
||||
for pipe_first in (False, True):
|
||||
with self.subTest(pipe_first=pipe_first), tempfile.TemporaryDirectory() as temporary:
|
||||
workspace = self.fixture(Path(temporary), '->')
|
||||
workspace.manual_resolutions_file.write_text(
|
||||
'Copie01 A -> |B\n' if pipe_first else 'Copie01 A -> B|\n')
|
||||
copies = workspace.copies_dir / 'Copie01'
|
||||
for label, status in (('A', 'low'), ('B', 'normal')):
|
||||
pdf = copies / f'{label}.pdf'
|
||||
save_parts(pdf, [{'margin_status': status} for _ in page_parts(pdf)])
|
||||
resolve_manual.resolve_manual(workspace)
|
||||
target = resolve_manual.get_actual_pdf(workspace.copies_dir, '01', 'B')
|
||||
self.assertEqual(layout_for(target)['margin_left'], 400)
|
||||
statuses = [part['margin_status'] for part in answer_parts(target)]
|
||||
self.assertEqual(statuses[0], 'low' if pipe_first else 'normal')
|
||||
self.assertEqual(statuses[-1], 'normal' if pipe_first else 'low')
|
||||
|
||||
def test_automatic_additional_content_records_origin_but_wrong_label_does_not(self):
|
||||
for kind in ("additional-answer", "wrong-label"):
|
||||
with self.subTest(kind=kind), tempfile.TemporaryDirectory() as temporary:
|
||||
|
||||
Reference in new issue
Block a user