feat: Support web
This commit is contained in:
parent
8b47d1295b
commit
c0f8076b56
26
README.md
26
README.md
|
|
@ -15,9 +15,12 @@ A high-performance Flutter plugin for cross-platform image format conversion usi
|
|||
| iOS | 14.0 | ImageIO (CoreFoundation, CoreGraphics) |
|
||||
| macOS | 10.15 | ImageIO (CoreFoundation, CoreGraphics) |
|
||||
| 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
|
||||
|
||||
|
|
@ -137,3 +140,22 @@ The Android implementation uses [BitmapFactory](https://developer.android.com/re
|
|||
**Key Limitations:**
|
||||
- HEIC can be read (input only) but cannot be written (output format not supported)
|
||||
- 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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -6,7 +6,7 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
namespace = "dr1009.com.image_ffi_example"
|
||||
namespace = "com.example.image_ffi_example"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
// 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.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package dr1009.com.image_ffi_example
|
||||
package com.example.image_ffi_example
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
|
@ -11,10 +11,10 @@
|
|||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
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 */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -146,9 +146,6 @@
|
|||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
|
|
@ -164,6 +161,9 @@
|
|||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
|
|
@ -172,9 +172,6 @@
|
|||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
|
|
@ -200,6 +197,9 @@
|
|||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
|
|
@ -378,7 +378,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -394,7 +394,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
|
|
@ -411,7 +411,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
|
|
@ -426,7 +426,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
|
|
@ -557,7 +557,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
|
|
@ -579,7 +579,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dr1009.com.imageFfiExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.imageFfiExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -621,12 +621,14 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Image Ffi</string>
|
||||
<string>Image Ffi Example</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
|
|
|||
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 |
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -3,10 +3,11 @@ library;
|
|||
import 'dart:async';
|
||||
|
||||
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';
|
||||
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';
|
||||
|
||||
|
|
@ -99,11 +100,11 @@ class _ConvertRequest {
|
|||
/// Returns the platform-specific converter instance.
|
||||
ImageConverterPlatform _getPlatformForTarget(TargetPlatform platform) {
|
||||
if (kIsWeb) {
|
||||
throw UnsupportedError('Image conversion is not supported on the web.');
|
||||
return const ImageConverterWeb();
|
||||
}
|
||||
return switch (platform) {
|
||||
TargetPlatform.android => ImageConverterAndroid(),
|
||||
TargetPlatform.iOS || TargetPlatform.macOS => ImageConverterDarwin(),
|
||||
TargetPlatform.android => const ImageConverterAndroid(),
|
||||
TargetPlatform.iOS || TargetPlatform.macOS => const ImageConverterDarwin(),
|
||||
_ => throw UnsupportedError(
|
||||
'Image conversion is not supported on this platform: $platform',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import 'package:jni/jni.dart';
|
|||
/// - Native image decoding via BitmapFactory
|
||||
/// - Efficient compression with quality adjustment
|
||||
final class ImageConverterAndroid implements ImageConverterPlatform {
|
||||
const ImageConverterAndroid();
|
||||
|
||||
@override
|
||||
Future<Uint8List> convert({
|
||||
required Uint8List inputData,
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export 'stub.dart'
|
||||
if (dart.library.io) 'native.dart'
|
||||
if (dart.library.js_interop) 'stub.dart';
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -29,6 +29,8 @@ import 'package:objective_c/objective_c.dart';
|
|||
/// - In-memory processing
|
||||
/// - Adjustable JPEG/WebP quality for size optimization
|
||||
final class ImageConverterDarwin implements ImageConverterPlatform {
|
||||
const ImageConverterDarwin();
|
||||
|
||||
@override
|
||||
Future<Uint8List> convert({
|
||||
required Uint8List inputData,
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export 'stub.dart'
|
||||
if (dart.library.io) 'native.dart'
|
||||
if (dart.library.js_interop) 'stub.dart';
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -3,17 +3,18 @@
|
|||
/// Specifies the target format when converting images.
|
||||
///
|
||||
/// **Format Support:**
|
||||
/// | Format | iOS/macOS | Android |
|
||||
/// |--------|-----------|---------|
|
||||
/// | jpeg | ✓ | ✓ |
|
||||
/// | png | ✓ | ✓ |
|
||||
/// | webp | | ✓ |
|
||||
/// | heic | ✓ | |
|
||||
/// | Format | iOS/macOS | Android | Web |
|
||||
/// |--------|-----------|---------| ----|
|
||||
/// | jpeg | ✓ | ✓ | ✓ |
|
||||
/// | png | ✓ | ✓ | ✓ |
|
||||
/// | webp | | ✓ | ✓ |
|
||||
/// | heic | ✓ | | |
|
||||
///
|
||||
/// **Notes:**
|
||||
/// - [jpeg]: Good compression with adjustable quality
|
||||
/// - [png]: Lossless compression, supports transparency
|
||||
/// - [webp]: Modern format with better compression than JPEG
|
||||
/// - [heic]: High Efficiency Image Format, not supported on Android
|
||||
enum OutputFormat {
|
||||
/// JPEG format (.jpg, .jpeg)
|
||||
/// Lossy compression, suitable for photos
|
||||
|
|
@ -28,6 +29,6 @@ enum OutputFormat {
|
|||
webp,
|
||||
|
||||
/// HEIC format (.heic)
|
||||
/// High Efficiency Image Format (not supported on Android)
|
||||
/// High Efficiency Image Format (not supported on Android and Web)
|
||||
heic,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export 'stub.dart'
|
||||
if (dart.library.io) 'stub.dart'
|
||||
if (dart.library.js_interop) 'web.dart';
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ dependencies:
|
|||
jni: ^0.15.2
|
||||
ffi: ^2.1.4
|
||||
objective_c: ^9.2.1
|
||||
web: ^1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in New Issue