Skip to content

thumbnail: add HEIC/HEIF support via pi-heif#5006

Open
sir-sigurd wants to merge 3 commits into
masterfrom
thumbnail-heic
Open

thumbnail: add HEIC/HEIF support via pi-heif#5006
sir-sigurd wants to merge 3 commits into
masterfrom
thumbnail-heic

Conversation

@sir-sigurd

@sir-sigurd sir-sigurd commented Jun 19, 2026

Copy link
Copy Markdown
Member

Description

Adds HEIC/HEIF image thumbnails and previews.

HEIC has no bioio reader, so it rides read_image()'s existing bioio-imageio fallback once a Pillow HEIF opener is registered. We register pi-heif's opener at module import — no new code path in the thumbnail pipeline.

pi-heif, not pillow-heif: pi-heif's wheels bundle only libheif + libde265 (decoder); pillow-heif additionally bundles libx265, a GPL-2.0 HEVC encoder we never use (thumbnails are re-encoded as PNG) and would otherwise convey in the lambda image. Decode-only keeps the bundled-license floor at LGPL. Self-contained manylinux wheels for x86_64 and aarch64, so no Dockerfile/apt changes.

Catalog: .heic/.heif added to SUPPORTED_EXTENSIONS, which drives thumbnails, the image preview renderer, and bucket summaries — all via the lambda (PNG out), so HEIC renders in every browser, not just Safari.

The user-facing docs (docs/Catalog/Preview.md format list) are intentionally deferred until this ships in a release.

Note: the lambda still applies no EXIF orientation (pre-existing, JPEG included), so iPhone HEICs render rotated-as-stored — a separate follow-up if desired.

TODO

  • Unit tests
  • Security: decode-only (no encoder); same HEVC-decode surface as any HEIC reader
  • Open and Embed: catalog change is an extension-list addition only
  • Documentation — deferred to release (see Description)
  • Changelog entry (catalog + lambda)

Greptile Summary

Adds HEIC/HEIF thumbnail and image preview support to both the thumbnail lambda and the catalog frontend. The lambda gains pi-heif as a decode-only dependency whose Pillow opener is registered at module import, letting HEIC files ride the existing bioio-imageio fallback in read_image() with no new code paths.

  • Lambda: pi_heif.register_heif_opener() called once at import; HEIC thumbnails are always re-encoded as PNG, so the decode-only dependency is sufficient. New test_read_image_fallback case pins the dispatch mechanism; test_heic_thumbnail provides an end-to-end golden test using a fuzzy pixel comparison (max diff ≤ 30) to account for HEVC chroma subsampling.
  • Catalog: .heic and .heif added to SUPPORTED_EXTENSIONS in Thumbnail.jsx; the image preview loader and bucket summary already consume this list, so no further frontend changes are needed.
  • Both CHANGELOG.md files use #XXXX as a placeholder PR link rather than the actual PR number thumbnail: add HEIC/HEIF support via pi-heif #5006.

Confidence Score: 5/5

Safe to merge; the change is additive and isolated — a new Pillow plugin registration at import plus two new extension strings, with no modifications to existing decode or resize paths.

The integration is minimal: one import, one module-level registration call, and two extension strings in the frontend. The new code path is fully covered by both a unit test that pins the dispatch mechanism and a standalone end-to-end test with a real HEIC fixture. The decode-only dependency choice is well-justified in the PR description and in code comments.

Both CHANGELOG.md files carry a #XXXX placeholder link that should be replaced with the actual PR number before the release is cut.

Important Files Changed

Filename Overview
lambdas/thumbnail/src/t4_lambda_thumbnail/init.py Adds module-level pi_heif.register_heif_opener() call; integrates cleanly with the existing bioio-imageio fallback in read_image() with no new code paths in the thumbnail pipeline.
lambdas/thumbnail/tests/test_thumbnail.py Adds cell.heic to the test_read_image_fallback parametrization (pins dispatch mechanism) and a standalone test_heic_thumbnail end-to-end test using a fuzzy golden comparison; logic and assertions look correct.
lambdas/thumbnail/pyproject.toml Adds pi-heif~=1.0 dependency; version constraint and lock resolve to 1.4.0 with cp313-only manylinux/musllinux wheels consistent with requires-python ~=3.13.0.
catalog/app/components/Thumbnail/Thumbnail.jsx Adds .heic/.heif to SUPPORTED_EXTENSIONS; automatically propagates to the image preview loader (Image.js uses this list directly) and bucket summaries.
lambdas/thumbnail/CHANGELOG.md New HEIC entry added but uses #XXXX placeholder PR link instead of the actual PR number #5006.
catalog/CHANGELOG.md New HEIC entry added but uses #XXXX placeholder PR link instead of the actual PR number #5006.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Lambda receives .heic/.heif URL"] --> B["download to tempfile"]
    B --> C["read_image(path)"]
    C --> D{"BioImage(path)"}
    D -- "UnsupportedFileFormatError\n(no bioio reader for HEIC)" --> E["BioImage(path, reader=bioio_imageio.Reader)"]
    D -- "success (other formats)" --> F["format_aicsimage_to_prepped()"]
    E --> F
    F --> G["generate_thumbnail()"]
    G --> H["re-encode as PNG"]

    subgraph "Module import (once)"
        I["pi_heif.register_heif_opener()"] --> J["Pillow can now open HEIC/HEIF"]
    end

    J -.-> E
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["Lambda receives .heic/.heif URL"] --> B["download to tempfile"]
    B --> C["read_image(path)"]
    C --> D{"BioImage(path)"}
    D -- "UnsupportedFileFormatError\n(no bioio reader for HEIC)" --> E["BioImage(path, reader=bioio_imageio.Reader)"]
    D -- "success (other formats)" --> F["format_aicsimage_to_prepped()"]
    E --> F
    F --> G["generate_thumbnail()"]
    G --> H["re-encode as PNG"]

    subgraph "Module import (once)"
        I["pi_heif.register_heif_opener()"] --> J["Pillow can now open HEIC/HEIF"]
    end

    J -.-> E
Loading

Reviews (1): Last reviewed commit: "thumbnail: add HEIC/HEIF support via pi-..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Decode-only HEIF/HEIC: register pi-heif's Pillow opener at import so .heic/.heif
ride read_image()'s existing bioio-imageio fallback (no new pipeline branch).
pi-heif over pillow-heif: it bundles only libheif + libde265 (decoder), not the
GPL-2.0 x265 encoder we'd never use and would otherwise convey in the image.

Catalog: add .heic/.heif to SUPPORTED_EXTENSIONS (drives thumbnails, image
preview, and bucket summaries). Tests + cell.heic fixture. Preview.md doc update
deferred until release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 47.48%. Comparing base (8e23c64) to head (311bf55).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5006      +/-   ##
==========================================
+ Coverage   47.44%   47.48%   +0.04%     
==========================================
  Files         837      837              
  Lines       34646    34675      +29     
  Branches     5882     5882              
==========================================
+ Hits        16438    16467      +29     
  Misses      16211    16211              
  Partials     1997     1997              
Flag Coverage Δ
api-python 93.14% <ø> (ø)
catalog 22.23% <ø> (ø)
lambda 97.12% <100.00%> (+0.02%) ⬆️
py-shared 98.02% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread catalog/CHANGELOG.md Outdated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sir-sigurd sir-sigurd left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Notes from an AI-assisted self-review pass on the HEIC/HEIF change.

Two should-fix items below — a decode defect (multi-image HEIC returns 500) and a missing wiring into the Qurator image list — plus a couple of cleanup nits. Holding the line-level polish pass for now: the multi-image fix would touch the shared image-prep path and make any nits there stale.

P2

Multi-image HEIC/HEIF returns HTTP 500 (reshape). A HEIC/HEIF holding more than one top-level image (image collections, animated HEIF, some burst-style containers) decodes through the bioio_imageio fallback as a 4-D (T, Y, X, S) array. format_aicsimage_to_prepped only collapses the time axis for the czi / ome_tiff / tifffile readers, so the fallback's 4-D array reaches Image.fromarray (lambdas/thumbnail/src/t4_lambda_thumbnail/__init__.py:700) and raises TypeError: Cannot handle this data type, surfacing as a 500. Reproduced by running a 2-frame HEIC through lambda_handler: single-image → 200, multi-image → 500. The common single-photo iPhone HEIC is unaffected — its depth/HDR maps are auxiliary items, not frames, so n_frames == 1. Direction: collapse to the primary/first frame before Image.fromarray on the fallback path; add a multi-frame fixture alongside test_heic_thumbnail if fixed.

HEIC not wired into the Qurator/Assistant image list. SUPPORTED_IMAGE_EXTENSIONS in catalog/app/components/Assistant/Model/GlobalContext/preview.ts:240 is a second hardcoded copy of the image-extension list (identical to the old SUPPORTED_EXTENSIONS minus the new entries). detectFileType keys off it (:381) to classify a file as an image for model-context attachment, so .heic/.heif won't be attachable to Qurator even though they now render as thumbnails, in the image preview, and in bucket summaries. Not a regression (HEIC was never attachable there), but a completeness gap for this change's goal. Add .heic/.heif here too — or, to stop the two lists drifting on the next format, import the shared SUPPORTED_EXTENSIONS from components/Thumbnail.

Nits / cleanup

  • pi-heif~=1.0 floors lower than the resolved 1.4.0. Sibling pins floor at the resolved minor (pillow~=12.2, numpy~=2.3); pi-heif~=1.0 admits the whole 1.x. uv.lock pins the exact build and the <2.0 cap matches the siblings, so this is cosmetic — tighten to ~=1.4 only if pin-floor consistency is wanted.
  • Greptile's #XXXX-placeholder note is already resolved — both CHANGELOG links read #5006 as of 6a072fbb.

Checked and clean

Decode-only dependency choice and license floor (no GPL encoder conveyed); wheel/arch coverage for the build target (cp313 manylinux x86_64/aarch64, glibc-compatible with the trixie base); the integration approach (riding the existing fallback, opener registered once at import — idempotent, and test_read_image_fallback fails loudly if a bioio reader ever claims these extensions); the untrusted-decode attack surface (on par with the existing native decoders, and MAX_IMAGE_PIXELS does cover the HEIF decode path); cold-start and image-size cost (~3.8 MB of native libs, negligible against the existing image); and catalog extension matching (case-insensitive in all three consumers).

🤖 Drafted with Claude; findings vetted by the reviewer — sanity-check rather than take as authoritative.
Reviewed against 6a072fb

Address self-review of #5006:

- Multi-image HEIF (image collections, animated/burst HEIC) decodes through
  the bioio-imageio fallback as a 4-D (T,Y,X,S) array, which reached
  Image.fromarray and 500'd. Collapse the leading frame axis to the primary
  (first) frame in format_aicsimage_to_prepped's fallback branch; single-frame
  images are unchanged. Adds cell-multiframe.heic fixture + test.
- Add .heic/.heif to the Assistant SUPPORTED_IMAGE_EXTENSIONS so HEIC files
  attach to Qurator, with a note to keep it in sync with Thumbnail's list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant