Standardisation 5
This commit is contained in:
+251
-274
@@ -1,366 +1,343 @@
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
import os
|
||||
import time
|
||||
import json # Added for schema output
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from collections.abc import Sequence
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
from threading import Thread
|
||||
from queue import Queue, Empty
|
||||
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
# --- Configuration ---
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
atomic_write_json,
|
||||
execute,
|
||||
target_parser,
|
||||
workspace_from_target,
|
||||
)
|
||||
from copienator.filesystem import staged_files
|
||||
|
||||
DELIMITER_WIDTH = 5
|
||||
DELIMITER_COLOR = (0, 0, 0)
|
||||
OUTPUT_SIZE = (1800, 1000)
|
||||
|
||||
parser = argparse.ArgumentParser(description="PDF Cropper")
|
||||
parser.add_argument("path", help="Directory path or PDF file path")
|
||||
parser.add_argument("--fullpage", action="store_true", help="Process all files in full page mode (1 page per output file)")
|
||||
args = parser.parse_args()
|
||||
|
||||
path_arg = args.path
|
||||
fullpage_mode = args.fullpage
|
||||
files = []
|
||||
INPUT_DIR = ""
|
||||
COPIES_DIR = ""
|
||||
|
||||
if os.path.isfile(path_arg) and path_arg.lower().endswith('.pdf'):
|
||||
COPIES_DIR = os.path.abspath(os.path.dirname(path_arg))
|
||||
# If the file is inside a "Copies" folder, set INPUT_DIR to the parent
|
||||
if os.path.basename(COPIES_DIR).lower() == 'copies':
|
||||
INPUT_DIR = os.path.dirname(COPIES_DIR)
|
||||
else:
|
||||
INPUT_DIR = COPIES_DIR
|
||||
files = [os.path.basename(path_arg)]
|
||||
elif os.path.isdir(path_arg):
|
||||
# Support passing either the base dir or the Copies dir directly
|
||||
abs_path = os.path.abspath(path_arg)
|
||||
if os.path.basename(abs_path).lower() == 'copies':
|
||||
COPIES_DIR = abs_path
|
||||
INPUT_DIR = os.path.dirname(abs_path)
|
||||
else:
|
||||
INPUT_DIR = abs_path
|
||||
COPIES_DIR = os.path.join(INPUT_DIR, 'Copies')
|
||||
|
||||
if os.path.exists(COPIES_DIR):
|
||||
files = sorted([f for f in os.listdir(COPIES_DIR) if f.lower().endswith('.pdf') and
|
||||
"nonc" not in f.lower()])
|
||||
else:
|
||||
sys.exit(f"Error: Could not find 'Copies' directory inside {INPUT_DIR}")
|
||||
else:
|
||||
sys.exit("Error: Input must be a directory or a PDF file.")
|
||||
|
||||
|
||||
OUTPUT_DIR = os.path.join(INPUT_DIR, 'Cutleft')
|
||||
|
||||
if not os.path.exists(OUTPUT_DIR):
|
||||
os.makedirs(OUTPUT_DIR)
|
||||
|
||||
# --- Processing Logic ---
|
||||
|
||||
def distribute_pages(total_pages, max_per_file=5):
|
||||
"""
|
||||
Calculates how to split pages into chunks <= max_per_file,
|
||||
balancing the number of columns per file.
|
||||
Example: 12 pages, max 5 -> [4, 4, 4]
|
||||
"""
|
||||
if total_pages == 0:
|
||||
return []
|
||||
|
||||
# Calculate minimum number of files needed
|
||||
num_files = (total_pages + max_per_file - 1) // max_per_file
|
||||
|
||||
# Calculate base size and remainder
|
||||
base_count = total_pages // num_files
|
||||
remainder = total_pages % num_files
|
||||
|
||||
distribution = []
|
||||
for i in range(num_files):
|
||||
# Distribute remainder to the first few files
|
||||
count = base_count + (1 if i < remainder else 0)
|
||||
distribution.append(count)
|
||||
|
||||
return distribution
|
||||
|
||||
def stitch_images(image_list):
|
||||
"""Helper to stitch a list of images horizontally with delimiters."""
|
||||
if not image_list:
|
||||
return None
|
||||
|
||||
num_images = len(image_list)
|
||||
total_width = sum(img.width for img in image_list) + (num_images - 1) * DELIMITER_WIDTH
|
||||
max_height = max(img.height for img in image_list)
|
||||
|
||||
combined = Image.new('RGB', (total_width, max_height), color=(255, 255, 255))
|
||||
|
||||
x_offset = 0
|
||||
for idx, img in enumerate(image_list):
|
||||
combined.paste(img, (x_offset, 0))
|
||||
x_offset += img.width
|
||||
if idx < num_images - 1:
|
||||
delimiter = Image.new('RGB', (DELIMITER_WIDTH, max_height), color=DELIMITER_COLOR)
|
||||
combined.paste(delimiter, (x_offset, 0))
|
||||
x_offset += DELIMITER_WIDTH
|
||||
|
||||
return combined
|
||||
|
||||
import threading
|
||||
pdf_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
def distribute_pages(total_pages: int, max_per_file: int = 5) -> list[int]:
|
||||
"""Distribute pages into balanced chunks no larger than max_per_file."""
|
||||
if total_pages == 0:
|
||||
return []
|
||||
number_of_files = (total_pages + max_per_file - 1) // max_per_file
|
||||
base_count, remainder = divmod(total_pages, number_of_files)
|
||||
return [
|
||||
base_count + (1 if index < remainder else 0)
|
||||
for index in range(number_of_files)
|
||||
]
|
||||
|
||||
|
||||
def stitch_images(image_list: list[Image.Image]) -> Image.Image | None:
|
||||
if not image_list:
|
||||
return None
|
||||
total_width = sum(image.width for image in image_list)
|
||||
total_width += (len(image_list) - 1) * DELIMITER_WIDTH
|
||||
max_height = max(image.height for image in image_list)
|
||||
combined = Image.new("RGB", (total_width, max_height), color="white")
|
||||
x_offset = 0
|
||||
for index, image in enumerate(image_list):
|
||||
combined.paste(image, (x_offset, 0))
|
||||
x_offset += image.width
|
||||
if index < len(image_list) - 1:
|
||||
delimiter = Image.new(
|
||||
"RGB", (DELIMITER_WIDTH, max_height), color=DELIMITER_COLOR
|
||||
)
|
||||
combined.paste(delimiter, (x_offset, 0))
|
||||
x_offset += DELIMITER_WIDTH
|
||||
return combined
|
||||
|
||||
|
||||
@lru_cache(maxsize=3)
|
||||
def _get_pdf_pages_cached(filename):
|
||||
pdf_path = os.path.join(COPIES_DIR, filename)
|
||||
def _get_pdf_pages_cached(pdf_path: Path) -> list[Image.Image]:
|
||||
return convert_from_path(pdf_path)
|
||||
|
||||
def get_pdf_pages(filename):
|
||||
"""Thread-safe wrapper for the cached PDF conversion."""
|
||||
|
||||
def get_pdf_pages(pdf_path: Path) -> list[Image.Image]:
|
||||
"""Thread-safe wrapper around the small PDF conversion cache."""
|
||||
with pdf_cache_lock:
|
||||
return _get_pdf_pages_cached(filename)
|
||||
return _get_pdf_pages_cached(pdf_path)
|
||||
|
||||
def process_single_pdf(filename, shift_offset=0, max_per_file=5):
|
||||
"""
|
||||
Converts PDF to stitched images.
|
||||
Returns a tuple: (preview_image_resized, list_of_split_images, schema_dict)
|
||||
"""
|
||||
|
||||
def process_single_pdf(
|
||||
pdf_path: Path,
|
||||
shift_offset: int = 0,
|
||||
max_per_file: int = 5,
|
||||
) -> tuple[Image.Image, list[Image.Image], dict[str, object]] | None:
|
||||
"""Convert one PDF into a preview, full-resolution splits and metadata."""
|
||||
try:
|
||||
pages = get_pdf_pages(filename)
|
||||
cropped_images = []
|
||||
|
||||
for img in pages:
|
||||
width, height = img.size
|
||||
|
||||
for image in get_pdf_pages(pdf_path):
|
||||
width, height = image.size
|
||||
if max_per_file == 1:
|
||||
# If Single Page mode, take the full width (ignore shift/crop)
|
||||
left = 0
|
||||
right = width
|
||||
left, right = 0, width
|
||||
else:
|
||||
# Original "Cutleft" logic (approx 1/3 width)
|
||||
left = 100 + shift_offset
|
||||
right = (width // 3) + 100 + shift_offset
|
||||
|
||||
# Ensure crop box is valid
|
||||
left = max(0, left)
|
||||
right = min(width, right)
|
||||
|
||||
left = max(0, 100 + shift_offset)
|
||||
right = min(width, width // 3 + 100 + shift_offset)
|
||||
if right > left:
|
||||
crop_box = (left, 0, right, height)
|
||||
cropped = img.crop(crop_box)
|
||||
cropped_images.append(cropped)
|
||||
|
||||
cropped_images.append(image.crop((left, 0, right, height)))
|
||||
if not cropped_images:
|
||||
return None
|
||||
|
||||
# 1. Generate Schema / Distribution
|
||||
col_distribution = distribute_pages(len(cropped_images), max_per_file=max_per_file)
|
||||
|
||||
# 2. Generate Split Images (Full Resolution)
|
||||
distribution = distribute_pages(len(cropped_images), max_per_file)
|
||||
split_images = []
|
||||
current_idx = 0
|
||||
for count in col_distribution:
|
||||
chunk = cropped_images[current_idx : current_idx + count]
|
||||
stitched_chunk = stitch_images(chunk)
|
||||
split_images.append(stitched_chunk)
|
||||
current_idx += count
|
||||
|
||||
# 3. Generate Preview (All stitched together, Resized)
|
||||
current_index = 0
|
||||
for count in distribution:
|
||||
stitched = stitch_images(cropped_images[current_index : current_index + count])
|
||||
if stitched is not None:
|
||||
split_images.append(stitched)
|
||||
current_index += count
|
||||
full_stitch = stitch_images(cropped_images)
|
||||
preview_resized = full_stitch.resize(OUTPUT_SIZE, Image.BILINEAR)
|
||||
|
||||
schema = {
|
||||
"original_filename": filename,
|
||||
if full_stitch is None:
|
||||
return None
|
||||
preview = full_stitch.resize(OUTPUT_SIZE, Image.Resampling.BILINEAR)
|
||||
schema: dict[str, object] = {
|
||||
"original_filename": pdf_path.name,
|
||||
"total_pages": len(cropped_images),
|
||||
"number_of_files": len(split_images),
|
||||
"columns_per_file": col_distribution
|
||||
"columns_per_file": distribution,
|
||||
}
|
||||
|
||||
return (preview_resized, split_images, schema)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing {filename}: {e}")
|
||||
return preview, split_images, schema
|
||||
except Exception as exc: # noqa: BLE001 - interactive item failure
|
||||
print(f"Error processing {pdf_path.name}: {exc}")
|
||||
return None
|
||||
|
||||
def save_results(result_tuple, filename):
|
||||
"""
|
||||
Saves the split images and the schema JSON.
|
||||
"""
|
||||
_, splits, schema = result_tuple
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
|
||||
# --- Cleanup: Delete existing files for this PDF ---
|
||||
for f in os.listdir(OUTPUT_DIR):
|
||||
file_path = os.path.join(OUTPUT_DIR, f)
|
||||
if f == f"{base_name}_schema.json":
|
||||
os.remove(file_path)
|
||||
elif f.startswith(f"{base_name}_") and f.endswith(".jpg"):
|
||||
suffix = f[len(base_name)+1:-4]
|
||||
if suffix.isdigit():
|
||||
os.remove(file_path)
|
||||
|
||||
# Save Images
|
||||
for i, img in enumerate(splits):
|
||||
suffix = f"_{i+1:02d}"
|
||||
output_filename = f"{base_name}{suffix}.jpg"
|
||||
output_path = os.path.join(OUTPUT_DIR, output_filename)
|
||||
img.save(output_path, "JPEG", quality=95)
|
||||
print(f"Saved: {output_filename}")
|
||||
|
||||
# Save Schema
|
||||
json_filename = f"{base_name}_schema.json"
|
||||
json_path = os.path.join(OUTPUT_DIR, json_filename)
|
||||
with open(json_path, 'w') as f:
|
||||
json.dump(schema, f, indent=4)
|
||||
print(f"Saved schema: {json_filename}")
|
||||
def _previous_cutleft_outputs(output_dir: Path, base_name: str) -> set[str]:
|
||||
if not output_dir.is_dir():
|
||||
return set()
|
||||
result = {f"{base_name}_schema.json"}
|
||||
for path in output_dir.glob(f"{base_name}_*.jpg"):
|
||||
suffix = path.stem.removeprefix(f"{base_name}_")
|
||||
if suffix.isdigit():
|
||||
result.add(path.name)
|
||||
return result
|
||||
|
||||
|
||||
# --- GUI Application ---
|
||||
def save_results(
|
||||
result: tuple[Image.Image, list[Image.Image], dict[str, object]],
|
||||
pdf_path: Path,
|
||||
output_dir: Path,
|
||||
) -> None:
|
||||
"""Atomically replace every Cutleft output associated with one copy."""
|
||||
_, splits, schema = result
|
||||
base_name = pdf_path.stem
|
||||
previous = _previous_cutleft_outputs(output_dir, base_name)
|
||||
with staged_files(output_dir, remove=previous) as staging:
|
||||
for index, image in enumerate(splits, start=1):
|
||||
filename = f"{base_name}_{index:02d}.jpg"
|
||||
image.save(staging / filename, "JPEG", quality=95)
|
||||
atomic_write_json(staging / f"{base_name}_schema.json", schema)
|
||||
for index in range(1, len(splits) + 1):
|
||||
print(f"Saved: {base_name}_{index:02d}.jpg")
|
||||
print(f"Saved schema: {base_name}_schema.json")
|
||||
|
||||
|
||||
class ImageReviewer:
|
||||
def __init__(self, file_list, default_max_per_file=5):
|
||||
self.files = file_list
|
||||
def __init__(
|
||||
self,
|
||||
files: list[Path],
|
||||
output_dir: Path,
|
||||
default_max_per_file: int = 5,
|
||||
) -> None:
|
||||
self.files = files
|
||||
self.output_dir = output_dir
|
||||
self.index = 0
|
||||
self.current_shift = 0
|
||||
self.default_max_per_file = default_max_per_file
|
||||
self.current_max_per_file = default_max_per_file
|
||||
self.current_preview = None # Only stores the resized preview for GUI
|
||||
self.current_preview: Image.Image | None = None
|
||||
self.is_processing = False
|
||||
self.manual_queue: Queue[
|
||||
tuple[Image.Image, list[Image.Image], dict[str, object]] | None
|
||||
] = Queue()
|
||||
|
||||
# Queue for manual re-processing results
|
||||
self.manual_queue = Queue()
|
||||
|
||||
# Setup GUI
|
||||
self.root = tk.Tk()
|
||||
self.root.title("PDF Cropper")
|
||||
self.root.geometry("+100+100")
|
||||
|
||||
self.label_img = tk.Label(self.root)
|
||||
self.label_img.pack()
|
||||
|
||||
self.label_info = tk.Label(self.root, text="", font=("Arial", 12, "bold"))
|
||||
self.label_info.pack(pady=5)
|
||||
self.root.bind("<Return>", self.on_next)
|
||||
self.root.bind("n", lambda _event: self.on_shift(50))
|
||||
self.root.bind("N", lambda _event: self.on_shift(100))
|
||||
self.root.bind("t", lambda _event: self.on_shift(-50))
|
||||
self.root.bind("1", lambda _event: self.on_set_max_pages(1))
|
||||
|
||||
# Bindings
|
||||
self.root.bind('<Return>', self.on_next)
|
||||
self.root.bind('n', lambda e: self.on_shift(50))
|
||||
self.root.bind('N', lambda e: self.on_shift(100))
|
||||
self.root.bind('t', lambda e: self.on_shift(-50))
|
||||
self.root.bind('1', lambda e: self.on_set_max_pages(1))
|
||||
|
||||
# Start background pre-fetcher
|
||||
self.bg_thread = Thread(target=self.prefetch_worker, daemon=True)
|
||||
self.bg_thread.start()
|
||||
|
||||
# Load first image
|
||||
Thread(target=self.prefetch_worker, daemon=True).start()
|
||||
self.load_current_image()
|
||||
|
||||
self.root.lift()
|
||||
self.root.focus_force()
|
||||
self.root.mainloop()
|
||||
|
||||
def on_set_max_pages(self, count):
|
||||
def on_set_max_pages(self, count: int) -> None:
|
||||
if self.is_processing:
|
||||
return
|
||||
self.current_max_per_file = count
|
||||
print(f"Setting max pages per file: {count}")
|
||||
self.trigger_processing(self.files[self.index], self.current_shift)
|
||||
|
||||
def prefetch_worker(self):
|
||||
"""Background thread to load the NEXT file's PDF pages into RAM."""
|
||||
idx_to_process = -1
|
||||
def prefetch_worker(self) -> None:
|
||||
processed_index = -1
|
||||
while True:
|
||||
target = self.index + 1
|
||||
if target < len(self.files) and target != idx_to_process:
|
||||
fname = self.files[target]
|
||||
get_pdf_pages(fname)
|
||||
idx_to_process = target
|
||||
if target < len(self.files) and target != processed_index:
|
||||
get_pdf_pages(self.files[target])
|
||||
processed_index = target
|
||||
time.sleep(0.05)
|
||||
|
||||
def load_current_image(self, use_prefetch=False):
|
||||
def load_current_image(self) -> None:
|
||||
if self.index >= len(self.files):
|
||||
print("All files processed.")
|
||||
self.root.destroy()
|
||||
return
|
||||
|
||||
filename = self.files[self.index]
|
||||
self.is_processing = False
|
||||
self.current_shift = 0
|
||||
self.trigger_processing(self.files[self.index], self.current_shift)
|
||||
|
||||
self.trigger_processing(filename, self.current_shift)
|
||||
|
||||
def trigger_processing(self, filename, shift):
|
||||
"""Starts a thread to process image so GUI doesn't freeze."""
|
||||
def trigger_processing(self, pdf_path: Path, shift: int) -> None:
|
||||
self.is_processing = True
|
||||
self.label_info.configure(text=f"Processing {filename} (Shift {shift})... Please wait.", fg="red")
|
||||
self.label_info.configure(
|
||||
text=f"Processing {pdf_path.name} (Shift {shift})... Please wait.",
|
||||
fg="red",
|
||||
)
|
||||
|
||||
def worker():
|
||||
res = process_single_pdf(filename, shift, self.current_max_per_file)
|
||||
self.manual_queue.put(res)
|
||||
|
||||
Thread(target=worker, daemon=True).start()
|
||||
self.check_manual_queue(filename)
|
||||
|
||||
def check_manual_queue(self, filename):
|
||||
"""Polls the manual queue for result."""
|
||||
try:
|
||||
result = self.manual_queue.get_nowait()
|
||||
if result:
|
||||
self.handle_processing_result(result, filename)
|
||||
else:
|
||||
print(f"Failed to process {filename}, skipping.")
|
||||
self.index += 1
|
||||
self.load_current_image(use_prefetch=True)
|
||||
self.is_processing = False
|
||||
except Empty:
|
||||
self.root.after(100, lambda: self.check_manual_queue(filename))
|
||||
|
||||
def handle_processing_result(self, result, filename):
|
||||
"""Unpacks result, saves files, and updates display."""
|
||||
preview, splits, schema = result
|
||||
self.current_preview = preview
|
||||
|
||||
Thread(target=save_results, args=(result, filename), daemon=True).start()
|
||||
|
||||
self.update_display(filename, schema)
|
||||
|
||||
def update_display(self, filename, schema=None):
|
||||
if self.current_preview:
|
||||
tk_image = ImageTk.PhotoImage(self.current_preview)
|
||||
self.label_img.configure(image=tk_image)
|
||||
self.label_img.image = tk_image
|
||||
|
||||
schema_info = ""
|
||||
if schema:
|
||||
cols = str(schema['columns_per_file'])
|
||||
schema_info = f"\nFiles: {schema['number_of_files']} | Cols: {cols}"
|
||||
|
||||
self.label_info.configure(
|
||||
text=f"[{self.index+1}/{len(self.files)}] {filename} | Shift: {self.current_shift}px"
|
||||
f"{schema_info}\n"
|
||||
f"Enter: Next | n: +50 | N: +100 | t: -50 | 1: use single column",
|
||||
fg="black"
|
||||
def worker() -> None:
|
||||
self.manual_queue.put(
|
||||
process_single_pdf(pdf_path, shift, self.current_max_per_file)
|
||||
)
|
||||
|
||||
def on_shift(self, amount):
|
||||
Thread(target=worker, daemon=True).start()
|
||||
self.check_manual_queue(pdf_path)
|
||||
|
||||
def check_manual_queue(self, pdf_path: Path) -> None:
|
||||
try:
|
||||
result = self.manual_queue.get_nowait()
|
||||
if result is None:
|
||||
print(f"Failed to process {pdf_path.name}, skipping.")
|
||||
self.index += 1
|
||||
self.load_current_image()
|
||||
else:
|
||||
self.handle_processing_result(result, pdf_path)
|
||||
self.is_processing = False
|
||||
except Empty:
|
||||
self.root.after(100, lambda: self.check_manual_queue(pdf_path))
|
||||
|
||||
def handle_processing_result(
|
||||
self,
|
||||
result: tuple[Image.Image, list[Image.Image], dict[str, object]],
|
||||
pdf_path: Path,
|
||||
) -> None:
|
||||
self.current_preview = result[0]
|
||||
save_results(result, pdf_path, self.output_dir)
|
||||
self.update_display(pdf_path.name, result[2])
|
||||
|
||||
def update_display(self, filename: str, schema: dict[str, object]) -> None:
|
||||
if self.current_preview is None:
|
||||
return
|
||||
tk_image = ImageTk.PhotoImage(self.current_preview)
|
||||
self.label_img.configure(image=tk_image)
|
||||
self.label_img.image = tk_image
|
||||
self.label_info.configure(
|
||||
text=(
|
||||
f"[{self.index + 1}/{len(self.files)}] {filename} | "
|
||||
f"Shift: {self.current_shift}px\nFiles: {schema['number_of_files']} | "
|
||||
f"Cols: {schema['columns_per_file']}\n"
|
||||
"Enter: Next | n: +50 | N: +100 | t: -50 | "
|
||||
"1: use single column"
|
||||
),
|
||||
fg="black",
|
||||
)
|
||||
|
||||
def on_shift(self, amount: int) -> None:
|
||||
if self.is_processing:
|
||||
return
|
||||
self.current_shift += amount
|
||||
print(f"Applying shift: {self.current_shift}")
|
||||
self.trigger_processing(self.files[self.index], self.current_shift)
|
||||
|
||||
def on_next(self, event):
|
||||
def on_next(self, _event: object) -> None:
|
||||
if self.is_processing:
|
||||
return
|
||||
self.index += 1
|
||||
self.current_shift = 0
|
||||
self.current_max_per_file = self.default_max_per_file
|
||||
self.load_current_image(use_prefetch=True)
|
||||
self.load_current_image()
|
||||
|
||||
|
||||
# --- Entry Point ---
|
||||
if __name__ == "__main__":
|
||||
def _selected_files(
|
||||
workspace: EvaluationWorkspace,
|
||||
target: Path,
|
||||
) -> list[Path]:
|
||||
workspace.require_directories("Copies")
|
||||
if target.is_file():
|
||||
if target.suffix.casefold() != ".pdf":
|
||||
raise CliError(f"Target is not a PDF: {target}", ExitCode.INVALID_ARGUMENTS)
|
||||
return [target]
|
||||
return sorted(
|
||||
(
|
||||
path
|
||||
for path in workspace.copies_dir.glob("*.pdf")
|
||||
if "nonc" not in path.name.casefold()
|
||||
),
|
||||
key=lambda path: path.name.casefold(),
|
||||
)
|
||||
|
||||
|
||||
def run(
|
||||
workspace: EvaluationWorkspace,
|
||||
target: Path,
|
||||
*,
|
||||
fullpage: bool = False,
|
||||
) -> ExitCode:
|
||||
files = _selected_files(workspace, target)
|
||||
if not files:
|
||||
print("No PDF files found.")
|
||||
else:
|
||||
app = ImageReviewer(files, default_max_per_file=1 if fullpage_mode else 5)
|
||||
return ExitCode.SUCCESS
|
||||
workspace.cutleft_dir.mkdir(parents=True, exist_ok=True)
|
||||
_get_pdf_pages_cached.cache_clear()
|
||||
ImageReviewer(
|
||||
files,
|
||||
workspace.cutleft_dir,
|
||||
default_max_per_file=1 if fullpage else 5,
|
||||
)
|
||||
return ExitCode.SUCCESS
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = target_parser("Interactively crop the label margin from PDF copies")
|
||||
parser.add_argument(
|
||||
"--fullpage",
|
||||
action="store_true",
|
||||
help="Use each complete page instead of cropping the label margin",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
|
||||
def handle(args: argparse.Namespace) -> ExitCode:
|
||||
workspace, target = workspace_from_target(args)
|
||||
return run(workspace, target, fullpage=args.fullpage)
|
||||
|
||||
return execute(parser, argv, handle)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user