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]) # The outer right is page 1; the overflowing outer left is page 4. self.assertGreater(doc[1].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()