Automatic page splitting

This commit is contained in:
sebastien committed 2026-11-03 22:08:36 +01:00
1 parent d4404e4c27
commit 78f3e4b2f0
13 files changed
+1959 -40

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
import numpy as np
import pymupdf
from copienator import EvaluationWorkspace, ExitCode
from copienator.commands import automatic_page_splitter as automatic
from copienator.commands.page_splitter import PDFPreviewer, _selected_inputs, main
from copienator.copy_errors import copy_errors
from copienator.orientation import load_provenance
from copienator_gui.workflow import build_command, build_workflow
class AutomaticPageSplitterTests(unittest.TestCase):
def make_source(self, root, *, blank=False):
(root / "labels").write_text("Ex 1\n")
source = root / "Copie01.pdf"
with pymupdf.open() as doc:
page = doc.new_page(width=420 * 72 / 25.4, height=297 * 72 / 25.4)
if not blank:
page.insert_text((60, 100), "Answer on the left page", fontsize=16)
page.insert_text((750, 100), "Answer on the right page", fontsize=16)
page.insert_text((560, 300), "x + y = z", fontsize=16)
doc.new_page(width=420 * 72 / 25.4, height=297 * 72 / 25.4)
page = doc.new_page(width=210 * 72 / 25.4, height=297 * 72 / 25.4)
page.insert_text((60, 100), "Exercise 3", fontsize=16)
doc.save(source)
return source
def test_gui_choices_build_manual_and_automatic_commands(self):
step = next(s for s in build_workflow(False) if s.id == "page_splitter")
self.assertEqual(
[v.label for v in step.variants], ["Manuellement", "Automatiquement"]
)
manual, auto = [
build_command(Path.cwd(), step, v, {"workers": 3}, "Exam")
for v in step.variants
]
self.assertNotIn("--automatic", manual)
self.assertNotIn("--workers", manual)
self.assertIn("--automatic", auto)
self.assertEqual(auto[auto.index("--workers") + 1], "3")
def test_cli_publishes_without_tk_and_preserves_original_and_provenance(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root)
before = source.read_bytes()
workspace = EvaluationWorkspace(root)
with patch("copienator.commands.page_splitter.tk.Tk") as tk:
self.assertEqual(main([str(root), "--automatic", "--workers", "1"]), 0)
tk.assert_not_called()
preserved = workspace.original_copies_dir / source.name
self.assertEqual(preserved.read_bytes(), before)
self.assertFalse(source.exists())
output = workspace.copies_dir / source.name
with pymupdf.open(output) as doc:
self.assertEqual(len(doc), 3)
origins = load_provenance(workspace, output, len(doc))
self.assertEqual([o["origin"] for o in origins], [1, 1, 3])
self.assertGreater(doc[0].rect.width, 210 * 72 / 25.4)
saved = automatic.load_decisions(workspace, preserved)
self.assertEqual(len(saved), 3)
self.assertEqual(_selected_inputs(workspace, root), [preserved])
self.assertFalse(copy_errors(workspace))
runs = list(workspace.runs_dir.glob("page-split-*"))
self.assertTrue((runs[0] / "result/pdfs" / source.name).exists())
self.assertEqual(
automatic.run(workspace, [preserved], workers=1), ExitCode.SUCCESS
)
self.assertEqual(preserved.read_bytes(), before)
def test_processing_failure_leaves_existing_files_in_place(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root)
before = source.read_bytes()
workspace = EvaluationWorkspace(root)
workspace.copies_dir.mkdir()
output = workspace.copies_dir / source.name
output.write_bytes(b"previous processed copy")
with (
patch.object(
automatic,
"analyse_batch",
side_effect=RuntimeError("analysis failed"),
),
self.assertRaisesRegex(RuntimeError, "analysis failed"),
):
automatic.run(workspace, [source], workers=1)
self.assertEqual(source.read_bytes(), before)
self.assertEqual(output.read_bytes(), b"previous processed copy")
self.assertFalse(workspace.original_copies_dir.exists())
def test_parallel_run_preserves_unselected_copies_and_ignores_stale_proposals(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root)
second = root / "Copie02.pdf"
shutil.copy2(source, second)
workspace = EvaluationWorkspace(root)
workspace.copies_dir.mkdir()
other = workspace.copies_dir / "Copie99.pdf"
other.write_bytes(b"unselected copy")
automatic.run(workspace, [source, second], workers=2)
self.assertEqual(other.read_bytes(), b"unselected copy")
original = workspace.original_copies_dir / source.name
self.assertIsNotNone(automatic.load_decisions(workspace, original))
output = workspace.copies_dir / source.name
output.write_bytes(b"edited after automatic splitting")
self.assertIsNone(automatic.load_decisions(workspace, original))
def test_wholly_blank_copy_is_preserved_and_flagged_without_stale_output(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root, blank=True)
before = source.read_bytes()
workspace = EvaluationWorkspace(root)
workspace.copies_dir.mkdir()
(workspace.copies_dir / source.name).write_bytes(b"stale output")
automatic.run(workspace, [source], workers=1)
self.assertFalse((workspace.copies_dir / source.name).exists())
self.assertEqual(
(workspace.original_copies_dir / source.name).read_bytes(), before
)
self.assertIn(source.name, copy_errors(workspace))
run = next(workspace.runs_dir.glob("page-split-*"))
self.assertEqual(
(run / "previous_copies" / source.name).read_bytes(), b"stale output"
)
def test_manual_acceptance_preserves_saved_overlap_blanks_and_order(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root)
workspace = EvaluationWorkspace(root)
automatic.run(workspace, [source], workers=1)
original = workspace.original_copies_dir / source.name
before = original.read_bytes()
output = workspace.copies_dir / source.name
expected = pymupdf.open(output)
self.addCleanup(expected.close)
preview = PDFPreviewer.__new__(PDFPreviewer)
preview.workspace = workspace
preview.inputs = [original]
preview.num = preview.global_rotation = 0
preview.history = []
preview.output_dir = None
preview.failed = False
preview.master = Mock()
preview.load_page = Mock()
self.assertTrue(preview.setup_next_file())
self.assertTrue(preview.automatic_review)
preview._initialize_current_page_settings()
for _ in range(3):
preview.confirm_and_next_page()
self.assertFalse(preview.failed)
self.assertEqual(
[s["keep"] for s in preview.page_settings], ["both", "none", "as_is"]
)
self.assertEqual(original.read_bytes(), before)
with pymupdf.open(output) as actual:
self.assertEqual(len(actual), len(expected))
for a, b in zip(actual, expected):
self.assertAlmostEqual(a.rect.width, b.rect.width, places=3)
aa, bb = a.get_pixmap(dpi=40), b.get_pixmap(dpi=40)
self.assertEqual((aa.width, aa.height), (bb.width, bb.height))
difference = np.abs(
np.frombuffer(aa.samples, np.uint8).astype(float)
- np.frombuffer(bb.samples, np.uint8)
)
self.assertLess(difference.mean(), 0.1)
def test_accepting_wholly_blank_proposal_never_creates_an_empty_pdf(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root, blank=True)
workspace = EvaluationWorkspace(root)
automatic.run(workspace, [source], workers=1)
original = workspace.original_copies_dir / source.name
before = original.read_bytes()
preview = PDFPreviewer.__new__(PDFPreviewer)
preview.workspace = workspace
preview.inputs = [original]
preview.num = preview.global_rotation = 0
preview.history = []
preview.failed = False
preview.master = Mock()
preview.load_page = Mock()
preview.setup_next_file()
preview._initialize_current_page_settings()
preview.confirm_and_next_page()
self.assertFalse(preview.failed)
self.assertEqual(original.read_bytes(), before)
self.assertFalse((workspace.copies_dir / source.name).exists())
self.assertNotIn(source.name, copy_errors(workspace))
if __name__ == "__main__":
unittest.main()