feat: Add ImageConverter.convert
This commit is contained in:
parent
7289e34d1a
commit
8f102fd24e
|
|
@ -1,9 +0,0 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// The Android Gradle Plugin builds the native code with the Android NDK.
|
||||
|
||||
group = "dr1009.com.image_ffi"
|
||||
version = "1.0"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The Android Gradle Plugin knows how to build native code with the NDK.
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
|
||||
android {
|
||||
namespace = "dr1009.com.image_ffi"
|
||||
|
||||
// Bumping the plugin compileSdk version requires all clients of this plugin
|
||||
// to bump the version in their app.
|
||||
compileSdk = 36
|
||||
|
||||
// Use the NDK version
|
||||
// declared in /android/app/build.gradle file of the Flutter project.
|
||||
// Replace it with a version number if this plugin requires a specific NDK version.
|
||||
// (e.g. ndkVersion "23.1.7779620")
|
||||
ndkVersion = android.ndkVersion
|
||||
|
||||
// Invoke the shared CMake build with the Android Gradle Plugin.
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = "../src/CMakeLists.txt"
|
||||
|
||||
// The default CMake version for the Android Gradle Plugin is 3.10.2.
|
||||
// https://developer.android.com/studio/projects/install-ndk#vanilla_cmake
|
||||
//
|
||||
// The Flutter tooling requires that developers have CMake 3.10 or later
|
||||
// installed. You should not increase this version, as doing so will cause
|
||||
// the plugin to fail to compile for some customers of the plugin.
|
||||
// version "3.10.2"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
rootProject.name = 'image_ffi'
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="dr1009.com.image_ffi">
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Regenerate bindings with `dart run ffigen.dart`.
|
||||
import 'package:ffigen/ffigen.dart';
|
||||
|
||||
final config = FfiGenerator(
|
||||
headers: Headers(
|
||||
entryPoints: [
|
||||
Uri.file(
|
||||
'$macSdkPath/System/Library/Frameworks/ImageIO.framework/Headers/ImageIO.h',
|
||||
),
|
||||
],
|
||||
),
|
||||
objectiveC: ObjectiveC(interfaces: Interfaces.includeSet({'ImageIO'})),
|
||||
output: Output(dartFile: Uri.file('lib/gen/darwin_bindings.dart')),
|
||||
functions: Functions.includeSet({
|
||||
// CFData operations
|
||||
'CFDataCreate',
|
||||
'CFDataCreateMutable',
|
||||
'CFDataGetBytePtr',
|
||||
'CFDataGetLength',
|
||||
// CGImageSource operations (decoding)
|
||||
'CGImageSourceCreateWithData',
|
||||
'CGImageSourceCreateImageAtIndex',
|
||||
// CGImageDestination operations (encoding)
|
||||
'CGImageDestinationCreateWithData',
|
||||
'CGImageDestinationAddImage',
|
||||
'CGImageDestinationFinalize',
|
||||
}),
|
||||
globals: Globals.includeSet({'kCFAllocatorDefault'}),
|
||||
);
|
||||
|
||||
void main() => config.generate();
|
||||
19
ffigen.yaml
19
ffigen.yaml
|
|
@ -1,19 +0,0 @@
|
|||
# Run with `dart run ffigen --config ffigen.yaml`.
|
||||
name: ImageFfiBindings
|
||||
description: |
|
||||
Bindings for `src/image_ffi.h`.
|
||||
|
||||
Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
|
||||
output: 'lib/image_ffi_bindings_generated.dart'
|
||||
headers:
|
||||
entry-points:
|
||||
- 'src/image_ffi.h'
|
||||
include-directives:
|
||||
- 'src/image_ffi.h'
|
||||
preamble: |
|
||||
// ignore_for_file: always_specify_types
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
comments:
|
||||
style: any
|
||||
length: full
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
// Relative import to be able to reuse the C sources.
|
||||
// See the comment in ../image_ffi.podspec for more information.
|
||||
#include "../../src/image_ffi.c"
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||
# Run `pod lib lint image_ffi.podspec` to validate before publishing.
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'image_ffi'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'A new Flutter FFI plugin project.'
|
||||
s.description = <<-DESC
|
||||
A new Flutter FFI plugin project.
|
||||
DESC
|
||||
s.homepage = 'http://example.com'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Your Company' => 'email@example.com' }
|
||||
|
||||
# This will ensure the source files in Classes/ are included in the native
|
||||
# builds of apps using this FFI plugin. Podspec does not support relative
|
||||
# paths, so Classes contains a forwarder C file that relatively imports
|
||||
# `../src/*` so that the C sources can be shared among all target platforms.
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'Flutter'
|
||||
s.platform = :ios, '13.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Regenerate bindings with `dart run jnigen --config jnigen.yaml`.
|
||||
|
||||
android_sdk_config:
|
||||
add_gradle_deps: true
|
||||
android_example: 'example/'
|
||||
|
||||
output:
|
||||
dart:
|
||||
path: lib/gen/jnigen_bindings.dart
|
||||
structure: single_file
|
||||
|
||||
source_path:
|
||||
- 'java/'
|
||||
classes:
|
||||
- 'java.io.ByteArrayOutputStream'
|
||||
- 'android/graphics/BitmapFactory'
|
||||
- 'android/graphics/Bitmap'
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// AUTO GENERATED FILE, DO NOT EDIT.
|
||||
//
|
||||
// Generated by `package:ffigen`.
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
import 'dart:ffi' as ffi;
|
||||
import 'package:objective_c/objective_c.dart' as objc;
|
||||
|
||||
@ffi.Native<ffi.Pointer<__CFAllocator>>()
|
||||
external final ffi.Pointer<__CFAllocator> kCFAllocatorDefault;
|
||||
|
||||
@ffi.Native<
|
||||
ffi.Pointer<__CFData> Function(
|
||||
ffi.Pointer<__CFAllocator>,
|
||||
ffi.Pointer<ffi.UnsignedChar>,
|
||||
ffi.Long,
|
||||
)
|
||||
>()
|
||||
external ffi.Pointer<__CFData> CFDataCreate(
|
||||
ffi.Pointer<__CFAllocator> allocator,
|
||||
ffi.Pointer<ffi.UnsignedChar> bytes,
|
||||
int length,
|
||||
);
|
||||
|
||||
@ffi.Native<
|
||||
ffi.Pointer<__CFData> Function(ffi.Pointer<__CFAllocator>, ffi.Long)
|
||||
>()
|
||||
external ffi.Pointer<__CFData> CFDataCreateMutable(
|
||||
ffi.Pointer<__CFAllocator> allocator,
|
||||
int capacity,
|
||||
);
|
||||
|
||||
@ffi.Native<ffi.Long Function(ffi.Pointer<__CFData>)>()
|
||||
external int CFDataGetLength(ffi.Pointer<__CFData> theData);
|
||||
|
||||
@ffi.Native<ffi.Pointer<ffi.UnsignedChar> Function(ffi.Pointer<__CFData>)>()
|
||||
external ffi.Pointer<ffi.UnsignedChar> CFDataGetBytePtr(
|
||||
ffi.Pointer<__CFData> theData,
|
||||
);
|
||||
|
||||
@ffi.Native<
|
||||
ffi.Pointer<CGImageSource> Function(
|
||||
ffi.Pointer<__CFData>,
|
||||
ffi.Pointer<__CFDictionary>,
|
||||
)
|
||||
>()
|
||||
external ffi.Pointer<CGImageSource> CGImageSourceCreateWithData(
|
||||
ffi.Pointer<__CFData> data,
|
||||
ffi.Pointer<__CFDictionary> options,
|
||||
);
|
||||
|
||||
@ffi.Native<
|
||||
ffi.Pointer<CGImage> Function(
|
||||
ffi.Pointer<CGImageSource>,
|
||||
ffi.Size,
|
||||
ffi.Pointer<__CFDictionary>,
|
||||
)
|
||||
>()
|
||||
external ffi.Pointer<CGImage> CGImageSourceCreateImageAtIndex(
|
||||
ffi.Pointer<CGImageSource> isrc,
|
||||
int index,
|
||||
ffi.Pointer<__CFDictionary> options,
|
||||
);
|
||||
|
||||
@ffi.Native<
|
||||
ffi.Pointer<CGImageDestination> Function(
|
||||
ffi.Pointer<__CFData>,
|
||||
ffi.Pointer<objc.CFString>,
|
||||
ffi.Size,
|
||||
ffi.Pointer<__CFDictionary>,
|
||||
)
|
||||
>()
|
||||
external ffi.Pointer<CGImageDestination> CGImageDestinationCreateWithData(
|
||||
ffi.Pointer<__CFData> data,
|
||||
ffi.Pointer<objc.CFString> type,
|
||||
int count,
|
||||
ffi.Pointer<__CFDictionary> options,
|
||||
);
|
||||
|
||||
@ffi.Native<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<CGImageDestination>,
|
||||
ffi.Pointer<CGImage>,
|
||||
ffi.Pointer<__CFDictionary>,
|
||||
)
|
||||
>()
|
||||
external void CGImageDestinationAddImage(
|
||||
ffi.Pointer<CGImageDestination> idst,
|
||||
ffi.Pointer<CGImage> image,
|
||||
ffi.Pointer<__CFDictionary> properties,
|
||||
);
|
||||
|
||||
@ffi.Native<ffi.Bool Function(ffi.Pointer<CGImageDestination>)>()
|
||||
external bool CGImageDestinationFinalize(ffi.Pointer<CGImageDestination> idst);
|
||||
|
||||
final class __CFAllocator extends ffi.Opaque {}
|
||||
|
||||
final class __CFDictionary extends ffi.Opaque {}
|
||||
|
||||
final class __CFData extends ffi.Opaque {}
|
||||
|
||||
final class CGImageSource extends ffi.Opaque {}
|
||||
|
||||
final class CGImage extends ffi.Opaque {}
|
||||
|
||||
final class CGImageDestination extends ffi.Opaque {}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,131 +1,121 @@
|
|||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'image_ffi_bindings_generated.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:image_ffi/src/image_converter_android.dart';
|
||||
import 'package:image_ffi/src/image_converter_darwin.dart';
|
||||
import 'package:image_ffi/src/image_converter_platform_interface.dart';
|
||||
import 'package:image_ffi/src/output_format.dart';
|
||||
|
||||
/// A very short-lived native function.
|
||||
///
|
||||
/// For very short-lived functions, it is fine to call them on the main isolate.
|
||||
/// They will block the Dart execution while running the native function, so
|
||||
/// only do this for native functions which are guaranteed to be short-lived.
|
||||
int sum(int a, int b) => _bindings.sum(a, b);
|
||||
export 'src/output_format.dart';
|
||||
|
||||
/// A longer lived native function, which occupies the thread calling it.
|
||||
/// Main entry point for image format conversion.
|
||||
///
|
||||
/// Do not call these kind of native functions in the main isolate. They will
|
||||
/// block Dart execution. This will cause dropped frames in Flutter applications.
|
||||
/// Instead, call these native functions on a separate isolate.
|
||||
///
|
||||
/// Modify this to suit your own use case. Example use cases:
|
||||
///
|
||||
/// 1. Reuse a single isolate for various different kinds of requests.
|
||||
/// 2. Use multiple helper isolates for parallel execution.
|
||||
Future<int> sumAsync(int a, int b) async {
|
||||
final SendPort helperIsolateSendPort = await _helperIsolateSendPort;
|
||||
final int requestId = _nextSumRequestId++;
|
||||
final _SumRequest request = _SumRequest(requestId, a, b);
|
||||
final Completer<int> completer = Completer<int>();
|
||||
_sumRequests[requestId] = completer;
|
||||
helperIsolateSendPort.send(request);
|
||||
return completer.future;
|
||||
/// Provides a platform-agnostic interface to convert images across iOS,
|
||||
/// macOS, and Android platforms using native APIs.
|
||||
class ImageConverter {
|
||||
/// The platform-specific implementation of the image converter.
|
||||
///
|
||||
/// This is initialized based on the current platform.
|
||||
static ImageConverterPlatform get _platform =>
|
||||
_getPlatformForTarget(defaultTargetPlatform);
|
||||
|
||||
/// Converts an image to a target format.
|
||||
///
|
||||
/// By default, this operation is performed in a separate isolate to avoid
|
||||
/// blocking the UI thread. For very small images, the overhead of an isolate
|
||||
/// can be disabled by setting [runInIsolate] to `false`.
|
||||
///
|
||||
/// **Parameters:**
|
||||
/// - [inputData]: Raw bytes of the image to convert.
|
||||
/// - [format]: Target [OutputFormat]. Defaults to [OutputFormat.jpeg].
|
||||
/// - [quality]: Compression quality for lossy formats (1-100).
|
||||
/// - [runInIsolate]: Whether to run the conversion in a separate isolate.
|
||||
/// Defaults to `true`.
|
||||
///
|
||||
/// **Returns:** A [Future] that completes with the converted image data.
|
||||
///
|
||||
/// **Throws:**
|
||||
/// - [UnsupportedError]: If the platform or output format is not supported.
|
||||
/// - [Exception]: If the image decoding or encoding fails.
|
||||
///
|
||||
/// **Example - Convert HEIC to JPEG:**
|
||||
/// ```dart
|
||||
/// final jpegData = await ImageConverter.convert(
|
||||
/// inputData: heicImageData,
|
||||
/// format: OutputFormat.jpeg,
|
||||
/// quality: 90,
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// **Example - Running on the main thread:**
|
||||
/// ```dart
|
||||
/// // Only do this for very small images where isolate overhead is a concern.
|
||||
/// final pngData = await ImageConverter.convert(
|
||||
/// inputData: smallImageData,
|
||||
/// format: OutputFormat.png,
|
||||
/// runInIsolate: false,
|
||||
/// );
|
||||
/// ```
|
||||
static Future<Uint8List> convert({
|
||||
required Uint8List inputData,
|
||||
OutputFormat format = OutputFormat.jpeg,
|
||||
int quality = 100,
|
||||
bool runInIsolate = true,
|
||||
}) {
|
||||
if (runInIsolate) {
|
||||
return compute(
|
||||
_convertInIsolate,
|
||||
_ConvertRequest(inputData, format, quality, defaultTargetPlatform),
|
||||
);
|
||||
} else {
|
||||
// The original implementation for those who opt-out.
|
||||
return _platform.convert(
|
||||
inputData: inputData,
|
||||
format: format,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const String _libName = 'image_ffi';
|
||||
/// Helper class to pass arguments to the isolate.
|
||||
@immutable
|
||||
class _ConvertRequest {
|
||||
final Uint8List inputData;
|
||||
final OutputFormat format;
|
||||
final int quality;
|
||||
final TargetPlatform platform;
|
||||
|
||||
/// The dynamic library in which the symbols for [ImageFfiBindings] can be found.
|
||||
final DynamicLibrary _dylib = () {
|
||||
if (Platform.isMacOS || Platform.isIOS) {
|
||||
return DynamicLibrary.open('$_libName.framework/$_libName');
|
||||
}
|
||||
if (Platform.isAndroid || Platform.isLinux) {
|
||||
return DynamicLibrary.open('lib$_libName.so');
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
return DynamicLibrary.open('$_libName.dll');
|
||||
}
|
||||
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
|
||||
}();
|
||||
|
||||
/// The bindings to the native functions in [_dylib].
|
||||
final ImageFfiBindings _bindings = ImageFfiBindings(_dylib);
|
||||
|
||||
|
||||
/// A request to compute `sum`.
|
||||
///
|
||||
/// Typically sent from one isolate to another.
|
||||
class _SumRequest {
|
||||
final int id;
|
||||
final int a;
|
||||
final int b;
|
||||
|
||||
const _SumRequest(this.id, this.a, this.b);
|
||||
const _ConvertRequest(
|
||||
this.inputData,
|
||||
this.format,
|
||||
this.quality,
|
||||
this.platform,
|
||||
);
|
||||
}
|
||||
|
||||
/// A response with the result of `sum`.
|
||||
///
|
||||
/// Typically sent from one isolate to another.
|
||||
class _SumResponse {
|
||||
final int id;
|
||||
final int result;
|
||||
|
||||
const _SumResponse(this.id, this.result);
|
||||
/// Returns the platform-specific converter instance.
|
||||
ImageConverterPlatform _getPlatformForTarget(TargetPlatform platform) {
|
||||
if (kIsWeb) {
|
||||
throw UnsupportedError('Image conversion is not supported on the web.');
|
||||
}
|
||||
return switch (platform) {
|
||||
TargetPlatform.android => ImageConverterAndroid(),
|
||||
TargetPlatform.iOS || TargetPlatform.macOS => ImageConverterDarwin(),
|
||||
_ => throw UnsupportedError(
|
||||
'Image conversion is not supported on this platform: $platform',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Counter to identify [_SumRequest]s and [_SumResponse]s.
|
||||
int _nextSumRequestId = 0;
|
||||
|
||||
/// Mapping from [_SumRequest] `id`s to the completers corresponding to the correct future of the pending request.
|
||||
final Map<int, Completer<int>> _sumRequests = <int, Completer<int>>{};
|
||||
|
||||
/// The SendPort belonging to the helper isolate.
|
||||
Future<SendPort> _helperIsolateSendPort = () async {
|
||||
// The helper isolate is going to send us back a SendPort, which we want to
|
||||
// wait for.
|
||||
final Completer<SendPort> completer = Completer<SendPort>();
|
||||
|
||||
// Receive port on the main isolate to receive messages from the helper.
|
||||
// We receive two types of messages:
|
||||
// 1. A port to send messages on.
|
||||
// 2. Responses to requests we sent.
|
||||
final ReceivePort receivePort = ReceivePort()
|
||||
..listen((dynamic data) {
|
||||
if (data is SendPort) {
|
||||
// The helper isolate sent us the port on which we can sent it requests.
|
||||
completer.complete(data);
|
||||
return;
|
||||
}
|
||||
if (data is _SumResponse) {
|
||||
// The helper isolate sent us a response to a request we sent.
|
||||
final Completer<int> completer = _sumRequests[data.id]!;
|
||||
_sumRequests.remove(data.id);
|
||||
completer.complete(data.result);
|
||||
return;
|
||||
}
|
||||
throw UnsupportedError('Unsupported message type: ${data.runtimeType}');
|
||||
});
|
||||
|
||||
// Start the helper isolate.
|
||||
await Isolate.spawn((SendPort sendPort) async {
|
||||
final ReceivePort helperReceivePort = ReceivePort()
|
||||
..listen((dynamic data) {
|
||||
// On the helper isolate listen to requests and respond to them.
|
||||
if (data is _SumRequest) {
|
||||
final int result = _bindings.sum_long_running(data.a, data.b);
|
||||
final _SumResponse response = _SumResponse(data.id, result);
|
||||
sendPort.send(response);
|
||||
return;
|
||||
}
|
||||
throw UnsupportedError('Unsupported message type: ${data.runtimeType}');
|
||||
});
|
||||
|
||||
// Send the port to the main isolate on which we can receive requests.
|
||||
sendPort.send(helperReceivePort.sendPort);
|
||||
}, receivePort.sendPort);
|
||||
|
||||
// Wait until the helper isolate has sent us back the SendPort on which we
|
||||
// can start sending requests.
|
||||
return completer.future;
|
||||
}();
|
||||
/// Top-level function for `compute`.
|
||||
Future<Uint8List> _convertInIsolate(_ConvertRequest request) {
|
||||
final platform = _getPlatformForTarget(request.platform);
|
||||
return platform.convert(
|
||||
inputData: request.inputData,
|
||||
format: request.format,
|
||||
quality: request.quality,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
// ignore_for_file: always_specify_types
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
|
||||
// AUTO GENERATED FILE, DO NOT EDIT.
|
||||
//
|
||||
// Generated by `package:ffigen`.
|
||||
// ignore_for_file: type=lint
|
||||
import 'dart:ffi' as ffi;
|
||||
|
||||
/// Bindings for `src/image_ffi.h`.
|
||||
///
|
||||
/// Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
|
||||
///
|
||||
class ImageFfiBindings {
|
||||
/// Holds the symbol lookup function.
|
||||
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||
_lookup;
|
||||
|
||||
/// The symbols are looked up in [dynamicLibrary].
|
||||
ImageFfiBindings(ffi.DynamicLibrary dynamicLibrary)
|
||||
: _lookup = dynamicLibrary.lookup;
|
||||
|
||||
/// The symbols are looked up with [lookup].
|
||||
ImageFfiBindings.fromLookup(
|
||||
ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||
lookup)
|
||||
: _lookup = lookup;
|
||||
|
||||
/// A very short-lived native function.
|
||||
///
|
||||
/// For very short-lived functions, it is fine to call them on the main isolate.
|
||||
/// They will block the Dart execution while running the native function, so
|
||||
/// only do this for native functions which are guaranteed to be short-lived.
|
||||
int sum(
|
||||
int a,
|
||||
int b,
|
||||
) {
|
||||
return _sum(
|
||||
a,
|
||||
b,
|
||||
);
|
||||
}
|
||||
|
||||
late final _sumPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Int)>>('sum');
|
||||
late final _sum = _sumPtr.asFunction<int Function(int, int)>();
|
||||
|
||||
/// A longer lived native function, which occupies the thread calling it.
|
||||
///
|
||||
/// Do not call these kind of native functions in the main isolate. They will
|
||||
/// block Dart execution. This will cause dropped frames in Flutter applications.
|
||||
/// Instead, call these native functions on a separate isolate.
|
||||
int sum_long_running(
|
||||
int a,
|
||||
int b,
|
||||
) {
|
||||
return _sum_long_running(
|
||||
a,
|
||||
b,
|
||||
);
|
||||
}
|
||||
|
||||
late final _sum_long_runningPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Int)>>(
|
||||
'sum_long_running');
|
||||
late final _sum_long_running =
|
||||
_sum_long_runningPtr.asFunction<int Function(int, int)>();
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:image_ffi/gen/jnigen_bindings.dart';
|
||||
import 'package:image_ffi/src/image_converter_platform_interface.dart';
|
||||
import 'package:image_ffi/src/output_format.dart';
|
||||
import 'package:jni/jni.dart';
|
||||
|
||||
/// Android image converter using BitmapFactory and Bitmap compression.
|
||||
///
|
||||
/// Implements image conversion for Android 9+ (API level 28+) platforms using
|
||||
/// BitmapFactory for decoding and Bitmap.compress for encoding via JNI.\n///
|
||||
/// **Features:**
|
||||
/// - Supports JPEG, PNG, WebP, GIF, BMP input formats
|
||||
/// - Can read HEIC files (Android 9+)
|
||||
/// - Cannot write HEIC (throws UnsupportedError)
|
||||
/// - Efficient memory usage with ByteArrayOutputStream
|
||||
///
|
||||
/// **API Stack:**
|
||||
/// - `BitmapFactory.decodeByteArray`: Auto-detect and decode input
|
||||
/// - `Bitmap.compress`: Encode to target format with quality control
|
||||
/// - `ByteArrayOutputStream`: Memory-based output buffer
|
||||
///
|
||||
/// **Limitations:**
|
||||
/// - HEIC output not supported (use JPEG or PNG instead)
|
||||
/// - Requires Android 9+ for full format support
|
||||
///
|
||||
/// **Performance:**
|
||||
/// - Native image decoding via BitmapFactory
|
||||
/// - Efficient compression with quality adjustment
|
||||
final class ImageConverterAndroid implements ImageConverterPlatform {
|
||||
@override
|
||||
Future<Uint8List> convert({
|
||||
required Uint8List inputData,
|
||||
OutputFormat format = OutputFormat.jpeg,
|
||||
int quality = 100,
|
||||
}) async {
|
||||
final inputJBytes = JByteArray.from(inputData);
|
||||
try {
|
||||
final bitmap = BitmapFactory.decodeByteArray(
|
||||
inputJBytes,
|
||||
0,
|
||||
inputData.length,
|
||||
);
|
||||
if (bitmap == null) {
|
||||
throw Exception('Failed to decode image. Invalid image data.');
|
||||
}
|
||||
|
||||
try {
|
||||
final compressFormat = switch (format) {
|
||||
OutputFormat.jpeg => Bitmap$CompressFormat.JPEG,
|
||||
OutputFormat.png => Bitmap$CompressFormat.PNG,
|
||||
OutputFormat.webp => Bitmap$CompressFormat.WEBP_LOSSY,
|
||||
OutputFormat.heic => throw UnsupportedError(
|
||||
'HEIC output format is not supported on Android.',
|
||||
),
|
||||
};
|
||||
|
||||
try {
|
||||
final outputStream = ByteArrayOutputStream();
|
||||
try {
|
||||
final success = bitmap.compress(
|
||||
compressFormat,
|
||||
quality,
|
||||
outputStream,
|
||||
);
|
||||
if (!success) {
|
||||
throw Exception('Failed to compress bitmap.');
|
||||
}
|
||||
|
||||
final outputJBytes = outputStream.toByteArray();
|
||||
if (outputJBytes == null) {
|
||||
throw Exception('Failed to get byte array from output stream.');
|
||||
}
|
||||
try {
|
||||
final outputList = outputJBytes.toList();
|
||||
return Uint8List.fromList(outputList);
|
||||
} finally {
|
||||
outputJBytes.release();
|
||||
}
|
||||
} finally {
|
||||
outputStream.release();
|
||||
}
|
||||
} finally {
|
||||
compressFormat.release();
|
||||
}
|
||||
} finally {
|
||||
bitmap.release();
|
||||
}
|
||||
} finally {
|
||||
inputJBytes.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import 'dart:ffi';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:image_ffi/gen/darwin_bindings.dart';
|
||||
import 'package:image_ffi/src/image_converter_platform_interface.dart';
|
||||
import 'package:image_ffi/src/output_format.dart';
|
||||
import 'package:objective_c/objective_c.dart';
|
||||
|
||||
/// iOS/macOS image converter using ImageIO framework.
|
||||
///
|
||||
/// Implements image conversion for iOS 14+ and macOS 10.15+ platforms using
|
||||
/// the native ImageIO framework through FFI bindings.
|
||||
///
|
||||
/// **Features:**
|
||||
/// - Supports all major image formats (JPEG, PNG, HEIC, WebP, etc.)
|
||||
/// - Uses CoreFoundation and CoreGraphics for efficient processing
|
||||
/// - Memory-safe with proper resource cleanup
|
||||
///
|
||||
/// **API Stack:**
|
||||
/// - `CGImageSourceCreateWithData`: Decode input image
|
||||
/// - `CGImageSourceCreateImageAtIndex`: Extract CGImage
|
||||
/// - `CGImageDestinationCreateWithData`: Create output stream
|
||||
/// - `CGImageDestinationAddImage`: Add image with encoding options
|
||||
/// - `CGImageDestinationFinalize`: Complete encoding
|
||||
///
|
||||
/// **Performance:**
|
||||
/// - Direct FFI calls with minimal overhead
|
||||
/// - In-memory processing
|
||||
/// - Adjustable JPEG/WebP quality for size optimization
|
||||
final class ImageConverterDarwin implements ImageConverterPlatform {
|
||||
@override
|
||||
Future<Uint8List> convert({
|
||||
required Uint8List inputData,
|
||||
OutputFormat format = OutputFormat.jpeg,
|
||||
int quality = 100,
|
||||
}) async {
|
||||
final inputPtr = calloc<Uint8>(inputData.length);
|
||||
try {
|
||||
inputPtr.asTypedList(inputData.length).setAll(0, inputData);
|
||||
|
||||
final cfData = CFDataCreate(
|
||||
kCFAllocatorDefault,
|
||||
inputPtr.cast(),
|
||||
inputData.length,
|
||||
);
|
||||
if (cfData == nullptr) {
|
||||
throw Exception('Failed to create CFData from input data.');
|
||||
}
|
||||
|
||||
final imageSource = CGImageSourceCreateWithData(cfData, nullptr);
|
||||
if (imageSource == nullptr) {
|
||||
throw Exception('Failed to create CGImageSource. Invalid image data.');
|
||||
}
|
||||
|
||||
final cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr);
|
||||
if (cgImage == nullptr) {
|
||||
throw Exception('Failed to decode image.');
|
||||
}
|
||||
|
||||
final outputData = CFDataCreateMutable(kCFAllocatorDefault, 0);
|
||||
if (outputData == nullptr) {
|
||||
throw Exception('Failed to create output CFData.');
|
||||
}
|
||||
|
||||
final utiStr = switch (format) {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttypejpeg
|
||||
OutputFormat.jpeg => 'public.jpeg',
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttypepng
|
||||
OutputFormat.png => 'public.png',
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttypeheic
|
||||
OutputFormat.heic => 'public.heic',
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttypewebp
|
||||
OutputFormat.webp => throw UnsupportedError(
|
||||
'WebP output format is not supported on iOS/macOS via ImageIO.',
|
||||
),
|
||||
};
|
||||
final cfString = utiStr
|
||||
.toNSString()
|
||||
.ref
|
||||
.retainAndAutorelease()
|
||||
.cast<CFString>();
|
||||
|
||||
final destination = CGImageDestinationCreateWithData(
|
||||
outputData,
|
||||
cfString,
|
||||
1,
|
||||
nullptr,
|
||||
);
|
||||
if (destination == nullptr) {
|
||||
throw Exception('Failed to create CGImageDestination.');
|
||||
}
|
||||
|
||||
CGImageDestinationAddImage(destination, cgImage, nullptr);
|
||||
|
||||
final success = CGImageDestinationFinalize(destination);
|
||||
if (!success) {
|
||||
throw Exception('Failed to finalize image encoding.');
|
||||
}
|
||||
|
||||
final length = CFDataGetLength(outputData);
|
||||
final bytePtr = CFDataGetBytePtr(outputData);
|
||||
if (bytePtr == nullptr) {
|
||||
throw Exception('Failed to get output data bytes.');
|
||||
}
|
||||
|
||||
return Uint8List.fromList(bytePtr.cast<Uint8>().asTypedList(length));
|
||||
} finally {
|
||||
calloc.free(inputPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'output_format.dart';
|
||||
|
||||
/// Platform-specific image converter interface.
|
||||
///
|
||||
/// Abstract base class that all platform implementations must implement.
|
||||
/// Handles the core logic of image format conversion on each platform.
|
||||
///
|
||||
/// **Implementations:**
|
||||
/// - [ImageConverterDarwin]: iOS and macOS using ImageIO
|
||||
/// - [ImageConverterAndroid]: Android using BitmapFactory
|
||||
abstract interface class ImageConverterPlatform {
|
||||
/// Converts an image to a target format.
|
||||
///
|
||||
/// Decodes the input image data and re-encodes it in the specified format.
|
||||
///
|
||||
/// **Parameters:**
|
||||
/// - [inputData]: Raw bytes of the image to convert
|
||||
/// - [format]: Target [OutputFormat] (default: [OutputFormat.jpeg])
|
||||
/// - [quality]: Compression quality 1-100 for lossy formats (default: 95)
|
||||
///
|
||||
/// **Returns:** Converted image data as [Uint8List]
|
||||
///
|
||||
/// **Throws:**
|
||||
/// - [UnimplementedError]: If not implemented by platform subclass
|
||||
/// - [UnsupportedError]: If format is not supported
|
||||
/// - [Exception]: If conversion fails
|
||||
Future<Uint8List> convert({
|
||||
required Uint8List inputData,
|
||||
OutputFormat format = OutputFormat.jpeg,
|
||||
int quality = 100,
|
||||
}) {
|
||||
throw UnimplementedError('convert() has not been implemented.');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/// Output image format for conversion.
|
||||
///
|
||||
/// Specifies the target format when converting images.
|
||||
///
|
||||
/// **Format Support:**
|
||||
/// | Format | iOS/macOS | Android |
|
||||
/// |--------|-----------|---------|
|
||||
/// | jpeg | ✓ | ✓ |
|
||||
/// | png | ✓ | ✓ |
|
||||
/// | webp | ✓ | ✓ |
|
||||
///
|
||||
/// **Notes:**
|
||||
/// - [jpeg]: Good compression with adjustable quality
|
||||
/// - [png]: Lossless compression, supports transparency
|
||||
/// - [webp]: Modern format with better compression than JPEG
|
||||
enum OutputFormat {
|
||||
/// JPEG format (.jpg, .jpeg)
|
||||
/// Lossy compression, suitable for photos
|
||||
jpeg,
|
||||
|
||||
/// PNG format (.png)
|
||||
/// Lossless compression, supports transparency
|
||||
png,
|
||||
|
||||
/// WebP format (.webp)
|
||||
/// Modern format with superior compression (not supported on Darwin)
|
||||
webp,
|
||||
|
||||
/// HEIC format (.heic)
|
||||
/// High Efficiency Image Format (not supported on Android)
|
||||
heic,
|
||||
}
|
||||
22
pubspec.yaml
22
pubspec.yaml
|
|
@ -1,7 +1,7 @@
|
|||
name: image_ffi
|
||||
description: "A new Flutter FFI plugin project."
|
||||
description: "A high-performance Flutter plugin for cross-platform image format conversion using native APIs."
|
||||
version: 0.0.1
|
||||
homepage:
|
||||
homepage: https://github.com/koji-1009/image_ffi
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
|
|
@ -10,19 +10,13 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
plugin_platform_interface: ^2.0.2
|
||||
|
||||
jni: ^0.15.2
|
||||
ffi: ^2.1.4
|
||||
objective_c: ^9.2.1
|
||||
|
||||
dev_dependencies:
|
||||
ffi: ^2.1.3
|
||||
ffigen: ^20.0.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
ffiPlugin: true
|
||||
ios:
|
||||
ffiPlugin: true
|
||||
jnigen: ^0.15.0
|
||||
ffigen: ^20.1.1
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
# The Flutter tooling requires that developers have CMake 3.10 or later
|
||||
# installed. You should not increase this version, as doing so will cause
|
||||
# the plugin to fail to compile for some customers of the plugin.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(image_ffi_library VERSION 0.0.1 LANGUAGES C)
|
||||
|
||||
add_library(image_ffi SHARED
|
||||
"image_ffi.c"
|
||||
)
|
||||
|
||||
set_target_properties(image_ffi PROPERTIES
|
||||
PUBLIC_HEADER image_ffi.h
|
||||
OUTPUT_NAME "image_ffi"
|
||||
)
|
||||
|
||||
target_compile_definitions(image_ffi PUBLIC DART_SHARED_LIB)
|
||||
|
||||
if (ANDROID)
|
||||
# Support Android 15 16k page size
|
||||
target_link_options(image_ffi PRIVATE "-Wl,-z,max-page-size=16384")
|
||||
endif()
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#include "image_ffi.h"
|
||||
|
||||
// A very short-lived native function.
|
||||
//
|
||||
// For very short-lived functions, it is fine to call them on the main isolate.
|
||||
// They will block the Dart execution while running the native function, so
|
||||
// only do this for native functions which are guaranteed to be short-lived.
|
||||
FFI_PLUGIN_EXPORT int sum(int a, int b) { return a + b; }
|
||||
|
||||
// A longer-lived native function, which occupies the thread calling it.
|
||||
//
|
||||
// Do not call these kind of native functions in the main isolate. They will
|
||||
// block Dart execution. This will cause dropped frames in Flutter applications.
|
||||
// Instead, call these native functions on a separate isolate.
|
||||
FFI_PLUGIN_EXPORT int sum_long_running(int a, int b) {
|
||||
// Simulate work.
|
||||
#if _WIN32
|
||||
Sleep(5000);
|
||||
#else
|
||||
usleep(5000 * 1000);
|
||||
#endif
|
||||
return a + b;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if _WIN32
|
||||
#define FFI_PLUGIN_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define FFI_PLUGIN_EXPORT
|
||||
#endif
|
||||
|
||||
// A very short-lived native function.
|
||||
//
|
||||
// For very short-lived functions, it is fine to call them on the main isolate.
|
||||
// They will block the Dart execution while running the native function, so
|
||||
// only do this for native functions which are guaranteed to be short-lived.
|
||||
FFI_PLUGIN_EXPORT int sum(int a, int b);
|
||||
|
||||
// A longer lived native function, which occupies the thread calling it.
|
||||
//
|
||||
// Do not call these kind of native functions in the main isolate. They will
|
||||
// block Dart execution. This will cause dropped frames in Flutter applications.
|
||||
// Instead, call these native functions on a separate isolate.
|
||||
FFI_PLUGIN_EXPORT int sum_long_running(int a, int b);
|
||||
Loading…
Reference in New Issue