Use per-page annotation spacing with inferred margin fallback
This commit is contained in:
1 parent
b069805988
commit
0bb6f7d97c
10 files changed
+170
-39
No files matched your search
@@ -61,13 +61,26 @@ def move_answer_pdf(source, destination):
|
||||
metadata_path(source).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def added_margin(margin_mm):
|
||||
"""Interpolate the configured (millimetres, pixels) points, clamping at ends."""
|
||||
if margin_mm is None:
|
||||
return configuration.ANNOTATION_MARGIN_LEFT
|
||||
points = configuration.ANNOTATION_MARGIN_POINTS
|
||||
if (not points or any(x < 0 or type(y) is not int or y < 0 for x, y in points)
|
||||
or any(a[0] >= b[0] for a, b in zip(points, points[1:]))):
|
||||
raise ValueError('Annotation margin points must have increasing mm and non-negative integer pixels')
|
||||
if margin_mm <= points[0][0]:
|
||||
return points[0][1]
|
||||
for (x0, y0), (x1, y1) in zip(points, points[1:]):
|
||||
if margin_mm <= x1:
|
||||
return round(y0 + (y1 - y0) * (margin_mm - x0) / (x1 - x0))
|
||||
return points[-1][1]
|
||||
|
||||
|
||||
def layout_for(pdf):
|
||||
low = any(part.get('margin_status') == 'low' for part in answer_parts(pdf))
|
||||
margin = (configuration.ANNOTATION_LOW_MARGIN_LEFT if low
|
||||
else configuration.ANNOTATION_MARGIN_LEFT)
|
||||
if type(margin) is not int or margin < 0:
|
||||
raise ValueError('Annotation left margins must be non-negative integers')
|
||||
return {'version': 1, 'margin_left': margin, 'narrow_left_pad_limit': 300}
|
||||
margins = [added_margin(part.get('margin_mm')) for part in answer_parts(pdf)]
|
||||
return {'version': 2, 'margin_left': max(margins, default=added_margin(None)),
|
||||
'page_margins': margins, 'narrow_left_pad_limit': 0}
|
||||
|
||||
|
||||
def legacy_layout():
|
||||
|
||||
@@ -63,9 +63,13 @@ def make_base_image(pdf_path):
|
||||
base_img = Image.new("RGBA", (max_w, total_h), "white")
|
||||
|
||||
current_y = 0
|
||||
page_regions = []
|
||||
for page in pages:
|
||||
base_img.paste(page.convert("RGBA"), ((max_w - page.width) // 2, current_y))
|
||||
x = (max_w - page.width) // 2
|
||||
base_img.paste(page.convert("RGBA"), (x, current_y))
|
||||
page_regions.append({'x': x, 'y': current_y, 'width': page.width, 'height': page.height})
|
||||
current_y += page.height
|
||||
base_img.info['page_regions'] = page_regions
|
||||
return (base_img, total_h, max_w)
|
||||
|
||||
def normalize_mathtext(text):
|
||||
@@ -352,6 +356,10 @@ def compose_label_image(base_img, label, result, hmin,
|
||||
|
||||
layout = dict(layout if layout is not None else layout_for(source_pdf))
|
||||
margin_left = layout['margin_left']
|
||||
page_regions = base_img.info.get('page_regions', [
|
||||
{'x': 0, 'y': 0, 'width': base_img.width, 'height': base_img.height}])
|
||||
page_margins = layout.get('page_margins', [])
|
||||
per_page = layout.get('version', 1) >= 2
|
||||
left_pad = 0
|
||||
if base_img.width < TARGET_MIN_WIDTH:
|
||||
total_missing = TARGET_MIN_WIDTH - base_img.width
|
||||
@@ -411,7 +419,11 @@ def compose_label_image(base_img, label, result, hmin,
|
||||
total_height = base_img.height + header_height
|
||||
|
||||
# Create Canvas
|
||||
final_img = Image.new("RGB", (base_img.width + margin_left, total_height), "white")
|
||||
final_width = (max(TARGET_MIN_WIDTH + margin_left,
|
||||
max(region['width'] + (page_margins[i] if i < len(page_margins) else margin_left)
|
||||
for i, region in enumerate(page_regions)))
|
||||
if per_page else base_img.width + margin_left)
|
||||
final_img = Image.new("RGB", (final_width, total_height), "white")
|
||||
|
||||
# Draw Headers
|
||||
current_y = 0
|
||||
@@ -434,7 +446,13 @@ def compose_label_image(base_img, label, result, hmin,
|
||||
|
||||
# Paste Base Image
|
||||
image_offset_y = current_y
|
||||
final_img.paste(base_img, (margin_left, image_offset_y))
|
||||
if per_page:
|
||||
for i, region in enumerate(page_regions):
|
||||
x, y, w, h = (region[key] for key in ('x', 'y', 'width', 'height'))
|
||||
margin = page_margins[i] if i < len(page_margins) else margin_left
|
||||
final_img.paste(base_img.crop((x, y, x + w, y + h)), (margin, image_offset_y + y))
|
||||
else:
|
||||
final_img.paste(base_img, (margin_left, image_offset_y))
|
||||
|
||||
# 2. Draw Local Annotations
|
||||
draw = ImageDraw.Draw(final_img, "RGBA") # Refresh draw object
|
||||
@@ -451,12 +469,24 @@ def compose_label_image(base_img, label, result, hmin,
|
||||
|
||||
# Draw Rectangle (if not suppressed)
|
||||
if "norectangle" not in fb:
|
||||
draw.rectangle([target_xmin, target_ymin, target_xmax, target_ymax], outline="red", width=3)
|
||||
boxes = [[target_xmin, target_ymin, target_xmax, target_ymax]]
|
||||
if per_page:
|
||||
boxes = []
|
||||
for i, region in enumerate(page_regions):
|
||||
top = max(ymin - hmin, region['y'])
|
||||
bottom = min(ymax - hmin, region['y'] + region['height'])
|
||||
if bottom <= top:
|
||||
continue
|
||||
margin = page_margins[i] if i < len(page_margins) else margin_left
|
||||
shift = margin - region['x']
|
||||
boxes.append([xmin + shift, top + image_offset_y,
|
||||
xmax + shift, bottom + image_offset_y])
|
||||
for box in boxes:
|
||||
draw.rectangle(box, outline="red", width=3)
|
||||
|
||||
if draw_callback:
|
||||
callback_items.append(("local_rect",
|
||||
{"box": [target_xmin, target_ymin, target_xmax, target_ymax]},
|
||||
{"data": fb, "index": idx}))
|
||||
if draw_callback:
|
||||
callback_items.append(("local_rect", {"box": box},
|
||||
{"data": fb, "index": idx}))
|
||||
|
||||
# Render Text
|
||||
txt_img = render_fn(fb['text'], width_px=ANNOT_WIDTH,
|
||||
|
||||
@@ -428,6 +428,11 @@ class ImageReviewer:
|
||||
for group in analysis['groups'])
|
||||
margin_info = (f"Auto margin: {offsets} mm from left"
|
||||
+ (' | Check uncertain pages' if needs_review else '') + '\n')
|
||||
for page in analysis.get('pages', []):
|
||||
if page.get('status') == 'unknown':
|
||||
value = page.get('effective_edge_mm')
|
||||
fallback = f" (copy average: {value:.1f} mm)" if value is not None else ''
|
||||
margin_info += f"Page {page['page']}: unknown margin{fallback}\n"
|
||||
if analysis.get('no_margin_from_page') is not None:
|
||||
margin_info += f"No margin from page {analysis['no_margin_from_page']} onwards\n"
|
||||
self.label_info.configure(
|
||||
|
||||
@@ -23,6 +23,7 @@ from copienator import (
|
||||
)
|
||||
from copienator.filesystem import staged_directory
|
||||
from copienator.annotation_layout import fingerprint, metadata_path, save_parts
|
||||
from copienator.left_margin import margins_with_fallback
|
||||
|
||||
SQUARE = 1000 // 38
|
||||
ANSWER_TOP_PADDING_POINTS = 4 * 72 / 25.4
|
||||
@@ -133,7 +134,7 @@ def _render_split_outputs(
|
||||
provenance: defaultdict[str, list[dict]] = defaultdict(list)
|
||||
schema = read_json(input_pdf.parent.parent / 'Cutleft' / f'{input_pdf.stem}_schema.json', default={})
|
||||
analysis = schema.get('auto_margin') or {}
|
||||
measurements = ({item['page']: item for item in analysis.get('pages', [])}
|
||||
measurements = ({item['page']: item for item in margins_with_fallback(analysis.get('pages', []))}
|
||||
if schema.get('source_sha256') == fingerprint(input_pdf) else {})
|
||||
with tempfile.TemporaryDirectory(prefix="copienator-split-") as temp_directory:
|
||||
temporary = Path(temp_directory)
|
||||
@@ -206,13 +207,14 @@ def _render_split_outputs(
|
||||
part_path,
|
||||
)
|
||||
parts_by_label[clean_label].append(part_path)
|
||||
edge = measurements.get(page_number + 1, {}).get('edge_mm')
|
||||
measurement = measurements.get(page_number + 1, {})
|
||||
edge = measurement.get('effective_edge_mm')
|
||||
remaining = None if edge is None else max(0, edge - clip.x0 * 25.4 / 72)
|
||||
provenance[clean_label].append({
|
||||
'source': input_pdf.name, 'source_sha256': schema.get('source_sha256'),
|
||||
'page': page_number + 1, 'crop_points': list(clip),
|
||||
'margin_mm': remaining,
|
||||
'margin_status': ('unknown' if remaining is None else
|
||||
'margin_status': ('unknown' if measurement.get('edge_mm') is None else
|
||||
'low' if remaining < 20 else 'normal'),
|
||||
})
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ THINKING_LEVEL_FLASH = "medium"
|
||||
THINKING_LEVEL_PRO = "high"
|
||||
ALWAYS_CROP = False
|
||||
ANNOTATION_MARGIN_LEFT = 300
|
||||
ANNOTATION_LOW_MARGIN_LEFT = 400
|
||||
ANNOTATION_MARGIN_POINTS = [(10, 450), (20, 400), (35, 300)]
|
||||
ANNOTATION_MAX_HEIGHT_PX = 33000
|
||||
ANNOTATION_MAX_PIXELS = 67_108_864
|
||||
SHOW_PERSONAL_STEPS = False
|
||||
|
||||
@@ -27,6 +27,15 @@ STANDARD_LINE_HEIGHT_MM = 8
|
||||
MAX_EXCEPTION_LINES = 4
|
||||
|
||||
|
||||
def margins_with_fallback(pages: list[dict]) -> list[dict]:
|
||||
"""Keep measurements intact; unknown pages borrow the copy's measured mean."""
|
||||
known = [page['edge_mm'] for page in pages if page.get('edge_mm') is not None]
|
||||
average = sum(known) / len(known) if known else None
|
||||
return [{**page, 'effective_edge_mm': (
|
||||
page['edge_mm'] if page.get('edge_mm') is not None else average
|
||||
)} for page in pages]
|
||||
|
||||
|
||||
def trimmed_left_edge(starts: list[float]) -> float | None:
|
||||
"""Discard the leftmost 10% of occupied bands; retain the next minimum."""
|
||||
if not starts:
|
||||
@@ -142,9 +151,13 @@ def analyse_margins(images: list[Image.Image], render_dpi: int, reference_width:
|
||||
if name_localized else estimate_page(images[index], render_dpi))
|
||||
return page_estimates[index]
|
||||
|
||||
page_estimates = dict(enumerate(margins_with_fallback(
|
||||
[page_estimate(index) for index in range(len(images))])))
|
||||
|
||||
for indices in format_groups(images, reference_width):
|
||||
estimates = {index: page_estimate(index) for index in indices}
|
||||
values = [item['edge_mm'] for item in estimates.values() if item['edge_mm'] is not None]
|
||||
values = [item['effective_edge_mm'] for item in estimates.values()
|
||||
if item['effective_edge_mm'] is not None]
|
||||
band_starts = [start for item in estimates.values() for start in item['band_starts_mm']]
|
||||
status = 'insufficient-ink'
|
||||
edge = round(float(np.median(values)), 1) if values else None
|
||||
@@ -178,10 +191,15 @@ def analyse_margins(images: list[Image.Image], render_dpi: int, reference_width:
|
||||
previous_coherent = 0
|
||||
for index in indices:
|
||||
estimate = page_estimate(index)
|
||||
low = estimate['edge_mm'] is not None and estimate['edge_mm'] < MIN_MARGIN_MM
|
||||
effective_edge = estimate['effective_edge_mm']
|
||||
low = effective_edge is not None and effective_edge < MIN_MARGIN_MM
|
||||
tolerated = (status == 'margin' and low and previous_coherent >= 2
|
||||
and short_margin_exception(estimate, edge))
|
||||
starts[index] = 0 if low and not tolerated else start
|
||||
if estimate['edge_mm'] is None and status not in {'narrow-page', 'labels-in-margin'}:
|
||||
fallback_left = (max(effective_edge / 2, effective_edge - MARGIN_CLEARANCE_MM)
|
||||
if effective_edge is not None and effective_edge >= MIN_MARGIN_MM else 0)
|
||||
starts[index] = round(fallback_left * render_dpi / 25.4)
|
||||
estimate['short_margin_exception'] = tolerated
|
||||
estimate['decision'] = ('short-exception' if tolerated else
|
||||
'page-near-edge' if low else status)
|
||||
@@ -209,7 +227,7 @@ def analyse_margins(images: list[Image.Image], render_dpi: int, reference_width:
|
||||
'status': 'unknown' if edge is None else
|
||||
('low' if edge < MIN_MARGIN_MM else 'normal')})
|
||||
|
||||
return {'version': 13, 'pages': pages, 'starts_px': starts, 'groups': groups,
|
||||
return {'version': 14, 'pages': pages, 'starts_px': starts, 'groups': groups,
|
||||
'no_margin_from_page': None,
|
||||
'discard_left_fraction': DISCARD_LEFT_FRACTION, 'margin_clearance_mm': MARGIN_CLEARANCE_MM,
|
||||
'cut_fraction_of_margin': .5, 'cut_rule': 'maximum',
|
||||
|
||||
+3
-1
@@ -8,7 +8,9 @@ IMPORT_DIR = Path("Import")
|
||||
|
||||
# Espace ajouté à gauche des réponses annotées, en pixels à 200 dpi.
|
||||
ANNOTATION_MARGIN_LEFT = 300
|
||||
ANNOTATION_LOW_MARGIN_LEFT = 400
|
||||
# (Marge mesurée en mm, espace ajouté en pixels à 200 dpi).
|
||||
# Interpolation linéaire entre les points ; valeurs constantes aux extrémités.
|
||||
ANNOTATION_MARGIN_POINTS = [(10, 450), (20, 400), (35, 300)]
|
||||
|
||||
# Hauteur maximale des documents Bnot et des groupes BGnot, en pixels.
|
||||
# Les blocs d'élèves trop grands sont séparés entre deux réponses.
|
||||
|
||||
+14
-6
@@ -10,13 +10,18 @@ extension **`.jpg`**, not `.jpeg`.
|
||||
|
||||
```python
|
||||
ANNOTATION_MARGIN_LEFT = 300
|
||||
ANNOTATION_LOW_MARGIN_LEFT = 400
|
||||
ANNOTATION_MARGIN_POINTS = [(10, 450), (20, 400), (35, 300)]
|
||||
```
|
||||
|
||||
These are added pixels at the 200 dpi annotation rendering scale. An answer
|
||||
uses the larger margin if any of its source fragments has a measured left
|
||||
margin below 20 mm (half the standard 40 mm). Unknown or insufficiently written
|
||||
pages retain the normal spacing. The 600 px comment width is unchanged, so
|
||||
The reference points are (measured margin in mm, added pixels at 200 dpi).
|
||||
Each page fragment gets its own spacing: 450 px at or below 10 mm, 400 px at
|
||||
20 mm, and 300 px at or above 35 mm, with linear interpolation between points.
|
||||
Unknown pages retain their unknown status but use the arithmetic mean of the
|
||||
copy's measured page margins, including during label-strip positioning.
|
||||
Inferred values never contribute to that mean. If no page has a measurement,
|
||||
`ANNOTATION_MARGIN_LEFT` supplies the default 300 px.
|
||||
The old `ANNOTATION_LOW_MARGIN_LEFT` setting is replaced by the reference points.
|
||||
The 600 px comment width is unchanged, so
|
||||
comments can still overlap handwriting on pages with very little whitespace.
|
||||
|
||||
Automatic `crop-labels` saves per-page measurements in the Cutleft schema.
|
||||
@@ -33,7 +38,10 @@ save their rendering geometry and reuse it when read back, even after settings
|
||||
change. Older batches without saved geometry keep their original 300 px offset.
|
||||
Do not overwrite a batch that still contains handwritten corrections to import.
|
||||
|
||||
All fragments of one answer share one left offset. BGnot pads narrower answer
|
||||
Each fragment is positioned independently, with its annotation rectangles shifted
|
||||
by the same amount. Rectangles crossing page boundaries are split at those boundaries.
|
||||
Narrow pages receive any additional width on the right, so left padding does not
|
||||
stack with the configured allowance. BGnot pads narrower answer
|
||||
images with white without resizing handwriting; fitting the wider document to
|
||||
screen or paper may reduce the displayed scale, and return-PDF page breaks can
|
||||
change. The extra space is also used by Anot and Bnot.
|
||||
|
||||
@@ -9,7 +9,7 @@ from PIL import Image
|
||||
from copienator import atomic_write_json, configuration, read_json
|
||||
from copienator.annotation_layout import (
|
||||
answer_parts, copy_answer_pdf, fingerprint, layout_for, legacy_layout,
|
||||
move_answer_pdf, save_parts,
|
||||
move_answer_pdf, save_parts, added_margin,
|
||||
)
|
||||
from copienator.commands import annotating, annotating_by_label, splitting_int
|
||||
from copienator.commands.annotating_with_checks import CheckboxRenderer
|
||||
@@ -42,7 +42,8 @@ class AnnotationLayoutTests(unittest.TestCase):
|
||||
with patch('copienator.left_margin.estimate_page', side_effect=estimate):
|
||||
result = analyse_margins(images, 200, 600)
|
||||
self.assertEqual([p['status'] for p in result['pages']],
|
||||
['low', 'low', 'normal', 'unknown', 'low', 'normal'])
|
||||
['low', 'low', 'normal', 'unknown', 'low', 'low'])
|
||||
self.assertAlmostEqual(result['pages'][3]['effective_edge_mm'], 19.98)
|
||||
|
||||
def test_split_records_pages_and_rejects_stale_measurements(self):
|
||||
schema_path = self.root / 'Cutleft' / 'Copie01_schema.json'
|
||||
@@ -55,7 +56,7 @@ class AnnotationLayoutTests(unittest.TestCase):
|
||||
splitting_int._render_split_outputs(self.pdf, [('Ex 1', 0, 0, 10, 0, 10)], staging)
|
||||
answer = staging / 'Ex 1.pdf'
|
||||
self.assertEqual([p['page'] for p in answer_parts(answer)], [1, 2])
|
||||
self.assertEqual(layout_for(answer)['margin_left'], 400)
|
||||
self.assertEqual(layout_for(answer)['page_margins'], [300, 450])
|
||||
atomic_write_json(schema_path, {'source_sha256': 'stale', 'auto_margin': {
|
||||
'pages': [{'page': 1, 'edge_mm': 10}]}})
|
||||
splitting_int._render_split_outputs(self.pdf, [('Ex 1', 0, 0, 10, 0, 10)], staging)
|
||||
@@ -63,8 +64,9 @@ class AnnotationLayoutTests(unittest.TestCase):
|
||||
|
||||
def test_configurable_margin_unknown_fallback_and_stale_sidecar(self):
|
||||
self.assertEqual(layout_for(self.pdf)['margin_left'], 300)
|
||||
save_parts(self.pdf, [{'margin_status': 'normal'}, {'margin_status': 'low'}])
|
||||
with patch.object(configuration, 'ANNOTATION_LOW_MARGIN_LEFT', 450):
|
||||
save_parts(self.pdf, [{'margin_status': 'normal', 'margin_mm': 40},
|
||||
{'margin_status': 'low', 'margin_mm': 20}])
|
||||
with patch.object(configuration, 'ANNOTATION_MARGIN_POINTS', [(10, 500), (20, 450), (35, 300)]):
|
||||
self.assertEqual(layout_for(self.pdf)['margin_left'], 450)
|
||||
self.pdf.write_bytes(self.pdf.read_bytes() + b'\n')
|
||||
self.assertEqual(layout_for(self.pdf)['margin_left'], 300)
|
||||
@@ -79,11 +81,12 @@ class AnnotationLayoutTests(unittest.TestCase):
|
||||
# 20% of a 600-point page removes 42.3 mm from the left.
|
||||
splitting_int._render_split_outputs(self.pdf, [('|Ex 1', 0, 0, 10, 100, 110)], staging)
|
||||
answer = staging / 'Ex 1.pdf'
|
||||
self.assertEqual(layout_for(answer)['margin_left'], 400)
|
||||
self.assertEqual(layout_for(answer)['margin_left'], 450)
|
||||
self.assertEqual(answer_parts(answer)[0]['margin_mm'], 0)
|
||||
|
||||
def test_copy_move_and_vertical_cut_preserve_page_classification(self):
|
||||
save_parts(self.pdf, [{'margin_status': 'normal'}, {'margin_status': 'low'}])
|
||||
save_parts(self.pdf, [{'margin_status': 'normal', 'margin_mm': 40},
|
||||
{'margin_status': 'low', 'margin_mm': 20}])
|
||||
copied = self.root / 'copied.pdf'
|
||||
moved = self.root / 'moved.pdf'
|
||||
copy_answer_pdf(self.pdf, copied)
|
||||
@@ -105,6 +108,47 @@ class AnnotationLayoutTests(unittest.TestCase):
|
||||
box = next(call.args[2]['box'] for call in callback.call_args_list if call.args[0] == 'local_rect')
|
||||
return image, header, box
|
||||
|
||||
def test_interpolation_and_clamping(self):
|
||||
for mm, pixels in [(None, 300), (0, 450), (10, 450), (15, 425),
|
||||
(20, 400), (27.5, 350), (35, 300), (80, 300)]:
|
||||
with self.subTest(mm=mm):
|
||||
self.assertEqual(added_margin(mm), pixels)
|
||||
|
||||
def test_unknown_measurement_inherits_average_during_answer_extraction(self):
|
||||
atomic_write_json(self.root / 'Cutleft' / 'Copie01_schema.json', {
|
||||
'source_sha256': fingerprint(self.pdf),
|
||||
'auto_margin': {'pages': [{'page': 1, 'edge_mm': 15}, {'page': 2, 'edge_mm': None}]},
|
||||
})
|
||||
staging = self.root / 'inferred'
|
||||
staging.mkdir()
|
||||
splitting_int._render_split_outputs(self.pdf, [('Ex 1', 0, 0, 10, 0, 10)], staging)
|
||||
answer = staging / 'Ex 1.pdf'
|
||||
self.assertEqual(answer_parts(answer)[1]['margin_status'], 'unknown')
|
||||
self.assertEqual(answer_parts(answer)[1]['margin_mm'], 15)
|
||||
self.assertEqual(layout_for(answer)['page_margins'], [425, 425])
|
||||
|
||||
def test_per_page_offsets_narrow_pages_and_saved_layout(self):
|
||||
save_parts(self.pdf, [{'margin_mm': 10}, {'margin_mm': 35}])
|
||||
pages = [Image.new('RGB', (1000, 100), 'blue'), Image.new('RGB', (800, 100), 'green')]
|
||||
with patch.object(annotating, 'convert_from_path', return_value=pages):
|
||||
base, _, _ = annotating.make_base_image(self.pdf)
|
||||
result = {'score': 2, 'feedback': [{'text': 'Across pages', 'box_2d': [80, 150, 120, 200]}]}
|
||||
callback = Mock()
|
||||
with patch.object(annotating, 'render_score_text', return_value=Image.new('RGB', (200, 40))):
|
||||
def render(**kwargs):
|
||||
return annotating.compose_label_image(
|
||||
base, 'Ex 1', result, 0, render_fn=lambda *a, **k: Image.new('RGBA', (10, 10)),
|
||||
**kwargs)
|
||||
image, header = render(source_pdf=self.pdf, draw_callback=callback)
|
||||
boxes = [call.args[2]['box'] for call in callback.call_args_list if call.args[0] == 'local_rect']
|
||||
self.assertEqual(boxes, [[600, 120, 650, 140], [350, 140, 400, 160]])
|
||||
for x, y, color in [(450, header + 20, (0, 0, 255)), (300, header + 150, (0, 128, 0))]:
|
||||
self.assertEqual(image.getpixel((x, y)), color)
|
||||
self.assertEqual(image.getpixel((x - 1, y)), (255, 255, 255))
|
||||
with patch.object(configuration, 'ANNOTATION_MARGIN_POINTS', [(10, 900)]):
|
||||
restored, _ = render(layout=image.info['annotation_layout'])
|
||||
self.assertEqual(restored.tobytes(), image.tobytes())
|
||||
|
||||
def test_wider_layout_translates_copy_and_feedback_without_scaling(self):
|
||||
normal, height, box = self.render(legacy_layout())
|
||||
low, low_height, low_box = self.render({**legacy_layout(), 'margin_left': 400})
|
||||
@@ -145,7 +189,7 @@ class AnnotationLayoutTests(unittest.TestCase):
|
||||
self.assertEqual(meta['width'], low.width)
|
||||
with Image.open(output / 'Reference.jpg') as reference:
|
||||
self.assertGreater(min(reference.getpixel((low.width - 10, 80))), 245)
|
||||
with patch.object(reader, 'detect_checks_and_notes', return_value=([], Image.new('RGBA', (low.width, low.height * 2)))), patch.object(configuration, 'ANNOTATION_LOW_MARGIN_LEFT', 500):
|
||||
with patch.object(reader, 'detect_checks_and_notes', return_value=([], Image.new('RGBA', (low.width, low.height * 2)))), patch.object(configuration, 'ANNOTATION_MARGIN_POINTS', [(10, 500)]):
|
||||
_, notes = reader._scan_annotation_directory(output)
|
||||
self.assertEqual(notes['02']['Ex 1']['layout']['margin_left'], 400)
|
||||
rerender, _, _ = self.render(notes['02']['Ex 1']['layout'])
|
||||
|
||||
@@ -324,9 +324,18 @@ class LeftMarginTests(unittest.TestCase):
|
||||
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)
|
||||
result = self.analyse([None] * 4)
|
||||
self.assertEqual(result['starts_px'], [0] * 4)
|
||||
self.assertTrue(all(page['effective_edge_mm'] is None for page in result['pages']))
|
||||
|
||||
def test_unknown_pages_borrow_only_measured_copy_average(self):
|
||||
result = self.analyse([40, 50, None, None])
|
||||
self.assertEqual([page['effective_edge_mm'] for page in result['pages']], [40, 50, 45, 45])
|
||||
self.assertEqual(result['pages'][2]['status'], 'unknown')
|
||||
self.assertIsNone(result['pages'][2]['edge_mm'])
|
||||
self.assertEqual(result['starts_px'][2:], [round(25 * 200 / 25.4)] * 2)
|
||||
sparse = self.analyse([40, None, None, None])
|
||||
self.assertEqual(sparse['starts_px'], [round(20 * 200 / 25.4)] * 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])
|
||||
|
||||
Reference in new issue
Block a user