Skip to content

Repository files navigation

imageio-native

CIJavadocsMaven Central

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.

Quick start

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:

modulecom.example.app {
requiresio.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.Main

Then use standard ImageIO:

BufferedImageimg = ImageIO.read(newFile("photo.heic"));
BufferedImageimg = ImageIO.read(newFile("photo.avif"));
BufferedImageimg = ImageIO.read(newFile("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.

Platform and format support

The main aggregator provides native backends for macOS and Windows:

ModulePlatformNative APIFormats
imageio-native-applemacOSCGImageSource (Apple ImageIO framework)60+
imageio-native-windowsWindows 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.

Windows codec requirements

FormatRequirement
HEIC/HEIFHEVC Video Extensions from Microsoft Store
AVIFAV1 Video Extensions from Microsoft Store
WebPBuilt-in (Windows 10 1809+)
JPEG-XRBuilt-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.

Native access

Every artifact is an explicit named JPMS module. Applications that also decode video can require the video aggregator alongside the image aggregator:

modulecom.example.app {
requiresio.github.ghosthack.imageio;
requiresio.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 imagesio.github.ghosthack.imageio.apple
Apple videoio.github.ghosthack.imageio.apple,io.github.ghosthack.imageio.video.apple
Windows imagesio.github.ghosthack.panama.media.core,io.github.ghosthack.panama.media.comruntime,io.github.ghosthack.panama.media.wic
Windows videoio.github.ghosthack.panama.media.core,io.github.ghosthack.panama.media.comruntime,io.github.ghosthack.panama.media.mediafoundation
libvipsio.github.ghosthack.imageio.vips
ImageMagickio.github.ghosthack.imageio.magick
FFmpegffmpeg.ffm

Class-path applications

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.

Backend selection and routing

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

Principles

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Non-intersecting capability sets do not collide. A format supported by only one added backend belongs to that backend without consulting collision policy.
  6. 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.
  7. Applications control intersections explicitly. A small, application-owned Java configuration can select a different backend—or the host JDK—for any intersecting format.
  8. 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.
  9. 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.
  10. Direct backend APIs bypass routing. Calling a WIC, Media Foundation, FFmpeg, libvips, or ImageMagick API directly always invokes that backend.

Ownership algorithm

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.

Examples

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 modulesApplication ruleJPEG owner
NoneNoneHost JDK
WindowsNoneWindows/WIC
FFmpegNoneFFmpeg
Windows + FFmpegNoneFFmpeg (portable/software default)
Windows + FFmpegjpeg -> windowsWindows/WIC
Windows + FFmpegjpeg -> hostHost 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

Application-owned routing

Applications can keep all intersection choices in one minimal Java class:

importio.github.ghosthack.imageio.common.ImageIoRouting;
publicfinalclassAppImageIoRouting {
privateAppImageIoRouting() {}
publicstaticvoidinstall() {
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.

Registration model

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:

booleanavailable = false;
try {
Class.forName("io.github.ghosthack.imageio.common.FormatRegistry");
available = true;
} catch (ClassNotFoundExceptionignored) { }

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");

Optional backends

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-dev

The 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().

ImageMagick

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-dev

Video poster frames

The 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 imageBufferedImageposter = ImageIO.read(newFile("clip.mp4"));

Or through the direct API for more control:

// Thumbnail (poster frame at or near t=0)BufferedImagethumb = VideoFrameExtractor.extractThumbnail(Path.of("clip.mp4"));
// Frame at a specific timeBufferedImageframe = VideoFrameExtractor.extractFrame(
Path.of("clip.mp4"), Duration.ofSeconds(30));
// Video metadata (dimensions, duration, codec, frame rate)VideoInfoinfo = VideoFrameExtractor.getInfo(Path.of("clip.mp4"));
ModulePlatformNative APIContainers
imageio-native-video-applemacOSAVFoundation (AVAssetImageGenerator)MP4, MOV, M4V, 3GP
imageio-native-video-windowsWindows 10+Media Foundation (IMFSourceReader)Media Foundation-supported containers
imageio-native-video-ffmpegmacOS ARM64, Windows x64, Linux x64 (optional)Bundled FFmpeg 8.1.2 via ffmpeg-ffmAll 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.

FFmpeg video backend

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

Architecture

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.

Advanced ImageIO behavior

ImageReadParam

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

EXIF orientation

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 valueTransform
1None (identity)
2Flip horizontal
3Rotate 180
4Flip vertical
5Rotate 90 + flip horizontal
6Rotate 90
7Rotate 270 + flip horizontal
8Rotate 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.

Project structure

├── 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)

Building

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)

Releasing

  1. Set the release version in pom.xml (remove -SNAPSHOT)
  2. Commit, push, and merge via PR; wait for CI to pass on the release commit
  3. Publish a non-prerelease GitHub Release tagged v<version> and targeting that commit
  4. 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.

License

MIT

About

Java ImageIO readers that delegate to platform-native image decoding APIs via Project Panama (FFM API, Java 22+). HEIC, AVIF, WEBP, JPEG 2000, camera RAW, and more through standard ImageIO.read().

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages