Enforce configurable height and pixel limits for tablet documents
This commit is contained in:
1 parent
8918d64452
commit
a38c1ff4b3
10 files changed
+187
-57
No files matched your search
@@ -1,12 +1,19 @@
|
||||
"""Locate tablet documents, including the parts of a large checked copy."""
|
||||
from pathlib import Path
|
||||
|
||||
from copienator.configuration import ANNOTATION_MAX_HEIGHT_PX
|
||||
from copienator.configuration import ANNOTATION_MAX_HEIGHT_PX, ANNOTATION_MAX_PIXELS
|
||||
from copienator.json_io import read_json
|
||||
|
||||
if type(ANNOTATION_MAX_HEIGHT_PX) is not int or ANNOTATION_MAX_HEIGHT_PX <= 0:
|
||||
raise ValueError("ANNOTATION_MAX_HEIGHT_PX must be a positive integer (pixels)")
|
||||
MAX_HEIGHT_PX = ANNOTATION_MAX_HEIGHT_PX
|
||||
if type(ANNOTATION_MAX_PIXELS) is not int or ANNOTATION_MAX_PIXELS <= 0:
|
||||
raise ValueError("ANNOTATION_MAX_PIXELS must be a positive integer (pixels)")
|
||||
MAX_PIXELS = ANNOTATION_MAX_PIXELS
|
||||
|
||||
|
||||
def document_fits(width: int, height: int, max_height: int, max_pixels: int) -> bool:
|
||||
return height <= max_height and width * height <= max_pixels
|
||||
|
||||
|
||||
def copy_documents(directory: Path) -> list[Path]:
|
||||
|
||||
@@ -23,7 +23,7 @@ from copienator import (
|
||||
workspace_from_args,
|
||||
)
|
||||
from copienator.annotation_data import load_annotation_data
|
||||
from copienator.annotation_documents import MAX_HEIGHT_PX
|
||||
from copienator.annotation_documents import MAX_HEIGHT_PX, MAX_PIXELS, document_fits
|
||||
from copienator.commands import annotating, annotating_with_checks
|
||||
from copienator.commands.import_annotations import clear_import_directory
|
||||
from copienator.filesystem import staged_directory
|
||||
@@ -64,10 +64,11 @@ def save_batch(batch, prefix, group_id, output_root: Path) -> None:
|
||||
print(f"Generating group PDF: {prefix} G{group_id} ({len(batch)} elements)")
|
||||
max_width = max(item[2].width for item in batch)
|
||||
total_height = sum(item[2].height for item in batch)
|
||||
if total_height > MAX_HEIGHT_PX and len({item[0] for item in batch}) > 1:
|
||||
if not document_fits(max_width, total_height, MAX_HEIGHT_PX, MAX_PIXELS):
|
||||
raise CliError(
|
||||
f"{prefix} G{group_id} would be {total_height} px high; "
|
||||
f"only a single student's block may exceed {MAX_HEIGHT_PX} px."
|
||||
f"{prefix} G{group_id} would be {max_width} × {total_height} px "
|
||||
f"({max_width * total_height} pixels); document limits are "
|
||||
f"{MAX_HEIGHT_PX} px high and {MAX_PIXELS} pixels."
|
||||
)
|
||||
concatenated = Image.new("RGB", (max_width, total_height), "white")
|
||||
draw = ImageDraw.Draw(concatenated)
|
||||
@@ -281,6 +282,26 @@ def _existing_group_state(
|
||||
def split_batches(rendered):
|
||||
batches = []
|
||||
heights = []
|
||||
widths = []
|
||||
|
||||
def fits(width, height):
|
||||
return document_fits(width, height, MAX_HEIGHT_PX, MAX_PIXELS)
|
||||
|
||||
def add_block(items):
|
||||
width = max(item[2].width for item in items)
|
||||
height = sum(item[2].height for item in items)
|
||||
# Reuse space, accounting for padding to the widest answer in a group.
|
||||
for batch_index in range(len(batches)):
|
||||
combined_width = max(widths[batch_index], width)
|
||||
combined_height = heights[batch_index] + height
|
||||
if fits(combined_width, combined_height):
|
||||
batches[batch_index].extend(items)
|
||||
widths[batch_index] = combined_width
|
||||
heights[batch_index] = combined_height
|
||||
return
|
||||
batches.append(list(items))
|
||||
widths.append(width)
|
||||
heights.append(height)
|
||||
|
||||
index = 0
|
||||
while index < len(rendered):
|
||||
@@ -291,22 +312,31 @@ def split_batches(rendered):
|
||||
student_items = rendered[index:end]
|
||||
student_height = sum(item[2].height for item in student_items)
|
||||
|
||||
if student_height > MAX_HEIGHT_PX:
|
||||
# Oversized blocks remain intact in a group of their own.
|
||||
batches.append(list(student_items))
|
||||
heights.append(student_height)
|
||||
index = end
|
||||
continue
|
||||
|
||||
# Reuse space in earlier groups, keeping each student's block intact.
|
||||
for batch_index, height in enumerate(heights):
|
||||
if height + student_height <= MAX_HEIGHT_PX:
|
||||
batches[batch_index].extend(student_items)
|
||||
heights[batch_index] += student_height
|
||||
break
|
||||
student_width = max(item[2].width for item in student_items)
|
||||
if fits(student_width, student_height):
|
||||
add_block(student_items)
|
||||
else:
|
||||
batches.append(list(student_items))
|
||||
heights.append(student_height)
|
||||
# Split oversized student blocks only between complete answers.
|
||||
block = []
|
||||
width = height = 0
|
||||
for item in student_items:
|
||||
image = item[2]
|
||||
if not fits(image.width, image.height):
|
||||
raise CliError(
|
||||
f"Copie{student_id} ({item[1]}) is {image.width} × "
|
||||
f"{image.height} px ({image.width * image.height} pixels); "
|
||||
f"a single answer cannot be split. Document limits are "
|
||||
f"{MAX_HEIGHT_PX} px high and {MAX_PIXELS} pixels."
|
||||
)
|
||||
if block and not fits(max(width, image.width), height + image.height):
|
||||
add_block(block)
|
||||
block = []
|
||||
width = height = 0
|
||||
block.append(item)
|
||||
width = max(width, image.width)
|
||||
height += image.height
|
||||
if block:
|
||||
add_block(block)
|
||||
index = end
|
||||
|
||||
return batches
|
||||
|
||||
@@ -26,7 +26,7 @@ from copienator import (
|
||||
workspace_from_target,
|
||||
)
|
||||
from copienator.annotation_data import load_annotation_data
|
||||
from copienator.annotation_documents import MAX_HEIGHT_PX, copy_documents
|
||||
from copienator.annotation_documents import MAX_HEIGHT_PX, MAX_PIXELS, copy_documents, document_fits
|
||||
from copienator.commands import annotating
|
||||
from copienator.commands.import_annotations import clear_import_directory
|
||||
from copienator.filesystem import staged_directory
|
||||
@@ -212,18 +212,23 @@ def _render_student(
|
||||
batches = []
|
||||
start = 0
|
||||
height = 0
|
||||
width = 0
|
||||
for index, image in enumerate(label_images):
|
||||
if image.height > MAX_HEIGHT_PX:
|
||||
if not document_fits(image.width, image.height, MAX_HEIGHT_PX, MAX_PIXELS):
|
||||
raise CliError(
|
||||
f"Copie{student_id} ({bnote_entries[index]['label']}) is "
|
||||
f"{image.height} px high; a single answer cannot be split and "
|
||||
f"the maximum document height is {MAX_HEIGHT_PX} px."
|
||||
f"{image.width} × {image.height} px ({image.width * image.height} pixels); "
|
||||
f"a single answer cannot be split. Document limits are "
|
||||
f"{MAX_HEIGHT_PX} px high and {MAX_PIXELS} pixels."
|
||||
)
|
||||
if height + image.height > MAX_HEIGHT_PX:
|
||||
if not document_fits(max(width, image.width), height + image.height,
|
||||
MAX_HEIGHT_PX, MAX_PIXELS):
|
||||
batches.append((start, index))
|
||||
start = index
|
||||
height = 0
|
||||
width = 0
|
||||
height += image.height
|
||||
width = max(width, image.width)
|
||||
batches.append((start, len(label_images)))
|
||||
|
||||
with staged_directory(output_dir) as staging:
|
||||
@@ -251,6 +256,12 @@ def _save_document(
|
||||
) -> None:
|
||||
max_width = max(image.width for image in label_images)
|
||||
total_height = sum(image.height for image in label_images)
|
||||
if not document_fits(max_width, total_height, MAX_HEIGHT_PX, MAX_PIXELS):
|
||||
raise CliError(
|
||||
f"Document would be {max_width} × {total_height} px "
|
||||
f"({max_width * total_height} pixels); document limits are "
|
||||
f"{MAX_HEIGHT_PX} px high and {MAX_PIXELS} pixels."
|
||||
)
|
||||
concatenated = Image.new("RGB", (max_width, total_height), "white")
|
||||
checkbox_map: list[dict[str, Any]] = []
|
||||
current_y = 0
|
||||
|
||||
@@ -41,7 +41,8 @@ THINKING_LEVEL_PRO = "high"
|
||||
ALWAYS_CROP = False
|
||||
ANNOTATION_MARGIN_LEFT = 300
|
||||
ANNOTATION_LOW_MARGIN_LEFT = 400
|
||||
ANNOTATION_MAX_HEIGHT_PX = 32000
|
||||
ANNOTATION_MAX_HEIGHT_PX = 33000
|
||||
ANNOTATION_MAX_PIXELS = 67_108_864
|
||||
SHOW_PERSONAL_STEPS = False
|
||||
RETURN_JPEG_ENABLED = True
|
||||
RETURN_PDF_ENABLED = True
|
||||
|
||||
Reference in new issue
Block a user