> Part of the PWA DevGuide (split from pwa_dev_style.md on 2026-07-02 — see that file for the index; ADR-099).

## 7. Photo Pipeline

### 7.1 Compression (`compress`)

```
compress(file, maxPxOverride?, qOverride?)
  └── maxPx = override || (hd → 1920, compressed → 1280)
  └── q     = override || (hd → 0.90, compressed → 0.78)
  └── Canvas: scale to fit maxPx on longest side
  └── canvas.toBlob(jpeg, q) → resolved blob
  └── onerror → resolves with original file
```

### 7.2 GPS Stamp (`stampPhoto`)

Only applied to **camera captures** (not gallery). Applied at bottom 18% of image height.

**Stamp layout (left to right):**
- Row 1 (26% of SH): 📍 StoreName · Brand (bold, white, truncated with fitText)
- Row 2 (55% of SH): Lat/Long OR address (if no GPS)
- Row 3 (82% of SH): Date and time (en-IN locale, 12h)
- Right badge: GPS quality pill
  - `acc ≤ 50m` → green "GPS ±Xm"
  - `acc ≤ 200m` → amber "~Xm"
  - `acc > 200m` → red "Cell ~Xm"
  - No GPS → grey "No GPS"
- Semi-transparent navy overlay (`rgba(10,18,35,0.78)`)
- Orange accent line at top of stamp (0.4% of height)

Output: `jpeg` at `hd → 0.88` / `compressed → 0.82` quality

### 7.3 Photo Source Dialog

```
capPhoto(cb, forceSource?, quality?)
  ├─ forceSource='camera' → _doCapture directly
  ├─ forceSource='gallery' → _doCapture directly
  └─ null → show photo-src-dialog (Camera / Gallery buttons)

_doCapture(cb, src, quality)
  └── creates hidden <input type=file> (capture=environment if camera)
  └── compresses on change → stamps if camera → savePhotoToDevice → cb({_blob, _src})
  └── input removed after 8000ms
```

**Activity PWA pattern (two persistent body-level inputs):**
```html
<!-- Place AFTER any modal that calls openPhotoSrc() — DOM order = z-index -->
<input type="file" id="photo-camera-input" accept="image/*" capture="environment" style="display:none" onchange="onPhotoChosen(event)">
<input type="file" id="photo-gallery-input" accept="image/*" style="display:none" onchange="onPhotoChosen(event)">
```
```js
let pendingPhotoCallback = null;
function openPhotoSrc(cb)  { pendingPhotoCallback = cb; document.getElementById('photo-src-modal').classList.add('open'); }
function closePhotoSrc()   { document.getElementById('photo-src-modal').classList.remove('open'); }  // keeps callback
function cancelPhotoSrc()  { pendingPhotoCallback = null; closePhotoSrc(); }  // clears callback
function pickCamera()      { closePhotoSrc(); document.getElementById('photo-camera-input').value=''; document.getElementById('photo-camera-input').click(); }
function pickGallery()     { closePhotoSrc(); document.getElementById('photo-gallery-input').value=''; document.getElementById('photo-gallery-input').click(); }
async function onPhotoChosen(e) {
  const file = e.target.files[0];
  if (!file || !pendingPhotoCallback) return;
  const blob = await compressImage(file, quality, false);
  const preview = URL.createObjectURL(blob);
  const cb = pendingPhotoCallback; pendingPhotoCallback = null;
  cb({ blob, preview, file });
}
```
Key: `pickCamera/pickGallery` call `closePhotoSrc()` (preserves callback); Cancel button and backdrop call `cancelPhotoSrc()` (clears callback). Parent modal's close function must also call `cancelPhotoSrc()`.

### 7.4 Device Save (`savePhotoToDevice`)

Auto-triggers a download of every captured photo to device gallery/Downloads using `<a download>` + `.click()`. Silent fail via try/catch.

---

