From 5a9842cdebe9decf2a5744148f67f4c8c184f438 Mon Sep 17 00:00:00 2001 From: Koji Wakamiya Date: Mon, 8 Dec 2025 18:20:56 +0900 Subject: [PATCH] fix: Fix memory leak and add image quality option --- ffigen.dart | 19 ++- lib/gen/darwin_bindings.dart | 208 +++++++++++++++++++++------- lib/src/image_converter_darwin.dart | 62 +++++++-- 3 files changed, 232 insertions(+), 57 deletions(-) diff --git a/ffigen.dart b/ffigen.dart index 626a98a..6d4ae97 100644 --- a/ffigen.dart +++ b/ffigen.dart @@ -17,6 +17,8 @@ final config = FfiGenerator( 'CFDataCreateMutable', 'CFDataGetBytePtr', 'CFDataGetLength', + // CFDictionary operations + 'CFDictionaryCreate', // CGImageSource operations (decoding) 'CGImageSourceCreateWithData', 'CGImageSourceCreateImageAtIndex', @@ -24,8 +26,23 @@ final config = FfiGenerator( 'CGImageDestinationCreateWithData', 'CGImageDestinationAddImage', 'CGImageDestinationFinalize', + // Memory management + 'CFRelease', + }), + globals: Globals.includeSet({ + 'kCFAllocatorDefault', + 'kCGImageDestinationLossyCompressionQuality', + 'kCFTypeDictionaryValueCallBacks', + 'kCFTypeDictionaryKeyCallBacks', + }), + typedefs: Typedefs.includeSet({ + 'CFDataRef', + 'CFDictionaryRef', + 'CGImageRef', + 'CGImageSourceRef', + 'CFMutableDataRef', + 'CGImageDestinationRef', }), - globals: Globals.includeSet({'kCFAllocatorDefault'}), ); void main() => config.generate(); diff --git a/lib/gen/darwin_bindings.dart b/lib/gen/darwin_bindings.dart index 2866e3c..5ca1664 100644 --- a/lib/gen/darwin_bindings.dart +++ b/lib/gen/darwin_bindings.dart @@ -8,98 +8,210 @@ import 'package:objective_c/objective_c.dart' as objc; @ffi.Native>() external final ffi.Pointer<__CFAllocator> kCFAllocatorDefault; +@ffi.Native)>() +external void CFRelease(ffi.Pointer cf); + +@ffi.Native() +external final CFDictionaryKeyCallBacks kCFTypeDictionaryKeyCallBacks; + +@ffi.Native() +external final CFDictionaryValueCallBacks kCFTypeDictionaryValueCallBacks; + @ffi.Native< - ffi.Pointer<__CFData> Function( + CFDictionaryRef Function( + ffi.Pointer<__CFAllocator>, + ffi.Pointer>, + ffi.Pointer>, + ffi.Long, + ffi.Pointer, + ffi.Pointer, + ) +>() +external CFDictionaryRef CFDictionaryCreate( + ffi.Pointer<__CFAllocator> allocator, + ffi.Pointer> keys, + ffi.Pointer> values, + int numValues, + ffi.Pointer keyCallBacks, + ffi.Pointer valueCallBacks, +); + +@ffi.Native< + CFDataRef Function( ffi.Pointer<__CFAllocator>, ffi.Pointer, ffi.Long, ) >() -external ffi.Pointer<__CFData> CFDataCreate( +external CFDataRef CFDataCreate( ffi.Pointer<__CFAllocator> allocator, ffi.Pointer bytes, int length, ); -@ffi.Native< - ffi.Pointer<__CFData> Function(ffi.Pointer<__CFAllocator>, ffi.Long) ->() -external ffi.Pointer<__CFData> CFDataCreateMutable( +@ffi.Native, ffi.Long)>() +external CFMutableDataRef CFDataCreateMutable( ffi.Pointer<__CFAllocator> allocator, int capacity, ); -@ffi.Native)>() -external int CFDataGetLength(ffi.Pointer<__CFData> theData); +@ffi.Native() +external int CFDataGetLength(CFDataRef theData); -@ffi.Native Function(ffi.Pointer<__CFData>)>() -external ffi.Pointer CFDataGetBytePtr( - ffi.Pointer<__CFData> theData, +@ffi.Native Function(CFDataRef)>() +external ffi.Pointer CFDataGetBytePtr(CFDataRef theData); + +@ffi.Native() +external CGImageSourceRef CGImageSourceCreateWithData( + CFDataRef data, + CFDictionaryRef options, ); -@ffi.Native< - ffi.Pointer Function( - ffi.Pointer<__CFData>, - ffi.Pointer<__CFDictionary>, - ) ->() -external ffi.Pointer CGImageSourceCreateWithData( - ffi.Pointer<__CFData> data, - ffi.Pointer<__CFDictionary> options, -); - -@ffi.Native< - ffi.Pointer Function( - ffi.Pointer, - ffi.Size, - ffi.Pointer<__CFDictionary>, - ) ->() -external ffi.Pointer CGImageSourceCreateImageAtIndex( - ffi.Pointer isrc, +@ffi.Native() +external CGImageRef CGImageSourceCreateImageAtIndex( + CGImageSourceRef isrc, int index, - ffi.Pointer<__CFDictionary> options, + CFDictionaryRef options, ); +@ffi.Native>() +external ffi.Pointer kCGImageDestinationLossyCompressionQuality; + @ffi.Native< - ffi.Pointer Function( - ffi.Pointer<__CFData>, + CGImageDestinationRef Function( + CFMutableDataRef, ffi.Pointer, ffi.Size, - ffi.Pointer<__CFDictionary>, + CFDictionaryRef, ) >() -external ffi.Pointer CGImageDestinationCreateWithData( - ffi.Pointer<__CFData> data, +external CGImageDestinationRef CGImageDestinationCreateWithData( + CFMutableDataRef data, ffi.Pointer type, int count, - ffi.Pointer<__CFDictionary> options, + CFDictionaryRef options, ); @ffi.Native< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Pointer<__CFDictionary>, - ) + ffi.Void Function(CGImageDestinationRef, CGImageRef, CFDictionaryRef) >() external void CGImageDestinationAddImage( - ffi.Pointer idst, - ffi.Pointer image, - ffi.Pointer<__CFDictionary> properties, + CGImageDestinationRef idst, + CGImageRef image, + CFDictionaryRef properties, ); -@ffi.Native)>() -external bool CGImageDestinationFinalize(ffi.Pointer idst); +@ffi.Native() +external bool CGImageDestinationFinalize(CGImageDestinationRef idst); final class __CFAllocator extends ffi.Opaque {} +final class CFDictionaryKeyCallBacks extends ffi.Struct { + @ffi.Long() + external int version; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer<__CFAllocator> allocator, + ffi.Pointer value, + ) + > + > + retain; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer<__CFAllocator> allocator, + ffi.Pointer value, + ) + > + > + release; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer value) + > + > + copyDescription; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.UnsignedChar Function( + ffi.Pointer value1, + ffi.Pointer value2, + ) + > + > + equal; + + external ffi.Pointer< + ffi.NativeFunction value)> + > + hash; +} + +final class CFDictionaryValueCallBacks extends ffi.Struct { + @ffi.Long() + external int version; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer<__CFAllocator> allocator, + ffi.Pointer value, + ) + > + > + retain; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer<__CFAllocator> allocator, + ffi.Pointer value, + ) + > + > + release; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer value) + > + > + copyDescription; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.UnsignedChar Function( + ffi.Pointer value1, + ffi.Pointer value2, + ) + > + > + equal; +} + final class __CFDictionary extends ffi.Opaque {} +typedef CFDictionaryRef = ffi.Pointer<__CFDictionary>; + final class __CFData extends ffi.Opaque {} +typedef CFDataRef = ffi.Pointer<__CFData>; +typedef CFMutableDataRef = ffi.Pointer<__CFData>; + final class CGImageSource extends ffi.Opaque {} +typedef CGImageSourceRef = ffi.Pointer; + final class CGImage extends ffi.Opaque {} +typedef CGImageRef = ffi.Pointer; + final class CGImageDestination extends ffi.Opaque {} + +typedef CGImageDestinationRef = ffi.Pointer; diff --git a/lib/src/image_converter_darwin.dart b/lib/src/image_converter_darwin.dart index 47484e4..8bac51e 100644 --- a/lib/src/image_converter_darwin.dart +++ b/lib/src/image_converter_darwin.dart @@ -35,11 +35,18 @@ final class ImageConverterDarwin implements ImageConverterPlatform { OutputFormat format = OutputFormat.jpeg, int quality = 100, }) async { - final inputPtr = calloc(inputData.length); + Pointer? inputPtr; + CFDataRef? cfData; + CGImageSourceRef? imageSource; + CGImageRef? cgImage; + CFMutableDataRef? outputData; + CGImageDestinationRef? destination; + CFDictionaryRef? properties; try { + inputPtr = calloc(inputData.length); inputPtr.asTypedList(inputData.length).setAll(0, inputData); - final cfData = CFDataCreate( + cfData = CFDataCreate( kCFAllocatorDefault, inputPtr.cast(), inputData.length, @@ -48,17 +55,17 @@ final class ImageConverterDarwin implements ImageConverterPlatform { throw Exception('Failed to create CFData from input data.'); } - final imageSource = CGImageSourceCreateWithData(cfData, nullptr); + imageSource = CGImageSourceCreateWithData(cfData, nullptr); if (imageSource == nullptr) { throw Exception('Failed to create CGImageSource. Invalid image data.'); } - final cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr); + cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr); if (cgImage == nullptr) { throw Exception('Failed to decode image.'); } - final outputData = CFDataCreateMutable(kCFAllocatorDefault, 0); + outputData = CFDataCreateMutable(kCFAllocatorDefault, 0); if (outputData == nullptr) { throw Exception('Failed to create output CFData.'); } @@ -81,7 +88,7 @@ final class ImageConverterDarwin implements ImageConverterPlatform { .retainAndAutorelease() .cast(); - final destination = CGImageDestinationCreateWithData( + destination = CGImageDestinationCreateWithData( outputData, cfString, 1, @@ -91,7 +98,8 @@ final class ImageConverterDarwin implements ImageConverterPlatform { throw Exception('Failed to create CGImageDestination.'); } - CGImageDestinationAddImage(destination, cgImage, nullptr); + properties = _createPropertiesForFormat(format, quality); + CGImageDestinationAddImage(destination, cgImage, properties ?? nullptr); final success = CGImageDestinationFinalize(destination); if (!success) { @@ -106,7 +114,45 @@ final class ImageConverterDarwin implements ImageConverterPlatform { return Uint8List.fromList(bytePtr.cast().asTypedList(length)); } finally { - calloc.free(inputPtr); + if (inputPtr != null) calloc.free(inputPtr); + if (cfData != null) CFRelease(cfData.cast()); + if (imageSource != null) CFRelease(imageSource.cast()); + if (cgImage != null) CFRelease(cgImage.cast()); + if (outputData != null) CFRelease(outputData.cast()); + if (destination != null) CFRelease(destination.cast()); + if (properties != null) CFRelease(properties.cast()); } } + + CFDictionaryRef? _createPropertiesForFormat( + OutputFormat format, + int quality, + ) { + if (format == OutputFormat.png || format == OutputFormat.webp) { + return null; + } + + return using((arena) { + final keys = arena>(1); + final values = arena>(1); + + keys[0] = kCGImageDestinationLossyCompressionQuality; + values[0] = (quality / 100.0) + .toNSNumber() + .ref + .retainAndAutorelease() + .cast(); + + final keyCallBacks = arena(); + final valueCallBacks = arena(); + return CFDictionaryCreate( + kCFAllocatorDefault, + keys.cast(), + values.cast(), + 1, + keyCallBacks, + valueCallBacks, + ); + }); + } }