Skip to content

feat(image): convert HEIC paste uploads to JPEG#151

Merged
Ark0N merged 2 commits into
Ark0N:masterfrom
aakhter:cod-167-heic-jpeg-conversion
Jul 12, 2026
Merged

feat(image): convert HEIC paste uploads to JPEG#151
Ark0N merged 2 commits into
Ark0N:masterfrom
aakhter:cod-167-heic-jpeg-conversion

Conversation

@aakhter

@aakhter aakhter commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

When a browser pastes an HEIC image (common on macOS/iOS when files aren't normalised to JPEG), the paste-image route now converts it server-side to JPEG via heic-convert before writing to .claude-images/.

  • Adds convertHeicToJpeg() helper that imports heic-convert dynamically, validates the output has valid JPEG magic bytes before returning
  • HEIC/HEIF extensions and MIME types are now in the ALLOWED_IMAGE_EXTS set (they were already listed in the MIME match but the set was missing them)
  • Conversion runs before the .claude-images/ dir setup so ext is already .jpg when the filename is generated
  • Returns HTTP 415 if conversion fails (unsupported media type)
  • Adds src/types/heic-convert.d.ts type declarations
  • Integration test: uploads a HEIC file, asserts the route converts it and returns a .jpg path

Test plan

  • npx tsc --noEmit — 0 errors
  • npm run lint — clean
  • npx prettier --check — clean
  • npm test -- test/routes/session-routes.test.ts — 54/54 passed (including new HEIC test)
  • npm run build — clean

Co-authored-by: Saqeb Akhter saqeb.akhter@gmail.com

aakhter and others added 2 commits July 11, 2026 16:32
When a browser pastes an HEIC file without normalising it first, the
paste-image route now converts it to JPEG server-side via heic-convert
before writing to .claude-images/. Magic-byte validation confirms the
output is valid JPEG. Adds type declarations for the heic-convert package.

Co-authored-by: Saqeb Akhter <saqeb.akhter@gmail.com>
…cy cap, and magic-byte routing (PR Ark0N#151)

- Event-loop blockage: HEIC decode/encode (CPU-synchronous libheif WASM +
  jpeg-js) now runs in a per-conversion worker_threads Worker
  (src/web/heic-jpeg-worker.ts, spawned by heic-jpeg-converter.ts) with
  resourceLimits and a 30s hard timeout that terminates the worker —
  verified end-to-end under tsx and against compiled dist/ output with a
  real iPhone HEIC (event-loop max stall 52ms during conversion).
- No server-side concurrency cap: conversions now acquire a slot from the
  existing global runWithConversionLimit() pool (document-conversion-limiter),
  bounding peak decode memory/CPU across simultaneous uploads.
- Decompression bomb: header-declared dimensions are read via heic-decode's
  allocation-free `.all` path and rejected above 64MP BEFORE decode() can
  allocate width*height*4 bytes (a <300-byte crafted file can declare
  30000x30000 = 3.6GB). Regression-tested with a crafted ISOBMFF fixture
  against the real heic-decode WASM (test/heic-jpeg-core.test.ts).
- Mislabeled HEIC (documented Android/MIUI case): conversion now routes on
  ftyp magic-byte sniff of the raw buffer regardless of declared
  ext/Content-Type, so a HEIF uploaded as image/jpeg converts instead of
  415ing; the magic-mismatch 415 only fires for genuinely unrecognized bytes.
- Brand allowlist narrowed to what heic-decode's isHeic() accepts
  (heim/heis/hevm/hevs dropped — they could only ever fail conversion).
- Converted-output size: the JPEG result is checked against
  MAX_PASTE_IMAGE_BYTES (jpeg-js can inflate a within-limit HEIC past the cap).
- Deps: heic-convert replaced with its underlying heic-decode + jpeg-js
  (the wrapper could not expose the pre-decode dimension check); lockfile
  synced, drops pngjs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Ark0N Ark0N merged commit 46493f3 into Ark0N:master Jul 12, 2026
2 checks passed
@Ark0N

Ark0N commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Merged — thank you @aakhter! HEIC→JPEG conversion removes a real iPhone paste-flow papercut. Review moved the decode off the main thread (a worker with resourceLimits + timeout — synchronous WASM decode was freezing every session's streaming for seconds per photo), capped concurrency via the existing conversion limiter, added a 64MP pre-decode guard (a <1MB crafted header could force a multi-GB allocation), and switched detection to magic bytes so mislabeled Android HEIFs convert too. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants