Skip to content

Decode HEIF images with ImageIO on macOS - #9

Open
amaztony wants to merge 2 commits into
Bitpainter75:mainfrom
amaztony:fix/macos-imageio-heif-decoding
Open

Decode HEIF images with ImageIO on macOS#9
amaztony wants to merge 2 commits into
Bitpainter75:mainfrom
amaztony:fix/macos-imageio-heif-decoding

Conversation

@amaztony

@amaztony amaztony commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Use the native macOS ImageIO frameworks to decode HEIC, HEIF, and supported AVIF images, and route the gallery, viewer, and editor display paths through the existing HEIF decoding service.

Windows and Linux retain the existing optional system libheif implementation. No codec library is bundled, and HEIF support remains read-only.

Background

FerrumPix already recognizes HEIC, HEIF, and AVIF files and includes an optional decoder based on a system-provided libheif. On macOS, however, the application did not use the HEIF decoding support built into ImageIO.

A standard macOS installation does not provide libheif, so HEIF decoding required an additional compatible library to be installed and discoverable by the application. When that library was unavailable, FerrumPix fell back to the generic SkiaSharp/Avalonia bitmap path, which cannot decode HEIC, leaving the image unopened even though macOS itself supports the format.

The decoding paths were also inconsistent. Thumbnail generation and the editor rendering pipeline could use HeifDecodeService, while the viewer, editor display path, and gallery quick preview could still pass HEIF files directly to the unsupported generic decoder. Installing libheif therefore did not guarantee consistent behavior across all views.

Changes

  • Use Core Foundation and ImageIO for native HEIF decoding on macOS.
  • Read image dimensions and orientation through ImageIO metadata.
  • Convert the decoded image to a PNG stream for the existing Avalonia and SkiaSharp pipelines.
  • Route the viewer and editor display paths through HeifDecodeService.
  • Route the gallery quick preview through the format-aware image loader.
  • Keep the existing dynamically loaded system libheif implementation on Windows and Linux.
  • Keep HEIF decoding read-only; edited images must still be saved as FPX or exported to an encodable format.
  • Update the service documentation in English to describe the ImageIO/libheif backend split while preserving the system-library, distribution, orientation, and read-only design rationale.

Dependency and distribution scope

This change does not bundle libheif, an HEVC decoder, or any other native codec dependency. It uses the decoder already provided by macOS and preserves the project's existing optional system-library policy on other platforms.

It does not add HEIF encoding or in-place HEIF saving.

Testing

Tested on a Mac mini with Apple M4:

  • Published and launched the macOS ARM64 application bundle.
  • Confirmed that HEIC gallery thumbnails render.
  • Confirmed that HEIC images open and render in the viewer.
  • Confirmed that HEIC images open in the editor and reach the ready state at the expected dimensions and orientation.
  • Confirmed that leaving the editor without saving returns to the gallery normally.
  • Rebased onto upstream 0.9.17 and completed a macOS ARM64 Release build successfully.

Edited-image saving was not retested because this change only replaces the source decoding path and does not modify the existing save or encoding pipeline.

The functional HEIC checks above were performed before the rebase. The rebased branch was build-verified but was not functionally retested.

Windows and Linux were not tested in this round.

@amaztony
amaztony force-pushed the fix/macos-imageio-heif-decoding branch from 4be2db8 to 122772f Compare July 31, 2026 01:47
@amaztony
amaztony marked this pull request as ready for review July 31, 2026 02:41
HeifDecodeService keeps its libheif path and now only asks whether a
platform decoder is available. All CoreFoundation and ImageIO interop
lives in MacImageIoService, which reports nothing on Linux and Windows.
A Mac without those frameworks now falls back to libheif.

- restore the German comments this project uses in source files
- run the ImageIO path under the same lock as libheif, so the
  application still decodes only one image at a time
- pass the standard CoreFoundation callbacks to CFDictionaryCreate
  instead of NULL, which compared keys by pointer
- drop kCGImageSourceThumbnailMaxPixelSize, leaving it out already
  gives the full resolution
- decode into a fixed SKImageInfo so TryDecode really returns Bgra8888
  on both paths
- log failures instead of swallowing them in a bare Catch
- keep the gallery quick preview on the plain loader with an explicit
  HEIF branch, so PSD is not pulled in with a different rotation rule
@Bitpainter75

Copy link
Copy Markdown
Owner

Thanks for this, and sorry for the slow reply. HEIC on macOS is a real gap and
the ImageIO approach is the right one, so I would like to take it. Before I
merge it I have reworked the implementation a bit, and since I have no Mac
here I need you to try the reworked version on real hardware.

The changes are on your branch now. Please pull before you look at the diff.

What I changed and why

All the Apple interop moved into a new file, Services/MacImageIoService.vb.
It holds the framework handles, every DllImport and the CoreFoundation helpers,
and exposes only IsAvailable, TryExtractPng, TryGetSize and DecoderName.
HeifDecodeService now has no P/Invoke and no OperatingSystem.IsMacOS() at
all, it just asks whether a platform decoder is there. That keeps the format
service readable and keeps everything platform specific in one place, the same
way the other system libraries are handled in this project.

A nice side effect: because the switch is on "is the decoder available" rather
than "are we on macOS", a Mac without those frameworks now falls back to
libheif instead of reporting nothing. In your version the libheif candidates
for macOS had become unreachable.

The German comments are back. The project convention is English identifiers
with German comments, so I restored the class documentation and the summaries
and wrote the new macOS parts in German as well. Nothing against your text, it
was clearer than what was there, it just has to be in the same language as the
rest of the file. Some of the reasoning had also been shortened away, for
instance which distributions ship libheif themselves and which function rejects
a HEIF save target.

The ImageIO path now runs under the same lock as libheif. ImageIO is thread
safe, but this application deliberately never runs two decodes at the same
time, and the gallery and the film strip do decode from several threads.

The options dictionary gets the standard CoreFoundation callbacks instead of
NULL. With NULL callbacks the dictionary neither retains its keys and values nor
compares keys with CFEqual, it compares pointers. That happens to work while the
constants are the same objects, but it is not something to rely on.

kCGImageSourceThumbnailMaxPixelSize is gone. Int32.MaxValue relies on
undocumented clamping. Leaving the key out gives the full resolution image,
which is what we want, with one CFNumber less to create and release.

TryDecode now guarantees Bgra8888 on both paths. The summary promises that
format, but SKBitmap.Decode picks the platform default, so on macOS the
promise could quietly stop holding. It decodes into a fixed SKImageInfo now.

Failures are logged instead of being swallowed by a bare Catch. On a
machine I cannot reach, "nothing appeared" is impossible to tell apart from
"a symbol was missing" without that.

The quick preview in the gallery went back to the plain loader with an
explicit HEIF branch next to the RAW branch. Switching it to the automatic
variant also pulled PSD in, and PSD would then get the rotation from its
sidecar file while the RAW branch right above it does not, so the same preview
would treat two formats differently.

The HEIF branches in the viewer and in the shared loader stayed. Those turn
out to be the actual fix for everyone, not just for macOS: HEIC and AVIF had
thumbnails and could be edited and exported, but the large picture in the
viewer, the film strip and the editor fell through to a decoder that cannot read
the format, so it stayed empty on Linux too. Good catch, that had gone
unnoticed. They only trigger for .heic, .heif, .hif and .avif, so
nothing that works today changes behaviour.

What I verified here

On Linux, with the built application: the macOS service stays completely silent
and reports nothing for every call, the decoder name still reports libheif,
HEIC and AVIF decode as Bgra8888 with header size and decoded size matching,
the preview stream is a readable PNG, the display path now returns HEIF at the
correct size, and JPEG still takes its old path unchanged. The build is clean.

What I need you to test on macOS

Please check, ideally with an iPhone photo straight off a device:

  1. HEIC and AVIF thumbnails appear in the gallery.
  2. A HEIC opens in the viewer and fills the window, not just as a thumbnail.
  3. The film strip in the viewer and in the editor shows HEIC entries.
  4. A portrait photo appears upright, not on its side, in all of those places.
    This is the one I am least able to guess at: the container rotation has to be
    applied exactly once.
  5. The dimensions shown match the real photo, and match for portrait shots too.
  6. Opening a HEIC in the editor and exporting it to JPEG or PNG works.
  7. The quick preview in the gallery, holding the middle mouse button or space,
    shows the photo.
  8. Scrolling quickly through a folder of many HEIC files stays stable, since the
    decodes are serialised now.
  9. If you have Homebrew's libheif installed, it should still say the system
    decoder is in use, not libheif.

If anything there behaves differently from the version you wrote, please say so
and I will take my change back out rather than yours. And if you would rather I
split the viewer and film strip part into its own pull request so this one stays
purely about macOS, that is fine 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.

2 participants