diff --git a/gui/main.py b/gui/main.py index aea7d16..0d7b7dd 100644 --- a/gui/main.py +++ b/gui/main.py @@ -26,7 +26,7 @@ from gui.locals_tab import LocalsTabMixin from gui.lorcana_tab import LorcanaTabMixin from gui.op_tab import OPTabMixin -from gui.paths import output_dir, work_dir +from gui.paths import app_base_dir, output_dir, work_dir from gui.rb_tab import RBTabMixin from gui.widgets import ( APP_TITLE, @@ -127,7 +127,7 @@ def __init__(self, root: tk.Tk) -> None: self.worker: threading.Thread | None = None self.cancel_event = threading.Event() self.running = False - self.keep_cache = tk.BooleanVar(value=False) + self._load_settings() self._dl_speed_str: str = "" self._custom_output_dir: Path | None = None @@ -144,6 +144,195 @@ def __init__(self, root: tk.Tk) -> None: self.root.after(80, self._drain_events) self.root.after(200, self._setup_dnd) + def _load_settings(self) -> None: + self.settings_path = app_base_dir() / "settings.json" + + # Initialize configuration variables + self.crop_color = tk.StringVar(value="#000000") + self.crop_width = tk.DoubleVar(value=1.0) + self.crop_placement = tk.StringVar(value="all") + self.crop_on_top = tk.BooleanVar(value=False) + self.crop_pnp = tk.BooleanVar(value=False) + self.keep_cache = tk.BooleanVar(value=False) + + if self.settings_path.exists(): + try: + with open(self.settings_path, "r", encoding="utf-8") as f: + data = json.load(f) + self.crop_color.set(data.get("crop_color", "#000000")) + self.crop_width.set(float(data.get("crop_width", 1.0))) + self.crop_placement.set(data.get("crop_placement", "all")) + self.crop_on_top.set(bool(data.get("crop_on_top", False))) + self.crop_pnp.set(bool(data.get("crop_pnp", False))) + self.keep_cache.set(bool(data.get("keep_cache", False))) + except Exception as e: + _log.warning("Could not load settings.json: %s", e) + + def _save_settings(self) -> None: + try: + data = { + "crop_color": self.crop_color.get(), + "crop_width": self.crop_width.get(), + "crop_placement": self.crop_placement.get(), + "crop_on_top": self.crop_on_top.get(), + "crop_pnp": self.crop_pnp.get(), + "keep_cache": self.keep_cache.get(), + } + with open(self.settings_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + except Exception as e: + _log.warning("Could not save settings.json: %s", e) + + def _open_settings(self, on_confirm=None) -> None: + from tkinter import colorchooser + + win = tk.Toplevel(self.root) + win.title("Configuración") + win.transient(self.root) + win.grab_set() + + # Center settings window relative to root window + win.update_idletasks() + w_width, w_height = 420, 310 + r_x = self.root.winfo_x() + r_y = self.root.winfo_y() + r_w = self.root.winfo_width() + r_h = self.root.winfo_height() + pos_x = r_x + (r_w - w_width) // 2 + pos_y = r_y + (r_h - w_height) // 2 + win.geometry(f"{w_width}x{w_height}+{pos_x}+{pos_y}") + win.resizable(False, False) + + content_frame = ttk.Frame(win) + content_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=10) + + # PNP Crops (Outside the frame) + pnp_var = tk.BooleanVar(value=self.crop_pnp.get()) + + def toggle_state(): + is_pnp = pnp_var.get() + state = "normal" if is_pnp else "disabled" + cb_placement.config(state="readonly" if is_pnp else "disabled") + sb_width.config(state=state) + cb_on_top.config(state=state) + btn_pick.config(state=state) + ent_color.config(state=state) + + cb_pnp = ttk.Checkbutton(content_frame, text="Corte PNP", variable=pnp_var, command=toggle_state) + cb_pnp.pack(anchor="w", pady=(0, 6)) + + # Box (LabelFrame) "Marcas de Corte" + lf_crops = ttk.LabelFrame(content_frame, text="Marcas de Corte") + lf_crops.pack(fill=tk.BOTH, expand=True, pady=5) + + lf_crops.columnconfigure(0, weight=1) + lf_crops.columnconfigure(1, weight=2) + + # Placement + lbl_placement = ttk.Label(lf_crops, text="Colocación de marcas:") + lbl_placement.grid(row=0, column=0, sticky="w", pady=6, padx=12) + + placement_map = { + "Todas las páginas": "all", + "Páginas impares (Frontales)": "fronts", + "Páginas pares (Traseras)": "backs" + } + reverse_placement_map = {v: k for k, v in placement_map.items()} + + placement_var = tk.StringVar(value=reverse_placement_map.get(self.crop_placement.get(), "Todas las páginas")) + cb_placement = ttk.Combobox(lf_crops, textvariable=placement_var, values=list(placement_map.keys()), state="readonly", width=22) + cb_placement.grid(row=0, column=1, sticky="w", pady=6, padx=12) + + # Thickness (Spinbox) + lbl_width = ttk.Label(lf_crops, text="Grosor de línea (puntos):") + lbl_width.grid(row=1, column=0, sticky="w", pady=6, padx=12) + + width_var = tk.DoubleVar(value=self.crop_width.get()) + sb_width = ttk.Spinbox(lf_crops, from_=0.1, to=3.0, increment=0.1, textvariable=width_var, format="%.1f", width=8) + sb_width.grid(row=1, column=1, sticky="w", pady=6, padx=12) + + # Layering/Overlay + lbl_on_top = ttk.Label(lf_crops, text="Capa de marcas:") + lbl_on_top.grid(row=2, column=0, sticky="w", pady=6, padx=12) + + on_top_var = tk.BooleanVar(value=self.crop_on_top.get()) + cb_on_top = ttk.Checkbutton(lf_crops, text="Dibujar marcas encima de las cartas", variable=on_top_var) + cb_on_top.grid(row=2, column=1, sticky="w", pady=6, padx=12) + + # Color (Hex + Preview + Pick button) + lbl_color = ttk.Label(lf_crops, text="Color de marcas:") + lbl_color.grid(row=3, column=0, sticky="w", pady=6, padx=12) + + color_frame = ttk.Frame(lf_crops) + color_frame.grid(row=3, column=1, sticky="w", pady=6, padx=12) + + color_var = tk.StringVar(value=self.crop_color.get()) + color_preview = tk.Frame(color_frame, width=20, height=20, relief=tk.SOLID, borderwidth=1) + color_preview.pack(side=tk.LEFT, padx=(0, 6)) + color_preview.pack_propagate(False) + + ent_color = ttk.Entry(color_frame, textvariable=color_var, width=10) + ent_color.pack(side=tk.LEFT, padx=(0, 6)) + + def pick_color(): + chosen = colorchooser.askcolor(initialcolor=color_var.get(), parent=win) + if chosen[1]: + color_var.set(chosen[1]) + + btn_pick = ttk.Button(color_frame, text="Elegir...", command=pick_color, width=9) + btn_pick.pack(side=tk.LEFT) + + def update_preview(*args): + try: + color_preview.config(bg=color_var.get()) + except tk.TclError: + pass + + color_var.trace_add("write", update_preview) + update_preview() + + # Set initial states based on pnp_var + toggle_state() + + # Bottom controls for Dialog + btn_frame = ttk.Frame(win) + btn_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=15, pady=(0, 10)) + + def save(): + c_hex = color_var.get().strip() + if not c_hex.startswith("#") or len(c_hex) not in (4, 7): + messagebox.showerror("Error", "Introduce un color hexadecimal válido (ej. #000000 o #FF0000).", parent=win) + return + try: + win.winfo_rgb(c_hex) + except tk.TclError: + messagebox.showerror("Error", "El color hexadecimal introducido no es válido.", parent=win) + return + + try: + w_val = float(width_var.get()) + if w_val <= 0: + raise ValueError() + except ValueError: + messagebox.showerror("Error", "El grosor de línea debe ser un número positivo.", parent=win) + return + + self.crop_placement.set(placement_map.get(placement_var.get(), "all")) + self.crop_width.set(w_val) + self.crop_color.set(c_hex) + self.crop_on_top.set(on_top_var.get()) + self.crop_pnp.set(pnp_var.get()) + self._save_settings() + win.destroy() + if on_confirm: + on_confirm() + + btn_cancel = ttk.Button(btn_frame, text="Cancelar", command=win.destroy) + btn_cancel.pack(side=tk.RIGHT, padx=(6, 0)) + + btn_save = ttk.Button(btn_frame, text="Generar", command=save) + btn_save.pack(side=tk.RIGHT) + def _build_ui(self) -> None: pad = {"padx": 10, "pady": 6} frm = ttk.Frame(self.root) @@ -156,6 +345,7 @@ def _build_ui(self) -> None: bottom_controls, text="Guardar en el PC las imágenes entre ejecuciones", variable=self.keep_cache, + command=self._save_settings, ) self.keep_cache_cb.pack(anchor=tk.W) @@ -220,6 +410,8 @@ def _build_ui(self) -> None: self._build_xml_pane(top) self._build_locals_pane(top) + # Settings button removed as per requirements. + def _build_xml_pane(self, parent: ttk.Frame) -> None: notebook = ttk.Notebook(parent) notebook.grid(row=0, column=0, sticky="nsew", padx=(0, 4)) @@ -609,6 +801,18 @@ def _start(self, fronts_only: bool = False) -> None: if not messagebox.askyesno(APP_TITLE, warning, icon=messagebox.WARNING): return + # Determine if it's an internet/downloaded deck + is_internet_deck = bool( + (self._op_decks or self._rb_decks or self._lorcana_decks or self.state.mtg_url_decks) + and not self.state.xml_paths + ) + + if is_internet_deck: + self._open_settings(on_confirm=lambda: self._continue_start(plan_, reports, fronts_only)) + else: + self._continue_start(plan_, reports, fronts_only) + + def _continue_start(self, plan_, reports, fronts_only: bool) -> None: self.running = True self.cancel_event.clear() self._dl_speed_str = "" @@ -620,9 +824,16 @@ def _start(self, fronts_only: bool = False) -> None: self.progress["value"] = 0 self.status_var.set("Preparando…") self._reset_xml_download_progress() + + c_color = self.crop_color.get() + c_width = self.crop_width.get() + c_placement = self.crop_placement.get() + c_on_top = self.crop_on_top.get() + c_pnp = self.crop_pnp.get() + self.worker = threading.Thread( target=self._work, - args=(plan_, reports, fronts_only), + args=(plan_, reports, fronts_only, c_color, c_width, c_placement, c_on_top, c_pnp), daemon=True, ) self.worker.start() @@ -640,7 +851,17 @@ def _request_stop(self) -> None: self.stop_btn.state(["disabled"]) self.status_var.set("Cancelando…") - def _work(self, plan_, reports, fronts_only: bool = False) -> None: + def _work( + self, + plan_, + reports, + fronts_only: bool = False, + crop_color: str = "#000000", + crop_width: float = 1.0, + crop_placement: str = "all", + crop_on_top: bool = False, + crop_pnp: bool = True, + ) -> None: run_dir = None wd = None try: @@ -947,6 +1168,11 @@ def on_speed_update(speed_mbps: float, eta_sec: float) -> None: on_xml_crop_progress=on_xml_crop_progress, fronts_only=fronts_only, on_speed_update=on_speed_update, + crop_color=crop_color, + crop_width=crop_width, + crop_placement=crop_placement, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, ) generated.extend(pdfs) manifest = write_manifest(plan_, reports, run_dir) @@ -994,6 +1220,11 @@ def cb(stage, done, total, _label=base): extra_backs=all_extra_backs, local_crop_map=all_crop_map, fronts_only=fronts_only, + crop_color=crop_color, + crop_width=crop_width, + crop_placement=crop_placement, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, ) generated.extend(pdfs) manifest = None diff --git a/src/pdf_generator.py b/src/pdf_generator.py index d9ed812..6aace45 100644 --- a/src/pdf_generator.py +++ b/src/pdf_generator.py @@ -25,10 +25,6 @@ MARGIN_X = 5.75 * mm MARGIN_Y = 11.15 * mm -# Crop mark style — ticks in the page margins -MARK_W = 1.0 -MARK_GAP = 3.0 # pt between trim line and tick endpoint - # Printer-mark assets ASSETS_DIR = Path(__file__).parent / "assets" CORNER_MARK_PATH = ASSETS_DIR / "corner_mark.png" @@ -52,8 +48,32 @@ def _trim_origin(col: int, row: int) -> tuple[float, float]: return x, y -def _draw_crop_marks(c: canvas.Canvas) -> None: - """Trim-edge ticks in the page margins only (no lines crossing inner gaps).""" +def hex_to_rgb(hex_str: str) -> tuple[float, float, float]: + """Convert `#RRGGBB` hex string to ReportLab RGB tuple of floats (0.0 - 1.0).""" + hex_str = hex_str.lstrip("#") + if len(hex_str) == 3: + hex_str = "".join(c * 2 for c in hex_str) + if len(hex_str) == 6: + try: + r = int(hex_str[0:2], 16) / 255.0 + g = int(hex_str[2:4], 16) / 255.0 + b = int(hex_str[4:6], 16) / 255.0 + return r, g, b + except ValueError: + pass + return 0.0, 0.0, 0.0 # fallback to black + + +def _draw_crop_marks( + c: canvas.Canvas, + color: str = "#000000", + width: float = 1.0, + crop_on_top: bool = False, + crop_pnp: bool = False, +) -> None: + """Draw crop marks. If crop_pnp is True, draw continuous trim-edge crop lines + from edge to edge of the page. If crop_pnp is False, draw ticks in the page + margins only.""" xs = [MARGIN_X + col * (CARD_W + GAP_X) + dx for col in range(COLS) for dx in (0.0, CARD_W)] ys = [ PAGE_H - MARGIN_Y - (row + 1) * CARD_H - row * GAP_Y + dy @@ -62,20 +82,34 @@ def _draw_crop_marks(c: canvas.Canvas) -> None: ] c.saveState() - c.setLineWidth(MARK_W) - c.setStrokeColorRGB(0, 0, 0) - # Vertical ticks at each column trim X, in the top and bottom margins only - top_y_end = PAGE_H - MARGIN_Y + MARK_GAP - bot_y_end = MARGIN_Y - MARK_GAP - for x in xs: - c.line(x, 0, x, bot_y_end) - c.line(x, top_y_end, x, PAGE_H) - # Horizontal ticks at each row trim Y, in the left and right margins only - left_x_end = MARGIN_X - MARK_GAP - right_x_end = PAGE_W - MARGIN_X + MARK_GAP - for y in ys: - c.line(0, y, left_x_end, y) - c.line(right_x_end, y, PAGE_W, y) + c.setLineWidth(width) + r, g, b = hex_to_rgb(color) + c.setStrokeColorRGB(r, g, b) + + if crop_pnp: + # Draw continuous lines across the page (PNP layout) + for x in xs: + c.line(x, 0, x, PAGE_H) + for y in ys: + c.line(0, y, PAGE_W, y) + else: + # Original: ticks in the page margins only (with gap) + mark_gap = 3.0 + gap = 0.0 if crop_on_top else mark_gap + + # Vertical ticks at each column trim X, in the top and bottom margins only + top_y_end = PAGE_H - MARGIN_Y + gap + bot_y_end = MARGIN_Y - gap + for x in xs: + c.line(x, 0, x, bot_y_end) + c.line(x, top_y_end, x, PAGE_H) + # Horizontal ticks at each row trim Y, in the left and right margins only + left_x_end = MARGIN_X - gap + right_x_end = PAGE_W - MARGIN_X + gap + for y in ys: + c.line(0, y, left_x_end, y) + c.line(right_x_end, y, PAGE_W, y) + c.restoreState() @@ -109,8 +143,20 @@ def _draw_page( id_to_path: dict[str, Path], slot_to_id: dict[int, str], page_label: str | None = None, + draw_crop_marks: bool = True, + crop_color: str = "#000000", + crop_width: float = 1.0, + crop_on_top: bool = False, + crop_pnp: bool = False, ) -> None: - _draw_crop_marks(c) + if draw_crop_marks and not crop_on_top: + _draw_crop_marks( + c, + color=crop_color, + width=crop_width, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, + ) _draw_printer_marks(c, page_label) for idx, slot in enumerate(slots): @@ -121,6 +167,15 @@ def _draw_page( if img_path and img_path.exists(): c.drawImage(str(img_path), x - BLEED, y - BLEED, width=IMAGE_W, height=IMAGE_H) + if draw_crop_marks and crop_on_top: + _draw_crop_marks( + c, + color=crop_color, + width=crop_width, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, + ) + # Cap each generated PDF at 500 MB on disk (decimal MB, as reported by file # managers). We aim for 480 MB so the final file stays comfortably under @@ -168,6 +223,11 @@ def generate( progress_callback=None, cancel_event: Event | None = None, fronts_only: bool = False, + crop_color: str = "#000000", + crop_width: float = 1.0, + crop_placement: str = "all", + crop_on_top: bool = False, + crop_pnp: bool = False, ) -> list[Path]: """Generate one or more PDFs in `output_dir`. A new chunk starts after every front/back pair whose addition would push the cumulative image @@ -228,7 +288,20 @@ def id_bytes(drive_id: str) -> int: pair_no += 1 padded = page_slots + [None] * (CARDS_PER_PAGE - len(page_slots)) - _draw_page(c, padded, id_to_path, front_slot_to_id, page_label=str(pair_no)) + # Draw crop marks on fronts if placement is 'all' or 'fronts' + draw_front_crops = crop_placement in ("all", "fronts") + _draw_page( + c, + padded, + id_to_path, + front_slot_to_id, + page_label=str(pair_no), + draw_crop_marks=draw_front_crops, + crop_color=crop_color, + crop_width=crop_width, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, + ) c.showPage() if not fronts_only: @@ -236,7 +309,20 @@ def id_bytes(drive_id: str) -> int: for row in range(ROWS): mirrored.extend(reversed(padded[row * COLS : (row + 1) * COLS])) - _draw_page(c, mirrored, id_to_path, back_slot_to_id, page_label=f"{pair_no}B") + # Draw crop marks on backs if placement is 'all' or 'backs' + draw_back_crops = crop_placement in ("all", "backs") + _draw_page( + c, + mirrored, + id_to_path, + back_slot_to_id, + page_label=f"{pair_no}B", + draw_crop_marks=draw_back_crops, + crop_color=crop_color, + crop_width=crop_width, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, + ) c.showPage() done_pairs += 1 diff --git a/src/pipeline.py b/src/pipeline.py index 8ce033b..a57e86a 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -91,6 +91,11 @@ def run( extra_backs: list[str | Path | None] | None = None, local_crop_map: dict[Path, bool] | None = None, fronts_only: bool = False, + crop_color: str = "#000000", + crop_width: float = 1.0, + crop_placement: str = "all", + crop_on_top: bool = False, + crop_pnp: bool = False, ) -> list[Path]: """Single-XML pipeline: XML → one or more PDFs named after the XML stem. @@ -114,6 +119,11 @@ def run( extra_backs=extra_backs, local_crop_map=local_crop_map, fronts_only=fronts_only, + crop_color=crop_color, + crop_width=crop_width, + crop_placement=crop_placement, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, ) @@ -128,6 +138,11 @@ def run_merged( extra_backs: list[str | Path | None] | None = None, local_crop_map: dict[Path, bool] | None = None, fronts_only: bool = False, + crop_color: str = "#000000", + crop_width: float = 1.0, + crop_placement: str = "all", + crop_on_top: bool = False, + crop_pnp: bool = False, ) -> list[Path]: """Multi-XML pipeline: concatenate the XMLs' fronts in order and emit one or more PDFs named `.pdf` (or `_1.pdf`, … when split). @@ -148,6 +163,11 @@ def run_merged( extra_backs=extra_backs, local_crop_map=local_crop_map, fronts_only=fronts_only, + crop_color=crop_color, + crop_width=crop_width, + crop_placement=crop_placement, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, ) @@ -162,6 +182,11 @@ def run_locals_only( extra_backs: list[str | Path | None] | None = None, local_crop_map: dict[Path, bool] | None = None, fronts_only: bool = False, + crop_color: str = "#000000", + crop_width: float = 1.0, + crop_placement: str = "all", + crop_on_top: bool = False, + crop_pnp: bool = False, ) -> list[Path]: """Generate PDF(s) only from local images (no XML). @@ -182,6 +207,11 @@ def run_locals_only( local_cardback=local_cardback, local_crop_map=local_crop_map, fronts_only=fronts_only, + crop_color=crop_color, + crop_width=crop_width, + crop_placement=crop_placement, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, ) @@ -264,6 +294,11 @@ def _run_xmls( local_cardback: str | Path | None = None, local_crop_map: dict[Path, bool] | None = None, fronts_only: bool = False, + crop_color: str = "#000000", + crop_width: float = 1.0, + crop_placement: str = "all", + crop_on_top: bool = False, + crop_pnp: bool = False, ) -> list[Path]: extra_fronts = [Path(p) for p in (extra_fronts or [])] # extra_backs is parallel to extra_fronts; entries may be None to mean @@ -379,6 +414,11 @@ def _on_crop(drive_id: str, done: int, total: int) -> None: progress_callback=_cb(Stage.PDF), cancel_event=cancel_event, fronts_only=fronts_only, + crop_color=crop_color, + crop_width=crop_width, + crop_placement=crop_placement, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, ) @@ -456,6 +496,11 @@ def run_plan( on_xml_crop_progress: StageCallback = None, fronts_only: bool = False, on_speed_update=None, + crop_color: str = "#000000", + crop_width: float = 1.0, + crop_placement: str = "all", + crop_on_top: bool = False, + crop_pnp: bool = False, ) -> list[Path]: """Download ALL images first, then crop all, then generate each job's PDFs. @@ -613,6 +658,11 @@ def _on_crop_plan(drive_id: str, done: int, total: int) -> None: progress_callback=_cb(Stage.PDF), cancel_event=cancel_event, fronts_only=fronts_only, + crop_color=crop_color, + crop_width=crop_width, + crop_placement=crop_placement, + crop_on_top=crop_on_top, + crop_pnp=crop_pnp, ) all_outputs.extend(outputs) diff --git a/tests/test_pdf_generator.py b/tests/test_pdf_generator.py index 3df88b0..a7677bd 100644 --- a/tests/test_pdf_generator.py +++ b/tests/test_pdf_generator.py @@ -256,3 +256,60 @@ def test_generate_split_all_files_exist(tmp_path): for r in results: assert r.exists() assert r.stat().st_size > 0 + + +def test_generate_supports_custom_crop_marks(tmp_path): + img = _img(tmp_path / "card.jpg") + slots, front, back = _slot_maps(1) + id_to_path = _id_to_path(front, back, img) + + # Test running generator with custom crop mark settings + results = generate( + tmp_path / "out", + "deck_custom_crops", + slots, + front, + back, + id_to_path, + crop_color="#FF0000", + crop_width=1.5, + crop_placement="fronts", + crop_on_top=True + ) + assert len(results) == 1 + assert results[0].exists() + assert results[0].stat().st_size > 0 + + +def test_generate_supports_crop_pnp(tmp_path): + img = _img(tmp_path / "card.jpg") + slots, front, back = _slot_maps(1) + id_to_path = _id_to_path(front, back, img) + + # Test with crop_pnp=True (PNP continuous lines) + results_pnp = generate( + tmp_path / "out", + "deck_pnp", + slots, + front, + back, + id_to_path, + crop_pnp=True + ) + assert len(results_pnp) == 1 + assert results_pnp[0].exists() + assert results_pnp[0].stat().st_size > 0 + + # Test with crop_pnp=False (original ticks) + results_ticks = generate( + tmp_path / "out", + "deck_ticks", + slots, + front, + back, + id_to_path, + crop_pnp=False + ) + assert len(results_ticks) == 1 + assert results_ticks[0].exists() + assert results_ticks[0].stat().st_size > 0 diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 7b07512..91170e9 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -702,3 +702,43 @@ def test_progress_callback_fires_download_zero_first(self, tmp_path): dl_events = [(s, d, t) for s, d, t in events if s == "download"] assert dl_events, "No download events fired" assert dl_events[0][1] == 0 + + +def test_run_locals_only_with_custom_crop_settings(tmp_path): + front = _img(tmp_path / "front.jpg") + back = _img(tmp_path / "back.jpg") + out = tmp_path / "out" + work = tmp_path / "work" + + results = run_locals_only( + [front], + back, + out, + "deck_custom", + work_dir=work, + crop_color="#00FF00", + crop_width=2.5, + crop_placement="backs", + crop_on_top=True + ) + assert len(results) == 1 + assert results[0].exists() + + +def test_run_locals_only_with_crop_pnp(tmp_path): + front = _img(tmp_path / "front.jpg") + back = _img(tmp_path / "back.jpg") + out = tmp_path / "out" + work = tmp_path / "work" + + results = run_locals_only( + [front], + back, + out, + "deck_custom_pnp", + work_dir=work, + crop_pnp=False + ) + assert len(results) == 1 + assert results[0].exists() +