improve image api & update version code

This commit is contained in:
2024-11-05 13:13:32 +08:00
parent 07f8f2a4af
commit b49e528ff4
9 changed files with 231 additions and 107 deletions

View File

@@ -224,7 +224,25 @@ let Convert = {
key: key, key: key,
isEncode: false isEncode: false
}); });
} },
/** Encode bytes to hex string
* @param bytes {ArrayBuffer}
* @return {string}
*/
hexEncode: (bytes) => {
const hexDigits = '0123456789abcdef';
const view = new Uint8Array(bytes);
let charCodes = new Uint8Array(view.length * 2);
let j = 0;
for (let i = 0; i < view.length; i++) {
let byte = view[i];
charCodes[j++] = hexDigits.charCodeAt((byte >> 4) & 0xF);
charCodes[j++] = hexDigits.charCodeAt(byte & 0xF);
}
return String.fromCharCode(...charCodes);
},
} }
/** /**
@@ -1064,26 +1082,28 @@ class Image {
} }
/** /**
* Create a new image based on the current image without copying the data. * fill [image] with range(srcX, srcY, width, height) to this image at (x, y)
* Modifying the new image will affect the current image.
* @param x * @param x
* @param y * @param y
* @param image
* @param srcX
* @param srcY
* @param width * @param width
* @param height * @param height
* @returns {Image|null}
*/ */
subImage(x, y, width, height) { fillImageRangeAt(x, y, image, srcX, srcY, width, height) {
let key = sendMessage({ sendMessage({
method: "image", method: "image",
function: "subImage", function: "fillImageRangeAt",
key: this.key, key: this.key,
x: x, x: x,
y: y, y: y,
image: image.key,
srcX: srcX,
srcY: srcY,
width: width, width: width,
height: height height: height
}) })
if(key == null) return null;
return new Image(key);
} }
get width() { get width() {

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.0.1"; final version = "1.0.2";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -87,17 +87,16 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
return await decode(buffer); return await decode(buffer);
} catch (e) { } catch (e) {
await CacheManager().delete(this.key); await CacheManager().delete(this.key);
Object error = e;
if (data.length < 2 * 1024) { if (data.length < 2 * 1024) {
// data is too short, it's likely that the data is text, not image // data is too short, it's likely that the data is text, not image
try { try {
var text = const Utf8Codec(allowMalformed: false).decoder.convert(data); var text = const Utf8Codec(allowMalformed: false).decoder.convert(data);
error = Exception("Expected image data, but got text: $text"); throw Exception("Expected image data, but got text: $text");
} catch (e) { } catch (e) {
// ignore // ignore
} }
} }
throw error; rethrow;
} }
} catch (e) { } catch (e) {
scheduleMicrotask(() { scheduleMicrotask(() {

View File

@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/consts.dart';
import 'package:venera/utils/image.dart';
import 'app_dio.dart'; import 'app_dio.dart';
@@ -27,8 +28,8 @@ class ImageDownloader {
configs = comicSource?.getThumbnailLoadingConfig?.call(url) ?? {}; configs = comicSource?.getThumbnailLoadingConfig?.call(url) ?? {};
} }
configs['headers'] ??= {}; configs['headers'] ??= {};
if(configs['headers']['user-agent'] == null if (configs['headers']['user-agent'] == null &&
&& configs['headers']['User-Agent'] == null) { configs['headers']['User-Agent'] == null) {
configs['headers']['user-agent'] = webUA; configs['headers']['user-agent'] = webUA;
} }
@@ -120,11 +121,22 @@ class ImageDownloader {
buffer = configs['onResponse'](buffer); buffer = configs['onResponse'](buffer);
} }
await CacheManager().writeCache(cacheKey, buffer); var data = Uint8List.fromList(buffer);
buffer.clear();
if (configs['modifyImage'] != null) {
var newData = await modifyImageWithScript(
data,
configs['modifyImage'],
);
data = newData;
}
await CacheManager().writeCache(cacheKey, data);
yield ImageDownloadProgress( yield ImageDownloadProgress(
currentBytes: buffer.length, currentBytes: data.length,
totalBytes: buffer.length, totalBytes: data.length,
imageBytes: Uint8List.fromList(buffer), imageBytes: data,
); );
} }
} }

View File

@@ -161,71 +161,76 @@ class _BodyState extends State<_Body> {
for (var item in source.settings!.entries) { for (var item in source.settings!.entries) {
var key = item.key; var key = item.key;
String type = item.value['type']; String type = item.value['type'];
if (type == "select") { try {
var current = source.data['settings'][key]; if (type == "select") {
if (current == null) { var current = source.data['settings'][key];
var d = item.value['default']; if (current == null) {
for (var option in item.value['options']) { var d = item.value['default'];
if (option['value'] == d) { for (var option in item.value['options']) {
current = option['text'] ?? option['value']; if (option['value'] == d) {
break; current = option['text'] ?? option['value'];
break;
}
} }
} }
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Select(
current: (current as String).ts(source.key),
values: (item.value['options'] as List)
.map<String>(
(e) => ((e['text'] ?? e['value']) as String).ts(source.key))
.toList(),
onTap: (i) {
source.data['settings'][key] = item.value['options'][i]['value'];
source.saveData();
setState(() {});
},
),
);
} else if (type == "switch") {
var current = source.data['settings'][key] ?? item.value['default'];
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Switch(
value: current,
onChanged: (v) {
source.data['settings'][key] = v;
source.saveData();
setState(() {});
},
),
);
} else if (type == "input") {
var current =
source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
subtitle: Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
showInputDialog(
context: context,
title: (item.value['title'] as String).ts(source.key),
initialValue: current,
inputValidator: item.value['validator'] == null
? null
: RegExp(item.value['validator']),
onConfirm: (value) {
source.data['settings'][key] = value;
source.saveData();
setState(() {});
return null;
},
);
},
),
);
} }
yield ListTile( }
title: Text((item.value['title'] as String).ts(source.key)), catch(e, s) {
trailing: Select( Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
current: (current as String).ts(source.key),
values: (item.value['options'] as List)
.map<String>(
(e) => ((e['text'] ?? e['value']) as String).ts(source.key))
.toList(),
onTap: (i) {
source.data['settings'][key] = item.value['options'][i]['value'];
source.saveData();
setState(() {});
},
),
);
} else if (type == "switch") {
var current = source.data['settings'][key] ?? item.value['default'];
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Switch(
value: current,
onChanged: (v) {
source.data['settings'][key] = v;
source.saveData();
setState(() {});
},
),
);
} else if (type == "input") {
var current =
source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
subtitle: Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
showInputDialog(
context: context,
title: (item.value['title'] as String).ts(source.key),
initialValue: current,
inputValidator: item.value['validator'] == null
? null
: RegExp(item.value['validator']),
onConfirm: (value) {
source.data['settings'][key] = value;
source.saveData();
setState(() {});
return null;
},
);
},
),
);
} }
} }
} }

View File

@@ -389,7 +389,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
Widget buildPageInfoText() { Widget buildPageInfoText() {
var epName = context.reader.widget.chapters?.values var epName = context.reader.widget.chapters?.values
.elementAt(context.reader.chapter - 1) ?? .elementAtOrNull(context.reader.chapter - 1) ??
"E${context.reader.chapter}"; "E${context.reader.chapter}";
if (epName.length > 8) { if (epName.length > 8) {
epName = "${epName.substring(0, 8)}..."; epName = "${epName.substring(0, 8)}...";

View File

@@ -1,3 +1,4 @@
import 'dart:ffi';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
@@ -12,7 +13,12 @@ class Image {
final int height; final int height;
Image(this._data, this.width, this.height); Image(this._data, this.width, this.height) {
if (_data.length != width * height) {
throw ArgumentError(
'Invalid argument: data length must be equal to width * height.');
}
}
Image.empty(this.width, this.height) : _data = Uint32List(width * height); Image.empty(this.width, this.height) : _data = Uint32List(width * height);
@@ -25,7 +31,7 @@ class Image {
throw Exception('Failed to decode image'); throw Exception('Failed to decode image');
} }
var image = Image( var image = Image(
Uint32List.fromList(info.buffer.asUint32List()), info.buffer.asUint32List(),
frame.image.width, frame.image.width,
frame.image.height, frame.image.height,
); );
@@ -34,6 +40,20 @@ class Image {
} }
Image copyRange(int x, int y, int width, int height) { Image copyRange(int x, int y, int width, int height) {
if (width + x > this.width) {
throw ArgumentError('''
Invalid argument: x + width must be less than or equal to the image width.
x: $x, width: $width, image width: ${this.width}
'''
.trim());
}
if (height + y > this.height) {
throw ArgumentError('''
Invalid argument: y + height must be less than or equal to the image height.
y: $y, height: $height, image height: ${this.height}
'''
.trim());
}
var data = Uint32List(width * height); var data = Uint32List(width * height);
for (var j = 0; j < height; j++) { for (var j = 0; j < height; j++) {
for (var i = 0; i < width; i++) { for (var i = 0; i < width; i++) {
@@ -44,6 +64,20 @@ class Image {
} }
void fillImageAt(int x, int y, Image image) { void fillImageAt(int x, int y, Image image) {
if (x + image.width > width) {
throw ArgumentError('''
Invalid argument: x + image width must be less than or equal to the image width.
x: $x, image width: ${image.width}, image width: $width
'''
.trim());
}
if (y + image.height > height) {
throw ArgumentError('''
Invalid argument: y + image height must be less than or equal to the image height.
y: $y, image height: ${image.height}, image height: $height
'''
.trim());
}
for (var j = 0; j < image.height && (j + y) < height; j++) { for (var j = 0; j < image.height && (j + y) < height; j++) {
for (var i = 0; i < image.width && (i + x) < width; i++) { for (var i = 0; i < image.width && (i + x) < width; i++) {
_data[(j + y) * width + i + x] = image._data[j * image.width + i]; _data[(j + y) * width + i + x] = image._data[j * image.width + i];
@@ -51,6 +85,44 @@ class Image {
} }
} }
void fillImageRangeAt(
int x, int y, Image image, int srcX, int srcY, int width, int height) {
if (x + width > this.width) {
throw ArgumentError('''
Invalid argument: x + width must be less than or equal to the image width.
x: $x, width: $width, image width: ${this.width}
'''
.trim());
}
if (y + height > this.height) {
throw ArgumentError('''
Invalid argument: y + height must be less than or equal to the image height.
y: $y, height: $height, image height: ${this.height}
'''
.trim());
}
if (srcX + width > image.width) {
throw ArgumentError('''
Invalid argument: srcX + width must be less than or equal to the image width.
srcX: $srcX, width: $width, image width: ${image.width}
'''
.trim());
}
if (srcY + height > image.height) {
throw ArgumentError('''
Invalid argument: srcY + height must be less than or equal to the image height.
srcY: $srcY, height: $height, image height: ${image.height}
'''
.trim());
}
for (var j = 0; j < height; j++) {
for (var i = 0; i < width; i++) {
_data[(j + y) * this.width + i + x] =
image._data[(j + srcY) * image.width + i + srcX];
}
}
}
Image copyAndRotate90() { Image copyAndRotate90() {
var data = Uint32List(width * height); var data = Uint32List(width * height);
for (var j = 0; j < height; j++) { for (var j = 0; j < height; j++) {
@@ -62,28 +134,37 @@ class Image {
} }
Color getPixel(int x, int y) { Color getPixel(int x, int y) {
if (x < 0 || x >= width) {
throw ArgumentError(
'Invalid argument: x must be in the range of [0, $width).');
}
if (y < 0 || y >= height) {
throw ArgumentError(
'Invalid argument: y must be in the range of [0, $height).');
}
return Color.fromValue(_data[y * width + x]); return Color.fromValue(_data[y * width + x]);
} }
void setPixel(int x, int y, Color color) { void setPixel(int x, int y, Color color) {
if (x < 0 || x >= width) {
throw ArgumentError(
'Invalid argument: x must be in the range of [0, $width).');
}
if (y < 0 || y >= height) {
throw ArgumentError(
'Invalid argument: y must be in the range of [0, $height).');
}
_data[y * width + x] = color.value; _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() { Uint8List encodePng() {
return lodepng.encodePng(lodepng.Image( var data = lodepng.encodePngToPointer(lodepng.Image(
_data.buffer.asUint8List(), _data.buffer.asUint8List(),
width, width,
height, height,
)); ));
return Pointer<Uint8>.fromAddress(data.address).asTypedList(data.length,
finalizer: lodepng.ByteBuffer.finalizer);
} }
} }
@@ -166,16 +247,21 @@ class JsEngine {
if (image2 == null) return null; if (image2 == null) return null;
image.fillImageAt(x, y, image2); image.fillImageAt(x, y, image2);
return null; return null;
case 'subImage': case 'fillImageRangeAt':
var key = message['key']; var key = message['key'];
var image = images[key]; var image = images[key];
if (image == null) return null; if (image == null) return null;
var x = message['x']; var x = message['x'];
var y = message['y']; var y = message['y'];
var key2 = message['image'];
var image2 = images[key2];
if (image2 == null) return null;
var srcX = message['srcX'];
var srcY = message['srcY'];
var width = message['width']; var width = message['width'];
var height = message['height']; var height = message['height'];
var newImage = image.subImage(x, y, width, height); image.fillImageRangeAt(x, y, image2, srcX, srcY, width, height);
return setImage(newImage); return null;
case 'getWidth': case 'getWidth':
var key = message['key']; var key = message['key'];
var image = images[key]; var image = images[key];
@@ -213,16 +299,18 @@ Future<Uint8List> modifyImageWithScript(Uint8List data, String script) async {
jsEngine.runCode(script); jsEngine.runCode(script);
var key = jsEngine.setImage(image); var key = jsEngine.setImage(image);
var res = jsEngine.runCode(''' var res = jsEngine.runCode('''
let image = new Image($key); let func = () => {
let result = modifyImage(image); let image = new Image($key);
return result.key; let result = modifyImage(image);
'''); return result.key;
}
func();
''');
var newImage = jsEngine.images[res]; var newImage = jsEngine.images[res];
var data = newImage!.encodePng(); var data = newImage!.encodePng();
return data; return Uint8List.fromList(data);
}); });
} } finally {
finally {
_tasksCount--; _tasksCount--;
} }
} }

View File

@@ -461,7 +461,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "115b896a04c270a8d6d5d7bea09dcd04047bfad3" resolved-ref: "5223cf4ce8aad1c2315db0093db3cc5c6c7191a8"
url: "https://github.com/venera-app/lodepng_flutter" url: "https://github.com/venera-app/lodepng_flutter"
source: git source: git
version: "0.0.1" version: "0.0.1"

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.0.1+101 version: 1.0.2+102
environment: environment:
sdk: '>=3.5.0 <4.0.0' sdk: '>=3.5.0 <4.0.0'