Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ a modular macOS application can launch with:

```sh
java --module-path lib \
--enable-native-access=io.github.ghosthack.imageio.apple \
--enable-native-access=io.github.ghosthack.panama.media.corefoundation,io.github.ghosthack.panama.media.cgimage \
--module com.example.app/com.example.Main
```

Expand Down Expand Up @@ -120,8 +120,8 @@ 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` |
| Apple images | `io.github.ghosthack.panama.media.corefoundation,io.github.ghosthack.panama.media.cgimage` |
| Apple video | `io.github.ghosthack.panama.media.corefoundation,io.github.ghosthack.panama.media.avfoundation` |
| 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` |
Expand Down Expand Up @@ -431,10 +431,11 @@ VideoInfo info = VideoFrameExtractor.getInfo(Path.of("clip.mp4"));
| `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`](https://github.com/ghosthack/panama-media) WIC and Media
Foundation bindings. Codec and container support ultimately depends on the
media components installed in Windows.
The platform still-image and video backends use the published
[`panama-media`](https://github.com/ghosthack/panama-media) bindings: CGImage
(ImageIO) and AVFoundation on macOS, WIC and Media Foundation on Windows.
Codec and container support ultimately depends on the media components
installed in the operating system.

### FFmpeg video backend

Expand Down Expand Up @@ -478,8 +479,8 @@ Backend ImageReader one decode attempt, lazy decode + cache
▼
┌─────────────────────┬──────────────────────────────┐
│ macOS │ Windows │
│ AppleNative │ panama-media WIC │
│ Panama downcalls │ shared COM/FFM bindings │
│ panama-media │ panama-media WIC │
│ CGImage bindings │ shared COM/FFM bindings │
│ CoreGraphics + │ ole32 + windowscodecs │
│ ImageIO.framework │ IWICImagingFactory → │
│ CGImageSource → │ IWICStream → Decoder → │
Expand Down
5 changes: 5 additions & 0 deletions imageio-native-apple/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
<artifactId>imageio-native-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.github.ghosthack</groupId>
<artifactId>panama-media-cgimage</artifactId>
<version>${panama-media.version}</version>
</dependency>
<dependency>
<groupId>io.github.ghosthack</groupId>
<artifactId>imageio-native-common</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,105 +1,37 @@
package io.github.ghosthack.imageio.apple;

import io.github.ghosthack.panama.media.cgimage.CGBitmapContext;
import io.github.ghosthack.panama.media.cgimage.CGColorSpace;
import io.github.ghosthack.panama.media.cgimage.CGContext;
import io.github.ghosthack.panama.media.cgimage.CGGeometry;
import io.github.ghosthack.panama.media.cgimage.CGImage;
import io.github.ghosthack.panama.media.corefoundation.CoreFoundation;

import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

/**
* Public helper that converts a CGImageRef to a {@link BufferedImage}.
* <p>
* Loads CoreFoundation and CoreGraphics frameworks defensively in its own
* static initialiser, so it works regardless of whether {@link AppleNative}
* has been class-loaded first. The {@code System.load} calls are idempotent —
* if the frameworks are already loaded, they are a no-op.
* The native calls go through the {@code panama-media-cgimage} and
* {@code panama-media-corefoundation} bindings.
* <p>
* Intended to be used by both {@link AppleNative} (for still-image decode)
* and the video-apple module (which lives in a different package and cannot
* access package-private members).
* <p>
* Requires macOS and native access for the containing module
* ({@code io.github.ghosthack.imageio.apple}), or {@code ALL-UNNAMED} when
* used on the class path.
* Requires macOS and native access for
* {@code io.github.ghosthack.panama.media.core},
* {@code io.github.ghosthack.panama.media.corefoundation} and
* {@code io.github.ghosthack.panama.media.cgimage}, or {@code ALL-UNNAMED}
* when used on the class path.
*/
public final class AppleCoreGraphicsHelper {

private AppleCoreGraphicsHelper() {}

// ── Constants ───────────────────────────────────────────────────────

/** kCGImageAlphaPremultipliedFirst = 2 */
private static final int kCGImageAlphaPremultipliedFirst = 2;

/** kCGBitmapByteOrder32Little = 2 << 12 = 8192 */
private static final int kCGBitmapByteOrder32Little = 2 << 12;

/** BGRA premultiplied, little-endian — matches TYPE_INT_ARGB_PRE when read as LE ints. */
private static final int BITMAP_INFO = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little;

/** CGRect struct layout (4 × double on 64-bit). */
private static final StructLayout CGRECT = MemoryLayout.structLayout(
ValueLayout.JAVA_DOUBLE.withName("x"),
ValueLayout.JAVA_DOUBLE.withName("y"),
ValueLayout.JAVA_DOUBLE.withName("width"),
ValueLayout.JAVA_DOUBLE.withName("height")
);

// ── Downcall handles ────────────────────────────────────────────────

private static final Linker LINKER = Linker.nativeLinker();
private static final SymbolLookup LOOKUP;

private static final MethodHandle CGImageGetWidth;
private static final MethodHandle CGImageGetHeight;
private static final MethodHandle CGColorSpaceCreateDeviceRGB;
private static final MethodHandle CGBitmapContextCreate;
private static final MethodHandle CGContextDrawImage;
private static final MethodHandle CFRelease;

private static final boolean IS_MACOS = System.getProperty("os.name", "")
.toLowerCase(java.util.Locale.ROOT).contains("mac");

static {
// Load CoreFoundation and CoreGraphics defensively. System.load is
// idempotent — if AppleNative already loaded them, these are no-ops.
if (IS_MACOS) {
System.load("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation");
System.load("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics");
}
LOOKUP = SymbolLookup.loaderLookup();

CGImageGetWidth = downcall("CGImageGetWidth",
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
CGImageGetHeight = downcall("CGImageGetHeight",
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
CGColorSpaceCreateDeviceRGB = downcall("CGColorSpaceCreateDeviceRGB",
FunctionDescriptor.of(ValueLayout.ADDRESS));
CGBitmapContextCreate = downcall("CGBitmapContextCreate",
FunctionDescriptor.of(ValueLayout.ADDRESS,
ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG,
ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS, ValueLayout.JAVA_INT));
CGContextDrawImage = downcall("CGContextDrawImage",
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, CGRECT, ValueLayout.ADDRESS));
CFRelease = downcall("CFRelease",
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
}

private static MethodHandle downcall(String name, FunctionDescriptor fd) {
MemorySegment addr = LOOKUP.find(name)
.orElseThrow(() -> new UnsatisfiedLinkError("Symbol not found: " + name));
return LINKER.downcallHandle(addr, fd);
}

private static void release(MemorySegment ref) {
if (ref != null && !MemorySegment.NULL.equals(ref)) {
try {
CFRelease.invokeExact(ref);
} catch (Throwable ignored) {}
}
}

// ── Public API ──────────────────────────────────────────────────────

/**
Expand All @@ -116,29 +48,25 @@ public static BufferedImage cgImageToBufferedImage(MemorySegment cgImage, Arena
MemorySegment colorSpace = MemorySegment.NULL;
MemorySegment ctx = MemorySegment.NULL;
try {
long w = (long) CGImageGetWidth.invokeExact(cgImage);
long h = (long) CGImageGetHeight.invokeExact(cgImage);
long w = CGImage.getWidth(cgImage);
long h = CGImage.getHeight(cgImage);
if (w <= 0 || h <= 0) {
throw new javax.imageio.IIOException("Invalid CGImage dimensions: " + w + "x" + h);
}

colorSpace = (MemorySegment) CGColorSpaceCreateDeviceRGB.invokeExact();
colorSpace = CGColorSpace.createDeviceRGB();

// BGRA premultiplied, little-endian — matches TYPE_INT_ARGB_PRE when read as LE ints.
long bytesPerRow = w * 4;
MemorySegment pixelData = arena.allocate(bytesPerRow * h, 16);
ctx = (MemorySegment) CGBitmapContextCreate.invokeExact(
pixelData, w, h, 8L, bytesPerRow, colorSpace, BITMAP_INFO);
ctx = CGBitmapContext.create(pixelData, w, h, 8L, bytesPerRow, colorSpace,
CGBitmapContext.BitmapInfo.BGRA_PREMULTIPLIED_LITTLE_ENDIAN);
if (MemorySegment.NULL.equals(ctx)) {
throw new javax.imageio.IIOException("CGBitmapContextCreate returned NULL");
}

// Draw the CGImage into the bitmap context
MemorySegment rect = arena.allocate(CGRECT);
rect.set(ValueLayout.JAVA_DOUBLE, 0, 0.0); // origin.x
rect.set(ValueLayout.JAVA_DOUBLE, 8, 0.0); // origin.y
rect.set(ValueLayout.JAVA_DOUBLE, 16, (double) w); // size.width
rect.set(ValueLayout.JAVA_DOUBLE, 24, (double) h); // size.height
CGContextDrawImage.invokeExact(ctx, rect, cgImage);
CGContext.drawImage(ctx, CGGeometry.CGRect.allocate(arena, 0, 0, w, h), cgImage);

// Copy pixels into a BufferedImage
int iw = (int) w;
Expand All @@ -153,8 +81,8 @@ public static BufferedImage cgImageToBufferedImage(MemorySegment cgImage, Arena
} catch (Throwable t) {
throw new javax.imageio.IIOException("CGImage to BufferedImage conversion failed", t);
} finally {
release(ctx);
release(colorSpace);
CoreFoundation.cfRelease(ctx);
CoreFoundation.cfRelease(colorSpace);
}
}
}
Loading
Loading