diff --git a/README.md b/README.md index 33e5ab1..4c9642f 100644 --- a/README.md +++ b/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) diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..20c0c40 --- /dev/null +++ b/example/.metadata @@ -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' diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts index 3a970f6..1fae4b6 100644 --- a/example/android/app/build.gradle.kts +++ b/example/android/app/build.gradle.kts @@ -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 diff --git a/example/android/app/src/main/kotlin/dr1009/com/image_ffi_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/image_ffi_example/MainActivity.kt similarity index 70% rename from example/android/app/src/main/kotlin/dr1009/com/image_ffi_example/MainActivity.kt rename to example/android/app/src/main/kotlin/com/example/image_ffi_example/MainActivity.kt index c4e4407..6d585bc 100644 --- a/example/android/app/src/main/kotlin/dr1009/com/image_ffi_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/example/image_ffi_example/MainActivity.kt @@ -1,4 +1,4 @@ -package dr1009.com.image_ffi_example +package com.example.image_ffi_example import io.flutter.embedding.android.FlutterActivity diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 083dbae..c970a3b 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -56,7 +57,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* 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; diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index e0242ef..6a44b50 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Image Ffi + Image Ffi Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..5c1cba0 --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + image_ffi_example + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..170d958 --- /dev/null +++ b/example/web/manifest.json @@ -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" + } + ] +} diff --git a/lib/image_ffi.dart b/lib/image_ffi.dart index ee507f3..f83f689 100644 --- a/lib/image_ffi.dart +++ b/lib/image_ffi.dart @@ -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', ), diff --git a/lib/src/image_converter_android.dart b/lib/src/android/native.dart similarity index 98% rename from lib/src/image_converter_android.dart rename to lib/src/android/native.dart index 117a386..754f8d9 100644 --- a/lib/src/image_converter_android.dart +++ b/lib/src/android/native.dart @@ -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 convert({ required Uint8List inputData, diff --git a/lib/src/android/shared.dart b/lib/src/android/shared.dart new file mode 100644 index 0000000..746b0fa --- /dev/null +++ b/lib/src/android/shared.dart @@ -0,0 +1,3 @@ +export 'stub.dart' + if (dart.library.io) 'native.dart' + if (dart.library.js_interop) 'stub.dart'; diff --git a/lib/src/android/stub.dart b/lib/src/android/stub.dart new file mode 100644 index 0000000..51247cd --- /dev/null +++ b/lib/src/android/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 convert({ + required Uint8List inputData, + OutputFormat format = OutputFormat.jpeg, + int quality = 100, + }) async => throw UnimplementedError(); +} diff --git a/lib/src/image_converter_darwin.dart b/lib/src/darwin/native.dart similarity index 99% rename from lib/src/image_converter_darwin.dart rename to lib/src/darwin/native.dart index 8bac51e..2d51780 100644 --- a/lib/src/image_converter_darwin.dart +++ b/lib/src/darwin/native.dart @@ -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 convert({ required Uint8List inputData, diff --git a/lib/src/darwin/shared.dart b/lib/src/darwin/shared.dart new file mode 100644 index 0000000..ee8895c --- /dev/null +++ b/lib/src/darwin/shared.dart @@ -0,0 +1,3 @@ +export 'stub.dart' + if (dart.library.io) 'native.dart' + if (dart.library.js_interop) 'stub.dart'; \ No newline at end of file diff --git a/lib/src/darwin/stub.dart b/lib/src/darwin/stub.dart new file mode 100644 index 0000000..59a1f39 --- /dev/null +++ b/lib/src/darwin/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 convert({ + required Uint8List inputData, + OutputFormat format = OutputFormat.jpeg, + int quality = 100, + }) async => throw UnimplementedError(); +} diff --git a/lib/src/output_format.dart b/lib/src/output_format.dart index eb586cb..98e01fd 100644 --- a/lib/src/output_format.dart +++ b/lib/src/output_format.dart @@ -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, } diff --git a/lib/src/web/shared.dart b/lib/src/web/shared.dart new file mode 100644 index 0000000..a483baf --- /dev/null +++ b/lib/src/web/shared.dart @@ -0,0 +1,3 @@ +export 'stub.dart' + if (dart.library.io) 'stub.dart' + if (dart.library.js_interop) 'web.dart'; diff --git a/lib/src/web/stub.dart b/lib/src/web/stub.dart new file mode 100644 index 0000000..8241075 --- /dev/null +++ b/lib/src/web/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 ImageConverterWeb implements ImageConverterPlatform { + const ImageConverterWeb(); + + @override + Future convert({ + required Uint8List inputData, + OutputFormat format = OutputFormat.jpeg, + int quality = 100, + }) async => throw UnimplementedError(); +} diff --git a/lib/src/web/web.dart b/lib/src/web/web.dart new file mode 100644 index 0000000..e1507b6 --- /dev/null +++ b/lib/src/web/web.dart @@ -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 convert({ + required Uint8List inputData, + OutputFormat format = OutputFormat.jpeg, + int quality = 100, + }) async { + final img = HTMLImageElement(); + final decodeCompeleter = Completer(); + + 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(); + 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(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 637ac85..5971432 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: