# Scanner Subsystem v2 — Plan

A parallel, modern scanner implementation that lives next to the existing
`IKScannerDeviceView`-based dialog and can be swapped in quickly because it
exposes the **same interface** (`hxScannerDelegate` + a drop-in window
controller). Goal: replace the fragile ImageKit UI layer while keeping the rest
of the app untouched.

## Why

The current dialog drives `IKScannerDeviceView` (deprecated ImageKit UI on top
of ImageCaptureCore). A large part of its fragility is the *conflict* between
our own code and `IKScannerDeviceView` both trying to manage the ICA session
(see the `-21345` retry cascades in `hxScannerViewController.swift`). Driving
`ICScannerDevice` directly and owning the session lifecycle removes that whole
class of bugs.

Network scanners are covered for free: Apple's ImageCaptureCore "Apple driver"
speaks eSCL/AirScan internally, so AirScan devices show up as remote
`ICScannerDevice` instances — no third-party HTTP/ESCL code required.

## Scope

- **Backend:** `ICScannerDevice` only (USB + network via the Apple driver).
- **UI:** SwiftUI, hosted in an `NSWindowController`.
- **Inspiration (read/port, not a dependency):**
  - DavidPhillipOster/imageCaptureScanner (Apache 2.0) — ICScannerDevice
    mechanics: session, functional units, overview, scan.
  - klep/scanline — headless scan flow, document feeder.
  - cyanfish/naps2 (GPL, reference only) — error-code handling / recovery.
- ScanSnap (Fujitsu/PFU) is **not** a backend: it is app-driven and delivers scans
  through its own software, not through ImageCaptureCore. The dialog only surfaces a
  dismissible hint when the ScanSnap software is installed (see Phase 5b), instead of
  faking a device entry the way the legacy dialog did.
- Continuity Camera stays an entirely separate system path and is **not** part of
  this dialog. Both apps already support it via the responder chain
  (`NSServicesMenuRequestor`: `validRequestor` + `readSelection(from:)`), which
  surfaces the system "Import from iPhone -> Scan Documents" menu and feeds the
  result straight into the normal import pipeline. ImageCaptureCore plays no role
  there, so folding it into the v2 dialog would only mix two unrelated mechanics.

## Architecture

```text
Libs/Scanner/V2/
  Engine/
    ScanModels.swift       value types: capabilities, settings, page, errors
    ScanDevice.swift       protocol the UI talks to
    ICAScanDevice.swift    ICScannerDevice-backed implementation (owns session)
  UI/
    ScannerViewModel.swift ObservableObject bridging engine <-> SwiftUI
    ScannerRootView.swift  device list + parameters + preview + actions
    ScanPreviewView.swift  overview image + selection rectangle
  hxScannerV2WindowController.swift  drop-in window controller (same interface)
```

Discovery reuses the existing, stable `hxScannerManager` (`ICDeviceBrowser`).

Output contract: the preferred delivery is `handleScannedPages(_:destination:)` -
one call per scan trigger, with a whole run of PNG pages plus the chosen
destination (new document vs. append to current). Hosts that do not implement it
fall back to per-page `handleScanData(_:)`, so the old dialog and any non-migrated
host stay untouched.

## Availability

All v2 types are annotated `@available(macOS 14, *)`. This is required, not
optional: the SwiftUI UI uses macOS 14 APIs (`ContentUnavailableView`, the
two-parameter `.onChange`), and `Libs/Scanner` compiles into PDFify, whose
deployment target is still macOS 10.14. The engine itself only needs
ImageCaptureCore (10.4+) but is gated together with the UI so the whole feature
is uniformly macOS 14+.

Receipts Space deploys to macOS 14, so no runtime check is needed there. PDFify
deploys lower, so the swap-in must be guarded with `if #available(macOS 14, *)`.

## How it is swapped in (done)

Both window controllers expose the same surface (`delegate: hxScannerDelegate?`,
`showWindow(_:)`, `window`) but share no common base beyond `NSWindowController`.

The v2 dialog is now the **default** on macOS 14+ in both apps; the old
`IKScannerDeviceView` dialog remains reachable only as an opt-out via the
`UseLegacyScannerDialog` user default. The guarded selection looks like this
(on Receipts the `#available` is always true):

```swift
if #available(macOS 14, *), !UserDefaults.standard.bool(forKey: "UseLegacyScannerDialog") {
  scannerWindowController = hxScannerV2WindowController()
} else {
  scannerWindowController = hxScannerWindowController()
}
```

## Phases

- [x] Phase 0 — Engine protocols + value types.
- [x] Phase 1 — `ICAScanDevice` engine (session, functional units, overview,
      scan). Hardware pass done: network-scanner open auto-retry, software B/W
      fallback, ImageCaptureCore error codes mapped to plain-text messages.
- [x] Phase 2 — SwiftUI UI. Went well beyond the original list: compact/detail
      two-state dialog, controller-driven window resize with per-mode snap-back,
      scanning/connecting HUD, single "append" checkbox, immediate per-run
      delivery (no page collecting).
- [x] Phase 3 — Continuity Camera. **Dropped as a dialog feature**: both apps
      already support it independently via the responder chain
      (`NSServicesMenuRequestor`), which is the correct home for it. Nothing to do
      inside the v2 dialog. See Scope.
- [x] Phase 4 — Hooked into both apps. The v2 dialog is the default on macOS 14+;
      the legacy dialog is opt-out via `UseLegacyScannerDialog`. Delivery uses
      `handleScannedPages(_:destination:)`.
- [x] Phase 5b — ScanSnap hint. The legacy dialog faked a "ScanSnap" device entry;
      v2 does not (ScanSnap is app-driven, not an ICScannerDevice — see Scope). When
      the ScanSnap software is detected, a slim, dismissible strip at the bottom of
      the dialog points at the app and the help page. Detection + launch live in
      `ScanSnapSupport`; the dismiss flag in `ScannerWorkflowPrefs`; the help URL is
      supplied per app via `hxScannerDelegate.scanSnapHelpURL()`.
- [ ] Phase 5 — Retire the old `IKScannerDeviceView` dialog after the beta has
      validated v2 on real hardware in the field.

## Open items / hardware-gated TODOs

- Verify `scanArea` coordinate origin/orientation against a real flatbed
  (selection rectangle mapping). Marked with `TODO(hardware)` in code.
- Confirm `requestSelect(_:)` / overview readiness timing on real devices.
- Memory-based (band) transfer is intentionally skipped; v2 uses file-based
  transfer to a temp directory to avoid manual band assembly. Revisit only if a
  device misbehaves with file-based transfer.
- Localization: the `scanner.v2.*` keys exist in `de` + `en` for both apps
  (PDFify `Localizable.strings`, Receipts `Localizable.xcstrings`). Further
  languages are still open (the app ships several beyond de/en).
