From 0fbe9677b94f657bef625b2f0c42a165e0c9c841 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 4 Nov 2024 12:28:58 +0800 Subject: [PATCH] image api --- assets/init.js | 114 +++++++++++- lib/utils/image.dart | 228 ++++++++++++++++++++++++ linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 11 +- pubspec.yaml | 3 + windows/flutter/generated_plugins.cmake | 1 + 6 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 lib/utils/image.dart diff --git a/assets/init.js b/assets/init.js index 9f00eb3..b9d68cd 100644 --- a/assets/init.js +++ b/assets/init.js @@ -999,4 +999,116 @@ class ComicSource { init() { } static sources = {} -} \ No newline at end of file +} + +/// A reference to dart object. +/// The api can only be used in the comic.onImageLoad.modifyImage function +class Image { + key = 0; + + constructor(key) { + this.key = key; + } + + /** + * Copy the specified range of the image + * @param x + * @param y + * @param width + * @param height + * @returns {Image|null} + */ + copyRange(x, y, width, height) { + let key = sendMessage({ + method: "image", + function: "copyRange", + key: this.key, + x: x, + y: y, + width: width, + height: height + }) + if(key == null) return null; + return new Image(key); + } + + /** + * Copy the image and rotate 90 degrees + * @returns {Image|null} + */ + copyAndRotate90() { + let key = sendMessage({ + method: "image", + function: "copyAndRotate90", + key: this.key + }) + if(key == null) return null; + return new Image(key); + } + + /** + * fill [image] to this image at (x, y) + * @param x + * @param y + * @param image + */ + fillImageAt(x, y, image) { + sendMessage({ + method: "image", + function: "fillImageAt", + key: this.key, + x: x, + y: y, + image: image.key + }) + } + + /** + * Create a new image based on the current image without copying the data. + * Modifying the new image will affect the current image. + * @param x + * @param y + * @param width + * @param height + * @returns {Image|null} + */ + subImage(x, y, width, height) { + let key = sendMessage({ + method: "image", + function: "subImage", + key: this.key, + x: x, + y: y, + width: width, + height: height + }) + if(key == null) return null; + return new Image(key); + } + + get width() { + return sendMessage({ + method: "image", + function: "getWidth", + key: this.key + }) + } + + get height() { + return sendMessage({ + method: "image", + function: "getHeight", + key: this.key + }) + } + + static empty(width, height) { + let key = sendMessage({ + method: "image", + function: "emptyImage", + width: width, + height: height + }) + return new Image(key); + } +} diff --git a/lib/utils/image.dart b/lib/utils/image.dart new file mode 100644 index 0000000..d8b1411 --- /dev/null +++ b/lib/utils/image.dart @@ -0,0 +1,228 @@ +import 'dart:isolate'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/services.dart'; +import 'package:flutter_qjs/flutter_qjs.dart'; +import 'package:lodepng_flutter/lodepng_flutter.dart' as lodepng; + +class Image { + final Uint32List _data; + + final int width; + + final int height; + + Image(this._data, this.width, this.height); + + Image.empty(this.width, this.height) : _data = Uint32List(width * height); + + static Future decodeImage(Uint8List data) async { + var codec = await ui.instantiateImageCodec(data); + var frame = await codec.getNextFrame(); + codec.dispose(); + var info = await frame.image.toByteData(); + if (info == null) { + throw Exception('Failed to decode image'); + } + var image = Image( + Uint32List.fromList(info.buffer.asUint32List()), + frame.image.width, + frame.image.height, + ); + frame.image.dispose(); + return image; + } + + Image copyRange(int x, int y, int width, int height) { + var data = Uint32List(width * height); + for (var j = 0; j < height; j++) { + for (var i = 0; i < width; i++) { + data[j * width + i] = _data[(j + y) * this.width + i + x]; + } + } + return Image(data, width, height); + } + + void fillImageAt(int x, int y, Image image) { + for (var j = 0; j < image.height && (j + y) < height; j++) { + for (var i = 0; i < image.width && (i + x) < width; i++) { + _data[(j + y) * width + i + x] = image._data[j * image.width + i]; + } + } + } + + Image copyAndRotate90() { + var data = Uint32List(width * height); + for (var j = 0; j < height; j++) { + for (var i = 0; i < width; i++) { + data[i * height + height - j - 1] = _data[j * width + i]; + } + } + return Image(data, height, width); + } + + Color getPixel(int x, int y) { + return Color.fromValue(_data[y * width + x]); + } + + void setPixel(int x, int y, Color color) { + _data[y * width + x] = color.value; + } + + Image subImage(int x, int y, int width, int height) { + var data = Uint32List.sublistView( + _data, + y * this.width + x, + width * height, + ); + return Image(data, width, height); + } + + Uint8List encodePng() { + return lodepng.encodePng(lodepng.Image( + _data.buffer.asUint8List(), + width, + height, + )); + } +} + +class Color { + final int value; + + Color(int r, int g, int b, [int a = 255]) + : value = (a << 24) | (r << 16) | (g << 8) | b; + + Color.fromValue(this.value); + + int get r => (value >> 16) & 0xFF; + + int get g => (value >> 8) & 0xFF; + + int get b => value & 0xFF; + + int get a => (value >> 24) & 0xFF; +} + +class JsEngine { + static final JsEngine _instance = JsEngine._(); + + factory JsEngine() => _instance; + + JsEngine._() { + _engine = FlutterQjs(); + _engine!.dispatch(); + var setGlobalFunc = + _engine!.evaluate("(key, value) => { this[key] = value; }"); + (setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]); + setGlobalFunc.free(); + } + + FlutterQjs? _engine; + + dynamic runCode(String js, [String? name]) { + return _engine!.evaluate(js, name: name); + } + + var images = {}; + + int _key = 0; + + int setImage(Image image) { + var key = _key++; + images[key] = image; + return key; + } + + Object? _messageReceiver(dynamic message) { + if (message is! Map) return null; + var method = message['method']; + if (method == 'image') { + switch (message['function']) { + case 'copyRange': + var key = message['key']; + var image = images[key]; + if (image == null) return null; + var x = message['x']; + var y = message['y']; + var width = message['width']; + var height = message['height']; + var newImage = image.copyRange(x, y, width, height); + return setImage(newImage); + case 'copyAndRotate90': + var key = message['key']; + var image = images[key]; + if (image == null) return null; + var newImage = image.copyAndRotate90(); + return setImage(newImage); + case 'fillImageAt': + var key = message['key']; + var image = images[key]; + if (image == null) return null; + var x = message['x']; + var y = message['y']; + var key2 = message['image']; + var image2 = images[key2]; + if (image2 == null) return null; + image.fillImageAt(x, y, image2); + return null; + case 'subImage': + var key = message['key']; + var image = images[key]; + if (image == null) return null; + var x = message['x']; + var y = message['y']; + var width = message['width']; + var height = message['height']; + var newImage = image.subImage(x, y, width, height); + return setImage(newImage); + case 'getWidth': + var key = message['key']; + var image = images[key]; + if (image == null) return null; + return image.width; + case 'getHeight': + var key = message['key']; + var image = images[key]; + if (image == null) return null; + return image.height; + case 'emptyImage': + var width = message['width']; + var height = message['height']; + var newImage = Image.empty(width, height); + return setImage(newImage); + } + } + return null; + } +} + +var _tasksCount = 0; + +Future modifyImageWithScript(Uint8List data, String script) async { + while (_tasksCount > 3) { + await Future.delayed(const Duration(milliseconds: 200)); + } + _tasksCount++; + try { + var image = await Image.decodeImage(data); + var initJs = await rootBundle.loadString('assets/init.js'); + return await Isolate.run(() { + var jsEngine = JsEngine(); + jsEngine.runCode(initJs, ''); + jsEngine.runCode(script); + var key = jsEngine.setImage(image); + var res = jsEngine.runCode(''' + let image = new Image($key); + let result = modifyImage(image); + return result.key; + '''); + var newImage = jsEngine.images[res]; + var data = newImage!.encodePng(); + return data; + }); + } + finally { + _tasksCount--; + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index c0fb5f6..6558394 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + lodepng_flutter zip_flutter ) diff --git a/pubspec.lock b/pubspec.lock index 623cf1d..8ef4e04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -456,6 +456,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + lodepng_flutter: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "115b896a04c270a8d6d5d7bea09dcd04047bfad3" + url: "https://github.com/venera-app/lodepng_flutter" + source: git + version: "0.0.1" matcher: dependency: transitive description: @@ -849,5 +858,5 @@ packages: source: git version: "0.0.1" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.5.4 <4.0.0" flutter: ">=3.24.4" diff --git a/pubspec.yaml b/pubspec.yaml index fb39da7..6bbdd3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,9 @@ dependencies: zip_flutter: git: url: https://github.com/wgh136/zip_flutter + lodepng_flutter: + git: + url: https://github.com/venera-app/lodepng_flutter dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 693c576..ff62697 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + lodepng_flutter zip_flutter )