Java ImageIO readers that delegate to platform-native image decoding APIs via Project Panama (Foreign Function & Memory API, Java 26+).
Add the JAR to your module path (or class path) and ImageIO.read() gains support for HEIC, AVIF, WEBP, JPEG 2000, JPEG XL, camera RAW, PSD, EXR, and more. No JNI, no native builds, no manual SPI wiring.
Image decoding only—no encoding. Optional video modules return a single still frame, not playback, audio, or frame sequences. The Java modules compile on any OS and detect compatible backends at runtime.
Add the dependency:
<dependency>
<groupId>io.github.ghosthack</groupId>
<artifactId>imageio-native</artifactId>
<version>2.0.0</version>
</dependency>Gradle
implementation("io.github.ghosthack:imageio-native:2.0.0")For a modular application, require the cross-platform image aggregator:
module com.example.app {
requires io.github.ghosthack.imageio;
}Grant native access only to the backend modules used on the host. For example, a modular macOS application can launch with:
java --module-path lib \
--enable-native-access=io.github.ghosthack.imageio.apple \
--module com.example.app/com.example.MainThen use standard ImageIO:
BufferedImage img = ImageIO.read(new File("photo.heic"));
BufferedImage img = ImageIO.read(new File("photo.avif"));
BufferedImage img = ImageIO.read(new File("photo.webp"));All standard lookup methods work: getImageReadersByFormatName, getImageReadersByMIMEType, getImageReadersBySuffix.
The imageio-native aggregator pulls in both platform modules and auto-selects at runtime. You can also depend on imageio-native-apple or imageio-native-windows directly.
The main aggregator provides native backends for macOS and Windows:
| Module | Platform | Native API | Formats |
|---|---|---|---|
imageio-native-apple |
macOS | CGImageSource (Apple ImageIO framework) | 60+ |
imageio-native-windows |
Windows 10+ | Windows Imaging Component (WIC) | 30+ |
-
Both platforms: HEIC, HEIF, AVIF, WebP, DNG, CR2, CR3, NEF, ARW, ICO, CUR, DDS, and many more camera RAW formats.
-
Apple-only: JPEG 2000, JPEG XL, PSD, OpenEXR, Radiance HDR, DICOM, ICNS, TGA, SGI, PBM/PGM/PPM, PICT, MPO, KTX, KTX2, ASTC, PVR, and ATX.
-
Windows-only: JPEG-XR (JXR/WDP/HDP).
Actual support depends on the codecs installed on the host. The optional libvips and ImageMagick backends add Linux support and additional formats; the optional video modules add poster-frame extraction.
| Format | Requirement |
|---|---|
| HEIC/HEIF | HEVC Video Extensions from Microsoft Store |
| AVIF | AV1 Video Extensions from Microsoft Store |
| WebP | Built-in (Windows 10 1809+) |
| JPEG-XR | Built-in |
To check whether the required codecs are already installed:
Get-AppxPackage -Name *hevc* # HEVC (for HEIC/HEIF)
Get-AppxPackage -Name *av1* # AV1 (for AVIF)Minimum image dimensions: The HEVC and AV1 codec extensions cannot decode images smaller than 8×8 pixels. Header parsing and format detection may still succeed, but pixel decoding fails with
E_INVALIDARG. This is a limitation of the Windows codec extensions, not WIC or this library.
Every artifact is an explicit named JPMS module. Applications that also decode video can require the video aggregator alongside the image aggregator:
module com.example.app {
requires io.github.ghosthack.imageio;
requires io.github.ghosthack.imageio.video;
}Native access can then be granted only to the backend modules used on the host, rather than to every class-path dependency:
| Backend | --enable-native-access value |
|---|---|
| Apple images | io.github.ghosthack.imageio.apple |
| Apple video | io.github.ghosthack.imageio.apple,io.github.ghosthack.imageio.video.apple |
| Windows images | io.github.ghosthack.panama.media.core,io.github.ghosthack.panama.media.comruntime,io.github.ghosthack.panama.media.wic |
| Windows video | io.github.ghosthack.panama.media.core,io.github.ghosthack.panama.media.comruntime,io.github.ghosthack.panama.media.mediafoundation |
| libvips | io.github.ghosthack.imageio.vips |
| ImageMagick | io.github.ghosthack.imageio.magick |
| FFmpeg | ffmpeg.ffm |
When the application and its dependencies run on the class path, they belong to the unnamed module. Grant native access with:
--enable-native-access=ALL-UNNAMED
The existing META-INF/services registrations remain in the JARs for
class-path compatibility. Equivalent uses and provides declarations enable
the same ImageIO and backend discovery on the module path. Shaded or fat JARs
that do not preserve a named module also use the class-path form.
The host Java runtime is the baseline. With no imageio-native backend modules installed, ImageIO behaves exactly as supplied by that runtime.
Adding a backend module is an explicit opt-in to that backend. If exactly one installed backend can decode an input, that backend owns the input, including formats that the JDK can also decode.
For example, adding the Windows module means WIC owns every input that WIC can actually decode. If WIC can decode a JPEG, the JPEG is routed to WIC rather than the JDK reader. Applications that want the JDK reader for a particular intersection can explicitly route that format back to the host.
Detailed routing contract and configuration
- Preserve the host by default. With no added backend capable of decoding an input, the routing SPI declines it and the host ImageIO implementation proceeds normally.
- Module presence is authorization. Adding a backend module opts into all formats that backend can actually decode. There is no implicit "supplemental formats only" mode.
- A single added backend overrides the JDK. If one installed backend is capable, it owns the input even when a host JDK reader is also capable.
- Capability is input-specific. Declaring support for a format or container only makes a backend a candidate. A lightweight probe must confirm that the backend on this machine can open the actual input. This accounts for optional Windows codecs and for different codecs inside the same video container.
- Non-intersecting capability sets do not collide. A format supported by only one added backend belongs to that backend without consulting collision policy.
- Intersections have one deterministic owner. When multiple added backends are capable, a portable/software-codec backend wins by default over a platform-native backend. This is a predictability and portability policy, not a claim that one implementation is inherently more secure.
- Applications control intersections explicitly. A small, application-owned Java configuration can select a different backend—or the host JDK—for any intersecting format.
- Routing is not decode-time fallback. The router filters candidates using capability probes, selects exactly one owner, and invokes it once. If the selected decoder subsequently fails, its error is returned; the router does not silently retry another implementation.
- Configuration is stable. Application routing must be installed before the first routed ImageIO operation. The configuration freezes on first use, making behavior independent of class-loading or SPI-registration timing.
- Direct backend APIs bypass routing. Calling a WIC, Media Foundation, FFmpeg, libvips, or ImageMagick API directly always invokes that backend.
For an input x with detected format f:
candidates = installed backends
that declare f
and whose lightweight probe accepts x
if candidates is empty:
decline x // host JDK behavior
if policy explicitly selects host for f:
decline x // host JDK behavior
if policy selects backend b for f and b is in candidates:
select b
if candidates contains exactly one backend:
select that backend // overrides the JDK
select the deterministic default:
portable/software before platform-native
stable backend ID as the tie-breaker within a class
An explicit backend preference only resolves among capable candidates. It never forces an unavailable backend to decode an unsupported input. Unknown backend IDs in application configuration fail fast as configuration errors.
The JPEG/FFmpeg combination is intentionally illustrative; it defines the routing semantics independently of the formats implemented by today's modules.
Assume the host JDK, Windows/WIC, and FFmpeg can all decode JPEG:
| Installed modules | Application rule | JPEG owner |
|---|---|---|
| None | None | Host JDK |
| Windows | None | Windows/WIC |
| FFmpeg | None | FFmpeg |
| Windows + FFmpeg | None | FFmpeg (portable/software default) |
| Windows + FFmpeg | jpeg -> windows |
Windows/WIC |
| Windows + FFmpeg | jpeg -> host |
Host JDK |
If the Windows module is installed but WIC's probe rejects a particular JPEG, WIC is not a candidate for that input. Another capable added backend owns it, or the router declines it and leaves it to the host.
For non-intersecting formats:
Windows supports HEIC, FFmpeg does not -> Windows owns HEIC
FFmpeg supports MKV, Windows does not -> FFmpeg owns MKV
Neither supports a PNG input -> host ImageIO handles PNG
Applications can keep all intersection choices in one minimal Java class:
import io.github.ghosthack.imageio.common.ImageIoRouting;
public final class AppImageIoRouting {
private AppImageIoRouting() {}
public static void install() {
ImageIoRouting.configure(routes -> routes
.prefer("jpeg", "windows")
.prefer("heic", "windows")
.prefer("mkv", "ffmpeg")
.preferHost("png"));
}
}Valid backend IDs are apple, windows, vips, magick, and ffmpeg when
the corresponding module is installed. The application calls the installation
method once during startup, before its first ImageIO operation:
AppImageIoRouting.install();Configuration is explicit Java code rather than registration-order tricks, system-property priority lists, or magically discovered application classes.
Backend modules register backend services, not mutually competing
ImageReaderSpi implementations. One routing SPI per media category owns
integration with ImageIO:
ImageIO
|
v
routing SPI
|
+-- detect the format once
+-- discover installed backend services
+-- run lightweight capability probes
+-- resolve ownership
`-- delegate to exactly one backend
The routing SPI is ordered before host readers, but returns false whenever no
added backend owns the input. Consequently it affects a host format only when
an installed backend has positively claimed that particular input.
Backend-to-backend IIORegistry.setOrdering(), shared format advertisements on
every backend SPI, global priority properties, and decode-time retry chains are
not part of this design.
Runtime detection for optional dependencies
To check at runtime whether imageio-native is on the classpath (e.g. when it's an optional dependency), probe a class from the imageio-native-common module — it's a transitive dependency of every platform module, so it's always present regardless of which artifact was included:
boolean available = false;
try {
Class.forName("io.github.ghosthack.imageio.common.FormatRegistry");
available = true;
} catch (ClassNotFoundException ignored) { }To detect a specific platform module, check its backend service class:
// macOS (imageio-native-apple)
Class.forName("io.github.ghosthack.imageio.apple.AppleImageBackend");
// Windows (imageio-native-windows)
Class.forName("io.github.ghosthack.imageio.windows.WindowsImageBackend");The imageio-native-vips module is an optional backend that delegates to libvips for image decoding. It is not included in the imageio-native aggregator -- add it explicitly to opt in.
<dependency>
<groupId>io.github.ghosthack</groupId>
<artifactId>imageio-native-vips</artifactId>
<version>2.0.0</version>
</dependency>Requires libvips installed on the system:
# macOS (MacPorts)
sudo port install vips
# macOS (Homebrew)
brew install vips
# Debian/Ubuntu
sudo apt install libvips-devThe vips backend adds cross-platform support (macOS, Linux, Windows) for HEIC, AVIF, WebP, JPEG 2000, PDF, SVG, EXR, FITS, Netpbm, HDR, and more -- depending on the libvips build configuration.
The SPI declares a fixed set of common formats. Formats not in the list but supported by the installed libvips can still be decoded via the direct VipsNative API -- they just won't be auto-discovered by ImageIO.read().
The imageio-native-magick module is an optional backend that delegates to ImageMagick 7 (MagickWand C API). Supports 200+ formats including EPS, PSD, XCF, DPX, TGA, PCX, XBM/XPM, and more.
<dependency>
<groupId>io.github.ghosthack</groupId>
<artifactId>imageio-native-magick</artifactId>
<version>2.0.0</version>
</dependency>Requires ImageMagick 7 installed on the system. Both Q16 and Q16HDRI builds are supported.
# macOS (MacPorts)
sudo port install ImageMagick7
# macOS (Homebrew)
brew install imagemagick
# Debian/Ubuntu
sudo apt install libmagickwand-7-devThe optional imageio-native-video module extracts a single still image from a video file -- the same way the image modules decode a still image from a HEIC or WebP file. The output is always a BufferedImage; no video playback, no audio, no frame sequences.
This means ImageIO.read(new File("clip.mp4")) works exactly like ImageIO.read(new File("photo.heic")) -- same API, same result type.
<dependency>
<groupId>io.github.ghosthack</groupId>
<artifactId>imageio-native-video</artifactId>
<version>2.0.0</version>
</dependency>Through the standard ImageIO SPI:
// Poster frame via ImageIO -- identical to reading any image
BufferedImage poster = ImageIO.read(new File("clip.mp4"));Or through the direct API for more control:
// Thumbnail (poster frame at or near t=0)
BufferedImage thumb = VideoFrameExtractor.extractThumbnail(Path.of("clip.mp4"));
// Frame at a specific time
BufferedImage frame = VideoFrameExtractor.extractFrame(
Path.of("clip.mp4"), Duration.ofSeconds(30));
// Video metadata (dimensions, duration, codec, frame rate)
VideoInfo info = VideoFrameExtractor.getInfo(Path.of("clip.mp4"));| Module | Platform | Native API | Containers |
|---|---|---|---|
imageio-native-video-apple |
macOS | AVFoundation (AVAssetImageGenerator) | MP4, MOV, M4V, 3GP |
imageio-native-video-windows |
Windows 10+ | Media Foundation (IMFSourceReader) | Media Foundation-supported containers |
imageio-native-video-ffmpeg |
macOS ARM64, Windows x64, Linux x64 (optional) | Bundled FFmpeg 8.1.2 via ffmpeg-ffm |
All bundled FFmpeg-supported containers |
The Windows still-image and video backends use the published
panama-media WIC and Media
Foundation bindings. Codec and container support ultimately depends on the
media components installed in Windows.
The imageio-native-video-ffmpeg module is an optional, self-contained
cross-platform video backend. It uses the published
ffmpeg-ffm jextract bindings and
matching FFmpeg 8.1.2 native libraries. The appropriate native classifier is
selected automatically for macOS ARM64, Windows x64, and Linux x64; users do
not need to install FFmpeg separately. It is the only video backend available
on Linux.
<dependency>
<groupId>io.github.ghosthack</groupId>
<artifactId>imageio-native-video-ffmpeg</artifactId>
<version>2.0.0</version>
</dependency>The native libraries are LGPL-only builds and are extracted to the
ffmpeg-ffm cache on first use. For LGPL relinking or a custom deployment,
override the bundled libraries with -Dffmpegffm.libdir=/path/to/libs or the
FFMPEG_FFM_LIBDIR environment variable. An override must provide an
ABI-compatible FFmpeg 8.x build (libavformat major 62).
ImageIO.read(file)
│
▼
RoutingImageReaderSpi one SPI for all still-image backends
│
├── detect format once
├── discover installed backend services
├── probe actual input capability
└── resolve exactly one owner
▼
Backend ImageReader one decode attempt, lazy decode + cache
│
▼
┌─────────────────────┬──────────────────────────────┐
│ macOS │ Windows │
│ AppleNative │ panama-media WIC │
│ Panama downcalls │ shared COM/FFM bindings │
│ CoreGraphics + │ ole32 + windowscodecs │
│ ImageIO.framework │ IWICImagingFactory → │
│ CGImageSource → │ IWICStream → Decoder → │
│ CGBitmapContext → │ FormatConverter → │
│ pixel copy │ CopyPixels │
└─────────────────────┴──────────────────────────────┘
│
▼
BufferedImage (TYPE_INT_ARGB_PRE)
Both platforms output BGRA premultiplied pixels that map directly to TYPE_INT_ARGB_PRE when read as little-endian ints — zero pixel conversion overhead.
Backend modules register ImageDecoderBackend services rather than competing
ImageReaderSpi implementations. The routing SPI declines inputs with no
capable added backend, allowing the host ImageIO readers to proceed normally.
Both platform modules compile on all operating systems; native loading remains
guarded by availability checks.
Native readers implement source regions, subsampling and subsampling offsets,
destination images and offsets, source and destination bands, destination
types, and source render sizes. Progressive-pass selection is not silently
ignored: requesting it fails with IIOException, because the native decoder
APIs do not expose compatible progressive passes.
Source render sizing is pushed into Apple, WIC, libvips, ImageMagick, AVFoundation, and FFmpeg native processing. libvips and ImageMagick also crop before materializing the Java image; the shared layer applies any remaining operations with the same normalized ImageIO regions for every backend.
Readers reject a required native or Java intermediate larger than 512 MiB
instead of unexpectedly allocating it. This includes full-source ImageMagick
processing and the worst-case thumbnail surfaces used for exact Apple/WIC
render sizes. Override the limit in bytes with
-Dimageio.native.maxIntermediateBytes=<bytes>.
Images from phones and cameras often carry an EXIF orientation tag (values 1-8) that describes how the sensor image should be rotated or flipped for correct display. Both backends apply this transform automatically during decode so the returned BufferedImage is always display-ready.
| EXIF value | Transform |
|---|---|
| 1 | None (identity) |
| 2 | Flip horizontal |
| 3 | Rotate 180 |
| 4 | Flip vertical |
| 5 | Rotate 90 + flip horizontal |
| 6 | Rotate 90 |
| 7 | Rotate 270 + flip horizontal |
| 8 | Rotate 270 |
macOS (CGImageSource): Uses CGImageSourceCreateThumbnailAtIndex with kCGImageSourceCreateThumbnailWithTransform = true and the thumbnail size set to the full image dimensions. CoreGraphics applies the EXIF transform internally during hardware-accelerated decode -- same decode path, same performance, correct orientation.
Windows (WIC): Reads the EXIF orientation tag via IWICMetadataQueryReader, then inserts an IWICBitmapFlipRotator between the frame decoder and the format converter. The flip-rotator is a zero-copy coordinate remap -- it transforms pixel coordinates during CopyPixels rather than allocating a second buffer. For orientation 1 (no rotation), the flip-rotator is skipped entirely.
Both getSize() and decode() are orientation-aware: dimensions are swapped for orientations 5-8 (90/270 rotations), so width and height always reflect the display-oriented image.
Performance impact: Negligible. Both platforms apply the transform as part of the existing decode pipeline with no extra buffer allocations or pixel copies. The macOS path creates a small options dictionary per decode call; the Windows flip-rotator is a zero-copy coordinate remap. Orientation 1 (the common case for non-phone images) skips the transform entirely.
├── pom.xml parent POM (reactor)
├── imageio-native-common/ shared format registry & detection
├── imageio-native-apple/ macOS image module
├── imageio-native-windows/ Windows image module
├── imageio-native/ cross-platform image aggregator
├── imageio-native-video-common/ shared video SPI & format detection
├── imageio-native-video-apple/ macOS video module (AVFoundation)
├── imageio-native-video-windows/ Windows video module (Media Foundation)
├── imageio-native-video/ cross-platform video aggregator
├── imageio-native-vips/ optional libvips backend
├── imageio-native-magick/ optional ImageMagick 7 backend
├── imageio-native-video-ffmpeg/ optional FFmpeg video backend
├── scripts/ test fixture generators
└── example-consumer/ standalone demo (not in reactor)
Requires Java 26+ and Maven 3.9+.
mvn clean test # compile + test
mvn install -DskipTests # install to local repo
mvn -f example-consumer/pom.xml test # example-consumer
swift scripts/generate-heic-avif-cgimage.swift # HEIC + AVIF (macOS + heif-enc)
./scripts/generate-png-webp-chrome.sh # PNG + WebP (Chrome headless)
python scripts/generate-all-pillow.py # all formats (pip: pillow + plugins)
swift scripts/generate-video-fixtures.swift # video fixtures (macOS AVFoundation)
./scripts/generate-video-bframes.sh # B-frame video fixture (ffmpeg)- Set the release version in
pom.xml(remove-SNAPSHOT) - Commit, push, and merge via PR; wait for CI to pass on the release commit
- Publish a non-prerelease GitHub Release tagged
v<version>and targeting that commit - The release workflow validates the tag, reruns CI at the tag, signs the artifacts, and publishes them to Maven Central
Maven Central publication is automatic; no separate portal approval is required.