From e62019e8467a31c9515a9434004c6315a735f7b9 Mon Sep 17 00:00:00 2001 From: ghosthack <837911+ghosthack@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:37:29 -0700 Subject: [PATCH] Use panama-media for the macOS image and video backends Replace the Apple backends' own CoreFoundation, CoreGraphics, ImageIO and AVFoundation downcalls with the published panama-media 0.1.4 bindings (corefoundation, cgimage, avfoundation), as the Windows backends already do with WIC and Media Foundation. Decoded pixels are byte-identical for still images and for video frames at the source aspect ratio; a frame requested at a different aspect ratio is now scaled down from a larger intermediate instead of up from a smaller one. Native access moves from the imageio modules to the panama-media modules that make the native calls (see the README table). On macOS, a missing grant now reports which modules need native access instead of claiming the platform is unsupported. Bump panama-media from 0.1.1 to 0.1.4 for all backends. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01JHHPeMi7jrfjcEgEXqm5gh --- README.md | 19 +- imageio-native-apple/pom.xml | 5 + .../apple/AppleCoreGraphicsHelper.java | 118 +--- .../ghosthack/imageio/apple/AppleNative.java | 463 ++++----------- .../src/main/java/module-info.java | 2 + imageio-native-video-apple/pom.xml | 4 +- .../video/apple/AppleVideoFrameExtractor.java | 525 +++--------------- .../src/main/java/module-info.java | 4 +- pom.xml | 2 +- 9 files changed, 242 insertions(+), 900 deletions(-) diff --git a/README.md b/README.md index ddcd91c..0caad06 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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` | @@ -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 @@ -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 → │ diff --git a/imageio-native-apple/pom.xml b/imageio-native-apple/pom.xml index 10330d6..a32f73a 100644 --- a/imageio-native-apple/pom.xml +++ b/imageio-native-apple/pom.xml @@ -26,6 +26,11 @@ imageio-native-common ${project.version} + + io.github.ghosthack + panama-media-cgimage + ${panama-media.version} + io.github.ghosthack imageio-native-common diff --git a/imageio-native-apple/src/main/java/io/github/ghosthack/imageio/apple/AppleCoreGraphicsHelper.java b/imageio-native-apple/src/main/java/io/github/ghosthack/imageio/apple/AppleCoreGraphicsHelper.java index d94b406..0a8e663 100644 --- a/imageio-native-apple/src/main/java/io/github/ghosthack/imageio/apple/AppleCoreGraphicsHelper.java +++ b/imageio-native-apple/src/main/java/io/github/ghosthack/imageio/apple/AppleCoreGraphicsHelper.java @@ -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}. *

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

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

- * 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 ────────────────────────────────────────────────────── /** @@ -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; @@ -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); } } } diff --git a/imageio-native-apple/src/main/java/io/github/ghosthack/imageio/apple/AppleNative.java b/imageio-native-apple/src/main/java/io/github/ghosthack/imageio/apple/AppleNative.java index 78ff20d..e0cc847 100644 --- a/imageio-native-apple/src/main/java/io/github/ghosthack/imageio/apple/AppleNative.java +++ b/imageio-native-apple/src/main/java/io/github/ghosthack/imageio/apple/AppleNative.java @@ -1,26 +1,30 @@ package io.github.ghosthack.imageio.apple; import io.github.ghosthack.imageio.common.ImageReadParamSupport; +import io.github.ghosthack.panama.media.cgimage.CGImageSource; +import io.github.ghosthack.panama.media.corefoundation.CoreFoundation; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.lang.foreign.*; -import java.lang.invoke.MethodHandle; /** - * Panama (FFM) bindings to macOS CoreFoundation, CoreGraphics, and ImageIO frameworks. + * Decodes images through Apple's CGImageSource API, which supports HEIC, AVIF, + * WEBP, and many other formats through the Apple Media Engine. *

- * Provides the native bridge to decode images via Apple's CGImageSource API, - * which supports HEIC, AVIF, WEBP, and many other formats through the Apple Media Engine. + * The native calls go through the {@code panama-media-corefoundation} and + * {@code panama-media-cgimage} bindings; this class owns the decode policy + * (pixel limit, render-size bound, orientation handling). *

* EXIF orientation is applied automatically: {@link #decode} uses * {@code CGImageSourceCreateThumbnailAtIndex} with * {@code kCGImageSourceCreateThumbnailWithTransform = true}, and {@link #getSize} * reads the orientation property to return display-oriented dimensions. *

- * Requires native access for the containing module - * ({@code io.github.ghosthack.imageio.apple}), or {@code ALL-UNNAMED} when - * used on the class path. + * Requires 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. * Only functional on macOS; all entry points return failure gracefully on other OSes. */ final class AppleNative { @@ -29,9 +33,24 @@ private AppleNative() {} // ── OS guard ──────────────────────────────────────────────────────── - static final boolean IS_MACOS = System.getProperty("os.name", "") + static final boolean IS_MACOS = + io.github.ghosthack.panama.media.cgimage.ImageIO.isAvailable(); + + private static final boolean ON_MACOS = System.getProperty("os.name", "") .toLowerCase(java.util.Locale.ROOT).contains("mac"); + /** + * Explains why decoding is unavailable: not macOS, or macOS where the + * panama-media bindings could not load (typically missing native access). + */ + static String unavailableMessage(String offMacMessage) { + return ON_MACOS + ? "Apple ImageIO bindings failed to load; grant native access to " + + "io.github.ghosthack.panama.media.corefoundation and " + + "io.github.ghosthack.panama.media.cgimage" + : offMacMessage; + } + /** * Maximum number of pixels (width * height) we are willing to decode. * Prevents OOM on malicious/corrupt images declaring huge dimensions. @@ -39,190 +58,6 @@ private AppleNative() {} */ static final long MAX_PIXELS = Long.getLong("imageio.native.maxPixels", 256L * 1024 * 1024); - // ── Constants ─────────────────────────────────────────────────────── - - /** kCFNumberIntType = 9 (C {@code int}, 32-bit signed). */ - static final int kCFNumberIntType = 9; - - // ── Framework loading and method handles ──────────────────────────── - // Load via System.load so dlopen resolves from the dyld shared cache - // (framework files may not exist on disk on macOS 11+). - // Guarded by IS_MACOS so this class can be loaded safely on Windows/Linux. - - private static final Linker LINKER = Linker.nativeLinker(); - - private static final SymbolLookup LOOKUP; - - // ── CoreFoundation handles ────────────────────────────────────────── - - private static final MethodHandle CFDataCreateWithBytesNoCopy; - private static final MethodHandle CFRelease; - private static final MethodHandle CFDictionaryCreate; - private static final MethodHandle CFDictionaryGetValue; - private static final MethodHandle CFNumberCreate; - private static final MethodHandle CFNumberGetValue; - - // ── CoreFoundation constant symbols ───────────────────────────────── - - private static final MemorySegment kCFAllocatorNull; - private static final MemorySegment kCFBooleanTrue; - private static final MemorySegment kCFTypeDictionaryKeyCallBacks; // struct, not pointer-to-pointer - private static final MemorySegment kCFTypeDictionaryValueCallBacks; // struct, not pointer-to-pointer - - // ── ImageIO handles ───────────────────────────────────────────────── - - private static final MethodHandle CGImageSourceCreateWithData; - private static final MethodHandle CGImageSourceCreateWithURL; - private static final MethodHandle CGImageSourceGetStatus; - private static final MethodHandle CGImageSourceCopyPropertiesAtIndex; - private static final MethodHandle CGImageSourceCreateThumbnailAtIndex; - - // ── CFURL handles ─────────────────────────────────────────────────── - - private static final MethodHandle CFStringCreateWithCString; - private static final MethodHandle CFURLCreateWithFileSystemPath; - - /** kCFStringEncodingUTF8 */ - private static final int kCFStringEncodingUTF8 = 0x08000100; - - /** kCFURLPOSIXPathStyle */ - private static final int kCFURLPOSIXPathStyle = 0; - - // ── ImageIO property key symbols (CFStringRef constants) ──────────── - - private static final MemorySegment kCGImagePropertyPixelWidth; - private static final MemorySegment kCGImagePropertyPixelHeight; - private static final MemorySegment kCGImagePropertyOrientation; - private static final MemorySegment kCGImageSourceCreateThumbnailFromImageAlways; - private static final MemorySegment kCGImageSourceCreateThumbnailWithTransform; - private static final MemorySegment kCGImageSourceThumbnailMaxPixelSize; - - - - static { - if (IS_MACOS) { - System.load("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"); - System.load("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"); - System.load("/System/Library/Frameworks/ImageIO.framework/ImageIO"); - LOOKUP = SymbolLookup.loaderLookup(); - - // ── CoreFoundation ────────────────────────────────────────── - CFDataCreateWithBytesNoCopy = downcall("CFDataCreateWithBytesNoCopy", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)); - CFRelease = downcall("CFRelease", - FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); - CFDictionaryCreate = downcall("CFDictionaryCreate", - FunctionDescriptor.of(ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, - ValueLayout.JAVA_LONG, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - CFDictionaryGetValue = downcall("CFDictionaryGetValue", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - CFNumberCreate = downcall("CFNumberCreate", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, - ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); - CFNumberGetValue = downcall("CFNumberGetValue", - FunctionDescriptor.of(ValueLayout.JAVA_BOOLEAN, ValueLayout.ADDRESS, - ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); - - kCFAllocatorNull = loadConstPtr("kCFAllocatorNull"); - kCFBooleanTrue = loadConstPtr("kCFBooleanTrue"); - // These are struct values (not pointer-to-pointer) — use the symbol address directly - kCFTypeDictionaryKeyCallBacks = loadSymbolAddr("kCFTypeDictionaryKeyCallBacks"); - kCFTypeDictionaryValueCallBacks = loadSymbolAddr("kCFTypeDictionaryValueCallBacks"); - - // ── CFURL helpers ─────────────────────────────────────────── - CFStringCreateWithCString = downcall("CFStringCreateWithCString", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); - CFURLCreateWithFileSystemPath = downcall("CFURLCreateWithFileSystemPath", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_BOOLEAN)); - - // ── ImageIO ───────────────────────────────────────────────── - CGImageSourceCreateWithData = downcall("CGImageSourceCreateWithData", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - CGImageSourceCreateWithURL = downcall("CGImageSourceCreateWithURL", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - CGImageSourceGetStatus = downcall("CGImageSourceGetStatus", - FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); - CGImageSourceCopyPropertiesAtIndex = downcall("CGImageSourceCopyPropertiesAtIndex", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, - ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)); - CGImageSourceCreateThumbnailAtIndex = downcall("CGImageSourceCreateThumbnailAtIndex", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, - ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)); - - kCGImagePropertyPixelWidth = loadConstPtr("kCGImagePropertyPixelWidth"); - kCGImagePropertyPixelHeight = loadConstPtr("kCGImagePropertyPixelHeight"); - kCGImagePropertyOrientation = loadConstPtr("kCGImagePropertyOrientation"); - kCGImageSourceCreateThumbnailFromImageAlways = loadConstPtr("kCGImageSourceCreateThumbnailFromImageAlways"); - kCGImageSourceCreateThumbnailWithTransform = loadConstPtr("kCGImageSourceCreateThumbnailWithTransform"); - kCGImageSourceThumbnailMaxPixelSize = loadConstPtr("kCGImageSourceThumbnailMaxPixelSize"); - - } else { - LOOKUP = null; - CFDataCreateWithBytesNoCopy = null; - CFRelease = null; - CFDictionaryCreate = null; - CFDictionaryGetValue = null; - CFNumberCreate = null; - CFNumberGetValue = null; - kCFAllocatorNull = null; - kCFBooleanTrue = null; - kCFTypeDictionaryKeyCallBacks = null; - kCFTypeDictionaryValueCallBacks = null; - CFStringCreateWithCString = null; - CFURLCreateWithFileSystemPath = null; - CGImageSourceCreateWithData = null; - CGImageSourceCreateWithURL = null; - CGImageSourceGetStatus = null; - CGImageSourceCopyPropertiesAtIndex = null; - CGImageSourceCreateThumbnailAtIndex = null; - kCGImagePropertyPixelWidth = null; - kCGImagePropertyPixelHeight = null; - kCGImagePropertyOrientation = null; - kCGImageSourceCreateThumbnailFromImageAlways = null; - kCGImageSourceCreateThumbnailWithTransform = null; - kCGImageSourceThumbnailMaxPixelSize = null; - - } - } - - static final int kCGImageStatusUnknownType = -3; - static final int kCGImageStatusIncomplete = -1; - - // ── Helpers: symbol loading ───────────────────────────────────────── - - 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); - } - - /** Dereferences a global constant pointer (e.g. {@code extern const CFStringRef kFoo}). */ - private static MemorySegment loadConstPtr(String name) { - MemorySegment sym = LOOKUP.find(name) - .orElseThrow(() -> new UnsatisfiedLinkError("Symbol not found: " + name)); - return sym.reinterpret(ValueLayout.ADDRESS.byteSize()).get(ValueLayout.ADDRESS, 0); - } - - /** Returns the raw symbol address (for struct-valued globals like callback structs). */ - private static MemorySegment loadSymbolAddr(String name) { - return LOOKUP.find(name) - .orElseThrow(() -> new UnsatisfiedLinkError("Symbol not found: " + name)); - } - - // ── Helper: release a non-NULL native reference ───────────────────── - - static void release(MemorySegment ref) { - if (ref != null && !MemorySegment.NULL.equals(ref)) { - try { - CFRelease.invokeExact(ref); - } catch (Throwable ignored) { /* invokeExact signature; native release cannot throw */ } - } - } - // ── Helper: read an int property from a CFDictionary ──────────────── /** @@ -231,15 +66,14 @@ static void release(MemorySegment ref) { */ private static int dictGetInt(MemorySegment dict, MemorySegment key, int defaultValue) { try { - MemorySegment val = (MemorySegment) CFDictionaryGetValue.invokeExact(dict, key); + MemorySegment val = CoreFoundation.cfDictionaryGetValue(dict, key); if (MemorySegment.NULL.equals(val)) return defaultValue; - // val is a CFNumber — extract as kCFNumberIntType (9) try (Arena a = Arena.ofConfined()) { MemorySegment buf = a.allocate(ValueLayout.JAVA_INT); - boolean ok = (boolean) CFNumberGetValue.invokeExact(val, kCFNumberIntType, buf); - return ok ? buf.get(ValueLayout.JAVA_INT, 0) : defaultValue; + return CoreFoundation.cfNumberGetInt(val, buf) + ? buf.get(ValueLayout.JAVA_INT, 0) : defaultValue; } - } catch (Throwable t) { + } catch (RuntimeException e) { return defaultValue; } } @@ -255,58 +89,21 @@ private static int dictGetInt(MemorySegment dict, MemorySegment key, int default * * Caller must release the returned dictionary. */ - private static MemorySegment createThumbnailOptions(Arena arena, int maxDim) throws Throwable { - // Create CFNumber for max pixel size - MemorySegment sizePtr = arena.allocate(ValueLayout.JAVA_INT); - sizePtr.set(ValueLayout.JAVA_INT, 0, maxDim); - MemorySegment cfMaxSize = (MemorySegment) CFNumberCreate.invokeExact( - MemorySegment.NULL, kCFNumberIntType, sizePtr); - + private static MemorySegment createThumbnailOptions(Arena arena, int maxDim) { + MemorySegment cfMaxSize = CoreFoundation.cfNumberCreateInt(arena, maxDim); try { - // Build key and value arrays (3 entries) - MemorySegment keys = arena.allocate(ValueLayout.ADDRESS, 3); - MemorySegment values = arena.allocate(ValueLayout.ADDRESS, 3); - - keys.setAtIndex(ValueLayout.ADDRESS, 0, kCGImageSourceCreateThumbnailFromImageAlways); - keys.setAtIndex(ValueLayout.ADDRESS, 1, kCGImageSourceCreateThumbnailWithTransform); - keys.setAtIndex(ValueLayout.ADDRESS, 2, kCGImageSourceThumbnailMaxPixelSize); - - values.setAtIndex(ValueLayout.ADDRESS, 0, kCFBooleanTrue); - values.setAtIndex(ValueLayout.ADDRESS, 1, kCFBooleanTrue); - values.setAtIndex(ValueLayout.ADDRESS, 2, cfMaxSize); - - return (MemorySegment) CFDictionaryCreate.invokeExact( - MemorySegment.NULL, keys, values, 3L, - kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks); + MemorySegment yes = CoreFoundation.kCFBooleanTrue(); + return CoreFoundation.cfDictionaryCreate(arena, + new MemorySegment[]{ + CGImageSource.kCreateThumbnailFromImageAlways(), + CGImageSource.kCreateThumbnailWithTransform(), + CGImageSource.kThumbnailMaxPixelSize()}, + new MemorySegment[]{yes, yes, cfMaxSize}); } finally { - release(cfMaxSize); + CoreFoundation.cfRelease(cfMaxSize); } } - // ── Helper: create CGImageSource from a file path ──────────────── - - /** - * Creates a CFString from a Java string (UTF-8 C-string). - * Caller must release the returned CFString. - */ - private static MemorySegment createCFString(Arena arena, String s) throws Throwable { - byte[] utf8 = s.getBytes(java.nio.charset.StandardCharsets.UTF_8); - MemorySegment cstr = arena.allocate(utf8.length + 1L); - MemorySegment.copy(utf8, 0, cstr, ValueLayout.JAVA_BYTE, 0, utf8.length); - cstr.set(ValueLayout.JAVA_BYTE, utf8.length, (byte) 0); // null terminator - return (MemorySegment) CFStringCreateWithCString.invokeExact( - MemorySegment.NULL, cstr, kCFStringEncodingUTF8); - } - - /** - * Creates a CFURL from a POSIX file path string. - * Caller must release the returned CFURL. - */ - private static MemorySegment createFileURL(MemorySegment cfString) throws Throwable { - return (MemorySegment) CFURLCreateWithFileSystemPath.invokeExact( - MemorySegment.NULL, cfString, kCFURLPOSIXPathStyle, false); - } - // ── Shared helpers: operate on a CGImageSource ────────────────────── /** @@ -316,17 +113,16 @@ private static MemorySegment createFileURL(MemorySegment cfString) throws Throwa private static int[] getSizeFromSource(MemorySegment imgSrc) throws java.io.IOException { MemorySegment props = MemorySegment.NULL; try { - props = (MemorySegment) CGImageSourceCopyPropertiesAtIndex.invokeExact( - imgSrc, 0L, MemorySegment.NULL); + props = CGImageSource.copyPropertiesAtIndex(imgSrc, 0L, MemorySegment.NULL); if (MemorySegment.NULL.equals(props)) throw new javax.imageio.IIOException("Failed to read image properties"); - int rawW = dictGetInt(props, kCGImagePropertyPixelWidth, -1); - int rawH = dictGetInt(props, kCGImagePropertyPixelHeight, -1); + int rawW = dictGetInt(props, CGImageSource.kPixelWidth(), -1); + int rawH = dictGetInt(props, CGImageSource.kPixelHeight(), -1); if (rawW <= 0 || rawH <= 0) throw new javax.imageio.IIOException("Invalid image dimensions: " + rawW + "x" + rawH); - int orientation = dictGetInt(props, kCGImagePropertyOrientation, 1); + int orientation = dictGetInt(props, CGImageSource.kOrientation(), 1); if (orientation >= 5 && orientation <= 8) { return new int[]{rawH, rawW}; } @@ -336,7 +132,7 @@ private static int[] getSizeFromSource(MemorySegment imgSrc) throws java.io.IOEx } catch (Throwable t) { throw new javax.imageio.IIOException("Native image size query failed", t); } finally { - release(props); + CoreFoundation.cfRelease(props); } } @@ -351,13 +147,12 @@ private static BufferedImage decodeFromSource( MemorySegment thumbOpts = MemorySegment.NULL; MemorySegment cgImage = MemorySegment.NULL; try { - props = (MemorySegment) CGImageSourceCopyPropertiesAtIndex.invokeExact( - imgSrc, 0L, MemorySegment.NULL); + props = CGImageSource.copyPropertiesAtIndex(imgSrc, 0L, MemorySegment.NULL); if (MemorySegment.NULL.equals(props)) throw new javax.imageio.IIOException("Failed to read image properties"); - int rawW = dictGetInt(props, kCGImagePropertyPixelWidth, -1); - int rawH = dictGetInt(props, kCGImagePropertyPixelHeight, -1); + int rawW = dictGetInt(props, CGImageSource.kPixelWidth(), -1); + int rawH = dictGetInt(props, CGImageSource.kPixelHeight(), -1); if (rawW <= 0 || rawH <= 0) throw new javax.imageio.IIOException("Invalid image dimensions: " + rawW + "x" + rawH); @@ -383,8 +178,7 @@ private static BufferedImage decodeFromSource( if (MemorySegment.NULL.equals(thumbOpts)) throw new javax.imageio.IIOException("Failed to create thumbnail options"); - cgImage = (MemorySegment) CGImageSourceCreateThumbnailAtIndex.invokeExact( - imgSrc, 0L, thumbOpts); + cgImage = CGImageSource.createThumbnailAtIndex(imgSrc, 0L, thumbOpts); if (MemorySegment.NULL.equals(cgImage)) throw new javax.imageio.IIOException( "CGImageSourceCreateThumbnailAtIndex returned NULL – decode failed"); @@ -397,9 +191,9 @@ private static BufferedImage decodeFromSource( } catch (Throwable t) { throw new javax.imageio.IIOException("Native image decode failed", t); } finally { - release(cgImage); - release(thumbOpts); - release(props); + CoreFoundation.cfRelease(cgImage); + CoreFoundation.cfRelease(thumbOpts); + CoreFoundation.cfRelease(props); } } @@ -422,26 +216,66 @@ static boolean canDecode(byte[] header, int len) { try { MemorySegment buf = arena.allocate(len); MemorySegment.copy(header, 0, buf, ValueLayout.JAVA_BYTE, 0, len); - cfData = (MemorySegment) CFDataCreateWithBytesNoCopy.invokeExact( - MemorySegment.NULL, buf, (long) len, kCFAllocatorNull); + cfData = CoreFoundation.cfDataCreateNoCopy( + MemorySegment.NULL, buf, len, CoreFoundation.kCFAllocatorNull()); if (MemorySegment.NULL.equals(cfData)) return false; - imgSrc = (MemorySegment) CGImageSourceCreateWithData.invokeExact( - cfData, MemorySegment.NULL); + imgSrc = CGImageSource.createWithData(cfData, MemorySegment.NULL); if (MemorySegment.NULL.equals(imgSrc)) return false; - int status = (int) CGImageSourceGetStatus.invokeExact(imgSrc); // status >= kCGImageStatusIncomplete means the format was identified - return status >= kCGImageStatusIncomplete; + return CGImageSource.getStatus(imgSrc) >= CGImageSource.STATUS_INCOMPLETE; } catch (Throwable t) { return false; } finally { - release(imgSrc); - release(cfData); + CoreFoundation.cfRelease(imgSrc); + CoreFoundation.cfRelease(cfData); } } } + // ── CGImageSource constructors ────────────────────────────────────── + + /** An image source and the CF object backing it; both released by the caller. */ + private record Source(MemorySegment imgSrc, MemorySegment backing) {} + + private static Source sourceFromBytes(Arena arena, byte[] imageData, String unsupported) + throws java.io.IOException { + MemorySegment nativeBuf = arena.allocateFrom(ValueLayout.JAVA_BYTE, imageData); + MemorySegment cfData = CoreFoundation.cfDataCreateNoCopy( + MemorySegment.NULL, nativeBuf, imageData.length, CoreFoundation.kCFAllocatorNull()); + if (MemorySegment.NULL.equals(cfData)) + throw new javax.imageio.IIOException("CFDataCreateWithBytesNoCopy returned NULL"); + + MemorySegment imgSrc = CGImageSource.createWithData(cfData, MemorySegment.NULL); + if (MemorySegment.NULL.equals(imgSrc)) { + CoreFoundation.cfRelease(cfData); + throw new javax.imageio.IIOException(unsupported); + } + return new Source(imgSrc, cfData); + } + + private static Source sourceFromPath(Arena arena, String path, String unsupported) + throws java.io.IOException { + MemorySegment cfUrl = CoreFoundation.cfUrlCreate(arena, path); + if (MemorySegment.NULL.equals(cfUrl)) + throw new javax.imageio.IIOException("Failed to create CFURL for path"); + + MemorySegment imgSrc = CGImageSource.createWithURL(cfUrl, MemorySegment.NULL); + if (MemorySegment.NULL.equals(imgSrc)) { + CoreFoundation.cfRelease(cfUrl); + throw new javax.imageio.IIOException(unsupported + path); + } + return new Source(imgSrc, cfUrl); + } + + private static void release(Source source) { + if (source != null) { + CoreFoundation.cfRelease(source.imgSrc()); + CoreFoundation.cfRelease(source.backing()); + } + } + // ── Lightweight size query ───────────────────────────────────────── /** @@ -453,31 +287,19 @@ static boolean canDecode(byte[] header, int len) { */ static int[] getSize(byte[] imageData) throws java.io.IOException { if (!IS_MACOS) - throw new javax.imageio.IIOException("Apple ImageIO is only available on macOS"); + throw new javax.imageio.IIOException(unavailableMessage("Apple ImageIO is only available on macOS")); try (Arena arena = Arena.ofConfined()) { - MemorySegment cfData = MemorySegment.NULL; - MemorySegment imgSrc = MemorySegment.NULL; + Source source = null; try { - MemorySegment nativeBuf = arena.allocateFrom(ValueLayout.JAVA_BYTE, imageData); - cfData = (MemorySegment) CFDataCreateWithBytesNoCopy.invokeExact( - MemorySegment.NULL, nativeBuf, (long) imageData.length, kCFAllocatorNull); - if (MemorySegment.NULL.equals(cfData)) - throw new javax.imageio.IIOException("CFDataCreateWithBytesNoCopy returned NULL"); - - imgSrc = (MemorySegment) CGImageSourceCreateWithData.invokeExact( - cfData, MemorySegment.NULL); - if (MemorySegment.NULL.equals(imgSrc)) - throw new javax.imageio.IIOException("Unsupported image format"); - - return getSizeFromSource(imgSrc); + source = sourceFromBytes(arena, imageData, "Unsupported image format"); + return getSizeFromSource(source.imgSrc()); } catch (java.io.IOException e) { throw e; } catch (Throwable t) { throw new javax.imageio.IIOException("Native image size query failed", t); } finally { - release(imgSrc); - release(cfData); + release(source); } } } @@ -492,34 +314,19 @@ static int[] getSize(byte[] imageData) throws java.io.IOException { */ static int[] getSizeFromPath(String path) throws java.io.IOException { if (!IS_MACOS) - throw new javax.imageio.IIOException("Apple ImageIO is only available on macOS"); + throw new javax.imageio.IIOException(unavailableMessage("Apple ImageIO is only available on macOS")); try (Arena arena = Arena.ofConfined()) { - MemorySegment cfString = MemorySegment.NULL; - MemorySegment cfUrl = MemorySegment.NULL; - MemorySegment imgSrc = MemorySegment.NULL; + Source source = null; try { - cfString = createCFString(arena, path); - if (MemorySegment.NULL.equals(cfString)) - throw new javax.imageio.IIOException("Failed to create CFString for path"); - cfUrl = createFileURL(cfString); - if (MemorySegment.NULL.equals(cfUrl)) - throw new javax.imageio.IIOException("Failed to create CFURL for path"); - - imgSrc = (MemorySegment) CGImageSourceCreateWithURL.invokeExact( - cfUrl, MemorySegment.NULL); - if (MemorySegment.NULL.equals(imgSrc)) - throw new javax.imageio.IIOException("Unsupported image format: " + path); - - return getSizeFromSource(imgSrc); + source = sourceFromPath(arena, path, "Unsupported image format: "); + return getSizeFromSource(source.imgSrc()); } catch (java.io.IOException e) { throw e; } catch (Throwable t) { throw new javax.imageio.IIOException("Native image size query failed for: " + path, t); } finally { - release(imgSrc); - release(cfUrl); - release(cfString); + release(source); } } } @@ -544,32 +351,19 @@ static BufferedImage decode(byte[] imageData) throws java.io.IOException { static BufferedImage decode(byte[] imageData, Dimension renderSize) throws java.io.IOException { if (!IS_MACOS) - throw new javax.imageio.IIOException("Apple ImageIO decoding is only available on macOS"); + throw new javax.imageio.IIOException(unavailableMessage("Apple ImageIO decoding is only available on macOS")); try (Arena arena = Arena.ofConfined()) { - MemorySegment cfData = MemorySegment.NULL; - MemorySegment imgSrc = MemorySegment.NULL; + Source source = null; try { - MemorySegment nativeBuf = arena.allocateFrom(ValueLayout.JAVA_BYTE, imageData); - cfData = (MemorySegment) CFDataCreateWithBytesNoCopy.invokeExact( - MemorySegment.NULL, nativeBuf, - (long) imageData.length, kCFAllocatorNull); - if (MemorySegment.NULL.equals(cfData)) - throw new javax.imageio.IIOException("CFDataCreateWithBytesNoCopy returned NULL"); - - imgSrc = (MemorySegment) CGImageSourceCreateWithData.invokeExact( - cfData, MemorySegment.NULL); - if (MemorySegment.NULL.equals(imgSrc)) - throw new javax.imageio.IIOException("Unsupported format"); - - return decodeFromSource(arena, imgSrc, renderSize); + source = sourceFromBytes(arena, imageData, "Unsupported format"); + return decodeFromSource(arena, source.imgSrc(), renderSize); } catch (java.io.IOException e) { throw e; } catch (Throwable t) { throw new javax.imageio.IIOException("Native image decode failed", t); } finally { - release(imgSrc); - release(cfData); + release(source); } } } @@ -593,34 +387,19 @@ static BufferedImage decodeFromPath(String path) throws java.io.IOException { static BufferedImage decodeFromPath(String path, Dimension renderSize) throws java.io.IOException { if (!IS_MACOS) - throw new javax.imageio.IIOException("Apple ImageIO decoding is only available on macOS"); + throw new javax.imageio.IIOException(unavailableMessage("Apple ImageIO decoding is only available on macOS")); try (Arena arena = Arena.ofConfined()) { - MemorySegment cfString = MemorySegment.NULL; - MemorySegment cfUrl = MemorySegment.NULL; - MemorySegment imgSrc = MemorySegment.NULL; + Source source = null; try { - cfString = createCFString(arena, path); - if (MemorySegment.NULL.equals(cfString)) - throw new javax.imageio.IIOException("Failed to create CFString for path"); - cfUrl = createFileURL(cfString); - if (MemorySegment.NULL.equals(cfUrl)) - throw new javax.imageio.IIOException("Failed to create CFURL for path"); - - imgSrc = (MemorySegment) CGImageSourceCreateWithURL.invokeExact( - cfUrl, MemorySegment.NULL); - if (MemorySegment.NULL.equals(imgSrc)) - throw new javax.imageio.IIOException("Unsupported format: " + path); - - return decodeFromSource(arena, imgSrc, renderSize); + source = sourceFromPath(arena, path, "Unsupported format: "); + return decodeFromSource(arena, source.imgSrc(), renderSize); } catch (java.io.IOException e) { throw e; } catch (Throwable t) { throw new javax.imageio.IIOException("Native image decode failed for: " + path, t); } finally { - release(imgSrc); - release(cfUrl); - release(cfString); + release(source); } } } diff --git a/imageio-native-apple/src/main/java/module-info.java b/imageio-native-apple/src/main/java/module-info.java index 1658c1a..db45e56 100644 --- a/imageio-native-apple/src/main/java/module-info.java +++ b/imageio-native-apple/src/main/java/module-info.java @@ -1,5 +1,7 @@ module io.github.ghosthack.imageio.apple { requires transitive io.github.ghosthack.imageio.common; + requires io.github.ghosthack.panama.media.corefoundation; + requires io.github.ghosthack.panama.media.cgimage; exports io.github.ghosthack.imageio.apple; diff --git a/imageio-native-video-apple/pom.xml b/imageio-native-video-apple/pom.xml index 9becaf4..3d7dd24 100644 --- a/imageio-native-video-apple/pom.xml +++ b/imageio-native-video-apple/pom.xml @@ -27,8 +27,8 @@ io.github.ghosthack - imageio-native-apple - ${project.version} + panama-media-avfoundation + ${panama-media.version} io.github.ghosthack diff --git a/imageio-native-video-apple/src/main/java/io/github/ghosthack/imageio/video/apple/AppleVideoFrameExtractor.java b/imageio-native-video-apple/src/main/java/io/github/ghosthack/imageio/video/apple/AppleVideoFrameExtractor.java index 30dfd6f..1f8ecf2 100644 --- a/imageio-native-video-apple/src/main/java/io/github/ghosthack/imageio/video/apple/AppleVideoFrameExtractor.java +++ b/imageio-native-video-apple/src/main/java/io/github/ghosthack/imageio/video/apple/AppleVideoFrameExtractor.java @@ -1,27 +1,29 @@ package io.github.ghosthack.imageio.video.apple; -import io.github.ghosthack.imageio.apple.AppleCoreGraphicsHelper; import io.github.ghosthack.imageio.common.ImageReadParamSupport; import io.github.ghosthack.imageio.common.RoutingBackend; import io.github.ghosthack.imageio.video.VideoFrameExtractorProvider; import io.github.ghosthack.imageio.video.VideoInfo; +import io.github.ghosthack.panama.media.avfoundation.AVFoundation; +import io.github.ghosthack.panama.media.core.DecodedImage; +import io.github.ghosthack.panama.media.core.PixelFormat; +import io.github.ghosthack.panama.media.corefoundation.CoreFoundation; import java.awt.Dimension; import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; import java.io.IOException; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.nio.file.Path; import java.time.Duration; -import java.util.Locale; import java.util.Set; /** * macOS implementation of {@link VideoFrameExtractorProvider}. *

- * Uses AVAssetImageGenerator (via Objective-C runtime bridge from Panama FFM) - * to extract frames from video files. The returned CGImage is converted to - * BufferedImage using {@link AppleCoreGraphicsHelper}. + * Uses AVAssetImageGenerator through the {@code panama-media-avfoundation} + * binding to extract frames from video files. */ public class AppleVideoFrameExtractor implements VideoFrameExtractorProvider { @@ -40,286 +42,102 @@ public Set formats() { return Set.of("mp4", "mov", "m4v", "3gp"); } - private static final boolean IS_MACOS = System.getProperty("os.name", "") - .toLowerCase(Locale.ROOT).contains("mac"); + private static final boolean AVAILABLE = AVFoundation.isAvailable(); - // ── CMTime struct layout (24 bytes) ───────────────────────────────── - // int64_t value; // 8 bytes - // int32_t timescale; // 4 bytes - // uint32_t flags; // 4 bytes - // int64_t epoch; // 8 bytes + private static final boolean ON_MACOS = System.getProperty("os.name", "") + .toLowerCase(java.util.Locale.ROOT).contains("mac"); - private static final StructLayout CMTIME = MemoryLayout.structLayout( - ValueLayout.JAVA_LONG.withName("value"), - ValueLayout.JAVA_INT.withName("timescale"), - ValueLayout.JAVA_INT.withName("flags"), - ValueLayout.JAVA_LONG.withName("epoch") - ); - - // ── CGSize struct layout (16 bytes) ───────────────────────────────── - - private static final StructLayout CGSIZE = MemoryLayout.structLayout( - ValueLayout.JAVA_DOUBLE.withName("width"), - ValueLayout.JAVA_DOUBLE.withName("height") - ); - - // ── Native handles (lazy-initialised on first use) ────────────────── - - private static final Linker LINKER = Linker.nativeLinker(); - private static final SymbolLookup LOOKUP; - - // ObjC runtime - private static final MethodHandle objc_getClass; - private static final MethodHandle sel_registerName; - private static final MemorySegment objc_msgSend_addr; - - // CoreFoundation - private static final MethodHandle CFStringCreateWithCString; - private static final MethodHandle CFRelease; + private static UnsupportedOperationException unavailable() { + return new UnsupportedOperationException(ON_MACOS + ? "AVFoundation bindings failed to load; grant native access to " + + "io.github.ghosthack.panama.media.corefoundation and " + + "io.github.ghosthack.panama.media.avfoundation" + : "Requires macOS"); + } - // CoreMedia - private static final MethodHandle CMTimeMake; + // ── canDecode probe handles ───────────────────────────────────────── + // AVFoundation has no playability probe, so canDecode asks the asset + // directly through the Objective-C runtime calls of the corefoundation + // binding. - // Pre-built objc_msgSend variants for different signatures /** (id, SEL) → id */ private static final MethodHandle msgSend; /** (id, SEL, id) → id */ private static final MethodHandle msgSend_ptr; /** (id, SEL, id, id) → id */ private static final MethodHandle msgSend_ptr_ptr; - /** (id, SEL, byte) → void */ - private static final MethodHandle msgSend_bool; /** (id, SEL) → BOOL */ private static final MethodHandle msgSend_ret_bool; - /** (id, SEL, CMTime) → void — set tolerance */ - private static final MethodHandle msgSend_cmtime_void; - /** (id, SEL, CGSize) → void — set maximum output size */ - private static final MethodHandle msgSend_cgsize_void; - /** (id, SEL, CMTime, ptr, ptr) → id — copyCGImageAtTime:actualTime:error: */ - private static final MethodHandle msgSend_cmtime_ptr_ptr; - /** (id, SEL) → CMTime — duration property */ - private static final MethodHandle msgSend_ret_cmtime; - /** (id, SEL) → CGSize — naturalSize property */ - private static final MethodHandle msgSend_ret_cgsize; - /** (id, SEL) → float — nominalFrameRate property */ - private static final MethodHandle msgSend_ret_float; - /** (id, SEL, id) → id — for tracksWithMediaType: */ - private static final MethodHandle msgSend_ptr_ret_ptr; static { - if (IS_MACOS) { - // Load frameworks - System.load("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"); - System.load("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"); - System.load("/System/Library/Frameworks/CoreMedia.framework/CoreMedia"); - System.load("/System/Library/Frameworks/AVFoundation.framework/AVFoundation"); - // libobjc is already loaded by the JVM on macOS, but load explicitly - System.load("/usr/lib/libobjc.A.dylib"); - - LOOKUP = SymbolLookup.loaderLookup(); - - // ObjC runtime - objc_getClass = downcall("objc_getClass", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - sel_registerName = downcall("sel_registerName", - FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - objc_msgSend_addr = LOOKUP.find("objc_msgSend") - .orElseThrow(() -> new UnsatisfiedLinkError("objc_msgSend not found")); - - // CoreFoundation - CFStringCreateWithCString = downcall("CFStringCreateWithCString", - FunctionDescriptor.of(ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); - CFRelease = downcall("CFRelease", - FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); - - // CoreMedia - CMTimeMake = downcall("CMTimeMake", - FunctionDescriptor.of(CMTIME, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT)); - - // ── objc_msgSend variants ─────────────────────────────────── - // (id, SEL) → id - msgSend = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.of(ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - // (id, SEL, id) → id - msgSend_ptr = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.of(ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - // (id, SEL, id, id) → id - msgSend_ptr_ptr = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.of(ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - // (id, SEL, byte) → void (for BOOL args like setAppliesPreferredTrackTransform:) - msgSend_bool = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.ofVoid( - ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_BYTE)); - - // (id, SEL) → BOOL (for properties like isPlayable) - msgSend_ret_bool = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.of( - ValueLayout.JAVA_BYTE, - ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - // (id, SEL, CMTime) → void (setRequestedTimeToleranceBefore:/After:) - msgSend_cmtime_void = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.ofVoid( - ValueLayout.ADDRESS, ValueLayout.ADDRESS, CMTIME)); - - // (id, SEL, CGSize) → void (setMaximumSize:) - msgSend_cgsize_void = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.ofVoid( - ValueLayout.ADDRESS, ValueLayout.ADDRESS, CGSIZE)); - - // (id, SEL, CMTime, ptr, ptr) → id (copyCGImageAtTime:actualTime:error:) - msgSend_cmtime_ptr_ptr = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.of(ValueLayout.ADDRESS, - ValueLayout.ADDRESS, ValueLayout.ADDRESS, - CMTIME, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - // (id, SEL) → CMTime (duration property) - msgSend_ret_cmtime = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.of(CMTIME, - ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - // (id, SEL) → CGSize (naturalSize property) - msgSend_ret_cgsize = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.of(CGSIZE, - ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - // (id, SEL) → float (nominalFrameRate property) - msgSend_ret_float = LINKER.downcallHandle(objc_msgSend_addr, - FunctionDescriptor.of(ValueLayout.JAVA_FLOAT, - ValueLayout.ADDRESS, ValueLayout.ADDRESS)); - - // (id, SEL, id) → id (same as msgSend_ptr, alias for clarity) - msgSend_ptr_ret_ptr = msgSend_ptr; - + if (AVAILABLE) { + msgSend = CoreFoundation.msgSend(FunctionDescriptor.of(ValueLayout.ADDRESS, + ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + msgSend_ptr = CoreFoundation.msgSend(FunctionDescriptor.of(ValueLayout.ADDRESS, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + msgSend_ptr_ptr = CoreFoundation.msgSend(FunctionDescriptor.of(ValueLayout.ADDRESS, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, + ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + msgSend_ret_bool = CoreFoundation.msgSend(FunctionDescriptor.of(ValueLayout.JAVA_BYTE, + ValueLayout.ADDRESS, ValueLayout.ADDRESS)); } else { - LOOKUP = null; - objc_getClass = null; - sel_registerName = null; - objc_msgSend_addr = null; - CFStringCreateWithCString = null; - CFRelease = null; - CMTimeMake = null; msgSend = null; msgSend_ptr = null; msgSend_ptr_ptr = null; - msgSend_bool = null; msgSend_ret_bool = null; - msgSend_cmtime_void = null; - msgSend_cgsize_void = null; - msgSend_cmtime_ptr_ptr = null; - msgSend_ret_cmtime = null; - msgSend_ret_cgsize = null; - msgSend_ret_float = null; - msgSend_ptr_ret_ptr = null; - } - } - - 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); - } - - // ── ObjC helpers ──────────────────────────────────────────────────── - - private static MemorySegment cls(Arena arena, String name) { - try { - return (MemorySegment) objc_getClass.invokeExact( - arena.allocateFrom(name)); - } catch (Throwable t) { - throw new RuntimeException("objc_getClass failed for " + name, t); - } - } - - private static MemorySegment sel(Arena arena, String name) { - try { - return (MemorySegment) sel_registerName.invokeExact( - arena.allocateFrom(name)); - } catch (Throwable t) { - throw new RuntimeException("sel_registerName failed for " + name, t); - } - } - - private static void release(MemorySegment ref) { - if (ref != null && !MemorySegment.NULL.equals(ref)) { - try { - CFRelease.invokeExact(ref); - } catch (Throwable ignored) {} } } - /** kCFStringEncodingUTF8 = 0x08000100 */ - private static final int kCFStringEncodingUTF8 = 0x08000100; - // ── VideoFrameExtractorProvider implementation ────────────────────── @Override public boolean isAvailable() { - return IS_MACOS; + return AVAILABLE; } @Override public boolean canDecode(Path videoFile) { - if (!IS_MACOS || videoFile == null) return false; + if (!AVAILABLE || videoFile == null) return false; try (Arena arena = Arena.ofConfined()) { String absPath = videoFile.toAbsolutePath().toString(); - MemorySegment cfPath = - (MemorySegment) CFStringCreateWithCString.invokeExact( - MemorySegment.NULL, - arena.allocateFrom(absPath), - kCFStringEncodingUTF8); + MemorySegment cfPath = CoreFoundation.cfStringCreate(arena, absPath); if (MemorySegment.NULL.equals(cfPath)) return false; try { MemorySegment url = (MemorySegment) msgSend_ptr.invokeExact( - cls(arena, "NSURL"), - sel(arena, "fileURLWithPath:"), + CoreFoundation.objcGetClass(arena, "NSURL"), + CoreFoundation.selRegisterName(arena, "fileURLWithPath:"), cfPath); if (MemorySegment.NULL.equals(url)) return false; MemorySegment asset = (MemorySegment) msgSend_ptr_ptr.invokeExact( - cls(arena, "AVURLAsset"), - sel(arena, "URLAssetWithURL:options:"), + CoreFoundation.objcGetClass(arena, "AVURLAsset"), + CoreFoundation.selRegisterName(arena, "URLAssetWithURL:options:"), url, MemorySegment.NULL); if (MemorySegment.NULL.equals(asset)) return false; byte playable = (byte) msgSend_ret_bool.invokeExact( - asset, sel(arena, "isPlayable")); + asset, CoreFoundation.selRegisterName(arena, "isPlayable")); if (playable == 0) return false; - MemorySegment mediaTypeSymbol = - LOOKUP.find("AVMediaTypeVideo").orElseThrow(); - MemorySegment mediaType = - mediaTypeSymbol - .reinterpret(ValueLayout.ADDRESS.byteSize()) - .get(ValueLayout.ADDRESS, 0); MemorySegment tracks = - (MemorySegment) msgSend_ptr_ret_ptr.invokeExact( + (MemorySegment) msgSend_ptr.invokeExact( asset, - sel(arena, "tracksWithMediaType:"), - mediaType); + CoreFoundation.selRegisterName(arena, "tracksWithMediaType:"), + CoreFoundation.loadConstPtr("AVMediaTypeVideo")); if (MemorySegment.NULL.equals(tracks)) return false; MemorySegment firstTrack = (MemorySegment) msgSend.invokeExact( - tracks, sel(arena, "firstObject")); + tracks, CoreFoundation.selRegisterName(arena, "firstObject")); return !MemorySegment.NULL.equals(firstTrack); } finally { - release(cfPath); + CoreFoundation.cfRelease(cfPath); } } catch (Throwable ignored) { return false; @@ -335,138 +153,18 @@ public BufferedImage extractFrame(Path videoFile, Duration time) throws IOExcept public BufferedImage extractFrame( Path videoFile, Duration time, Dimension renderSize) throws IOException { - if (!IS_MACOS) throw new UnsupportedOperationException("Requires macOS"); + if (!AVAILABLE) throw unavailable(); + // AVAssetImageGenerator caps the frame to an aspect-preserving box; + // a square box on the longer requested edge never undershoots the + // render size, and renderToSize makes the result exact. + int maxEdge = renderSize != null ? Math.max(renderSize.width, renderSize.height) : 0; try (Arena arena = Arena.ofConfined()) { - String absPath = videoFile.toAbsolutePath().toString(); - - // 1. Create CFString from file path - MemorySegment cfPath = (MemorySegment) CFStringCreateWithCString.invokeExact( - MemorySegment.NULL, - arena.allocateFrom(absPath), - kCFStringEncodingUTF8); - if (MemorySegment.NULL.equals(cfPath)) { - throw new IOException("CFStringCreateWithCString failed for: " + absPath); - } - - MemorySegment url = MemorySegment.NULL; - MemorySegment asset = MemorySegment.NULL; - MemorySegment generator = MemorySegment.NULL; - MemorySegment cgImage = MemorySegment.NULL; - try { - // 2. NSURL fileURLWithPath: - MemorySegment nsurlClass = cls(arena, "NSURL"); - MemorySegment selFileURL = sel(arena, "fileURLWithPath:"); - url = (MemorySegment) msgSend_ptr.invokeExact(nsurlClass, selFileURL, cfPath); - if (MemorySegment.NULL.equals(url)) { - throw new IOException("NSURL fileURLWithPath: returned nil"); - } - - // 3. AVURLAsset URLAssetWithURL:options: - MemorySegment avAssetClass = cls(arena, "AVURLAsset"); - MemorySegment selURLAsset = sel(arena, "URLAssetWithURL:options:"); - asset = (MemorySegment) msgSend_ptr_ptr.invokeExact( - avAssetClass, selURLAsset, url, MemorySegment.NULL); - if (MemorySegment.NULL.equals(asset)) { - throw new IOException("AVURLAsset creation failed for: " + absPath); - } - - // 4. AVAssetImageGenerator assetImageGeneratorWithAsset: - MemorySegment genClass = cls(arena, "AVAssetImageGenerator"); - MemorySegment selGenWithAsset = sel(arena, "assetImageGeneratorWithAsset:"); - generator = (MemorySegment) msgSend_ptr.invokeExact( - genClass, selGenWithAsset, asset); - if (MemorySegment.NULL.equals(generator)) { - throw new IOException("AVAssetImageGenerator creation failed"); - } - - // 5. setAppliesPreferredTrackTransform: YES - MemorySegment selSetTransform = sel(arena, "setAppliesPreferredTrackTransform:"); - msgSend_bool.invokeExact(generator, selSetTransform, (byte) 1); - - if (renderSize != null) { - MemorySegment maximumSize = arena.allocate(CGSIZE); - maximumSize.set(ValueLayout.JAVA_DOUBLE, 0, renderSize.width); - maximumSize.set( - ValueLayout.JAVA_DOUBLE, - ValueLayout.JAVA_DOUBLE.byteSize(), - renderSize.height); - MemorySegment selSetMaximumSize = - sel(arena, "setMaximumSize:"); - msgSend_cgsize_void.invokeExact( - generator, selSetMaximumSize, maximumSize); - } - - // 6. Set tolerances for frame seek. - // Using exact tolerance (zero) can fail for videos that don't have - // a decodable frame at exactly the requested time (common at t=0 - // with B-frame / long-GOP content). - // Start with a 1-second tolerance window so AVFoundation can pick - // the nearest keyframe. - MemorySegment selSetBefore = sel(arena, "setRequestedTimeToleranceBefore:"); - MemorySegment selSetAfter = sel(arena, "setRequestedTimeToleranceAfter:"); - - MemorySegment toleranceBefore = (MemorySegment) CMTimeMake.invokeExact( - (SegmentAllocator) arena, 1L, 1); // 1 second - MemorySegment toleranceAfter = (MemorySegment) CMTimeMake.invokeExact( - (SegmentAllocator) arena, 1L, 1); // 1 second - msgSend_cmtime_void.invokeExact(generator, selSetBefore, toleranceBefore); - msgSend_cmtime_void.invokeExact(generator, selSetAfter, toleranceAfter); - - // 7. Create CMTime from Duration - long millis = time.toMillis(); - MemorySegment cmTime = (MemorySegment) CMTimeMake.invokeExact( - (SegmentAllocator) arena, millis, 1000); - - // 8. copyCGImageAtTime:actualTime:error: - MemorySegment selCopy = sel(arena, "copyCGImageAtTime:actualTime:error:"); - MemorySegment errorPtr = arena.allocate(ValueLayout.ADDRESS); - errorPtr.set(ValueLayout.ADDRESS, 0, MemorySegment.NULL); - - cgImage = (MemorySegment) msgSend_cmtime_ptr_ptr.invokeExact( - generator, selCopy, - cmTime, - MemorySegment.NULL, // actualTime (don't need it) - errorPtr); - - // 8b. If NULL, retry with default (unbounded) tolerances. - // This handles edge cases where no keyframe exists within - // the initial 1-second window (e.g. very long GOPs). - if (MemorySegment.NULL.equals(cgImage)) { - MemorySegment largeTolerance = (MemorySegment) CMTimeMake.invokeExact( - (SegmentAllocator) arena, 3600L, 1); // 1 hour - msgSend_cmtime_void.invokeExact(generator, selSetBefore, largeTolerance); - msgSend_cmtime_void.invokeExact(generator, selSetAfter, largeTolerance); - - errorPtr.set(ValueLayout.ADDRESS, 0, MemorySegment.NULL); - cgImage = (MemorySegment) msgSend_cmtime_ptr_ptr.invokeExact( - generator, selCopy, - cmTime, - MemorySegment.NULL, - errorPtr); - } - - if (MemorySegment.NULL.equals(cgImage)) { - throw new IOException("copyCGImageAtTime returned NULL for time " + time); - } - - // 9. Convert CGImage to BufferedImage - BufferedImage decoded = - AppleCoreGraphicsHelper.cgImageToBufferedImage( - cgImage, arena); - return ImageReadParamSupport.renderToSize( - decoded, renderSize); - - } finally { - // cgImage is a CF object returned by "copy" — we own it - release(cgImage); - // url, asset, generator are autoreleased ObjC objects — don't CFRelease them - release(cfPath); - } - } catch (IOException e) { - throw e; - } catch (Throwable t) { - throw new IOException("Video frame extraction failed", t); + DecodedImage frame = AVFoundation.extractFrame( + arena, videoFile.toAbsolutePath().toString(), time.toMillis(), maxEdge); + return ImageReadParamSupport.renderToSize(toBufferedImage(frame), renderSize); + } catch (RuntimeException e) { + throw new IOException("Video frame extraction failed for time " + time, e); } } @@ -477,103 +175,30 @@ public boolean supportsRenderSizeReduction() { @Override public VideoInfo getInfo(Path videoFile) throws IOException { - if (!IS_MACOS) throw new UnsupportedOperationException("Requires macOS"); + if (!AVAILABLE) throw unavailable(); try (Arena arena = Arena.ofConfined()) { - String absPath = videoFile.toAbsolutePath().toString(); - - // 1. Create CFString from file path - MemorySegment cfPath = (MemorySegment) CFStringCreateWithCString.invokeExact( - MemorySegment.NULL, - arena.allocateFrom(absPath), - kCFStringEncodingUTF8); - if (MemorySegment.NULL.equals(cfPath)) { - throw new IOException("CFStringCreateWithCString failed for: " + absPath); - } - - try { - // 2. NSURL fileURLWithPath: - MemorySegment nsurlClass = cls(arena, "NSURL"); - MemorySegment selFileURL = sel(arena, "fileURLWithPath:"); - MemorySegment url = (MemorySegment) msgSend_ptr.invokeExact( - nsurlClass, selFileURL, cfPath); - if (MemorySegment.NULL.equals(url)) { - throw new IOException("NSURL fileURLWithPath: returned nil"); - } - - // 3. AVURLAsset URLAssetWithURL:options: - MemorySegment avAssetClass = cls(arena, "AVURLAsset"); - MemorySegment selURLAsset = sel(arena, "URLAssetWithURL:options:"); - MemorySegment asset = (MemorySegment) msgSend_ptr_ptr.invokeExact( - avAssetClass, selURLAsset, url, MemorySegment.NULL); - if (MemorySegment.NULL.equals(asset)) { - throw new IOException("AVURLAsset creation failed for: " + absPath); - } - - // 4. Get duration: [asset duration] → CMTime struct - MemorySegment selDuration = sel(arena, "duration"); - MemorySegment durationTime = (MemorySegment) msgSend_ret_cmtime.invokeExact( - (SegmentAllocator) arena, asset, selDuration); - long durationValue = durationTime.get(ValueLayout.JAVA_LONG, 0); - int durationTimescale = durationTime.get(ValueLayout.JAVA_INT, 8); - Duration duration; - if (durationTimescale <= 0) { - duration = Duration.ZERO; - } else { - // Convert CMTime to milliseconds without overflowing: - // split into whole-seconds and remainder to keep intermediate - // values within long range. - long seconds = durationValue / durationTimescale; - long remainder = durationValue % durationTimescale; - long totalMillis = seconds * 1000L + (remainder * 1000L) / durationTimescale; - duration = Duration.ofMillis(totalMillis); - } - - // 5. Get video tracks: [asset tracksWithMediaType:AVMediaTypeVideo] - // AVMediaTypeVideo is an NSString constant; we need to look it up - MemorySegment avMediaTypeVideo = LOOKUP.find("AVMediaTypeVideo") - .orElseThrow(() -> new UnsatisfiedLinkError("AVMediaTypeVideo not found")); - MemorySegment mediaTypeStr = avMediaTypeVideo.reinterpret(ValueLayout.ADDRESS.byteSize()) - .get(ValueLayout.ADDRESS, 0); - - MemorySegment selTracks = sel(arena, "tracksWithMediaType:"); - MemorySegment tracks = (MemorySegment) msgSend_ptr_ret_ptr.invokeExact( - asset, selTracks, mediaTypeStr); - - int width = 0; - int height = 0; - double frameRate = 0.0; - - if (!MemorySegment.NULL.equals(tracks)) { - // 6. Get first track: [tracks firstObject] - MemorySegment selFirstObject = sel(arena, "firstObject"); - MemorySegment track = (MemorySegment) msgSend.invokeExact(tracks, selFirstObject); - - if (!MemorySegment.NULL.equals(track)) { - // 7. Get naturalSize: [track naturalSize] → CGSize struct - MemorySegment selNaturalSize = sel(arena, "naturalSize"); - MemorySegment size = (MemorySegment) msgSend_ret_cgsize.invokeExact( - (SegmentAllocator) arena, track, selNaturalSize); - double w = size.get(ValueLayout.JAVA_DOUBLE, 0); - double h = size.get(ValueLayout.JAVA_DOUBLE, 8); - width = (int) Math.round(w); - height = (int) Math.round(h); - - // 8. Get nominalFrameRate: [track nominalFrameRate] → float - MemorySegment selFrameRate = sel(arena, "nominalFrameRate"); - frameRate = (float) msgSend_ret_float.invokeExact(track, selFrameRate); - } - } - - return new VideoInfo(width, height, duration, null, frameRate); + var info = AVFoundation.getVideoInfo(arena, videoFile.toAbsolutePath().toString()); + return new VideoInfo(info.width(), info.height(), + Duration.ofMillis(info.durationMillis()), null, info.frameRate()); + } catch (RuntimeException e) { + throw new IOException("Video info extraction failed", e); + } + } - } finally { - release(cfPath); - } - } catch (IOException e) { - throw e; - } catch (Throwable t) { - throw new IOException("Video info extraction failed", t); + /** + * Copies BGRA premultiplied pixels into a {@code TYPE_INT_ARGB_PRE} + * image (the same int layout when read little-endian). + */ + private static BufferedImage toBufferedImage(DecodedImage frame) { + int w = frame.width(); + int h = frame.height(); + BufferedImage result = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); + int[] dest = ((DataBufferInt) result.getRaster().getDataBuffer()).getData(); + for (int y = 0; y < h; y++) { + MemorySegment.copy(frame.pixels(), ValueLayout.JAVA_INT_UNALIGNED, + (long) y * frame.stride(), dest, y * w, w); } + return result; } } diff --git a/imageio-native-video-apple/src/main/java/module-info.java b/imageio-native-video-apple/src/main/java/module-info.java index d937465..5cf8adc 100644 --- a/imageio-native-video-apple/src/main/java/module-info.java +++ b/imageio-native-video-apple/src/main/java/module-info.java @@ -1,6 +1,8 @@ module io.github.ghosthack.imageio.video.apple { requires transitive io.github.ghosthack.imageio.video.common; - requires io.github.ghosthack.imageio.apple; + requires io.github.ghosthack.panama.media.core; + requires io.github.ghosthack.panama.media.corefoundation; + requires io.github.ghosthack.panama.media.avfoundation; exports io.github.ghosthack.imageio.video.apple; diff --git a/pom.xml b/pom.xml index 257fc2f..d909eab 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ UTF-8 26 6.1.2 - 0.1.1 + 0.1.4 8.1.2-0.3.2