import unittest import cv2 import numpy as np import pymupdf from copienator.auto_page_split import _analyse_page_full as analyse_page from copienator.fast_page_split import analyse_page_fast, analyse_pdf_page_fast, quick_page_content class FastPageSplitTests(unittest.TestCase): def test_direct_pdf_strip_respects_rotation_and_crop_offsets(self): image = np.full((1400, 2000, 3), 255, np.uint8) for y in range(150, 650, 70): for x in (150, 1300): cv2.putText(image, 'answer x y z', (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 20, 20), 2) cv2.putText(image, 'x + y = z', (870, 1000), cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 20, 20), 2) for rotation in (0, 90, 180, 270): with self.subTest(rotation=rotation), pymupdf.open() as doc: source = np.ascontiguousarray(np.rot90(image, rotation//90)) h,w = source.shape[:2] page = doc.new_page(width=w*72/150+20,height=h*72/150+30) box = pymupdf.Rect(10,15,w*72/150+10,h*72/150+15) pix = pymupdf.Pixmap(pymupdf.csRGB,w,h,source.tobytes(),False) page.insert_image(box,pixmap=pix) page.set_cropbox(box) page.set_rotation(rotation) rendered = page.get_pixmap(dpi=150,alpha=False) full = np.frombuffer(rendered.samples,np.uint8).reshape(rendered.height,rendered.width,3) reference = analyse_page(full,150,'a3') fast,overview = analyse_pdf_page_fast(page,150,'a3') self.assertIsNone(fast['analysis']['fallback']) self.assertEqual(fast['size_px'],reference['size_px']) self.assertLess(overview.size,full.size) self.assertEqual([o['keep'] for o in fast['outputs']],[True,True]) for a,b in zip(fast['outputs'],reference['outputs']): self.assertLessEqual(max(abs(x-y) for x,y in zip(a['rect_px'],b['rect_px'])),2) def test_plain_blank_and_sparse_pencil_use_full_detector(self): for pencil in (False, True): with self.subTest(pencil=pencil): image = np.full((1200, 850, 3), 255, np.uint8) if pencil: cv2.putText(image, 'pencil', (100, 600), cv2.FONT_HERSHEY_SIMPLEX, 1, (195, 195, 195), 2) mask, early = quick_page_content(image, 150) self.assertFalse(early) expected = analyse_page(image, 150, 'a4') actual = analyse_page_fast(image, 150, 'a4') self.assertEqual(actual['outputs'][0]['keep'], expected['outputs'][0]['keep']) self.assertEqual(actual['outputs'][0]['rect_px'], [0, 0, 850, 1200]) self.assertEqual(bool(mask.any()), pencil) def test_compact_roi_maps_bounds_back_to_full_spread(self): image = np.full((1400, 2000, 3), 255, np.uint8) for y in range(150, 650, 70): for x in (150, 1300): cv2.putText(image, 'answer x y z', (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 20, 20), 2) cv2.putText(image, 'x + y = z', (870, 1000), cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 20, 20), 2) for mirrored in (False, True): with self.subTest(mirrored=mirrored): rgb = image[:, ::-1].copy() if mirrored else image reference = analyse_page(rgb, 150, 'a3') fast = analyse_page_fast(rgb, 150, 'a3') self.assertIsNone(fast['analysis']['fallback']) self.assertEqual(fast['size_px'], [2000, 1400]) self.assertEqual(fast['extensions_mm'], reference['extensions_mm']) self.assertEqual([o['rect_px'] for o in fast['outputs']], [o['rect_px'] for o in reference['outputs']]) self.assertTrue(all(o['keep'] for o in fast['outputs'])) def test_ambiguous_or_wide_writing_falls_back_without_truncation(self): image = np.full((700, 1000, 3), 255, np.uint8) image[200:230, 300:650] = 20 reference = analyse_page(image, 100, 'a3') fast = analyse_page_fast(image, 100, 'a3') self.assertEqual(fast['analysis']['fallback'], 'full-spread') self.assertEqual(fast['outputs'], reference['outputs']) self.assertEqual(fast['crossings'], reference['crossings']) def test_unknown_geometry_keeps_reference_behaviour(self): image = np.full((400, 500, 3), 255, np.uint8) self.assertEqual(analyse_page_fast(image, 100, 'unknown'), analyse_page(image, 100, 'unknown'))