# ADR-072 — Proof Image Capture and Annotation Standard

## Status
Accepted

## Status History
```yaml
- 2026-06-27: Accepted
  Decision: All PWAs capturing proof images must implement the exact two-phase flow 
            defined in this ADR. Standard for cash transaction proofs via CTH composite.
            Lazy-fetch (never include receipt_image in list queries) is mandatory.
```

---

## Context

Many 360LM PWAs capture images as proof of transactions — receipts, documents, Cash Hand-to-Hand verification. Before this decision, image handling was ad-hoc, leading to:

- **Redundant implementations** across PWAs (no shared pattern)
- **Performance bloat** when receipt_image was included in list queries (100 KB per row)
- **Inconsistent UX** — some PWAs offered crop only, others had no editing, others had full annotation
- **Ambiguity on CTH flow** — no standard for front-camera capture + receiver signature composite

This ADR defines a single canonical two-phase image capture and editing flow that all PWAs must follow:
1. **Capture** — file picker, mobile camera, or CTH (Cash Hand-to-Hand) flow
2. **Edit** — fullscreen canvas editor with crop, rectangle, oval, line, pen, colors, stroke weights, and undo
3. **Compress** — JPEG quality slider (10–95, default 70), max 100 KB enforced

### Affected PWAs
- **Custodian** (v11, 2026-06-17 — first implementation)
- Any PWA introducing payment modes with `cash_with_receipt`, `cash_hand_to_hand`, or optional proof images
- Future PWAs requiring document/receipt/verification imagery

### Constraints
- Single HTML file per PWA (ADR-013) — no shared HTML imports; copy canvas editor code verbatim
- Storage: base64 JPEG in `receipt_image TEXT` column (nullable)
- Offline-first support required (ADR-021) — capture and edit must work offline; compression can defer network sync
- Max canvas dimension: 800 px on longest side (keeps output <100 KB when compressed)
- Desktop fallback for CTH: no camera → signature-only composite without photo pane

---

## Decision

**All PWAs capturing proof images MUST implement the canonical two-phase flow:**

1. **Capture Phase**
   - **File picker** (supports JPG, PNG): standard for receipts, documents, verification photos
   - **Mobile camera** with `capture="environment"` (rear camera by default)
   - **CTH (Cash Hand-to-Hand)** flow (primary for cash proof):
     - Front-facing camera (`facingMode: 'user'`) with 3-2-1 countdown overlay on `<video>`
     - Receiver signs on a canvas below the video
     - Composite image created at 800 px wide with three panes:
       - Photo (mirrored during preview; un-mirrored in captured canvas via `ctx.translate()`)
       - "RECEIVER SIGNATURE" label
       - Signature canvas
     - Desktop fallback (no camera): offer signature-only composite (no photo pane)

2. **Edit Phase** (fullscreen canvas editor)
   - **Tools:**
     - Crop (orange dashed selection, corner handles, darkens outside, undoable)
     - Rectangle annotation
     - Oval annotation
     - Line annotation
     - Pen (freehand) annotation
   - **Color palette:** 5 swatches
     - Red `#ef4444`
     - Black `#111827`
     - Blue `#2563eb`
     - Green `#16a34a`
     - Orange `#f97316`
   - **Stroke weights:** 3 options
     - Thin: 2 px
     - Medium: 4 px
     - Thick: 7 px
   - **Undo stack:** up to 20 levels, preserving both imageData AND canvas dimensions (crop undo restores original size)
   - **Z-index:** 950 (above lightbox at 800)

3. **Compression Phase**
   - JPEG quality slider: 10–95 (default 70)
   - Size estimate shown to user: `Math.round((b64.length - header) * 3 / 4)` bytes
   - Max size enforced: 100 KB (user gets feedback if over limit; slider resets to quality 70 to try again)
   - Compressed base64 stored in `receipt_image TEXT` column

4. **Storage & Retrieval**
   - Column: `receipt_image TEXT` (nullable base64 JPEG) on each transaction table
   - **NEVER include `receipt_image` in list/ledger queries** — bloats every row by ~100 KB
   - Lazy-fetch: show 📎 (paperclip) icon on rows with images; fetch via separate REST call on tap
   - Lightbox viewer: fullscreen overlay (`z-index: 800`) for viewing stored images

### Implementation Rule
**Copy-verbatim implementation only.** Do not import a shared HTML/JS module (ADR-013 mandates single-file PWAs). Each PWA receives a copy of the canvas editor code and must integrate it into its own HTML file.

### When to Apply
- **Mandatory:** `cash_with_receipt` payment mode (mandatory image) OR `cash_hand_to_hand` payment mode (mandatory CTH composite)
- **Optional:** other payment modes (file pick may be offered but not required)
- **Confirmation before implementation:** ask user which modes require mandatory vs optional image, and whether CTH applies

---

## Implementation Notes

### Canvas Editor Architecture

```javascript
// Undo stack: saves BOTH imageData AND canvas dimensions
// so crop undo restores to original size
_edUndoStack.push({
  imageData: ctx.getImageData(0, 0, edC.width, edC.height),
  width:  edC.width,
  height: edC.height
});
```

### CTH Countdown
3-2-1 overlay on `<video>` element using `setInterval`. Video uses `transform:scaleX(-1)` for mirror preview; canvas capture un-mirrors with:
```javascript
ctx.translate(w, 0);
ctx.scale(-1, 1);
```

### Desktop Fallback for CTH
If `getUserMedia({video:{facingMode:'user'}})` fails (desktop without webcam or permission denied), skip the camera step and proceed to signature-only mode. Composite is then label + signature without a photo pane.

### Size Estimate Formula
```javascript
const header    = b64.indexOf(',') + 1;
const sizeBytes = Math.round((b64.length - header) * 3 / 4);
```

### Canvas Scaling
Scale image to max 800 px on the longest side before opening the editor. This keeps the compressed output manageable and ensures JPEG quality slider can achieve <100 KB target.

---

## Alternatives Considered

### 1. Shared Canvas Editor HTML Module
**Rejected.** ADR-013 mandates single self-contained HTML files. Importing a shared HTML module would violate that principle and require a build pipeline or iframe bridge (added complexity). Copy-verbatim implementation preserves single-file autonomy.

### 2. No Editing — Compress Immediately After Capture
**Rejected.** Users often need to crop blurry edges, annotate proof (rectangle highlight, redaction), or frame the shot. Skipping the edit phase would reduce usability and force users to pre-edit photos on their device before uploading. The edit phase is essential for proof clarity.

### 3. Crop-Only Mode (No Full Annotation Tools)
**Rejected.** While simpler, crop-only does not allow users to annotate (highlight receipt totals, redact sensitive details). The full tool palette (rectangle, oval, line, pen) is needed for proof documentation. Simplicity is not the priority when clarity of proof is.

### 4. Store receipt_image in list Queries
**Rejected.** Including 100 KB per row in every ledger/list query would bloat network payloads and slow UI rendering. Lazy-fetch with a 📎 icon is the standard pattern for large binary data in web apps.

### 5. Client-Side File Picker Only (No Camera or CTH)
**Rejected.** Mobile users expect camera capture; removing it would force desktop-like workflows on field PWAs. CTH (Camera + Signature) is the gold standard for cash proof because it captures receiver identity + intent at the moment of transaction.

---

## Consequences

### Benefits
- **Consistency across PWAs:** all image capture flows follow one standard, reducing learning curve for users
- **Clear performance baseline:** lazy-fetch pattern ensures list queries stay fast regardless of image count
- **User clarity:** full annotation tools allow users to highlight or redact sensitive proof data, improving documentation quality
- **Offline support:** capture and edit work offline; compression/storage syncs on network availability (ADR-021)
- **Reduced implementation burden:** copy-verbatim code means each PWA gets a working editor without debugging or reinvention
- **Mobile-first:** CTH flow (front camera + signature) is ideal for field cash transactions, where receiver identity is the key proof

### Trade-offs
- **Code duplication:** each PWA carries its own copy of the canvas editor (no shared module). This is intentional per ADR-013, but trades code reuse for single-file autonomy.
- **Development effort:** first implementation in Custodian took ~2 hours; subsequent PWAs must copy-paste and adapt. Not a build-time burden (no compilation step).
- **Canvas size limit (800 px):** images larger than 800 px are scaled down, potentially losing fine detail. Acceptable for proof use cases (receipts, documents); not suitable for high-resolution scans.
- **JPEG only:** base64 storage assumes JPEG format (quality slider, max size). PNG support is possible but adds complexity (larger uncompressed size, no quality slider). Future PWA can request PNG support via amendment.

### Risks and Mitigations
- **Risk:** Users disable camera permission on mobile, breaking CTH flow.
  - **Mitigation:** desktop fallback (signature-only) and graceful error messaging. File picker option always available as fallback.
- **Risk:** Base64 image bloats database backups over time.
  - **Mitigation:** (future) implement image archival/cleanup RPC to move old images to cold storage. For now, monitor growth via `SELECT sum(octet_length(receipt_image)) FROM <table>`.
- **Risk:** Canvas editor introduces new browser incompatibilities.
  - **Mitigation:** canvas element has >95% browser support. `ctx.getImageData` and `ctx.putImageData` are standard. Test on target devices (iOS 14+, Android 8+, desktop Chrome/Safari).
- **Risk:** Undo stack memory overhead for large images.
  - **Mitigation:** limit to 20 levels. At 800 x 600 px RGBA, each imageData is ~2 MB; 20 levels = ~40 MB in memory. Acceptable for typical mobile RAM.

---

## Related Decisions

### [ADR-013](ADR-013-single-html-file-no-framework.md) — Single HTML File, No Framework
This ADR mandates single self-contained HTML files. ADR-072 respects this by implementing copy-verbatim code in each PWA, not shared module imports.

### [ADR-021](ADR-021-sw-cache-first-network-fallback.md) — Service Worker Cache-First Strategy
Image capture and editing must work offline. Compression and DB sync can defer until network is available (soft-sync via SW cache strategy).

### [ADR-009](ADR-009-each-pwa-owns-its-db-schema.md) — Each PWA Owns Its DB Schema
Each transaction table (custodian.transactions, expense.expense, etc.) owns its `receipt_image` column. Cross-PWA image sharing uses proxy RPC, not direct cross-schema FK.

### [ADR-014](ADR-014-postgrest-as-api-layer.md) — PostgREST as API Layer
Image retrieval uses PostgREST RPC for lazy-fetch (with HMAC signing if file is sensitive). List queries exclude `receipt_image` column via SELECT projection.

---

## References

- **Full spec:** [docs/input_and_image_standards.md](../input_and_image_standards.md) Section 2
- **First implementation:** Custodian PWA v11 (2026-06-17)
  - Captures cash transfers via CTH composite
  - Supports optional receipt image for all payment modes
- **Canvas editor pattern:** Industry standard (Fabric.js-free vanilla JS; no external library)
- **Base64 compression formula:** RFC 4648 (base64 encoding/decoding)

---

## Decision Maker
**hkl** (Harish) — 2026-06-27

**Changed via:** adr-kit (360lm)
