import sys import tempfile import unittest from pathlib import Path from types import SimpleNamespace from unittest.mock import patch from PIL import Image, PdfParser sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) from scan_document import create_document class ScanDocumentTest(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) def pages(self, count=4): paths = [] for index in range(1, count + 1): target = self.root / f"page-{index:06}.png" # Distinct geometry identifies every front/back page and its order. with Image.new("RGB", (100 + index, 200 + index), "white") as image: image.save(target) paths.append(target) return paths def assert_pdf(self, output, count, resolution=300): with PdfParser.PdfParser(filename=str(output)) as pdf: self.assertEqual(len(pdf.pages), count) for index, reference in enumerate(pdf.pages, 1): page = pdf.read_indirect(reference) box = page[b"MediaBox"] self.assertAlmostEqual(box[2], (100 + index) * 72 / resolution, places=4) self.assertAlmostEqual(box[3], (200 + index) * 72 / resolution, places=4) def test_twelve_pages_keep_front_back_order_blank_pages_and_scan_resolution(self): output = self.root / "scan.pdf" result = create_document(self.pages(12), output, 150) self.assertEqual(result, {"pageCount": 12, "warnings": []}) self.assert_pdf(output, 12, 150) def test_missing_opencv_still_creates_complete_pdf(self): output = self.root / "scan.pdf" with patch.dict(sys.modules, {"opencv_postprocess": None}): result = create_document(self.pages(), output, 300, use_postprocess=True) self.assertEqual(len(result["warnings"]), 1) self.assert_pdf(output, 4) def test_failed_correction_falls_back_per_page_without_losing_sides(self): def correct(source, target, profile): if source.name.endswith("000002.png"): raise RuntimeError("correction failed") target.write_bytes(source.read_bytes()) output = self.root / "scan.pdf" with patch.dict(sys.modules, {"opencv_postprocess": SimpleNamespace(postprocess=correct)}): result = create_document(self.pages(), output, 300, use_postprocess=True) self.assertEqual(len(result["warnings"]), 1) self.assertIn("Seite 2", result["warnings"][0]) self.assert_pdf(output, 4) def test_strict_mode_does_not_silently_skip_missing_correction(self): output = self.root / "scan.pdf" with patch.dict(sys.modules, {"opencv_postprocess": None}): with self.assertRaises(ImportError): create_document(self.pages(), output, 300, use_postprocess=True, strict=True) self.assertFalse(output.exists()) def test_corrupt_page_never_publishes_partial_pdf(self): pages = self.pages() pages[2].write_bytes(b"corrupt") output = self.root / "scan.pdf" with self.assertRaises(OSError): create_document(pages, output, 300) self.assertFalse(output.exists()) self.assertFalse((self.root / "scan.pdf.partial").exists()) self.assertTrue(all(page.exists() for page in pages)) def test_single_page_png_and_tiff_remain_supported(self): pages = self.pages(1) for extension in ["png", "tiff"]: output = self.root / f"scan.{extension}" create_document(pages, output, 150) with Image.open(output) as image: self.assertEqual(image.size, (101, 201)) self.assertAlmostEqual(image.info["dpi"][0], 150, delta=0.1) if __name__ == "__main__": unittest.main()