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

+88
View File
@@ -0,0 +1,88 @@
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'))