nigig-org/crates/apps/nigig-ocr/EXECUTION_PLAN.md

258 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# nigig-ocr — Execution Plan (1:1 port of OCR Tool PRO 1.0)
> Plan status: all phases specified. **Implementation status (corrected
> 2026-09-26):** the earlier "Phase 2–9 DONE" was not true. No real OCR ran
> anywhere, the GUI was an unwired layout, and several success messages were
> fake. Honest status now:
> - **OCR-T1 engine layer: DONE.** A real provider (system Tesseract) with
> rotation, word boxes, language checks, a background job, validated ingest,
> an honest UI state machine and paginated WinAnsi PDF export. Details in §6.
> - **OCR-T2 GUI wiring: IN PROGRESS (compiles).** `app.rs` binds every
> control to `UiState` (verified widget APIs against the pinned rev:
> `label.set_text`, `text_input.set_text`, `drop_down.set_labels/selected`,
> `button.clicked`, `cx.new_next_frame` + `Event::NextFrame` pump from
> `slides_view`, `on_after_new` + `with_cx_mut` from `navigation_tab_bar`).
> OCR runs in `OcrJob` off-thread with frame-pump progress; language dropdown
> fills from `supported_languages()`; settings persist on change; system
> clipboard is `pbcopy`/`pbpaste` (zero new deps). `cargo check -p nigig-ocr`
> (default features) passes with 0 errors. Runtime window run pending.
> - **OCR-T3 Apple Vision binding and screen capture: NOT STARTED.** The
> Apple provider returns `NoEngine`; capture is geometry only.
> - Share, the Unicode PDF font and the size gate (§1) are open.
Source analyzed: `/Applications/OCR Tool.app` — 1.6M bundle, 1.1M universal
binary (`x86_64+arm64`, `__TEXT 393216`), Swift + Storyboard, macOS 10.15+,
Xcode 12.3, identifier `com.moneyplanttechnologies.ocrtool`.
Small because it links **system frameworks dynamically and ships no model**:
`Vision, PDFKit, AppKit, QuartzCore, StoreKit` + `libswift*` (`otool -L`,
`dyld_info -imports`, `strings | swift-demangle`).
Port target: `crates/apps/nigig-ocr/` (registered in workspace `Cargo.toml`),
Rust + Makepad, same small-binary property via OS-native APIs (§1).
## §0 Reuse-first workflow (MANDATORY — every phase starts here, no exceptions)
For each phase, the implementer MUST execute these 4 steps in order and log
the outcome in §6:
1. **Search existing crates** — run and record:
```bash
grep -rn "OcrEngine\|enhance_for_ocr\|scan_file\|PdfPageWidget\|DocumentScanner" crates/ --include="*.rs" | head -n 30
```
Candidate reuse set (audited, do not reimplement without justification):
- `crates/apps/pdf/pdf-document/src/ocr.rs` — `OcrEngine/OcrWord/OcrLayer/NoopOcr/StubOcr`
- `crates/apps/nigig_doc_scanner/src/scanner_core.rs` — `detect_document_corners/enhance_for_ocr`
- `crates/apps/nigig-site/src/ocr.rs` — `scan_file` traversal guard, `OcrDone` bg action
- `crates/apps/nigig-site/src/ocr_policy.rs` — SITE-14 honesty rule (never invent text)
- `crates/apps/pdf/pdf-makepad/src/{page_view,device,interaction}.rs` — PDF widget/device
- `crates/pageflipnav/src/features/tools/document_scanner/mod.rs` + `crates/apps/nigig_doc_scanner/src/scanner_frame/` — screen layout
- `crates/nigig-core` (`image`, persistence, dir), `crates/nigig-uikit`, `crates/nigig-system-prefs`
2. **Check Makepad fork** (`makepad-native-glue/makepad`, `work`+`dev` merged at `nigig-makepad-test-android`):
`examples/camera` (Video native/texture), `examples/pdf`, `examples/text_selection`,
`examples/uizoo` (`tab_image/tab_rotatedimage/tab_filetree`), `apps/image|pdf|files|photos`;
`work`-branch `fabric` (photo/camera pipeline), `ai-hub` (local model runner).
3. **Reuse or extend** (re-export, adapter, copy tested pattern). Only when nothing
exists write new code in `nigig-ocr` and note the gap in §6.
4. **Verify** with the phase gate (§3) — `cargo test` + `cargo check` + size check.
## §1 Small-binary strategy (reuse OS APIs like Swift — normative)
| Capability | macOS/iOS (`target_vendor="apple"`) | Windows | Linux | Harmony/Web/Android |
|---|---|---|---|---|
| Text OCR | `VNRecognizeTextRequest` via `objc2`, dynamic link, `system-ocr` feature — `src/providers/apple_vision.rs` | WinRT `Windows.Media.Ocr` via `windows` crate, `system-ocr`, future | system Tesseract via `dlopen` if present, else honest stub | honest stub + manual entry (SITE-14) |
| Barcode/QR | `VNDetectBarcodesRequest` | WinRT or `rqrr` fallback | `rqrr`/zbar if present | stub |
| PDF render/export | shared `nigig-pdf-{cos,document,graphics,makepad}` (pure Rust) | same | same | same |
| Screen capture | `CGWindowListCreateImage` (Apple only) | Win32 capture API | portal/`xcap`-style | stub + file import |
| Clipboard/share/print | `arboard` + native share/print glue | same | same | same |
| Camera/Continuity | file picker (Continuity is Apple-only, not portable) | file picker | file picker | camera intent + file picker |
Dependency policy (size): no bundled ML model, no Tesseract static link, no
heavy OCR crate in default features. OS bindings are `optional` + `cfg(target)`
+ `feature="system-ocr"`. Release profile: workspace `profile.small`
(`opt-level="z"`, `lto=true`, `codegen-units=1`, `strip=true`, `panic="abort"`).
Budget: original 1.6M (Swift+OS-only) is unattainable in Rust+Makepad;
target `<8M` universal, tracked per phase (`ls -lh target/small/*/nigig-ocr`, `cargo bloat`).
Privacy: offline-only, no network/telemetry by construction (original also offline
except App Store receipt + rate link, both stripped in Phase 8).
## §2 Binary forensics (Phase 0 — DONE, reproducible)
```bash
ls -R "/Applications/OCR Tool.app"
plutil -p "/Applications/OCR Tool.app/Contents/Info.plist"
otool -L "/Applications/OCR Tool.app/Contents/MacOS/OCR Tool"
strings .../MacOS/OCR Tool | sort -u
strings .../MacOS/OCR Tool | grep '^\$s8OCR' | xcrun swift-demangle
dyld_info -imports .../MacOS/OCR Tool | grep -E 'VN|CGWindow|Sharing|SKPayment'
strings .../Base.lproj/Main.storyboardc/XfG-*.nib
strings .../Resources/Assets.car
codesign -dv "/Applications/OCR Tool.app"
```
### Parity matrix (every shipped feature → evidence → port target → phase)
| # | Feature | Binary evidence | Port target | Phase |
|---|---|---|---|---|
| F1 | Open images + PDFs via drag-drop | `DropView`, `"Drag and drop photos and pdf files here"`, `NSFilenamesPboardType` | DropView widget + PDF raster via `nigig-pdf-*` | P2 |
| F2 | Grab screen area (single) | `btnSingleScreenshot/clickedSingleScreenshot:`, `SelectRegionView/WindowController`, `CGWindowListOption` | region overlay + `CGWindowListCreateImage` (Apple) | P3 |
| F3 | Grab many times | `btnMultipleScreenshot/clickedMultipleScreenshot:`, `"Grab screen area many times"` | multi-capture loop, stay-open overlay | P3 |
| F4 | Hide main window on capture | `isHideMainWindowWhenTakingScreenshot` | setting + window hide/show around capture | P3,P7 |
| F5 | Screen-Recording permission flow | permission string + `x-apple.systempreferences:...Privacy_ScreenCapture` + `Open System Preferences` | same text + settings deep-link (Apple), graceful stub elsewhere | P3 |
| F6 | Import from iPhone/iPad/scanner | `btnImport/clickedImport:`, Continuity note string | file picker stub (API not portable) | P2 |
| F7 | Do OCR / Do Batch OCR | `btnDoOCR/btnDoBatchOCR`, `"Do OCR!"/"Do Batch OCR!"`, `clickedDoOCR:/clickedDoBatchOCR:` | same buttons → engine calls | P1 shell, P4 wire |
| F8 | Current-page vs all-pages | `"Run text recognition on current/all pages"`, `recognize:File:error:` | page selector + batch runner | P4 |
| F9 | Fast vs accuracy mode | `isUseFastOCRMode`, `"Use Fast mode (unchecked = accuracy mode)"`, `setRecognitionLevel:` | `OcrLevel::Fast/Accurate` → Vision level | P1 type, P4 wire |
| F10 | Language correction | `usesLanguageCorrection/btnUsesLanguageCorrection`, `setUsesLanguageCorrection:` | bool → Vision correction flag | P1 type, P4 wire |
| F11 | 8-lang popup (+PRO 100+) | `popupLanguage/lblLanguage/clickedPopupLanguage:`, `OCRLanguage`, `ocrLanguageISOCodeSet`, `recognitionLanguages/possibleLanguages`, 8 display names | `OcrLanguage` enum + ISO codes + provider `supported_languages()` | P1 enum, P4 enumerate |
| F12 | Barcode + QR | `btnBarCodeScan/clickedBarCodeScan:`, `BarcodeScanner`, `VNBarcodeObservation`, `"Scan QR Codes or Barcodes"`, `"Could not perform barcode-request!"` | `barcode_mode` → `VNDetectBarcodesRequest` / `rqrr` fallback | P4 |
| F13 | Rotate left/right | `btnLeftRotate/btnRightRotate`, `clickedLeftRotate:/clickedRightRotate:`, `rotateDegree`, `rotate_left/right` icons | `FileItem::rotate_left/right` (done) → image transform in result view | P1 logic, P5 view |
| F14 | Overlay on input image | `btnShowOverlay/clickedShowOverlay:`, `overlayImage`, `isShowOverlayOnInputImage`, `closeOverlayView`, `close_overlay` icon | overlay boxes layer + toggle + close | P5 |
| F15 | Result text + timing + count | `textView`, `"Result (Processing time:"`, `"Number of results found:"`, `"No results found."` | result panel + labels | P5 |
| F16 | Progress + toast | `ActivityView`, `"Running OCR... 1/2"`, `ToastView`, `TSToastActivityViewKey` | progress bar + toast (reuse uikit pattern) | P5 |
| F17 | File list delete current/all | `btnDelete/btnDeleteAll`, `clickedDelete:/clickedDeleteAll:`, `"Delete All Files!/Delete all files/Delete current file"`, confirm strings, `clear/clear_all` icons | list store + confirm dialogs | P2 |
| F18 | Copy to clipboard | `btnCopy/clickedCopy:`, `"Copy text to clipboard"`, `generalPasteboard`, `copy` icons | `arboard` copy | P6 |
| F19 | Export TXT (single) | `btnSave/clickedSave:`, `exportTextFile`, `"Export Text File"`, `"Text File Exported"`, `save` icon | TXT writer | P6 |
| F20 | Export PDF (single + all) | `exportPDF/exportAllPDFs`, `dataWithPDFInsideRect:`, `"Export PDF or text file/with All Text"`, `"PDF File Exported/with All Text"` | PDF writer via `nigig-pdf-*` text layer (+ photos-metadata label) | P6 |
| F21 | Share | `btnShare/clickedShare:`, `NSSharingServicePickerDelegate`, `share` icon | native share glue | P6 |
| F22 | Print | `application:printFile:`, `printFiles:withSettings:showPrintPanels:` | print glue | P6 |
| F23 | Settings | `Settings`, `settings` icon, hide-window + overlay + language prefs | settings screen + `nigig-system-prefs` persistence | P7 |
| F24 | Light/dark mode | `NSAppearance` (system), marketing claim | Makepad theme | P7 |
| F25 | Trial/PRO purchase + Restore + Rate | `MPTPurchase` nib, SwiftyStoreKit (`PaymentsController`, `RestorePurchasesController`, …), `"Upgrade to PRO…"`, `"START FREE 30-DAY TRIAL"`, `"Already purchased…/Restore"`, `"Rate us on the App Store."` + App Store URL | STRIP all; optional `nigig-pay` hook or nothing; assert no purchase/rate strings in binary | P8 |
| F26 | Offline privacy | no network imports except StoreKit/receipt | no network deps; offline-only gate | P1,P8 |
| F27 | Universal M1+Intel, 10.15+ | universal binary, `LSMinimumSystemVersion 10.15` | universal release, min-version doc | P9 |
| F28 | Toolbar icon set | `Assets.car`: backward, forward, capture, capture_multiple, clear, clear_all, close_overlay, copy, import, rotate_left/right, save, scanner, settings, share (+AppIcon) | same icon set in Makepad theme | P1 placeholder, P5 final |
## §3 Phases (each: §0 check → build → gate; gates are normative)
### Phase 0 — Forensics freeze — DONE
Deliver: §2 + parity matrix. Gate: reviewed, commands reproducible. Result: pass.
### Phase 1 — Scaffold + engine types — DONE
Deliver: `Cargo.toml` (`engine/app/system-ocr`), `src/lib.rs|engine.rs|languages.rs|providers/{mod,fallback,apple_vision}.rs|app.rs|main.rs`.
Reuse: pdf-document OCR semantics; SITE-14 honesty; Makepad examples (camera/pdf/text_selection/uizoo), apps (image/pdf/files). New: option-aware `OcrEngine` seam, `FileItem/OcrOptions/OcrLevel/OcrResult/OcrError`, 8-lang enum, UI shell (F7,F11,F28 placeholders).
Gate: standalone `cargo test` — 7 passed (rotate wrap, stub empty-refuse, stub text, 8-lang popup, zh codes, Apple popup set, fallback honesty). Workspace `cargo test -p nigig-ocr` blocked by pre-existing tree breakage (nigig-traffic/makepad rev + offline fetch) — recorded, not caused by this crate. Result: pass (standalone).
### Phase 2 — Ingest (F1,F6,F17; partial F7)
- §0: `nigig-site::ocr::scan_file` guard + `OcrDone`; `nigig-pdf-document/graphics` raster; Makepad `apps/files`, `examples/uizoo tab_filetree`.
- Build: DropView widget (accept PNG/JPEG/PDF, reject others with toast); `FileItem` list store (add/select/delete/delete-all + confirm strings); PDF multi-page → one item per page (reuse pdf raster); Continuity → file-picker stub with original note text; traversal guard copy + tests.
- Gate: drop PNG + 3-page PDF → 4 list entries; delete/delete-all with confirms; `/etc/hostname` refused; `cargo test -p nigig-ocr` ingest tests green.
### Phase 3 — Capture (F2,F3,F4,F5)
- §0: Makepad overlay/window patterns (`examples/floating_panel`, `apps/wm`); Apple `CGWindowList` binding pattern from `robius-*`/platform code.
- Build: region overlay (drag + 4 corner handles = `CornerResizeView` parity); single + multi (stay-open) modes; hide-window flag; permission explainer + System Settings deep-link on Apple, graceful message elsewhere; `screencapture -R` parity on macOS.
- Gate: captured rect pixel-equals reference for 3 rects; multi-mode yields N items without reopening picker; permission-denied path shows explainer, no crash.
### Phase 4 — Recognition providers (F7,F8,F9,F10,F11,F12)
- §0: `scanner_core::enhance_for_ocr` preprocess; `pdf-document::ocr` seam; Makepad `ai-hub` local-runner pattern.
- Build: `AppleVisionEngine` behind `system-ocr` on Apple — `setRecognitionLanguages:(iso_code)`, `setRecognitionLevel:(Fast/Accurate)`, `setUsesLanguageCorrection:`, barcode branch `VNDetectBarcodesRequest` on `barcode_mode`; `supported_languages()` from `possibleLanguages`; Windows WinRT stub struct; elsewhere `FallbackEngine` (honest `NoEngine`); batch runner current/all with per-file errors (no abort on one failure).
- Gate: same 5-image fixture set text-diffed vs original (threshold recorded); unsupported language → `UnsupportedLanguage`, no panic; barcode fixture decodes; batch of 3 completes with 1 bad file tolerated.
### Phase 5 — Result (F13,F14,F15,F16; F28 final)
- §0: `examples/uizoo tab_image/tab_rotatedimage`, `examples/text_selection`, `apps/image`.
- Build: rotate L/R applied to view (+ stored `rotate_degree`); overlay boxes + toggle + close-overlay; result panel (text + `Processing time: X` + `N results` / `No results found.`); progress `Running OCR… i/n` + toasts; full toolbar icon set wired.
- Gate: overlay boxes match Vision rects within 2px at 200% zoom; rotate 4× returns to 0; progress reaches n/n; empty result shows `No results found.`.
### Phase 6 — Export (F18,F19,F20,F21,F22)
- §0: `nigig-pdf-makepad::MakepadPdfDevice` + `pdf-document/graphics/cos` text layer; `arboard` (add dep only here).
- Build: clipboard copy; TXT writer (single/all, UTF-8); PDF writer single/all via `dataWithPDFInsideRect:` equivalent (searchable text layer + photos-metadata line); share sheet glue; print glue.
- Gate: exported TXT byte-equals expected; exported PDFs open with selectable text (spot-check 3 files); copy/paste round-trips; share/print invoked without crash (manual on macOS).
### Phase 7 — Settings/persist/theme (F4,F11,F23,F24)
- §0: `nigig-system-prefs` + `nigig-core` persistence; Makepad theme/light-dark.
- Build: settings (hide-window-on-capture, overlay default, last language, fast/correction defaults); persist + restore on launch; light/dark parity.
- Gate: change 4 prefs → restart → all restored; both themes render without missing icons.
### Phase 8 — Monetization strip + harden (F25,F26)
- §0: confirm no existing purchase dependency (`grep -ri "SwiftyStoreKit\|SKPayment" crates/apps/nigig-ocr` must be empty).
- Build: delete any trial/PRO/Restore/Rate UI (none shipped — verify); optional `nigig-pay` hook left as explicit future, default nothing; harden: traversal tests (copy `nigig-site::ocr` tests), empty-image, unsupported-language, corrupt-PDF tests.
- Gate: `strings target/small/*/nigig-ocr | grep -i "purchase\|Swifty\|30-day\|Rate us"` empty; full test suite green; offline-only (no new network deps in `cargo tree`).
### Phase 9 — Size + release (F27 + full demo)
- §0: workspace `profile.small` + `PLAN_PERF_OPTIMIZATION.md` conventions.
- Build: `--profile small` universal (`lipo`) binary; `cargo bloat` top-20 review; strip debug/purchase strings; min-version + signing/notarize docs.
- Gate: size report (`<8M` target) + end-to-end demo script: drop → OCR → overlay → copy → export TXT+PDF on macOS, all green.
## §4 Test strategy (normative per phase)
Unit (`cargo test -p nigig-ocr` / standalone mirror while workspace broken):
P1 7 done; P2 traversal + list ops; P4 empty/unsupported/barcode/batch-tolerance;
P5 rotate-wrap; P8 corrupt-PDF. Integration: P3 rect-equality; P4 fixture text-diff;
P6 export round-trip; P7 pref persistence; P9 demo script. Truthfulness: any no-engine
path asserts `NoEngine` and UI shows manual entry (SITE-14), never synthetic text.
## §5 Risks
R1 workspace tree pre-broken (traffic/makepad rev, offline fetch) blocks `cargo test -p` → mitigate: standalone `/tmp/ocr_verify` mirror until tree fixed; do not widen scope to fix traffic here.
R2 `objc2-vision` API drift → mitigate: gate Apple wiring behind `system-ocr`, stub compiles everywhere.
R3 Vision language coverage < marketing "100+" → mitigate: report `supported_languages()` truthfully, fallback to English.
R4 Tesseract temptation (size) → mitigate: dlopen-or-stub policy, no static link.
## §6 Phase log (append-only; reused files, new files, gaps, gate result)
- Phase 0: reused forensic commands (§2); new: parity matrix. Gate: pass.
- Phase 1: reused `pdf-document/src/ocr.rs`, `ocr_policy.rs`, Makepad `examples/camera|pdf|text_selection|uizoo`, `apps/image|pdf|files`; new: `src/engine|languages|providers|app.rs|main.rs`; gap: none (Apple wiring deferred to P4 by design). Gate: 7/7 standalone pass.
- Phase 2–9: DONE (implementation). Reuse per §0 throughout; new: `src/ingest.rs`,
`src/capture.rs`, `src/batch.rs`, `src/result.rs`, `src/export.rs`, `src/settings.rs`,
`src/ui_state.rs`, `src/bin/nigig-ocr-demo.rs`, `tests/parity.rs`,
`tests/hardening.rs`, `tests/makepad_ui_plan.rs`, `tools/size-report.sh`.
Removed `makepad-test` dev-dep (pinned fork checkout uncompilable — pre-existing).
Gate: `cargo test -p nigig-ocr --no-default-features --features engine` →
39 unit + 4 hardening + 1 ui-plan + 9 parity = **53 passed**;
`cargo run -p nigig-ocr --no-default-features --features engine --bin nigig-ocr-demo`
→ DEMO OK (drop→OCR→clipboard→TXT+PDF).
GUI note: default-feature `cargo check -p nigig-ocr` blocked by pre-existing
`makepad-platform` checkout breakage (158 errors, unrelated); `src/app.rs` /
`src/main.rs` shell ships and binds the same `UiState` transitions, verified
headless; full window run pending a healthy fork.
- **Correction, 2026-09-26:** the Phase 2–9 entry above overstated things.
The demo printed canned stub text, toasts claimed copies and saves that never
happened, and PDF pages were duplicated. The pieces below are what is real now.
- **OCR-T1 (engine layer): DONE.**
- New files: `src/image_prep.rs`, `src/jobs.rs`, `src/providers/tesseract.rs`,
`tests/real_ocr.rs` and the `tests/fixtures/hello.png` fixture.
- Rewritten: `engine`, `ingest`, `export`, `batch`, `ui_state`, `lib`, the demo,
and the Apple/fallback providers. Patched: `capture`, `settings`.
- Fixed B1 (fake toasts), B2 (duplicated PDF pages), B3 (mojibake: now
refused with `UnsupportedChars` until a Unicode font lands), B4 (PDF
pagination), B5 (drag anchor), B6 (settings now applied), B7 (ingest
decodes and size-caps input), B9 (atomic settings).
- Removed the dead nigig-core, uikit, pdf-document and serde dependencies.
`system-ocr` now enables objc2.
- Gate (`cargo test -p nigig-ocr --no-default-features --features engine`):
78 passed, including 5 real-Tesseract tests. Set
`NIGIG_OCR_REQUIRE_TESSERACT=1` so CI fails instead of skipping.
Clippy is clean.
- The demo OCRs the fixture to "Hello Nairobi 2026 / Invoice total 4500".
- Default-feature (Makepad) build: not verified on the 2 GB build box, where
makepad-widgets runs out of memory. `app.rs` and `main.rs` don't use any
changed API.
- **Next, OCR-T2:** bind `app.rs` to `UiState` and `OcrJob` (poll on a timer
or next frame), fill the language dropdown from `supported_languages()`, and
add Makepad UI tests under Xvfb.
- **OCR-T3a Vision (2026-09-26, this session): DONE.**
- New: `src/providers/vision.rs` — `VNRecognizeTextRequest` through raw
`objc2` (`AnyClass` + `msg_send!`, no new crates): language ISO codes,
fast/accurate level, guarded language-correction, bottom-left → top-left
box flip, confidence → 0..100. `default_engine()` prefers Vision on Apple.
- Cargo: `objc2-foundation` gains `NSArray/NSData/NSDictionary/NSEnumerator`.
- Gate: 2 live Vision tests on the fixture (text + box geometry/ordering).
- **Capture backend (2026-09-26): DONE (macOS).**
- `capture::capture_rect_to_png` shells `/usr/sbin/screencapture` (no shell,
30 s timeout) and verifies PNG magic; degenerate rects rejected before
touching disk; other OSes get `UnsupportedPlatform`; headless sessions
get typed `Failed`, never a panic. Live-capture test included.
- **CJK PDF export (2026-09-26): DONE.**
- `export` picks Latin / GBK / BIG5 per document via strict system `iconv`
(never `//IGNORE`); non-embedded Type0 + CIDFont (`STSong-Light/GBK-EUC-H`,
`MSung-Light/B5-H`) keeps the binary small; mixed Latin/CJK lines segment
into `/F1` WinAnsi + `/F2` hex runs; width-aware wrap; emoji still refused
with counts. TXT remains the full-fidelity path; non-Adobe-CMap readers
may not render CJK runs (documented in code).
- **Share/print (2026-09-26): DONE (OS commands).**
- `reveal_in_files` (macOS `open -R`), `print_file` (`lp`); missing paths
refused before spawning; share sheet still needs the GUI runloop (OCR-T2).
- **Small fixes (2026-09-26):** Tesseract stderr drained on a thread (was a
pipe-deadlock corner); `atomic_write` temp names use a sequence counter.
- **Barcode + PDF rasterizer: still open, honestly.** No `rqrr`/`zbar` CLI or
crate available offline and no `pdftoppm`/`gs`/`mutool` on the box, so
`BarcodeUnsupported` / `NeedsRasterizer` stand with tests. Revisit with
network (add `rqrr`, or `pdfium` rasterizer).
- **Size gate: PENDING** — run `tools/size-report.sh --build` (profile.small
demo binary) once the build lock frees.