Skip to content

Add Plustek (USB) scanner backend for OpticFilm 8200i SE and enable the Scan tab on Windows - #783

Draft
jboneng wants to merge 14 commits into
marcinz606:mainfrom
jboneng:feat/plustek_pyUSB_backend
Draft

Add Plustek (USB) scanner backend for OpticFilm 8200i SE and enable the Scan tab on Windows#783
jboneng wants to merge 14 commits into
marcinz606:mainfrom
jboneng:feat/plustek_pyUSB_backend

Conversation

@jboneng

@jboneng jboneng commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a first-party Plustek (USB) scanner backend and enables the Scan tab on Windows by implementing a pyUSB based driver for the the OpticFilm series of scanners into NegPy.

Until now, film-scanner acquisition went only through SANE (python-sane), which is unavailable on Windows, so the Scan UI was a placeholder there. This PR:

  • Implement a raw-USB Genesys/OpticFilm communication backend as negpy/infrastructure/scanners/plustek/
  • Implements plustek_backend.py against NegPy’s real ScannerBackend / ScanResult types (same pattern as SANE)
  • Registers Plustek (USB) in the backend registry and turns on ScanSidebar on Windows
  • Documents WinUSB setup and optional PyUSB install
  • Hardens SE colour/IR capture (geometry, ASIC shading / host fallback, IR registration) and capability-gates Scan UI controls

Supported scan hardware today: Plustek OpticFilm 8200i SE only (07b3:1825, GL128). Other OpticFilm PIDs may enumerate/probe but are locked out of scan until validated.

Backend selection

Platform Backends in the dropdown Default
Windows Plustek (USB) only plustek
macOS / Linux SANE + Plustek (USB) sane

Persisted backend: "sane" on Windows is coerced to plustek on load so old settings do not try to open python-sane.

Architecture

ScanSidebar → ScanWorker → ScannerService → create_backend("plustek")
  → PlustekBackend → scanners.plustek.Scanner → pyusb / libusb-package
  • Colour + IR = two USB passes when capture_ir is enabled; IR is registered to RGB with whole-pixel phase correlation after the carriage re-homes
  • Missing PyUSB → ScannerUnavailable with an install / WinUSB hint (soft fail; app still runs)
  • USB transport glitches → TransientScanError (ScannerService retries)

SE capture quality (post-landing fixes on this branch)

  • Full-window geometry at every supported PPI so scans clear the motor gate (Lab-aligned window, not a bare area that stalls feed)
  • ASIC shading (DVDSET) path rewritten for RGB; when HW post-unity whites are not ready, host dark/white stretch is used instead (intentional fallback)
  • Shear fix: keep Full-window line width (~2592 @ 1800) and pad the shading table rather than shrinking image X to AHB width
  • Host-path border clamp: after exposure makeup, clamp Full-window holder-chrome highlights to the film-inset peak so NegPy auto-bounds are not skewed by chrome
  • Calib cache versioned; see docs/PLUSTEK_WINDOWS.md troubleshooting for DVDSET / host-path symptoms

Scan UI capability gating

Controls appear only when the selected device reports a real choice / feature:

Control Shown when
Autofocus caps.autofocus (SANE when "autofocus" in opt; Plustek SE = false)
Auto-exposure caps.auto_exposure (SANE ae; Plustek SE = false)
Depth len(supported_depths) > 1 (Plustek SE is 16-bit only → hidden; sole depth still sent in scan params)
Exposure / Frames unchanged (range / capacity as before)

Packaging

  • Driver source is in-tree (no sibling PlustekLib checkout, no path dependency)
  • Optional group:
    plustek = ["pyusb>=1.2.1", "libusb-package>=1.0.26"]
  • make install / release sync keep --no-group plustek so CI does not require USB deps; developers who scan run uv sync --group plustek
  • Plustek backend unit tests stub usb / usb.core so they pass without the optional group (CI)
  • Setuptools discovery switched to include = ["negpy*"] so nested packages ship correctly
  • SANE/genesys attribution added to NOTICE.md

Docs

  • docs/PLUSTEK_WINDOWS.md — Zadig / WinUSB steps + SE calib troubleshooting
  • README, CONTRIBUTING, USER_GUIDE §13, CHANGELOG updated (capability-gated Depth / AF / AE called out)

Known follow-ups (out of scope)

  • Prefer HW DVDSET when post-unity whites succeed consistently; host fallback remains the reliable path today
  • Non-SE OpticFilm models: enumerate but do not scan until validated

Future ToDos

  • Validate the existing ported SANE tables for other plustek models on real hardware, then flip scan_ready (and fix whatever motor/calib/geometry breaks).

Test plan

  • uv sync --group plustek
  • uv run pytest tests/scanners/test_backend_registry.py tests/scanners/test_plustek_backend.py tests/scanners/plustek/ tests/scanners/test_capabilities.py tests/test_scan_sidebar.py -q
  • Plustek backend tests pass without PyUSB installed (stub / CI simulation)
  • Windows: Scan tab opens (no “unsupported” placeholder); Backend shows only Plustek (USB)
  • Windows: Without scanner / without WinUSB — refresh lists empty devices (or clear hint), no SANE traceback
  • Windows + SE + WinUSB: list OpticFilm 8200i SE → colour scan @ 1800 → TIFF opens via asset discovery
  • Same with IR enabled → RGB + _IR sidecar / dust path works
  • SE Scan UI: Autofocus, Auto-exposure, and Depth hidden; IR still available
  • Windows + SE + WinUSB: colour scan @ every supported PPI → TIFF opens
  • Windows + SE + WinUSB: colour + IR @ every supported PPI → TIFF opens
  • Linux/macOS: Backend dropdown offers SANE and Plustek (USB); default remains SANE
  • Saved settings with backend: "sane" on Windows do not crash; coerce to Plustek
  • No regressions in SANE implementation
  • Coolscan-like device (multi depth + AF/AE): Depth / Autofocus / Auto-exposure visible and flow into scan params (SANE Backend)
  • Plustek HW other than 8200i SE that SANE marks complete: detect/show in dropdown, but do not allow scan (unvalidated motor path) (Plustek (USB) backend)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant