Merge pull request #4 from koji-1009/feat/web

feat: Support web
This commit is contained in:
Koji Wakamiya 2025-12-08 21:30:51 +09:00 committed by GitHub
commit e53f3208ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 318 additions and 32 deletions

View File

@ -15,9 +15,12 @@ A high-performance Flutter plugin for cross-platform image format conversion usi
| iOS | 14.0 | ImageIO (CoreFoundation, CoreGraphics) | | iOS | 14.0 | ImageIO (CoreFoundation, CoreGraphics) |
| macOS | 10.15 | ImageIO (CoreFoundation, CoreGraphics) | | macOS | 10.15 | ImageIO (CoreFoundation, CoreGraphics) |
| Android | 7 | BitmapFactory, Bitmap compression | | Android | 7 | BitmapFactory, Bitmap compression |
| Web | N/A | Not supported | | Web | - | Canvas API |
on Android, HEIC input is supported on Android 9+ but HEIC output is not supported. **Note:**
- On iOS and macOS, WebP input is supported but WebP output is not supported.
- On Android, HEIC input is supported on Android 9+ but HEIC output is not supported.
- On Web, HEIC is not supported.
## Getting Started ## Getting Started
@ -137,3 +140,22 @@ The Android implementation uses [BitmapFactory](https://developer.android.com/re
**Key Limitations:** **Key Limitations:**
- HEIC can be read (input only) but cannot be written (output format not supported) - HEIC can be read (input only) but cannot be written (output format not supported)
- Requires Android 9+ for full HEIC support - Requires Android 9+ for full HEIC support
### Web Implementation
The Web implementation uses the [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) for image conversion:
1. **Decoding**: `HTMLImageElement` loads image data via Blob URL
2. **Rendering**: `CanvasRenderingContext2D.drawImage` renders the image to canvas
3. **Encoding**: `HTMLCanvasElement.toBlob` encodes to target format
4. **Quality**: Supports quality parameter for JPEG and WebP (0.0-1.0 scale)
**Key Steps:**
- `Blob` and `URL.createObjectURL`: Create temporary URL from input bytes
- `HTMLImageElement.onLoad`: Wait for image to load asynchronously
- `canvas.width/height = img.width/height`: Set canvas size to match image dimensions
- `canvas.toBlob`: Convert canvas content to target format
**Key Limitations:**
- HEIC format is not supported on Web platform
- Output format depends on browser support (JPEG and PNG are universally supported, WebP is widely supported)

36
example/.metadata Normal file
View File

@ -0,0 +1,36 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "66dd93f9a27ffe2a9bfc8297506ce066ff51265f"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
- platform: android
create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
- platform: ios
create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
- platform: web
create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@ -6,7 +6,7 @@ plugins {
} }
android { android {
namespace = "dr1009.com.image_ffi_example" namespace = "com.example.image_ffi_example"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@ -21,7 +21,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "dr1009.com.image_ffi_example" applicationId = "com.example.image_ffi_example"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View File

@ -1,4 +1,4 @@
package dr1009.com.image_ffi_example package com.example.image_ffi_example
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

View File

@ -11,10 +11,10 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -48,6 +48,7 @@
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@ -56,7 +57,6 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -146,9 +146,6 @@
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
97C146ED1CF9000F007C117D /* Runner */ = { 97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
@ -164,6 +161,9 @@
dependencies = ( dependencies = (
); );
name = Runner; name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner; productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@ -172,9 +172,6 @@
/* Begin PBXProject section */ /* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
@ -200,6 +197,9 @@
Base, Base,
); );
mainGroup = 97C146E51CF9000F007C117D; mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */; productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@ -378,7 +378,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample; PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -394,7 +394,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -411,7 +411,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -426,7 +426,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -557,7 +557,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample; PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -579,7 +579,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample; PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -621,12 +621,14 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
}; };
/* End XCLocalSwiftPackageReference section */ /* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Image Ffi</string> <string>Image Ffi Example</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>

BIN
example/web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

38
example/web/index.html Normal file
View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="image_ffi_example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>image_ffi_example</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

35
example/web/manifest.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "image_ffi_example",
"short_name": "image_ffi_example",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

View File

@ -3,10 +3,11 @@ library;
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.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/image_converter_platform_interface.dart';
import 'package:image_ffi/src/output_format.dart'; import 'package:image_ffi/src/output_format.dart';
import 'package:image_ffi/src/android/shared.dart';
import 'package:image_ffi/src/darwin/shared.dart';
import 'package:image_ffi/src/web/shared.dart';
export 'src/output_format.dart'; export 'src/output_format.dart';
@ -99,11 +100,11 @@ class _ConvertRequest {
/// Returns the platform-specific converter instance. /// Returns the platform-specific converter instance.
ImageConverterPlatform _getPlatformForTarget(TargetPlatform platform) { ImageConverterPlatform _getPlatformForTarget(TargetPlatform platform) {
if (kIsWeb) { if (kIsWeb) {
throw UnsupportedError('Image conversion is not supported on the web.'); return const ImageConverterWeb();
} }
return switch (platform) { return switch (platform) {
TargetPlatform.android => ImageConverterAndroid(), TargetPlatform.android => const ImageConverterAndroid(),
TargetPlatform.iOS || TargetPlatform.macOS => ImageConverterDarwin(), TargetPlatform.iOS || TargetPlatform.macOS => const ImageConverterDarwin(),
_ => throw UnsupportedError( _ => throw UnsupportedError(
'Image conversion is not supported on this platform: $platform', 'Image conversion is not supported on this platform: $platform',
), ),

View File

@ -28,6 +28,8 @@ import 'package:jni/jni.dart';
/// - Native image decoding via BitmapFactory /// - Native image decoding via BitmapFactory
/// - Efficient compression with quality adjustment /// - Efficient compression with quality adjustment
final class ImageConverterAndroid implements ImageConverterPlatform { final class ImageConverterAndroid implements ImageConverterPlatform {
const ImageConverterAndroid();
@override @override
Future<Uint8List> convert({ Future<Uint8List> convert({
required Uint8List inputData, required Uint8List inputData,

View File

@ -0,0 +1,3 @@
export 'stub.dart'
if (dart.library.io) 'native.dart'
if (dart.library.js_interop) 'stub.dart';

15
lib/src/android/stub.dart Normal file
View File

@ -0,0 +1,15 @@
import 'dart:typed_data';
import 'package:image_ffi/src/image_converter_platform_interface.dart';
import 'package:image_ffi/src/output_format.dart';
final class ImageConverterAndroid implements ImageConverterPlatform {
const ImageConverterAndroid();
@override
Future<Uint8List> convert({
required Uint8List inputData,
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
}) async => throw UnimplementedError();
}

View File

@ -29,6 +29,8 @@ import 'package:objective_c/objective_c.dart';
/// - In-memory processing /// - In-memory processing
/// - Adjustable JPEG/WebP quality for size optimization /// - Adjustable JPEG/WebP quality for size optimization
final class ImageConverterDarwin implements ImageConverterPlatform { final class ImageConverterDarwin implements ImageConverterPlatform {
const ImageConverterDarwin();
@override @override
Future<Uint8List> convert({ Future<Uint8List> convert({
required Uint8List inputData, required Uint8List inputData,

View File

@ -0,0 +1,3 @@
export 'stub.dart'
if (dart.library.io) 'native.dart'
if (dart.library.js_interop) 'stub.dart';

15
lib/src/darwin/stub.dart Normal file
View File

@ -0,0 +1,15 @@
import 'dart:typed_data';
import 'package:image_ffi/src/image_converter_platform_interface.dart';
import 'package:image_ffi/src/output_format.dart';
final class ImageConverterDarwin implements ImageConverterPlatform {
const ImageConverterDarwin();
@override
Future<Uint8List> convert({
required Uint8List inputData,
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
}) async => throw UnimplementedError();
}

View File

@ -3,17 +3,18 @@
/// Specifies the target format when converting images. /// Specifies the target format when converting images.
/// ///
/// **Format Support:** /// **Format Support:**
/// | Format | iOS/macOS | Android | /// | Format | iOS/macOS | Android | Web |
/// |--------|-----------|---------| /// |--------|-----------|---------| ----|
/// | jpeg | | | /// | jpeg | | | |
/// | png | | | /// | png | | | |
/// | webp | | | /// | webp | | | |
/// | heic | | | /// | heic | | | |
/// ///
/// **Notes:** /// **Notes:**
/// - [jpeg]: Good compression with adjustable quality /// - [jpeg]: Good compression with adjustable quality
/// - [png]: Lossless compression, supports transparency /// - [png]: Lossless compression, supports transparency
/// - [webp]: Modern format with better compression than JPEG /// - [webp]: Modern format with better compression than JPEG
/// - [heic]: High Efficiency Image Format, not supported on Android
enum OutputFormat { enum OutputFormat {
/// JPEG format (.jpg, .jpeg) /// JPEG format (.jpg, .jpeg)
/// Lossy compression, suitable for photos /// Lossy compression, suitable for photos
@ -28,6 +29,6 @@ enum OutputFormat {
webp, webp,
/// HEIC format (.heic) /// HEIC format (.heic)
/// High Efficiency Image Format (not supported on Android) /// High Efficiency Image Format (not supported on Android and Web)
heic, heic,
} }

3
lib/src/web/shared.dart Normal file
View File

@ -0,0 +1,3 @@
export 'stub.dart'
if (dart.library.io) 'stub.dart'
if (dart.library.js_interop) 'web.dart';

15
lib/src/web/stub.dart Normal file
View File

@ -0,0 +1,15 @@
import 'dart:typed_data';
import 'package:image_ffi/src/image_converter_platform_interface.dart';
import 'package:image_ffi/src/output_format.dart';
final class ImageConverterWeb implements ImageConverterPlatform {
const ImageConverterWeb();
@override
Future<Uint8List> convert({
required Uint8List inputData,
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
}) async => throw UnimplementedError();
}

92
lib/src/web/web.dart Normal file
View File

@ -0,0 +1,92 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:typed_data';
import 'package:image_ffi/src/image_converter_platform_interface.dart';
import 'package:image_ffi/src/output_format.dart';
import 'package:web/web.dart';
/// Web image converter using Canvas API.
///
/// Implements image conversion for web platforms using the HTML5 Canvas API
/// for decoding and encoding images in the browser.
///
/// **Features:**
/// - Supports JPEG, PNG, WebP formats (browser-dependent)
/// - HEIC format not supported on Web
/// - Adjustable quality for JPEG and WebP compression
/// - Asynchronous image loading and encoding
///
/// **API Stack:**
/// - `HTMLImageElement`: Load and decode input image via Blob URL
/// - `CanvasRenderingContext2D.drawImage`: Render image to canvas
/// - `HTMLCanvasElement.toBlob`: Encode canvas to target format
///
/// **Limitations:**
/// - HEIC not supported (throws UnsupportedError)
/// - Output format support depends on browser capabilities
/// - JPEG and PNG are universally supported
/// - WebP is widely supported in modern browsers
///
/// **Performance:**
/// - Browser-native image decoding and encoding
/// - In-memory processing with Blob/ArrayBuffer
/// - Quality parameter controls compression ratio
final class ImageConverterWeb implements ImageConverterPlatform {
const ImageConverterWeb();
@override
Future<Uint8List> convert({
required Uint8List inputData,
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
}) async {
final img = HTMLImageElement();
final decodeCompeleter = Completer<void>();
final blob = Blob([inputData.toJS].toJS);
final url = URL.createObjectURL(blob);
img.onLoad.listen((_) => decodeCompeleter.complete());
img.onError.listen((e) {
URL.revokeObjectURL(url);
decodeCompeleter.completeError('Failed to load image: $e');
});
img.src = url;
await decodeCompeleter.future;
URL.revokeObjectURL(url);
final canvas = HTMLCanvasElement();
canvas.width = img.width;
canvas.height = img.height;
final ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
ctx.drawImage(img, 0, 0);
final encodeCompleter = Completer<Blob>();
final type = switch (format) {
OutputFormat.jpeg => 'image/jpeg',
OutputFormat.png => 'image/png',
OutputFormat.webp => 'image/webp',
OutputFormat.heic => throw UnsupportedError(
'HEIC output format is not supported on Web.',
),
};
canvas.toBlob(
(Blob? blob) {
if (blob != null) {
encodeCompleter.complete(blob);
} else {
encodeCompleter.completeError(
'Failed to convert canvas to JPEG Blob.',
);
}
}.toJS,
type,
(quality / 100).toJS,
);
final result = await encodeCompleter.future;
final arrayBuffer = await result.arrayBuffer().toDart;
return arrayBuffer.toDart.asUint8List();
}
}

View File

@ -13,6 +13,7 @@ dependencies:
jni: ^0.15.2 jni: ^0.15.2
ffi: ^2.1.4 ffi: ^2.1.4
objective_c: ^9.2.1 objective_c: ^9.2.1
web: ^1.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: