Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions negpy/desktop/view/widgets/prescan_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from negpy.desktop.view.widgets.scan_window_label import ScanWindowLabel
from negpy.desktop.workers.scan_worker import PrescanRequest
from negpy.infrastructure.scanners.base import ScannerDevice
from negpy.infrastructure.scanners.params import clamp_scan_area
from negpy.infrastructure.scanners.params import crop_to_scan_window
from negpy.infrastructure.scanners.result import ScanResult


Expand Down Expand Up @@ -53,8 +53,10 @@ def __init__(
super().__init__(parent)
self._controller = controller
self._device = device
caps = device.capabilities
# TA / backend space (what ScanParams.window stores).
self._scan_window: tuple[float, float, float, float] | None = initial_window
self._prescan_mirror_x = bool(caps.prescan_mirror_x)
self._busy = False

self.setWindowTitle("Prescan — set crop")
Expand Down Expand Up @@ -152,7 +154,7 @@ def _on_prescan_ready(self, result: object) -> None:
if default_crop is not None:
self._scan_window = default_crop
if self._scan_window is not None:
image_rect = clamp_scan_area(self._scan_window)
image_rect = crop_to_scan_window(self._scan_window, mirror_x=self._prescan_mirror_x)
self._label.set_window(image_rect)
self._ok_btn.setEnabled(True)
self._status.setText("Drag the rectangle to set the scan crop")
Expand All @@ -177,7 +179,7 @@ def _on_window_changed(self, rect: object) -> None:
if rect is None:
self._scan_window = None
return
self._scan_window = clamp_scan_area(tuple(rect)) # type: ignore[arg-type]
self._scan_window = crop_to_scan_window(tuple(rect), mirror_x=self._prescan_mirror_x) # type: ignore[arg-type]

def _on_clear_crop(self) -> None:
self._scan_window = None
Expand All @@ -197,7 +199,7 @@ def accept(self) -> None:
# Sync from widget in case the last drag did not emit.
rect = self._label.window()
if rect is not None:
self._scan_window = clamp_scan_area(rect)
self._scan_window = crop_to_scan_window(rect, mirror_x=self._prescan_mirror_x)
self._disconnect_controller()
super().accept()

Expand Down
2 changes: 2 additions & 0 deletions negpy/infrastructure/scanners/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class ScannerCapabilities:
#: Low-DPI full-window preview then interactive crop (Plustek SE).
prescan: bool = False
prescan_dpi: int = 0
#: Left–right mirrored sensor (sensor order flipped in pyopticfilm's assemble()).
prescan_mirror_x: bool = False
prescan_default_crop: tuple[float, float, float, float] | None = None
multi_exposure: bool = False
adapter_frame_capacity: int | None = None # transport capacity bound, not an exposure count
Expand Down
16 changes: 16 additions & 0 deletions negpy/infrastructure/scanners/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ def clamp_scan_area(area: ScanArea) -> ScanArea:
return (x1, y1, x2, y2)


def crop_to_scan_window(crop: ScanArea, *, mirror_x: bool) -> ScanArea:
"""Map Prescan widget coords ↔ TA ``area`` for ``ScanParams.window``.

Self-inverse when ``mirror_x`` is fixed: image-left is sensor-right on mirrored
scanners, so trimming left chrome on the Prescan must crop the opposite TA side.

Mirrors the pre-#958 behaviour that PR #958 accidentally flattened to a plain
clamp, which displaced Prescan crops on mirror_x Plustek devices.
"""
area = clamp_scan_area(crop)
if mirror_x:
x1, y1, x2, y2 = area
return (1.0 - x2, y1, 1.0 - x1, y2)
return area


def clamp_frame_offset_mm(offset_mm: float, pitch_mm: float) -> float:
"""Effective feed-axis offset, floored at 0 and held short of one frame pitch.

Expand Down
1 change: 1 addition & 0 deletions negpy/infrastructure/scanners/plustek_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def _caps_for(model: Any) -> ScannerCapabilities:
autofocus=False,
prescan=prescan_ready,
prescan_dpi=PRESCAN_DPI if prescan_ready else 0,
prescan_mirror_x=bool(getattr(model, "mirror_x", False)) if prescan_ready else False,
prescan_default_crop=default_frame_crop_norm(model) if prescan_ready else None,
multi_exposure=bool(getattr(model, "scan_ready", False) and getattr(model, "exposure_long", None)),
adapter_frame_capacity=None,
Expand Down
23 changes: 22 additions & 1 deletion tests/scanners/test_params.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""Tests for ScanParams dataclass and ScanMode validation."""

import pytest
from negpy.infrastructure.scanners.params import ScanMode, ScanParams, clamp_scan_area
from negpy.infrastructure.scanners.params import (
ScanMode,
ScanParams,
clamp_scan_area,
crop_to_scan_window,
)
from negpy.infrastructure.scanners.base import ScannerCapabilities


Expand Down Expand Up @@ -72,3 +77,19 @@ def test_dpi_range_from_caps(self) -> None:
class TestPrescanGeometry:
def test_clamp_scan_area_enforces_positive_extent(self) -> None:
assert clamp_scan_area((0.5, 0.5, 0.5, 0.5)) == (0.5, 0.5, 0.501, 0.501)

def test_crop_to_scan_window_reflects_x_when_mirrored(self) -> None:
"""Mirror_x scanners: display-left is sensor-right, so x must be reflected.

Regression for PR #958, which flattened this conversion to a plain clamp and
displaced Prescan crops on mirror_x Plustek devices (8200i SE / 8100 V2).
"""
assert crop_to_scan_window((0.1, 0.2, 0.4, 0.8), mirror_x=True) == pytest.approx((0.6, 0.2, 0.9, 0.8))

def test_crop_to_scan_window_passthrough_when_not_mirrored(self) -> None:
assert crop_to_scan_window((0.1, 0.2, 0.4, 0.8), mirror_x=False) == pytest.approx((0.1, 0.2, 0.4, 0.8))

def test_crop_to_scan_window_is_self_inverse(self) -> None:
crop = (0.1, 0.2, 0.4, 0.8)
reflected = crop_to_scan_window(crop, mirror_x=True)
assert crop_to_scan_window(reflected, mirror_x=True) == pytest.approx(crop)
1 change: 1 addition & 0 deletions tests/scanners/test_plustek_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def test_8100_v2_caps_match_pyopticfilm_model(monkeypatch):
assert caps.ir_channel is False
assert caps.multi_exposure is True
assert caps.prescan is True
assert caps.prescan_mirror_x is True # 8100 V2 inherits mirror_x from 8200i SE
assert 1200 in caps.supported_dpi


Expand Down
Loading