Better (?) default left margin
This commit is contained in:
1 parent
b6c2a634ca
commit
247b124754
7 files changed
+421
-9
No files matched your search
@@ -26,6 +26,7 @@ from copienator import (
|
||||
from copienator.filesystem import staged_files
|
||||
from copienator.copy_errors import clear_copy_error, mark_copy_error, marked_copy_paths
|
||||
from copienator.configuration import SHOW_PERSONAL_STEPS
|
||||
from copienator.left_margin import analyse_margins
|
||||
|
||||
DELIMITER_WIDTH = 5
|
||||
DELIMITER_COLOR = (0, 0, 0)
|
||||
@@ -34,6 +35,7 @@ CROP_SHIFT_STEP = 50
|
||||
CROP_WIDTH_STEP = 50
|
||||
RENDER_DPI = 200
|
||||
NARROW_PAGE_RATIO = 0.8
|
||||
AUTO_STRIP_WIDTH_FACTOR = 1.10
|
||||
pdf_cache_lock = threading.Lock()
|
||||
|
||||
# The requested spelling is kept as the public switch for this alternate layout.
|
||||
@@ -116,6 +118,9 @@ def process_single_pdf(
|
||||
shift_offset: int = 0,
|
||||
max_per_file: int = 5,
|
||||
width_offset: int = 0,
|
||||
*,
|
||||
automatic_margin: bool = True,
|
||||
margin_analysis: dict | None = None,
|
||||
) -> tuple[Image.Image, list[Image.Image], dict[str, object]] | None:
|
||||
"""Convert one PDF into a preview, full-resolution splits and metadata."""
|
||||
try:
|
||||
@@ -124,7 +129,10 @@ def process_single_pdf(
|
||||
if not images:
|
||||
return None
|
||||
reference_width = normal_page_width(pdf_path, images)
|
||||
if automatic_margin and max_per_file != 1 and margin_analysis is None:
|
||||
margin_analysis = analyse_margins(images, RENDER_DPI, reference_width)
|
||||
full_width_pages = []
|
||||
page_crops = []
|
||||
for page_index, image in enumerate(images):
|
||||
width, height = image.size
|
||||
if max_per_file == 1:
|
||||
@@ -140,8 +148,17 @@ def process_single_pdf(
|
||||
width,
|
||||
width // 3 + 100 + shift_offset + max(0, width_offset),
|
||||
)
|
||||
if automatic_margin and max_per_file != 1:
|
||||
left = min(width - 1, max(0, margin_analysis['starts_px'][page_index] + shift_offset))
|
||||
strip_width = round(
|
||||
((reference_width if width < NARROW_PAGE_RATIO * reference_width else width) // 3)
|
||||
* AUTO_STRIP_WIDTH_FACTOR
|
||||
)
|
||||
right = min(width, left + strip_width + max(0, width_offset))
|
||||
if right > left:
|
||||
cropped_images.append(image.crop((left, 0, right, height)))
|
||||
page_crops.append({'page': page_index + 1, 'left_px': left, 'right_px': right,
|
||||
'width_px': width, 'height_px': height})
|
||||
if left == 0 and right == width:
|
||||
full_width_pages.append(page_index + 1)
|
||||
if not cropped_images:
|
||||
@@ -165,6 +182,8 @@ def process_single_pdf(
|
||||
"number_of_files": len(split_images),
|
||||
"columns_per_file": distribution,
|
||||
"full_width_pages": full_width_pages,
|
||||
"page_crops": page_crops,
|
||||
"auto_margin": margin_analysis if automatic_margin and max_per_file != 1 else None,
|
||||
}
|
||||
return preview, split_images, schema
|
||||
except Exception as exc: # noqa: BLE001 - interactive item failure
|
||||
@@ -208,6 +227,7 @@ class ImageReviewer:
|
||||
files: list[Path],
|
||||
output_dir: Path,
|
||||
default_max_per_file: int = 5,
|
||||
automatic_margin: bool = True,
|
||||
) -> None:
|
||||
self.files = files
|
||||
self.output_dir = output_dir
|
||||
@@ -216,6 +236,8 @@ class ImageReviewer:
|
||||
self.had_errors = False
|
||||
self.stop_prefetch = threading.Event()
|
||||
self.current_result = None
|
||||
self.automatic_margin = automatic_margin
|
||||
self.margin_analysis = None
|
||||
self.index = 0
|
||||
self.current_shift = 0
|
||||
self.current_width_offset = 0
|
||||
@@ -283,6 +305,7 @@ class ImageReviewer:
|
||||
self.current_shift = 0
|
||||
self.current_width_offset = 0
|
||||
self.current_result = None
|
||||
self.margin_analysis = None
|
||||
self.trigger_processing(self.files[self.index], self.current_shift)
|
||||
|
||||
def trigger_processing(self, pdf_path: Path, shift: int) -> None:
|
||||
@@ -299,6 +322,8 @@ class ImageReviewer:
|
||||
shift,
|
||||
self.current_max_per_file,
|
||||
self.current_width_offset,
|
||||
automatic_margin=self.automatic_margin,
|
||||
margin_analysis=self.margin_analysis,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -325,6 +350,8 @@ class ImageReviewer:
|
||||
) -> None:
|
||||
self.current_preview = result[0]
|
||||
self.current_result = result
|
||||
if result[2].get('auto_margin') is not None:
|
||||
self.margin_analysis = result[2]['auto_margin']
|
||||
self.update_display(pdf_path.name, result[2])
|
||||
|
||||
def update_display(self, filename: str, schema: dict[str, object]) -> None:
|
||||
@@ -338,6 +365,17 @@ class ImageReviewer:
|
||||
if personnal_binding
|
||||
else ""
|
||||
)
|
||||
analysis = schema.get('auto_margin')
|
||||
margin_info = ''
|
||||
if analysis:
|
||||
offsets = sorted({round(crop['left_px'] * 25.4 / RENDER_DPI, 1)
|
||||
for crop in schema['page_crops']})
|
||||
needs_review = any(group['status'] in {'inconsistent', 'uncertain', 'insufficient-ink'}
|
||||
for group in analysis['groups'])
|
||||
margin_info = (f"Auto margin: {offsets} mm from left"
|
||||
+ (' | Check uncertain pages' if needs_review else '') + '\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(
|
||||
text=(
|
||||
f"[{self.index + 1}/{len(self.files)}] {filename} | "
|
||||
@@ -345,6 +383,7 @@ class ImageReviewer:
|
||||
f"Extra width: {self.current_width_offset}px\n"
|
||||
f"Files: {schema['number_of_files']} | "
|
||||
f"Cols: {schema['columns_per_file']}\n"
|
||||
f"{margin_info}"
|
||||
"Arrows: left/right move by 50px | up/down widen/narrow by 50px\n"
|
||||
"Enter: Save and next | !: flag error and skip | 1: use full pages"
|
||||
f"{personal_instructions}"
|
||||
@@ -428,6 +467,7 @@ def run(
|
||||
fullpage: bool = False,
|
||||
no_visual_check: bool = False,
|
||||
marked: bool = False,
|
||||
automatic_margin: bool = True,
|
||||
) -> ExitCode:
|
||||
files = marked_copy_paths(workspace) if marked else _selected_files(workspace, target)
|
||||
if not files:
|
||||
@@ -463,6 +503,7 @@ def run(
|
||||
files,
|
||||
workspace.cutleft_dir,
|
||||
default_max_per_file=1 if fullpage else 5,
|
||||
automatic_margin=automatic_margin,
|
||||
)
|
||||
if not reviewer.completed:
|
||||
return ExitCode.INTERRUPTED
|
||||
@@ -472,6 +513,10 @@ def run(
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = target_parser("Interactively crop the label margin from PDF copies")
|
||||
parser.add_argument("--marked", action="store_true", help="Review flagged copies and clear only margin-cropping flags after saving with Enter")
|
||||
parser.add_argument(
|
||||
'--manual-margin', action='store_true',
|
||||
help='Use the previous fixed margin instead of automatically estimating body-text positions',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fullpage",
|
||||
action="store_true",
|
||||
@@ -496,6 +541,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
fullpage=args.fullpage,
|
||||
no_visual_check=args.no_visual_check,
|
||||
marked=args.marked,
|
||||
automatic_margin=not args.manual_margin,
|
||||
)
|
||||
|
||||
return execute(parser, argv, handle)
|
||||
|
||||
Reference in new issue
Block a user