Uh oh!
There was an error while loading. Please reload this page.
feat(files): let the agent read HEIC photos - #6346
Conversation
iPhone photos reach the model as HEIC, which no vision model accepts - the Claude Messages API takes JPEG, PNG, GIF and WebP only - so the agent saw nothing. 75 HEIC files are already in production, 64 of them in one workspace uploaded over the last two days. sharp cannot cover this: its prebuilt libvips ships libheif with AV1 but not HEVC (sharp.format.heif.input.fileSuffix is ['.avif']), so a real iPhone photo fails with 'Security limit exceeded'. Verified against both a HEVC-coded sample (sharp fails, heic-convert decodes 2.99MB to a 3992x2992 JPEG in ~950ms) and an AV1-coded mif1 sample (sharp decodes it natively). Decoder selection is capability-based, not brand-based: sharp is always tried first and the WebAssembly decoder runs only on bytes it could not read. The container brand cannot identify the codec anyway - mif1 carries either - so choosing from it would push AV1 files down the slow path. This mirrors how PhotoPrism layers libvips over libheif. Also route the image path on the effective MIME type, since a phone upload commonly stores as application/octet-stream and would otherwise be read as a binary the model never sees, and stop reporting an undecodable image as 'too large'.
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview When sharp cannot read metadata (typical for HEVC-coded HEIC), the reader sniffs ISO-BMFF Image routing uses Adds Reviewed by Cursor Bugbot for commit d0889a8. Configure here. |
Greptile SummaryThe PR enables Copilot VFS reads of HEIC/HEIF images by trying Sharp first and falling back to a WASM decoder when necessary.
Confidence Score: 3/5The PR does not yet appear safe to merge because tenant-controlled HEIF decoding remains able to consume unbounded in-process memory or CPU. The complete workspace object can still reach Files Needing Attention: apps/sim/lib/copilot/vfs/file-reader.ts and apps/sim/lib/uploads/server/heic.ts
|
| Filename | Overview |
|---|---|
| apps/sim/lib/copilot/vfs/file-reader.ts | Routes filename-resolved image types through vision preparation, adds HEIF fallback decoding, and re-encodes unsupported model formats. |
| apps/sim/lib/uploads/server/heic.ts | Adds HEIF brand detection and WASM transcoding; decoding remains unbounded as previously reported. |
| apps/sim/lib/uploads/server/heic.test.ts | Covers major and compatible HEIF brands, box boundaries, malformed inputs, and decoder failure. |
| apps/sim/package.json | Adds pinned runtime and type dependencies for heic-convert. |
| bun.lock | Locks heic-convert and its HEIF, JPEG, PNG, and WASM transitive dependencies. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Workspace file record] --> B[Resolve effective MIME]
B --> C{Image type?}
C -- No --> D[Existing non-image path]
C -- Yes --> E[Download file buffer]
E --> F[Try Sharp metadata]
F --> G{Decoded?}
G -- No --> H{HEIF container?}
H -- Yes --> I[Transcode with heic-convert]
I --> J[Read JPEG metadata]
H -- No --> K[Reject or supported passthrough]
G -- Yes --> L{Supported format and within limits?}
J --> L
L -- Yes --> M[Pass image to vision model]
L -- No --> N[Resize or re-encode to JPEG/WebP]
N --> M
Reviews (3): Last reviewed commit: "fix(files): read HEIF compatible brands,..." | Re-trigger Greptile
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…ia types Review found two passthroughs that still handed the model bytes it cannot decode. The sharp-load-failure branch returned raw HEIF, and the already-small-enough branch returned raw AVIF, TIFF, BMP or ICO — all of which isImageFileType accepts and no vision model does. Gating all three on the existing MODEL_SUPPORTED_IMAGE_MIME_TYPES subsumes the ad-hoc isHeifContainer re-sniff, and re-encoding an unsupported format falls out of the resize ladder that was already there. Also drop two constants that were pure indirection (a one-use alias for 'image/jpeg', and a quality value identical to heic-convert's default), trim the oversized comments, log successful transcodes so the ratio is visible in prod, and replace a detection test that could not fail.
waleedlatif1
commented
Aug 6, 2026
waleedlatif1
commented
Aug 6, 2026
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6188438. Configure here.
A standards-valid HEIF may carry a generic major brand such as isom and declare heic, heix or mif1 only among the compatible brands that follow the minor_version at offset 12. Reading bytes 8-11 alone classified those as non-HEIF, skipping the fallback decode and leaving a small undecodable file to reach the model as raw bytes.
waleedlatif1
commented
Aug 6, 2026
waleedlatif1
commented
Aug 6, 2026
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit d0889a8. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
waleedlatif1
commented
Aug 6, 2026
Addressed the resource-exhaustion point in 4fd5ec7 rather than pushing back on it — it was correct. Nothing upstream bounded what could reach the decoder: uploads allow 100MB (
Being explicit about what that does and does not cover: it bounds file size, not pixel count. A small file declaring enormous dimensions is a separate vector, and it stays bounded by libheif's own security limits during parse — those are demonstrably active here, since the sharp attempt on a real iPhone HEIC fails with libheif's |
* feat(files): preview HEIC photos in the file viewer The agent can read HEIC since #6346, but the Files page still showed 'Preview not available' — an <img> pointed at the serve route got the stored HEIF under nosniff, which no browser outside Safari renders. The serve route now resolves a JPEG derivative for HEIF bytes, cached in the artifact store and keyed by the source's storage key. Workspace keys are regenerated on every content replacement, so the key is already a content version and using it avoids streaming the original just to hash it. Caching matters here in a way it did not for the vision path: a preview is re-fetched on every view and the WASM decode costs roughly a second for a phone photo. The original stays the stored object — downloads and raw=1 serve it untouched, so this never changes what a user gets back. compileDocumentIfNeeded becomes resolveServableBytes, since it now resolves images as well as generated documents. .tif/.tiff stay download-only: nothing decodes those on either side. * fix(files): make the preview derivative opt-in and never show a broken image Five issues from review, all interlocking around one decision. The derivative is now requested with preview=1 rather than suppressed with raw=1. raw=1 would have corrupted generated-document downloads: every non-markdown workspace download routes through the serve route and relies on resolveServableDocBytes compiling stored source into the real binary. Opt-in separates the three consumers cleanly — previews get the JPEG, downloads get untouched stored bytes, and doc compilation stays unconditional. - Public shares resolve the derivative too, with the same preview/download split; the viewer requests it, the download button does not. - Split the brand predicate. isHeifContainer stays broad for the vision path, where it only runs after sharp has already failed. The serve path runs first, so it uses isHevcHeifContainer — an AVIF was costing a storage round-trip, a WASM load and a misleading warn per request. - A derivative that cannot be produced (past the 20MB ceiling, or a decode failure) now falls back to 'Preview not available' instead of a broken image. UnsupportedPreview moved to preview-shared to avoid a module cycle. - The chat composer chip requests the derivative, so HEIC attachments stop rendering as broken thumbnails. * fix(files): reset the image preview when the file is overwritten An overwrite preserves the storage key, which is what the parent keys this component on, so only the URL version changes and it never remounts. The previous bytes' outcome therefore stuck, leaving a replaced image parked on 'Preview not available' until something else forced a remount. Reset on URL change during render rather than in an effect — this is derived state, and an effect would render the stale outcome first. * improvement(files): drop the dead preview reset and cap the ftyp brand scan - Content writes mint a new storage key, so the parent's key={file.key} already remounts ImagePreview; the render-phase reset was unreachable and made renames flash a loading overlay. - Clamp the ftyp compatible-brand scan to a real box size. The declared size is attacker-controlled and this now runs on every preview request. - UnsupportedPreview takes a primitive name so memo is load-bearing. - Fix the hardcoded ? in the public preview URL builder. * improvement(copilot): only ask for a preview derivative on image thumbnails A video has no derivative path, so preview=1 there only spent a brand sniff per request. Adds the missing test coverage for the helper.
Summary
sharpcannot cover this: its prebuilt libvips ships libheif with AV1 but not HEVC (sharp.format.heif.input.fileSuffixis['.avif']), because HEVC is patent-encumbered and excluded from the prebuilt binaries. A real iPhone photo fails with "Security limit exceeded".heic-convert(a WebAssembly build of libheif, ~1.05M weekly downloads) as a fallback decoder, and routes the image path on the effective MIME type so a phone upload stored asapplication/octet-streamis no longer read as an opaque binary the model never sees.Design
Decoder selection is capability-based, not brand-based. sharp is always tried first; the WASM decoder runs only on bytes sharp could not read. The container brand cannot identify the codec anyway —
mif1carries either HEVC or AV1 — so choosing from it would push AV1 files down the slow path. This mirrors how PhotoPrism layers libvips over libheif.Running libheif in WASM rather than natively also keeps a historically CVE-prone parser inside a sandbox, which matters more for multi-tenant SaaS than it does for the self-hosted photo apps (Immich, PhotoPrism, Nextcloud) that link it natively.
An undecodable image is now reported as unavailable rather than "too large", and HEIF that neither decoder can read is no longer passed through — handing the model bytes it cannot decode is what produced the confidently-empty description in the first place.
Verified against real files
heic(HEVC)mif1(AV1)Type of Change
Testing
Unit tests for container detection, including that AV1-coded and non-HEIF formats are classified correctly. Decode verified end-to-end against both samples above. 408 tests, typecheck, and
check:api-validationpass.Checklist