386 lines
22 KiB
Python
386 lines
22 KiB
Python
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch
|
|
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
from copienator import left_margin
|
|
from copienator.commands import cutleft
|
|
|
|
|
|
class LeftMarginTests(unittest.TestCase):
|
|
def test_review_previous_restores_settings_and_respects_navigation_guards(self):
|
|
reviewer = cutleft.ImageReviewer.__new__(cutleft.ImageReviewer)
|
|
reviewer.files = [Path('Copie01.pdf'), Path('Copie02.pdf')]
|
|
reviewer.index = 0
|
|
reviewer.is_processing = False
|
|
reviewer.current_shift = -50
|
|
reviewer.current_width_offset = 100
|
|
reviewer.current_max_per_file = 1
|
|
reviewer.default_max_per_file = 5
|
|
reviewer.review_settings = {}
|
|
reviewer.trigger_processing = Mock()
|
|
reviewer.on_previous()
|
|
reviewer.trigger_processing.assert_not_called()
|
|
reviewer._advance()
|
|
self.assertEqual(reviewer.index, 1)
|
|
self.assertEqual((reviewer.current_shift, reviewer.current_width_offset, reviewer.current_max_per_file), (0, 0, 5))
|
|
reviewer.current_shift = 50
|
|
reviewer.is_processing = True
|
|
reviewer.on_previous()
|
|
self.assertEqual(reviewer.index, 1)
|
|
reviewer.is_processing = False
|
|
reviewer.on_previous()
|
|
self.assertEqual(reviewer.index, 0)
|
|
self.assertEqual((reviewer.current_shift, reviewer.current_width_offset, reviewer.current_max_per_file), (-50, 100, 1))
|
|
reviewer.trigger_processing.assert_called_with(Path('Copie01.pdf'), -50)
|
|
reviewer._advance()
|
|
self.assertEqual(reviewer.current_shift, 50)
|
|
|
|
def test_trim_discards_exactly_ten_percent_without_interpolation(self):
|
|
self.assertEqual(left_margin.trimmed_left_edge([100, 1, 9, 8, 7, 6, 5, 4, 3, 2]), 2)
|
|
self.assertEqual(left_margin.trimmed_left_edge([5] * 9), 5)
|
|
self.assertIsNone(left_margin.trimmed_left_edge([]))
|
|
|
|
def test_one_margin_comment_does_not_veto_other_pages(self):
|
|
images = [Image.new('RGB', (1654, 200)) for _ in range(4)]
|
|
bands = [[42] * 40, [42] * 40, [42] * 40, [4] * 8 + [42] * 12]
|
|
estimates = [{'edge_mm': left_margin.trimmed_left_edge(values),
|
|
'bands': len(values), 'band_starts_mm': values} for values in bands]
|
|
with patch.object(left_margin, 'estimate_page', side_effect=estimates):
|
|
result = left_margin.analyse_margins(images, 200, 1654)
|
|
group = result['groups'][0]
|
|
self.assertEqual(group['edge_mm'], 42)
|
|
self.assertEqual(group['left_mm'], 22)
|
|
self.assertEqual(group['aggregation'], 'median-of-page-deciles')
|
|
self.assertEqual(result['starts_px'], [round(22 * 200 / 25.4)] * 3 + [0])
|
|
|
|
def test_short_exception_requires_prior_margin_and_one_small_block(self):
|
|
# At 4 mm per band, eight bands cover four standard 8 mm lines.
|
|
for prior, low_positions, expected in (
|
|
(2, list(range(8)), True),
|
|
(2, list(range(9)), False),
|
|
(1, list(range(8)), False),
|
|
(0, list(range(8)), False),
|
|
(2, [0, 2, 4, 6, 8], False)):
|
|
with self.subTest(prior=prior, low_positions=low_positions):
|
|
images = [Image.new('RGB', (1654, 200)) for _ in range(5)]
|
|
def estimate(image, _dpi):
|
|
index = next(i for i, candidate in enumerate(images) if candidate is image)
|
|
bands = [{'start_mm': 5 if index == prior and i in low_positions else 42,
|
|
'top_mm': i * 4, 'bottom_mm': (i + 1) * 4} for i in range(30)]
|
|
starts = [b['start_mm'] for b in bands]
|
|
return {'edge_mm': left_margin.trimmed_left_edge(starts),
|
|
'bands': len(bands), 'body_bands': bands, 'band_starts_mm': starts}
|
|
with patch.object(left_margin, 'estimate_page', side_effect=estimate):
|
|
result = left_margin.analyse_margins(images, 200, 1654)
|
|
self.assertEqual(result['pages'][prior]['short_margin_exception'], expected)
|
|
self.assertEqual(result['starts_px'][prior], round(22 * 200 / 25.4) if expected else 0)
|
|
self.assertGreater(result['starts_px'][-1], 0)
|
|
|
|
def test_short_exception_rejects_inconsistent_or_insufficient_remainder(self):
|
|
for remainder in ([65] * 22, [42] * 5):
|
|
starts = [5] * 8 + remainder
|
|
estimate = {'body_bands': [{'start_mm': x, 'top_mm': i * 4, 'bottom_mm': (i + 1) * 4}
|
|
for i, x in enumerate(starts)]}
|
|
self.assertFalse(left_margin.short_margin_exception(estimate, 42))
|
|
|
|
def test_repeated_left_writing_beyond_ten_percent_is_retained(self):
|
|
self.assertEqual(left_margin.trimmed_left_edge([10] * 20 + [42] * 80), 10)
|
|
|
|
def test_disagreeing_pages_use_half_margin_when_it_removes_more(self):
|
|
result = self.analyse([33, 35, 41, 39])
|
|
self.assertEqual(result['groups'][0]['left_mm'], 18.5)
|
|
self.assertEqual(result['starts_px'][1:], [round(18.5 * 200 / 25.4)] * 3)
|
|
|
|
def test_small_margin_starts_at_left(self):
|
|
result = self.analyse([10, 15, 8, 13])
|
|
self.assertEqual(result['groups'][0]['left_mm'], 0)
|
|
self.assertEqual(result['starts_px'], [0] * 4)
|
|
|
|
def test_low_pages_do_not_cancel_later_pages_margin(self):
|
|
images = [Image.new('RGB', (1654, 200)) for _ in range(8)]
|
|
# Low pages have fewer occupied bands, so pooling still proposes a margin.
|
|
edges = [42, 42, 10, 12, 42, 42, 42, 42]
|
|
by_id = {id(image): edge for image, edge in zip(images, edges)}
|
|
def estimate(image, _dpi):
|
|
edge = by_id[id(image)]
|
|
bands = 6 if edge < 20 else 100
|
|
return {'edge_mm': edge, 'bands': bands, 'band_starts_mm': [edge] * bands}
|
|
with patch.object(left_margin, 'estimate_page', side_effect=estimate):
|
|
result = left_margin.analyse_margins(images, 200, 1654)
|
|
self.assertEqual(result['groups'][0]['left_mm'], 22)
|
|
self.assertIsNone(result['no_margin_from_page'])
|
|
self.assertEqual(result['starts_px'], [round(22 * 200 / 25.4)] * 2 + [0] * 2 + [round(22 * 200 / 25.4)] * 4)
|
|
|
|
def test_isolated_low_or_blank_pages_do_not_trigger_no_margin_rule(self):
|
|
for edges in ([42, 10, 42, 12], [42, 10, None, 12]):
|
|
with self.subTest(edges=edges):
|
|
result = self.analyse(edges)
|
|
self.assertIsNone(result['no_margin_from_page'])
|
|
|
|
def test_low_pages_do_not_cancel_later_width_groups(self):
|
|
result = self.analyse([42, 42, 10, 12, 42, 42],
|
|
widths=[1654, 1654, 1654, 1654, 2000, 2000],
|
|
band_counts=[100, 100, 6, 6, 100, 100])
|
|
self.assertIsNone(result['no_margin_from_page'])
|
|
# First group's median is 26 mm; the later width group still has 42 mm.
|
|
self.assertEqual(result['starts_px'][0], 0)
|
|
self.assertEqual(result['starts_px'][2:4], [0, 0])
|
|
self.assertTrue(all(result['starts_px'][4:]))
|
|
|
|
def test_larger_cut_distance_and_no_margin_threshold(self):
|
|
for edge, expected in ((0, 0), (20, 0), (25.4, 0), (26.4, 0), (27.9, 0), (28, 0), (31, 0), (32.5, 0), (32.9, 0), (33, 16.5), (33.1, 16.55), (40, 20), (60, 40)):
|
|
with self.subTest(edge=edge):
|
|
result = self.analyse([edge] * 4)
|
|
self.assertEqual(result['starts_px'][1:], [round(expected * 200 / 25.4)] * 3)
|
|
self.assertEqual(result['groups'][0]['status'], 'near-edge' if edge < 33 else 'margin')
|
|
self.assertEqual(result['margin_clearance_mm'], 20)
|
|
self.assertEqual(result['minimum_margin_mm'], 33)
|
|
|
|
def test_body_bands_ignore_header_footer_and_abstain_on_sparse_ink(self):
|
|
mask = np.zeros((1169, 827), bool)
|
|
mask[60:340, 2:120] = True # Name/class block extends past 25% height.
|
|
mask[1000:1050, 2:50] = True # Pagination.
|
|
for y in range(330, 940, 35):
|
|
mask[y:y + 5, 170:270] = True
|
|
result = left_margin.body_edge(mask)
|
|
self.assertAlmostEqual(result['edge_mm'], 43.2, delta=.3)
|
|
self.assertGreaterEqual(result['bands'], 6)
|
|
mask[300:960] = False
|
|
self.assertIsNone(left_margin.body_edge(mask)['edge_mm'])
|
|
|
|
def test_bands_starting_beyond_midpoint_are_empty_for_both_windows(self):
|
|
mask = np.zeros((1169, 827), bool)
|
|
for y in range(370, 1050, 35):
|
|
mask[y:y + 5, 450:550] = True
|
|
result = left_margin.body_edge(mask)
|
|
self.assertIsNone(result['edge_mm'])
|
|
self.assertEqual(result['band_starts_mm'], [])
|
|
self.assertEqual(result['label_band_starts_mm'], [])
|
|
mask[400:410, 100:200] = True
|
|
self.assertTrue(left_margin.body_edge(mask)['band_starts_mm'])
|
|
|
|
def test_label_window_keeps_lower_body_but_excludes_names_and_bottom_eight_percent(self):
|
|
mask = np.zeros((1169, 827), bool)
|
|
mask[100:300, 5:80] = True # Name/header; exclude from both pools.
|
|
mask[1100:1150, 5:80] = True # Footer; below 92%.
|
|
for y in range(370, 920, 35):
|
|
mask[y:y + 5, 170:270] = True
|
|
mask[1000:1010, 40:140] = True # Label at 85.5%; new window only.
|
|
result = left_margin.body_edge(mask)
|
|
self.assertGreater(min(result['band_starts_mm']), 40)
|
|
self.assertAlmostEqual(min(result['label_band_starts_mm']), 10.16)
|
|
|
|
def test_fifth_percentile_detects_labels_while_page_decile_stays_at_body(self):
|
|
images = [Image.new('RGB', (1654, 200)) for _ in range(6)]
|
|
bands = [5.] * 7 + [42.] * 93
|
|
estimate = {'edge_mm': left_margin.trimmed_left_edge(bands),
|
|
'bands': len(bands), 'band_starts_mm': bands,
|
|
'label_band_starts_mm': bands}
|
|
with patch.object(left_margin, 'estimate_page', return_value=estimate):
|
|
result = left_margin.analyse_margins(images, 200, 1654)
|
|
group = result['groups'][0]
|
|
self.assertEqual(group['edge_mm'], 42)
|
|
self.assertEqual(group['body_left_mm'], 22)
|
|
self.assertEqual(group['label_edge_mm'], 5)
|
|
self.assertTrue(group['labels_in_margin'])
|
|
self.assertEqual(result['starts_px'], [0] * 6)
|
|
|
|
def test_any_coherent_page_can_move_the_whole_group_left(self):
|
|
images = [Image.new('RGB', (1654, 200)) for _ in range(6)]
|
|
def estimate(image, _dpi):
|
|
labels = [5.] * 20 if image is images[2] else []
|
|
return {'edge_mm': 42, 'bands': 12, 'band_starts_mm': [42.] * 12,
|
|
'label_band_starts_mm': labels + [42.] * 12}
|
|
with patch.object(left_margin, 'estimate_page', side_effect=estimate):
|
|
result = left_margin.analyse_margins(images, 200, 1654)
|
|
group = result['groups'][0]
|
|
self.assertIn(3, [p['page'] for p in group['samples']])
|
|
self.assertIn(3, group['coherent_pages'])
|
|
self.assertEqual(result['starts_px'], [0] * 6)
|
|
|
|
def test_sparse_or_incoherent_pages_do_not_supply_label_evidence(self):
|
|
images = [Image.new('RGB', (1654, 200)) for _ in range(6)]
|
|
edges = [42, 42, None, 42, 65, 42]
|
|
def estimate(image, _dpi):
|
|
edge = edges[next(i for i, candidate in enumerate(images) if candidate is image)]
|
|
return {'edge_mm': edge, 'bands': 12, 'band_starts_mm': [edge or 42.] * 12,
|
|
'label_band_starts_mm': [1.] * 20 if edge in (None, 65) else [42.] * 12}
|
|
with patch.object(left_margin, 'estimate_page', side_effect=estimate):
|
|
result = left_margin.analyse_margins(images, 200, 1654)
|
|
group = result['groups'][0]
|
|
self.assertEqual(group['coherent_pages'], [1, 2, 4, 6])
|
|
self.assertFalse(group['labels_in_margin'])
|
|
self.assertGreater(result['starts_px'][0], 0)
|
|
|
|
def test_less_than_five_percent_noise_or_small_gap_does_not_trigger_labels(self):
|
|
for bands in ([1.] * 4 + [42.] * 96, [35.] * 7 + [42.] * 93):
|
|
with self.subTest(bands=bands[:8]):
|
|
images = [Image.new('RGB', (1654, 200)) for _ in range(2)]
|
|
estimate = {'edge_mm': 42, 'bands': 100, 'band_starts_mm': [42.] * 100,
|
|
'label_band_starts_mm': bands}
|
|
with patch.object(left_margin, 'estimate_page', return_value=estimate):
|
|
result = left_margin.analyse_margins(images, 200, 1654)
|
|
self.assertFalse(result['groups'][0]['labels_in_margin'])
|
|
self.assertGreater(result['starts_px'][0], 0)
|
|
|
|
def test_localized_name_bottom_reveals_labels_above_old_cutoff(self):
|
|
mask = np.zeros((1169, 827), bool)
|
|
mask[50:150, 4:100] = True # Name above localized bottom.
|
|
mask[250:265, 30:110] = True # Heading between 20% and 30%.
|
|
for y in range(400, 920, 35):
|
|
mask[y:y + 5, 170:270] = True
|
|
old = left_margin.body_edge(mask)
|
|
localized = left_margin.body_edge(mask, top_fraction=.20)
|
|
self.assertGreater(min(old['label_band_starts_mm']), 40)
|
|
self.assertAlmostEqual(min(localized['label_band_starts_mm']), 7.62)
|
|
self.assertTrue(all(start > 2 for start in localized['label_band_starts_mm']))
|
|
|
|
def test_first_page_name_fraction_is_reused_on_every_page(self):
|
|
images = [Image.new('RGB', (1654, 200)) for _ in range(6)]
|
|
estimate = {'edge_mm': 42, 'bands': 12, 'band_starts_mm': [42.] * 12,
|
|
'label_band_starts_mm': [42.] * 12}
|
|
with patch.object(left_margin, 'estimate_page', return_value=estimate) as estimator:
|
|
result = left_margin.analyse_margins(images, 200, 1654,
|
|
name_bottom_height_fraction=.14)
|
|
self.assertEqual(estimator.call_count, 6)
|
|
self.assertTrue(all(call.kwargs == {'top_fraction': .14}
|
|
for call in estimator.call_args_list))
|
|
self.assertEqual(result['body_height_fraction'], [.14, .82])
|
|
self.assertEqual(result['label_height_fraction'], [.14, .92])
|
|
self.assertEqual(result['top_cutoff_source'], 'localized-name')
|
|
|
|
def test_unavailable_or_invalid_name_fraction_keeps_fallback(self):
|
|
images = [Image.new('RGB', (1654, 200))]
|
|
estimate = {'edge_mm': None, 'bands': 0, 'band_starts_mm': [],
|
|
'label_band_starts_mm': []}
|
|
for value in (None, 0, -1, 1.1, True, '0.1', float('nan')):
|
|
with self.subTest(value=value), patch.object(left_margin, 'estimate_page', return_value=estimate):
|
|
result = left_margin.analyse_margins(images, 200, 1654,
|
|
name_bottom_height_fraction=value)
|
|
self.assertEqual(result['body_height_fraction'][0], .30)
|
|
self.assertEqual(result['top_cutoff_source'], 'fallback')
|
|
self.assertIsNone(result['name_bottom_height_fraction'])
|
|
self.assertIsNone(left_margin.body_edge(np.zeros((100, 100), bool), .95)['edge_mm'])
|
|
|
|
def test_cutleft_reads_validated_name_cache_and_rejects_stale_source(self):
|
|
import json
|
|
import tempfile
|
|
from copienator.annotation_layout import fingerprint
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / 'Copies').mkdir()
|
|
names = root / 'Cutleft' / 'Names'
|
|
names.mkdir(parents=True)
|
|
pdf = root / 'Copies' / 'Copie01.pdf'
|
|
pdf.write_bytes(b'original-pdf')
|
|
crop = names / 'Copie01.jpg'
|
|
crop.write_bytes(b'name-crop')
|
|
(names / 'Copie01_result.json').write_text(json.dumps({
|
|
'name': 'Student', 'source_sha256': fingerprint(pdf),
|
|
'crop_sha256': fingerprint(crop), 'name_bottom_height_fraction': .14}))
|
|
images = [Image.new('RGB', (900, 600), 'white')]
|
|
for expected in (.14, None):
|
|
with patch.object(cutleft, 'get_pdf_pages', return_value=images), \
|
|
patch.object(cutleft, 'analyse_margins', return_value={'starts_px': [0], 'groups': []}) as analyse:
|
|
self.assertIsNotNone(cutleft.process_single_pdf(pdf))
|
|
self.assertEqual(analyse.call_args.kwargs['name_bottom_height_fraction'], expected)
|
|
pdf.write_bytes(b'changed-pdf')
|
|
|
|
def test_real_mask_rejects_ruling_and_preserves_handwriting(self):
|
|
image = Image.new('RGB', (827, 1169), 'white')
|
|
draw = ImageDraw.Draw(image)
|
|
for x in range(12, 827, 20):
|
|
draw.line((x, 0, x, 1169), fill=(170, 190, 210))
|
|
for y in range(10, 1169, 20):
|
|
draw.line((0, y, 827, y), fill=(170, 190, 210))
|
|
font = ImageFont.truetype('DejaVuSans.ttf', 16)
|
|
for y in range(330, 920, 35):
|
|
draw.text((170, y), '1) Pour tout x, f(x) = x + 1', font=font, fill=(20, 20, 100))
|
|
result = left_margin.estimate_page(image, 100)
|
|
self.assertAlmostEqual(result['edge_mm'], 43.2, delta=3)
|
|
|
|
def test_width_groups_tolerate_split_adjustments_and_separate_narrow_pages(self):
|
|
pages = [Image.new('RGB', (w, 200)) for w in (1600, 1694, 1654, 800, 810, 2000)]
|
|
self.assertEqual(left_margin.format_groups(pages, 1654), [[0, 1, 2], [3, 4], [5]])
|
|
|
|
def analyse(self, edges, widths=None, band_counts=None):
|
|
images = [Image.new('RGB', (w, 200)) for w in (widths or [1654] * len(edges))]
|
|
by_id = {id(image): edge for image, edge in zip(images, edges)}
|
|
counts = {id(image): count for image, count in zip(images, band_counts or [12] * len(edges))}
|
|
def estimate(image, _dpi):
|
|
edge = by_id[id(image)]
|
|
return {'edge_mm': edge, 'bands': counts[id(image)] if edge is not None else 0,
|
|
'band_starts_mm': [edge] * counts[id(image)] if edge is not None else []}
|
|
with patch.object(left_margin, 'estimate_page', side_effect=estimate):
|
|
return left_margin.analyse_margins(images, 200, max(im.width for im in images))
|
|
|
|
def test_clear_margin_uses_same_conservative_edge_on_every_page(self):
|
|
result = self.analyse([43, 41, 46, 44])
|
|
self.assertEqual(result['groups'][0]['status'], 'margin')
|
|
self.assertEqual(result['starts_px'], [round(23.5 * 200 / 25.4)] * 4)
|
|
self.assertNotIn('first_page_body_start_px', result)
|
|
|
|
def test_sparse_groups_stay_at_left(self):
|
|
for edges in ([None] * 4, [40, None, None, None]):
|
|
with self.subTest(edges=edges):
|
|
self.assertEqual(self.analyse(edges)['starts_px'], [0] * 4)
|
|
|
|
def test_narrow_subpages_stay_at_left_even_with_indented_text(self):
|
|
result = self.analyse([45, 45, 70, 72], widths=[1654, 1654, 800, 800])
|
|
self.assertEqual(result['starts_px'][2:], [0, 0])
|
|
self.assertEqual(result['groups'][1]['status'], 'narrow-page')
|
|
|
|
def test_auto_crop_applies_manual_offsets_to_cached_estimate(self):
|
|
images = [Image.new('RGB', (900, 600), 'white') for _ in range(2)]
|
|
images[0].paste('blue', (150, 0, 190, 100))
|
|
analysis = {'starts_px': [150, 150], 'groups': []}
|
|
with patch.object(cutleft, 'get_pdf_pages', return_value=images), \
|
|
patch.object(cutleft, 'analyse_margins', return_value=analysis) as analyse:
|
|
result = cutleft.process_single_pdf(Path('/tmp/left-margin/Copy.pdf'))
|
|
shifted = cutleft.process_single_pdf(Path('/tmp/left-margin/Copy.pdf'),
|
|
shift_offset=-50, width_offset=50,
|
|
margin_analysis=analysis)
|
|
analyse.assert_called_once()
|
|
self.assertEqual(result[1][0].getpixel((0, 0)), (0, 0, 255))
|
|
self.assertEqual([p['left_px'] for p in result[2]['page_crops']], [150, 150])
|
|
self.assertEqual([p['left_px'] for p in shifted[2]['page_crops']], [100, 100])
|
|
self.assertEqual(result[2]['columns_per_file'], [2])
|
|
self.assertEqual(result[1][0].size, (665, 600))
|
|
self.assertEqual(shifted[1][0].size, (765, 600))
|
|
|
|
def test_fullpage_bypasses_analysis(self):
|
|
with patch.object(cutleft, 'get_pdf_pages', return_value=[Image.new('RGB', (900, 600))]), \
|
|
patch.object(cutleft, 'analyse_margins') as analyse:
|
|
result = cutleft.process_single_pdf(Path('/tmp/left-margin/Copy.pdf'), max_per_file=1)
|
|
analyse.assert_not_called()
|
|
self.assertIsNone(result[2]['auto_margin'])
|
|
|
|
def test_wider_auto_strip_is_capped_at_page_edge(self):
|
|
images = [Image.new('RGB', (900, 600)), Image.new('RGB', (200, 600))]
|
|
with patch.object(cutleft, 'get_pdf_pages', return_value=images):
|
|
result = cutleft.process_single_pdf(Path('/tmp/left-margin/Copy.pdf'),
|
|
margin_analysis={'starts_px': [750, 0], 'groups': []})
|
|
self.assertEqual([crop['right_px'] for crop in result[2]['page_crops']], [900, 200])
|
|
self.assertEqual(result[2]['full_width_pages'], [2])
|
|
|
|
def test_first_page_uses_same_crop_width_as_later_pages(self):
|
|
images = [Image.new('RGB', (900, 600), 'white') for _ in range(2)]
|
|
images[0].paste('blue', (420, 100, 450, 200))
|
|
analysis = {'starts_px': [150, 150], 'groups': []}
|
|
with patch.object(cutleft, 'get_pdf_pages', return_value=images):
|
|
result = cutleft.process_single_pdf(Path('/tmp/left-margin/Copy.pdf'),
|
|
margin_analysis=analysis)
|
|
self.assertEqual(result[2]['page_crops'][0]['right_px'], 480)
|
|
self.assertEqual(result[1][0].getpixel((290, 120)), (0, 0, 255))
|
|
self.assertEqual(result[1][0].size, (665, 600))
|
|
self.assertEqual([crop['left_px'] for crop in result[2]['page_crops']], [150, 150])
|
|
|
|
def test_manual_margin_cli_disables_automatic_positioning(self):
|
|
with patch.object(cutleft, 'run', return_value=0) as run, \
|
|
patch.object(cutleft, 'workspace_from_target', return_value=(None, Path('/tmp'))):
|
|
self.assertEqual(cutleft.main(['/tmp', '--manual-margin']), 0)
|
|
self.assertFalse(run.call_args.kwargs['automatic_margin'])
|