Files
Copies/tests/test_auto_page_split.py
2026-11-03 22:08:36 +01:00

168 lines
7.8 KiB
Python

import tempfile
import unittest
import json
from pathlib import Path
import numpy as np
import pymupdf
from copienator.auto_page_split import analyse_ink, analyse_page, classify_format, plan_masks, run
class AutomaticSplitTests(unittest.TestCase):
def mask(self):
return np.zeros((300, 420), bool)
def plan(self, mask, kind='a3'):
return plan_masks(mask, 25.4, kind)
def test_formats_use_size_not_landscape_alone(self):
pt = 72/25.4
for w,h,expected in [(420,297,'a3'),(210,297,'a4'),(297,210,'a4'),
(297,420,'unknown'),(320,200,'unknown')]:
self.assertEqual(classify_format(w*pt,h*pt),expected)
def test_both_directions_extend_independently(self):
mask = self.mask()
mask[40:43,150:218] = True
mask[100:103,203:280] = True
plan = self.plan(mask)
self.assertEqual([x['owner'] for x in plan['crossings']],['left','right'])
self.assertEqual(plan['extensions_mm'],[10,9])
self.assertTrue(all(o['keep'] for o in plan['outputs']))
def test_disconnected_letters_form_a_line(self):
mask = self.mask()
for x in range(170,219,4):
mask[40:44,x:x+2] = True
plan = self.plan(mask)
self.assertGreater(plan['extensions_mm'][0],0)
self.assertEqual(plan['crossings'][0]['owner'],'left')
def test_detached_terminal_strokes_follow_supported_text_on_either_side(self):
mask=self.mask()
mask[40:44,160:199]=True
mask[41:45,205:219]=True
mask[100:104,221:260]=True
mask[101:105,201:215]=True
plan=self.plan(mask)
self.assertEqual([c['owner'] for c in plan['crossings']],['left','right'])
self.assertGreater(plan['extensions_mm'][0],0)
self.assertGreater(plan['extensions_mm'][1],0)
def test_fold_only_groups_are_artifacts_without_page_anchors(self):
mask=self.mask()
mask[40:43,205:215]=True
plan=self.plan(mask)
self.assertEqual(plan['crossings'][0]['owner'],'artifact')
self.assertEqual(plan['extensions_mm'],[0,0])
self.assertFalse(plan['review_reasons'])
def test_pale_ruling_does_not_attach_to_nearby_text_but_dark_bar_does(self):
mask=np.zeros((600,840),bool)
mask[100:103,418:468]=True
mask[100:120,490:580]=True
credible=np.zeros_like(mask)
credible[100:120,490:580]=True
for colour,owner in [((134,162,200),'artifact'),((80,80,80),'right')]:
with self.subTest(colour=colour):
rgb=np.full((*mask.shape,3),255,np.uint8)
rgb[mask]=(20,20,20)
rgb[100:103,418:468]=colour
plan=plan_masks(mask,200,'a3',content_ink=credible,rgb=rgb)
self.assertEqual(plan['crossings'][0]['owner'],owner)
def test_overflow_does_not_make_other_page_nonblank(self):
mask = self.mask()
mask[40:43,150:218] = True
plan = self.plan(mask)
self.assertEqual([o['state'] for o in plan['outputs']],['content','blank'])
self.assertFalse(plan['outputs'][1]['keep'])
def test_ambiguous_crossing_preserved_on_both(self):
mask = self.mask()
mask[40:43,180:250] = True
plan = self.plan(mask)
self.assertIn('ambiguous-centre-writing',plan['review_reasons'])
self.assertLess(plan['outputs'][1]['rect_px'][0],180)
self.assertGreater(plan['outputs'][0]['rect_px'][2],250)
def test_unknown_format_and_a4(self):
mask = self.mask()
self.assertTrue(self.plan(mask,kind='unknown')['outputs'][0]['keep'])
mask[40:44,50:54] = True
self.assertEqual(self.plan(mask,kind='a4')['outputs'][0]['rect_px'],[0,0,420,300])
def test_unseeded_scan_noise_does_not_veto_blank(self):
rgb = np.full((600,420,3),255,np.uint8)
rgb[200:203,200:202] = 195
plan = analyse_page(rgb,100,'a4')
self.assertEqual(plan['outputs'][0]['state'],'blank')
self.assertEqual(plan['analysis']['early_written'],[False])
def test_faint_strokes_near_reliable_ink_are_recovered(self):
rgb = np.full((600,420,3),255,np.uint8)
rgb[200:210,200:204] = 195
rgb[200:210,204:208] = 30
ink,_ = analyse_ink(rgb,100)
self.assertTrue(ink[200:210,204:208].any())
self.assertEqual(analyse_page(rgb,100,'a4')['outputs'][0]['state'],'content')
def test_spread_artifacts_cannot_veto_blank_logical_half(self):
spread=self.mask()
spread[100:105,208:213]=True
content=self.mask()
content[30:40,300:350]=True
plan=plan_masks(spread,25.4,'a3',content_ink=content)
self.assertEqual([o['state'] for o in plan['outputs']],['blank','content'])
self.assertEqual(plan['crossings'][0]['owner'],'artifact')
self.assertFalse(plan['review_reasons'])
def test_export_preserves_visible_rotation_cropbox_and_sources(self):
for rotation in (0,90,180,270):
with self.subTest(rotation=rotation), tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root/'source.pdf'
with pymupdf.open() as doc:
# Crop offsets and all rotations; displayed geometry is A3.
w,h = (420,297) if rotation%180==0 else (297,420)
pt = 72/25.4
p = doc.new_page(width=w*pt+20,height=h*pt+30)
p.draw_rect(pymupdf.Rect(50,50,w*pt-50,h*pt-50),fill=(0,0,1))
p.draw_rect(pymupdf.Rect(100,100,150,180),fill=(1,0,0))
p.set_cropbox(pymupdf.Rect(10,15,w*pt+10,h*pt+15))
p.set_rotation(rotation)
doc.save(source)
before = source.read_bytes()
summary = run(source,root/'result',dpi=100)
self.assertEqual(summary['output_pages'],2)
self.assertEqual(source.read_bytes(),before)
with pymupdf.open(source) as original, pymupdf.open(root/'result/pdfs/source.pdf') as result:
outputs=json.loads((root/'result/report.json').read_text())['pages'][0]['outputs']
for page,output in zip(result,outputs):
self.assertEqual(page.rotation,0)
self.assertAlmostEqual(page.rect.height,original[0].rect.height,places=3)
# Ambiguous full-width rectangle is deliberately duplicated.
a = original[0].get_pixmap(matrix=pymupdf.Matrix(.3,.3))
b = page.get_pixmap(matrix=pymupdf.Matrix(.3,.3))
self.assertGreater(b.width, a.width*.8)
expected=original[0].get_pixmap(clip=pymupdf.Rect(output['crop_points']))
actual=page.get_pixmap()
aa=np.frombuffer(expected.samples,np.uint8).reshape(expected.height,expected.width,3)
bb=np.frombuffer(actual.samples,np.uint8).reshape(actual.height,actual.width,3)
h,w=min(aa.shape[0],bb.shape[0]),min(aa.shape[1],bb.shape[1])
self.assertLess(np.abs(aa[:h,:w].astype(float)-bb[:h,:w]).mean(),2)
with self.assertRaises(FileExistsError):
run(source,root/'result',dpi=100)
def test_all_blank_copy_has_report_without_invalid_empty_pdf(self):
with tempfile.TemporaryDirectory() as directory:
root=Path(directory)
with pymupdf.open() as doc:
doc.new_page(width=210*72/25.4,height=297*72/25.4)
doc.save(root/'blank.pdf')
summary=run(root/'blank.pdf',root/'result',dpi=100)
self.assertEqual(summary['blank_removed'],1)
self.assertFalse((root/'result/pdfs/blank.pdf').exists())
self.assertTrue((root/'result/report.json').exists())