Automatic page splitting
This commit is contained in:
1 parent
d4404e4c27
commit
78f3e4b2f0
13 files changed
+1959
-40
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user