Skip to content

feat(files): let the agent read HEIC photos - #6346

Merged
waleedlatif1 merged 3 commits into
stagingfrom
feat/heic-vision-transcode
Aug 6, 2026
Merged

feat(files): let the agent read HEIC photos#6346
waleedlatif1 merged 3 commits into
stagingfrom
feat/heic-vision-transcode

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • iPhone photos reach the model as HEIC, which no vision model accepts — the Claude Messages API takes JPEG, PNG, GIF and WebP only. The agent saw nothing and reported the image as empty.
  • 75 HEIC files are already in production, 64 of them in a single 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']), because HEVC is patent-encumbered and excluded from the prebuilt binaries. A real iPhone photo fails with "Security limit exceeded".
  • Adds 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 as application/octet-stream is 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 — mif1 carries 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

SampleBrandsharpheic-convert
iPhone photoheic (HEVC)❌ "Security limit exceeded"✅ 2.99MB → 3992×2992 JPEG, ~950ms
libheif examplemif1 (AV1)✅ decodes nativelynot invoked

Type of Change

  • Bug fix

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-validation pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

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'.
@waleedlatif1
waleedlatif1 requested a review from a team as a code ownerAugust 6, 2026 22:32
@vercel

vercelBot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedAug 6, 2026 10:46pm

Request Review

@cursor

cursorBot commented Aug 6, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes copilot file-read vision prep (CPU-heavy transcoding, new WASM dependency) on a user-facing agent path; logic is bounded by existing size limits and null-on-failure handling.

Overview
Copilot VFS vision now treats iPhone HEIC/HEIF photos as real images instead of opaque binaries or empty vision input.

When sharp cannot read metadata (typical for HEVC-coded HEIC), the reader sniffs ISO-BMFF ftyp brands via isHeifContainer and falls back to transcodeHeicToJpeg (heic-convert / libheif WASM), then continues resize/compression. Sharp stays first for formats it handles (e.g. AV1 in mif1).

Image routing uses resolveEffectiveMimeType on the filename so uploads stored as application/octet-stream still go through the image path. Passthrough and re-encode decisions gate on MODEL_SUPPORTED_IMAGE_MIME_TYPES so unsupported formats are re-encoded to JPEG/WebP rather than passed through as undecodable bytes. Failure messaging shifts from “too large” to unavailable when decode or the 5MB vision budget fails.

Adds heic-convert and unit tests for container detection and the transcode null path on invalid bytes.

Reviewed by Cursor Bugbot for commit d0889a8. Configure here.

@greptile-apps

greptile-appsBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR enables Copilot VFS reads of HEIC/HEIF images by trying Sharp first and falling back to a WASM decoder when necessary.

  • Resolves effective MIME types from filenames before choosing the image-read path.
  • Re-encodes formats unsupported by vision models to JPEG or WebP.
  • Detects HEIF through both major and compatible ISO-BMFF brands.
  • Adds heic-convert, its type definitions, and focused HEIF detection tests.

Confidence Score: 3/5

The 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 heic-convert before any byte, pixel, timeout, or cancellation bound, leaving the previously reported cross-tenant resource-exhaustion path outstanding.

Files Needing Attention: apps/sim/lib/copilot/vfs/file-reader.ts and apps/sim/lib/uploads/server/heic.ts

Important Files Changed

FilenameOverview
apps/sim/lib/copilot/vfs/file-reader.tsRoutes filename-resolved image types through vision preparation, adds HEIF fallback decoding, and re-encodes unsupported model formats.
apps/sim/lib/uploads/server/heic.tsAdds HEIF brand detection and WASM transcoding; decoding remains unbounded as previously reported.
apps/sim/lib/uploads/server/heic.test.tsCovers major and compatible HEIF brands, box boundaries, malformed inputs, and decoder failure.
apps/sim/package.jsonAdds pinned runtime and type dependencies for heic-convert.
bun.lockLocks 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
Loading

Reviews (3): Last reviewed commit: "fix(files): read HEIF compatible brands,..." | Re-trigger Greptile

Comment threadapps/sim/lib/copilot/vfs/file-reader.ts
Comment threadapps/sim/lib/uploads/server/heic.ts Outdated
Comment threadapps/sim/lib/uploads/server/heic.ts
Comment threadapps/sim/lib/copilot/vfs/file-reader.ts
Comment threadapps/sim/lib/copilot/vfs/file-reader.ts
…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

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@waleedlatif1
waleedlatif1 merged commit 5596640 into stagingAug 6, 2026
5 checks passed
@waleedlatif1
waleedlatif1 deleted the feat/heic-vision-transcode branch August 6, 2026 22:52
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

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 (validation.ts:15) and prepareImageForVision runs sharp with limitInputPixels: false, so a tenant-controlled file could spend unbounded CPU and memory on a single-threaded WASM decode during one read.

transcodeHeicToJpeg now refuses input above a 20MB ceiling and returns null, which the existing "[Image unavailable]" path already handles. 20MB is generous headroom over any phone photo — a 12MP iPhone HEIC runs 1-4MB — while bounding what one read can cost.

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 Security limit exceeded before any decode happens. There is no timeout or cancellation on the decode itself; the byte ceiling is what bounds its duration.

waleedlatif1 added a commit that referenced this pull request Aug 7, 2026
* 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.
Sign up for freeto 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.

1 participant

@waleedlatif1