Skip to content

Commit

Permalink
feat(wasm): replace dart:html and dart:js with package:web and …
Browse files Browse the repository at this point in the history
…`dart:js_interop` for WebAssembly support (#304)

* feat(video): begin implementation of video combine example

- Created the initial setup for the video combine feature
- Added `combine_video_editor_example.dart` to handle the main logic
- Introduced utility for video image elements in `video_image_element.dart`
- Added widgets for video editing pages and media picking
- Included a bottom bar widget for video sending functionality

Note: The implementation is not yet complete.

* Merge branch 'stable' of https://github.com/hm21/pro_image_editor into dev

* feat(examples): add download button functionality

- Added a download button to enhance usability in example pages.
- Updated `main.dart` and relevant pages to support the feature.
- Removed unused video-related files and widgets to streamline the codebase.
- Updated generated files and configurations in `linux`, `macOS`, and `Windows` to reflect changes.

* fix(zoom): correct layer rotation calculation during user drag

Resolved an issue where the layer rotation was calculated incorrectly when the user dragged the rotation button.

Resolve #266

* Merge branch 'stable' into dev

* Merge branch 'stable' of https://github.com/hm21/pro_image_editor into dev

* fix(merge): resolve conflicts during stable to dev merge

Resolved merge conflicts in `preview_img.dart` and `zoom_example.dart` while merging changes from the `stable` branch into the `dev` branch.

* feat(wasm): replace `dart:html` and `dart:js` with `package:web` and `dart:js_interop` for WebAssembly support

- Updated dependencies in `pubspec.yaml` to use `package:web` and `dart:js_interop`.
- Added new constants in `editor_web_constants.dart`.
- Refactored threading logic in `thread_request_model.dart`, `web_worker_manager.dart`, and related files to enable WebAssembly support.
- Introduced `web_worker.dart.wasm` and related files for WebAssembly integration.
- Updated image utilities (`convert_raw_image.dart`, `encode_image.dart`) for compatibility.
- Adjusted configurations in `analysis_options.yaml` and added relevant changes to `CHANGELOG.md`.

These changes enable better performance and compatibility for WebAssembly (Wasm).

* Merge remote-tracking branch 'origin/stable' into dev
  • Loading branch information
hm21 authored Dec 28, 2024
1 parent bc283ba commit 56a4b3e
Show file tree
Hide file tree
Showing 23 changed files with 5,894 additions and 6,471 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 7.1.0
- **Feat**(Wasm): Replaced the `dart:html` and `dart:js` packages with `package:web` and `dart:js_interop` to enable WebAssembly (Wasm) support.
The current Flutter version `3.27.1` has an open issue with the `ColorFiltered` widget. As a result, the tune and filter editor will not function in Wasm. Once Flutter resolves this issue, the editor should work without requiring further updates.

## 7.0.1
- **Fix**(zoom): Corrected the layer rotation calculation when the user drags the rotation button. This resolves issue [#266](https://github.com/hm21/pro_image_editor/issues/266)

Expand Down
1 change: 0 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ include: package:flutter_lints/flutter.yaml

analyzer:
language:
strict-casts: true
strict-raw-types: true

linter:
Expand Down
6 changes: 6 additions & 0 deletions lib/common/editor_web_constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// The URL of the web worker script.
///
/// This URL points to the JavaScript file that will be loaded by the
/// [web.Worker] instance to run the web worker's code.
const kImageEditorWebWorkerPath =
'assets/packages/pro_image_editor/lib/web/web_worker.dart.js';
3 changes: 1 addition & 2 deletions lib/models/multi_threading/thread_request_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import 'package:image/image.dart' as img;
import 'package:image/image.dart' show JpegChroma, PngFilter;

// Project imports:
import 'package:pro_image_editor/models/editor_configs/image_generation_configs/output_formats.dart';
import '../editor_configs/image_generation_configs/output_formats.dart';

/// Represents an image object sent from the main thread.
class ImageConvertThreadRequest extends ThreadRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import '../../models/multi_threading/thread_request_model.dart';
import '../unique_id_generator.dart';
import 'threads_managers/isolate/isolate_manager.dart';
import 'threads_managers/web_worker/web_worker_manager_dummy.dart'
if (dart.library.html) 'threads_managers/web_worker/web_worker_manager.dart';
if (dart.library.js_interop) 'threads_managers/web_worker/web_worker_manager.dart';
import 'utils/dart_ui_remove_transparent_image_areas.dart';
import 'utils/encode_image.dart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ class IsolateThread extends Thread {
///
/// [onMessage] is a callback that is triggered when a message is received
/// by the thread.
/// [coreNumber] specifies the number of cores allocated to the thread.
/// [coreNumber] specifies the number of the used processor core.
IsolateThread({
required super.onMessage,
required this.coreNumber,
required super.coreNumber,
});

/// The isolate used for offloading image processing tasks.
Expand All @@ -31,9 +31,6 @@ class IsolateThread extends Thread {
/// The port used to receive messages from the isolate.
late ReceivePort receivePort;

/// Number of processor cores available for the isolate to utilize.
final int coreNumber;

@override
void init() async {
receivePort = ReceivePort();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ abstract class Thread {
/// [readyState] completer, and calls the [init] method to set up the thread.
Thread({
required this.onMessage,
required this.coreNumber,
}) {
id = generateUniqueId();
readyState = Completer();
Expand All @@ -33,6 +34,9 @@ abstract class Thread {
/// Number of active tasks currently being processed by the isolate.
int activeTasks = 0;

/// Number from the used processor core.
final int coreNumber;

/// Callback function for handling messages received from the isolate.
final Function(ThreadResponse) onMessage;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'dart:js_interop';
import 'dart:typed_data';

/// Returns Dart representation from JS Object.
dynamic dartify(dynamic object) {
// Convert JSObject to Dart equivalents directly
// Cannot be done with Dart 3.2 constraints
// ignore: invalid_runtime_check_with_js_interop_types
if (object is! JSObject) {
return object;
}

final jsObject = object;

// Convert nested structures
final dartObject = jsObject.dartify();
return convertNested(dartObject);
}

/// Convert nested objects
dynamic convertNested(dynamic object) {
if (object is ByteBuffer) {
return object;
} else if (object is List) {
return object.map(convertNested).toList();
} else if (object is Map) {
var map = <dynamic, dynamic>{};
object.forEach((key, value) {
map[key] = convertNested(value);
});
return map;
} else {
// For non-nested types, attempt to convert directly
return dartify(object);
}
}

/// Returns the JS implementation from Dart Object.
JSAny? jsify(Object? dartObject) {
if (dartObject == null) {
return dartObject?.jsify();
}

if (dartObject is List) {
return dartObject.map(jsify).toList().toJS;
}

if (dartObject is Map) {
return dartObject
.map((key, value) => MapEntry(jsify(key), jsify(value)))
.jsify();
}

if (dartObject is JSAny Function()) {
return dartObject.toJS;
}

return dartObject.jsify();
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
// ignore_for_file: avoid_web_libraries_in_flutter
import 'dart:js_interop' as js;

// Dart imports:
import 'dart:html' as html;
import 'dart:js' as js;
import 'package:web/web.dart' as web;

// Project imports:
import 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart';
import 'package:pro_image_editor/utils/content_recorder.dart/threads_managers/threads/thread_manager.dart';
import 'package:pro_image_editor/utils/content_recorder.dart/threads_managers/web_worker/web_worker_thread.dart';
import 'package:pro_image_editor/utils/content_recorder.dart/utils/processor_helper.dart';
import '/models/editor_configs/pro_image_editor_configs.dart';
import '/utils/content_recorder.dart/threads_managers/threads/thread_manager.dart';
import '/utils/content_recorder.dart/threads_managers/web_worker/web_worker_thread.dart';
import '/utils/content_recorder.dart/utils/processor_helper.dart';
import '../../../../common/editor_web_constants.dart';
import 'web_utils.dart';

/// Manages web workers for background processing tasks.
///
Expand All @@ -20,7 +19,8 @@ class WebWorkerManager extends ThreadManager {
final List<WebWorkerThread> threads = [];

/// Indicates whether web workers are supported by the browser.
final bool supportWebWorkers = html.Worker.supported;
late final bool supportWebWorkers =
web.Worker(jsify(kImageEditorWebWorkerPath)!).isDefinedAndNotNull;

@override
void init(ProImageEditorConfigs configs) {
Expand All @@ -33,6 +33,7 @@ class WebWorkerManager extends ThreadManager {

for (var i = 0; i < processors && !isDestroyed; i++) {
threads.add(WebWorkerThread(
coreNumber: i + 1,
onMessage: (message) {
int i = tasks.indexWhere((el) => el.taskId == message.id);
if (i >= 0) tasks[i].bytes$.complete(message.bytes);
Expand All @@ -43,9 +44,6 @@ class WebWorkerManager extends ThreadManager {

/// Retrieves the number of processors available on the device.
int _deviceNumberOfProcessors() {
var hardwareConcurrency = js.context['navigator']?['hardwareConcurrency'];
return hardwareConcurrency != null && hardwareConcurrency.runtimeType is int
? hardwareConcurrency as int
: 1;
return web.window.navigator.hardwareConcurrency;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// ignore_for_file: unused_field

// Project imports:
import 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart';
import 'package:pro_image_editor/utils/content_recorder.dart/threads_managers/threads/thread.dart';
import 'package:pro_image_editor/utils/content_recorder.dart/threads_managers/threads/thread_manager.dart';
import '/models/editor_configs/pro_image_editor_configs.dart';
import '/utils/content_recorder.dart/threads_managers/threads/thread.dart';
import '/utils/content_recorder.dart/threads_managers/threads/thread_manager.dart';

/// Manages web workers for background operations.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

// Dart imports:
import 'dart:async';
import 'dart:html' as html;
import 'dart:typed_data';
import 'dart:js_interop' as js;

// Flutter imports:
import 'package:flutter/rendering.dart';
import 'package:flutter/foundation.dart';
// ignore: depend_on_referenced_packages
import 'package:web/web.dart' as web;

// Project imports:
import 'package:pro_image_editor/models/multi_threading/thread_request_model.dart';
import 'package:pro_image_editor/models/multi_threading/thread_response_model.dart';
import 'package:pro_image_editor/utils/content_recorder.dart/threads_managers/threads/thread.dart';
import '/models/multi_threading/thread_request_model.dart';
import '/models/multi_threading/thread_response_model.dart';
import '/utils/content_recorder.dart/threads_managers/threads/thread.dart';
import '../../../../common/editor_web_constants.dart';
import 'web_utils.dart';

/// A class representing a web worker thread.
///
Expand All @@ -24,36 +25,40 @@ class WebWorkerThread extends Thread {
/// from the web worker.
WebWorkerThread({
required super.onMessage,
required super.coreNumber,
});

/// The URL of the web worker script.
///
/// This URL points to the JavaScript file that will be loaded by the
/// [html.Worker] instance to run the web worker's code.
final String _workerUrl =
'assets/packages/pro_image_editor/lib/web/web_worker.dart.js';

/// The [html.Worker] instance managing the web worker.
/// The [web.Worker] instance managing the web worker.
///
/// This is the web worker instance that executes the worker script
/// and communicates with the main thread.
late final html.Worker worker;
late final web.Worker worker;

@override
void init() {
try {
if (html.Worker.supported) {
worker = html.Worker(_workerUrl);
worker.onMessage.listen((event) {
var data = event.data;
worker = web.Worker(
kImageEditorWebWorkerPath.toJS,
web.WorkerOptions(
name: 'PIE-Thread-$coreNumber',
),
);

if (worker.isDefinedAndNotNull) {
worker.onmessage = (web.MessageEvent event) {
var data = dartify(event.data);
if (data?['id'] != null) {
activeTasks--;
List<dynamic>? bytes = data['bytes'] as List<dynamic>?;
onMessage(ThreadResponse(
bytes: data['bytes'] as Uint8List?,
bytes: bytes != null
? Uint8List.fromList(List.castFrom<dynamic, int>(bytes))
: null,
id: data['id'] as String,
));
}
});
}.toJS;

readyState.complete(true);
isReady = true;
} else {
Expand All @@ -69,8 +74,7 @@ class WebWorkerThread extends Thread {
@override
void send(ThreadRequest data) {
activeTasks++;

worker.postMessage({
worker.postMessage(jsify({
'mode': data is ImageConvertThreadRequest ? 'convert' : 'encode',
'id': data.id,
'generateOnlyImageBounds': data is ImageConvertThreadRequest
Expand Down Expand Up @@ -98,21 +102,21 @@ class WebWorkerThread extends Thread {
// 'palette': data.image.palette,
// 'backgroundColor': data.image.backgroundColor,
}
});
}));
}

@override
void destroyActiveTasks(String ignoreTaskId) async {
worker.postMessage({
worker.postMessage(jsify({
'mode': 'destroyActiveTasks',
'ignoreTaskId': ignoreTaskId,
});
}));
}

@override
void destroy() {
worker
..postMessage({'mode': 'kill'})
..postMessage(jsify({'mode': 'kill'}))
..terminate();
}
}
6 changes: 3 additions & 3 deletions lib/utils/content_recorder.dart/utils/convert_raw_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import 'dart:typed_data';
import 'package:image/image.dart' as img;

// Project imports:
import 'package:pro_image_editor/models/multi_threading/thread_request_model.dart';
import 'package:pro_image_editor/models/multi_threading/thread_response_model.dart';
import 'package:pro_image_editor/utils/content_recorder.dart/utils/encode_image.dart';
import '/models/multi_threading/thread_request_model.dart';
import '/models/multi_threading/thread_response_model.dart';
import '/utils/content_recorder.dart/utils/encode_image.dart';

/// Converts an image to PNG format and finds the bounding box of
/// non-transparent areas.
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/content_recorder.dart/utils/encode_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'dart:typed_data';
import 'package:image/image.dart' as img;

// Project imports:
import 'package:pro_image_editor/pro_image_editor.dart';
import '/models/editor_configs/image_generation_configs/output_formats.dart';
import 'encoder/jpeg_encoder.dart';

/// Encodes an image into the specified format and returns the encoded data.
Expand Down
Loading

0 comments on commit 56a4b3e

Please sign in to comment.