# Design-Notizen — v0.2.0 Flachbett-Multipage

**Stand:** 2026-05-03
**Bezug:** ROADMAP.md → v0.2.0
**Status:** Design-Phase, Pre-Implementation

---

## Designziel

Aus N Einzelscans vom Flachbett wird eine PDF, ohne dass der User das Programm verlässt. Der Workflow soll sich anfühlen wie eine Erweiterung des bestehenden Software-Duplex-Modus — nicht wie ein neuer Subbereich der App.

Drei Anforderungen treiben das Design:

1. **Vorhersagbarkeit.** Der User weiß zu jedem Zeitpunkt: Wieviele Seiten habe ich, was passiert wenn ich jetzt klicke, kann ich noch zurück.
2. **Abbruch-Sicherheit.** Cancel ist immer verfügbar und macht das, was der User intuitiv erwartet — Verwerfen, nicht „Speichern bis hier“.
3. **Wiederverwendung.** Die PDF-Assembly-Pipeline ist da. Der eSCL-Single-Page-Scan ist da. Multipage ist die Verkettung — kein neuer Stack.

---

## UI-Flow

Der Ablauf besteht aus drei Phasen, die alle aus dem bestehenden ScannerPanel heraus angestoßen werden.

```
ScannerPanel (Quelle "Flachbett — Mehrseitig")
       │
       ▼
   Erste Seite scannen (eSCL-Job, blockierend, mit Spinner)
       │
       ▼
   ┌──────────────────────────────────────┐
   │       MultipageScanDialog             │
   │  ┌──────┐ ┌──────┐ ┌──────┐           │
   │  │ S.1  │ │ S.2  │ │ S.3  │  ...      │  ← Thumbnails der bisherigen Seiten
   │  └──────┘ └──────┘ └──────┘           │
   │                                       │
   │  [ Weiter scannen ]  [ Fertigstellen ]│
   │                       [ Abbrechen ]   │
   └──────────────────────────────────────┘
       │
       ├── "Weiter scannen" → nächste Seite scannen, Dialog refresht
       │
       ├── "Fertigstellen"  → PDF zusammenbauen, Pfad anzeigen
       │
       └── "Abbrechen"      → Bestätigungsdialog → alles verwerfen
```

Die Phase „Erste Seite scannen“ wird ohne den Multipage-Dialog gestartet — wie ein normaler Single-Page-Scan, der Spinner+Cancel-Pattern. Erst wenn die erste Seite vorhanden ist, geht der Dialog auf. Damit verhält sich der Multipage-Modus für Single-Page-User nicht anders als der existierende Single-Page-Modus, wenn sie nach der ersten Seite „Fertigstellen“ klicken.

Hier eine Skizze des MultipageScanDialog-Layouts:

```
┌────────────────────────────────────────────────────────────────┐
│  Mehrseitiger Flachbett-Scan                              [×]  │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│   3 Seiten gescannt   ·   Auflösung 300 dpi   ·   Farbe        │
│                                                                │
│   ╭────────╮  ╭────────╮  ╭────────╮                           │
│   │        │  │        │  │        │                           │
│   │  ░░░░  │  │  ░░░░  │  │  ░░░░  │                           │
│   │  ░░░░  │  │  ░░░░  │  │  ░░░░  │                           │
│   │   1    │  │   2    │  │   3    │                           │
│   ╰────────╯  ╰────────╯  ╰────────╯                           │
│                                                                │
│   Nächste Seite einlegen und auf "Weiter scannen" drücken.     │
│   Wenn fertig, "Als PDF speichern" wählen.                     │
│                                                                │
├────────────────────────────────────────────────────────────────┤
│  [ Abbrechen ]                  [ Weiter scannen ] [ Speichern │
│                                                       als PDF ]│
└────────────────────────────────────────────────────────────────┘
```

Während ein Scan läuft (also zwischen Klick auf „Weiter scannen“ und dem Eintreffen der Bilddaten), wird der Dialog folgendermaßen umgeschaltet:

```
┌────────────────────────────────────────────────────────────────┐
│  Mehrseitiger Flachbett-Scan                              [×]  │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│   3 Seiten gescannt   ·   Auflösung 300 dpi   ·   Farbe        │
│                                                                │
│   ╭────────╮  ╭────────╮  ╭────────╮  ┌╴╴╴╴╴╴╴╴┐               │
│   │        │  │        │  │        │  ╎  (○)   ╎               │
│   │  ░░░░  │  │  ░░░░  │  │  ░░░░  │  ╎ scant  ╎               │
│   │  ░░░░  │  │  ░░░░  │  │  ░░░░  │  ╎  ...   ╎               │
│   │   1    │  │   2    │  │   3    │  ╎   4    ╎               │
│   ╰────────╯  ╰────────╯  ╰────────╯  └╴╴╴╴╴╴╴╴┘               │
│                                                                │
│   Seite 4 wird gescannt …                                      │
│                                                                │
├────────────────────────────────────────────────────────────────┤
│  [ Scan abbrechen ]                                            │
└────────────────────────────────────────────────────────────────┘
```

Der Scan-laufend-Zustand sperrt alle anderen Buttons. „Scan abbrechen“ bricht nur den laufenden eSCL-Job ab, **nicht** das gesamte Multipage-Set; nach dem Abbruch landet der Dialog wieder im ersten Zustand mit den 3 bisherigen Seiten und der Möglichkeit, einen erneuten Versuch zu starten oder fertigzustellen.

---

## Zustandsmodell

Drei Hauptzustände für den Dialog plus zwei Übergangszustände:

| State | Beschreibung | Buttons aktiv |
|---|---|---|
| `IDLE` | N Seiten gescannt, Dialog offen, kein Scan läuft | Weiter scannen, Speichern als PDF, Abbrechen |
| `SCANNING` | eSCL-Job läuft gerade | Scan abbrechen |
| `ASSEMBLING` | Pillow erstellt PDF, dauert je nach Seitenzahl <1 s bis ein paar Sekunden | (keine, nur Spinner) |
| `CONFIRM_CANCEL` | Bestätigungsdialog vor Verwerfen | Ja/Nein |
| `DONE` | PDF erstellt, Dialog schließt sich, Hauptpanel zeigt Pfad-Hinweis | — |

Das `IDLE → ASSEMBLING → DONE`-Trio ist der Fertigstellen-Pfad. Das `IDLE → SCANNING → IDLE`-Pendant ist der Loop, in dem der User typischerweise N Mal hängt. Der Cancel-Pfad ist `IDLE → CONFIRM_CANCEL → (verworfen, Dialog zu)`.

---

## Edge Cases

**Erste Seite schlägt fehl.** Wenn der allererste Scan einen eSCL-Fehler wirft, kommt der Dialog gar nicht erst auf — das Hauptpanel zeigt einen Toast mit der Fehlermeldung wie heute beim Single-Page-Scan. Das Verhalten ist also identisch zum existierenden Single-Page-Pfad.

**Spätere Seite schlägt fehl.** Im `SCANNING`-State führt ein Fehler zurück nach `IDLE`, mit einem inline angezeigten Fehler-Banner über den Buttons (Adw.Banner-Pattern, in PrintEase schon etabliert für CUPS-Verbindungsfehler). Die bisher gescannten Seiten bleiben erhalten — der User kann erneut versuchen oder mit den vorhandenen Seiten fertigstellen.

**Dialog versehentlich geschlossen (Esc, Fenster-X).** Wird wie ein Cancel behandelt — Bestätigungsdialog erscheint, Standardantwort ist „Abbrechen“ (also Schließung verhindern), zweite Option ist „Verwerfen“. Das ist die GTK-Standard-Convention für unsaved-state-Dialoge.

**System schläft / Suspend mitten im Scan.** Der eSCL-Job wird vom Scanner aus Sicht von PrintEase entweder mit Timeout oder mit Connection-Reset enden. Behandlung wie bei einem normalen Scan-Fehler: Banner, gescannte Seiten bleiben erhalten.

**Out of memory bei vielen Seiten.** Die Soft-Limit-Warnung bei N>30 wird inline im Dialog angezeigt (Adw.Banner unter dem Statusbereich): „Bei mehr als 30 Seiten kann die PDF-Erstellung viel Speicher benötigen. Erwägen Sie eine niedrigere Auflösung oder einen Schwarz-Weiß-Modus.“ Kein Hard-Stop, nur Hinweis.

**Auflösungsänderung mitten im Set.** Im `IDLE`-State sind die Auflösungs-/Farbmodus-Dropdowns im Haupt-`ScannerPanel` gesperrt, sobald Seite 1 gescannt wurde. Konsistenz innerhalb des Sets ist garantiert.

**Scanner zwischen Seiten weggefahren.** Wenn der Scanner zwischen Seiten verschwindet (Netzwerk weg, ausgesteckt), schlägt der nächste Scan-Versuch in `_create_scan_job` mit `URLError` fehl. Behandlung wie Edge Case 2.

---

## Code-Entwurf

Die zentrale Funktion in `scanner_client.py`:

```python
def scan_flatbed_multipage(
    scanner: ScannerInfo,
    color_mode: str = "RGB24",
    resolution: int = 300,
    next_page_callback: Callable[[list[bytes]], str] = None,
    cancel_event=None,
) -> str:
    """Mehrseitiger Flachbett-Scan, eine eSCL-Anfrage pro Seite.

    next_page_callback(pages_so_far) -> str blockiert auf dem GTK-Main-
    Thread und liefert eine Aktion zurück: 'continue' (weitere Seite),
    'finish' (PDF erstellen), 'cancel' (alles verwerfen, raised
    InterruptedError).

    Gibt den Pfad zur erstellten PDF-Datei zurück.
    """
    pages: list[bytes] = []

    while True:
        if cancel_event and cancel_event.is_set():
            raise InterruptedError("Scan abgebrochen")

        page_bytes = _scan_to_bytes(
            scanner,
            color_mode=color_mode,
            resolution=resolution,
            source="Platen",
            cancel_event=cancel_event,
        )
        pages.append(page_bytes)

        action = next_page_callback(pages) if next_page_callback else "finish"
        if action == "finish":
            break
        if action == "cancel":
            raise InterruptedError("Vom Benutzer abgebrochen")
        # else: "continue" -> nächste Iteration

    output_path = _default_multipage_output_path()
    _create_pdf(pages, output_path)
    log.info("Multipage-PDF erstellt: %s (%d Seiten)", output_path, len(pages))
    return output_path
```

Anmerkung zur In-Memory-Pipeline: Die existierende `scan_document`-Funktion (Zeile 140) schreibt das Ergebnis als JPEG auf die Platte. Für Multipage ist das verschwenderisch — die Bytes liegen schon im Memory in `_fetch_next_document`. Refactoring-Vorschlag: eine neue private Funktion `_scan_to_bytes(scanner, ...)` extrahieren, die `scan_document` und die ADF-Funktionen gemeinsam nutzen. Das ist kein Multipage-spezifisches Refactoring, kommt aber natürlich mit dem Feature.

Die Funktion `_default_multipage_output_path()` ist analog zu `_default_duplex_output_path()` (Zeile 476) zu bauen — `~/Pictures/Scans/scan_multipage_<TS>.pdf`.

Im UI-Pfad wird der `next_page_callback` an einen GLib-MainContext-Sync-Call gebunden, der den `MultipageScanDialog` öffnet und auf User-Aktion blockiert. Pattern entspricht dem `flip_callback` in der Software-Duplex-Logik.

---

## Offene Fragen

**Soll der erste Scan ohne Dialog erfolgen, oder mit Dialog im SCANNING-Zustand?** Argument für „ohne Dialog“: konsistent zum Single-Page-Pfad, einfacher mentaler Wechsel. Argument für „mit Dialog“: der User sieht von Anfang an, dass er sich im Multipage-Modus befindet, weniger Verwirrung. **Vorläufige Position: ohne Dialog für die erste Seite, weil das den Zwei-Zustands-Aufbau (Single → Multi) sauber hält.**

**Thumbnail-Größe.** 80×120 px? 100×140 px? Hängt davon ab, wieviele Thumbnails horizontal in den Dialog passen sollen, ohne dass eine zweite Reihe nötig wird. Bei 5 Thumbnails à 100 px + Abständen ist 600 px Dialogbreite knapp — entweder schmalere Thumbnails oder horizontaler Scroll bei N>5. **Vorläufige Position: 80×120 px, horizontaler Scroll ab Seite 6.**

**Speicherort konfigurierbar?** Aktuell hartkodiert auf `~/Pictures/Scans/`. Für Multipage könnte ein „Speichern als…“-Dialog vor dem Fertigstellen sinnvoll sein. Aber: nicht in v0.2.0. Erst in v0.5.0+ als allgemeines Feature für alle Scan-Modi gleichzeitig.

**Drag & Drop / Reorder im Dialog?** Vier Seiten gescannt, aber Reihenfolge ist falsch — könnte der User die Thumbnails per Drag verschieben? **Vorläufige Position: nicht in v0.2.0.** Die Komplexität der Implementierung steht in keinem Verhältnis zum Use-Case (User hat Vorlagen meist in der richtigen Reihenfolge auf dem Tisch). Wenn das Bedürfnis real ist, kommt es als eigenes Feature in v0.5.0+.

**Seite löschen im Dialog?** „Hoppla, Seite 3 war Mist“ — Right-Click-Menü auf Thumbnail: „Diese Seite verwerfen“. **Vorläufige Position: ja, einfach umsetzbar, wertvoll für den User.** Kein Reorder, aber Delete.

---

## Test-Plan

Vor Release:

1. **Glücklicher Pfad.** Ricoh M C240FW, 5 Seiten A4 RGB 300 dpi → 5-seitige PDF, lesbar in Evince und Adobe Reader.
2. **Single-Page-Verhalten.** Direkt nach Seite 1 „Fertigstellen“ → 1-seitige PDF, identisch zu Single-Page-Modus.
3. **Cancel früh.** Nach Seite 1 „Abbrechen“ → Bestätigungsdialog, Bestätigung → keine Datei im Filesystem, Spool-Verzeichnis leer.
4. **Cancel im Scan.** Zwischen Seite 2 und Seite 3 während des laufenden eSCL-Jobs „Scan abbrechen“ → Scan-Job sauber beendet, 2 Seiten erhalten, Dialog im IDLE-Zustand.
5. **Großes Set.** 30 Seiten 200 dpi SW → PDF erstellt, Memory-Peak unter 200 MB (überprüft mit `htop`).
6. **Edge: Scanner während Scan trennen.** Netzwerk während Seite 3 trennen → Banner mit Fehlermeldung, 2 Seiten erhalten, Wiederversuch nach Reconnect klappt.
7. **Edge: Seite löschen.** Nach 4 Seiten Seite 2 löschen → PDF mit 3 Seiten in originaler 1-3-4-Reihenfolge.
8. **Locale-Test.** Mit Sprachumschaltung (de → en → ja) während des Sets — keine Crashes, Dialog-Texte korrekt nach Sprachwechsel.
9. **Suspend-Test.** Nach Seite 2 System suspenden, aufwecken, weiter scannen → entweder funktioniert oder zeigt sauber Fehler.
10. **Plasma + i3wm.** Dialog-Modalität korrekt auf beiden Window-Managern.
