168 lines
7.8 KiB
Python
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())
|