mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 08:21:02 +03:00
fix(cursor): decode predefined PNG images with correct alpha
Use Flutter's PNG codec before native rasterization and retain the straight-alpha image cache. Cover both bundled assets through PNG and Windows BGRA registration.
This commit is contained in:
@@ -2985,7 +2985,8 @@ class PredefinedCursor {
|
|||||||
CursorData? get cache => _cache;
|
CursorData? get cache => _cache;
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
_image2 = img2.decodePng(base64Decode(png));
|
final pngBytes = base64Decode(png);
|
||||||
|
_image2 = img2.decodePng(pngBytes);
|
||||||
if (_image2 != null) {
|
if (_image2 != null) {
|
||||||
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
||||||
if (id == kPreForbiddenCursorId) {
|
if (id == kPreForbiddenCursorId) {
|
||||||
@@ -2993,18 +2994,18 @@ class PredefinedCursor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
() async {
|
() async {
|
||||||
final defaultImg = _image2!;
|
|
||||||
// This function is called only one time, no need to care about the performance.
|
|
||||||
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
|
|
||||||
_image?.dispose();
|
_image?.dispose();
|
||||||
final nativeImage = await img.decodeImageFromPixels(
|
// PNG stores straight alpha; let the codec prepare the pixels for ui.Image.
|
||||||
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
final codec = await ui.instantiateImageCodec(pngBytes);
|
||||||
_image = nativeImage;
|
final ui.Image nativeImage;
|
||||||
if (nativeImage == null) {
|
try {
|
||||||
print("decodeImageFromPixels failed, pre-defined cursor $id");
|
nativeImage = (await codec.getNextFrame()).image;
|
||||||
return;
|
} finally {
|
||||||
|
codec.dispose();
|
||||||
}
|
}
|
||||||
|
_image = nativeImage;
|
||||||
double scale = 1.0;
|
double scale = 1.0;
|
||||||
|
final Uint8List data;
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
@@ -142,6 +143,13 @@ void main() {
|
|||||||
view.resetDevicePixelRatio();
|
view.resetDevicePixelRatio();
|
||||||
binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
|
binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
|
||||||
});
|
});
|
||||||
|
for (final forbidden in [false, true]) {
|
||||||
|
test('predefined cursor forbidden=$forbidden preserves native RGBA', () {
|
||||||
|
view.devicePixelRatio = 1;
|
||||||
|
return _checkPredefinedCursor(
|
||||||
|
forbidden ? preForbiddenCursor : preDefaultCursor, registrations);
|
||||||
|
});
|
||||||
|
}
|
||||||
for (final testCase in [
|
for (final testCase in [
|
||||||
(kRemoteViewStyleAdaptive, false, 0, 0.25, windows ? 1.0 : 4 / 3),
|
(kRemoteViewStyleAdaptive, false, 0, 0.25, windows ? 1.0 : 4 / 3),
|
||||||
(kRemoteViewStyleCustom, false, 0, 0.25, windows ? 1.0 : 4 / 3),
|
(kRemoteViewStyleCustom, false, 0, 0.25, windows ? 1.0 : 4 / 3),
|
||||||
@@ -226,3 +234,49 @@ Future<void> _checkDprChange(
|
|||||||
cursor.dispose();
|
cursor.dispose();
|
||||||
canvas.dispose();
|
canvas.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkPredefinedCursor(PredefinedCursor predefined,
|
||||||
|
List<Map<dynamic, dynamic>> registrations) async {
|
||||||
|
await Future.doWhile(() async {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||||
|
return predefined.cache == null;
|
||||||
|
}).timeout(const Duration(seconds: 5));
|
||||||
|
final cache = predefined.cache!;
|
||||||
|
final original = img
|
||||||
|
.decodePng(base64Decode(predefined.png))!
|
||||||
|
.convert(format: img.Format.uint8, numChannels: 4);
|
||||||
|
final canvas = _Canvas(1, style: kRemoteViewStyleAdaptive, scale: 1);
|
||||||
|
final cursor = _Cursor(cache, _FFI(canvas));
|
||||||
|
buildCursorOfCache(cursor, 1, cache);
|
||||||
|
await deleteCustomCursor(cursor.cachedKeys.single);
|
||||||
|
cursor.dispose();
|
||||||
|
canvas.dispose();
|
||||||
|
|
||||||
|
final args = registrations.single;
|
||||||
|
final bytes = args['buffer'] as Uint8List;
|
||||||
|
final decoded = Platform.isWindows
|
||||||
|
? img.Image.fromBytes(
|
||||||
|
width: args['width'] as int,
|
||||||
|
height: args['height'] as int,
|
||||||
|
bytes: bytes.buffer,
|
||||||
|
bytesOffset: bytes.offsetInBytes,
|
||||||
|
order: img.ChannelOrder.bgra)
|
||||||
|
: img.decodePng(bytes)!;
|
||||||
|
expect((decoded.width, decoded.height), (original.width, original.height));
|
||||||
|
expect(args['imagePixelRatio'], 1.0);
|
||||||
|
expect((args['hotX'], args['hotY']), (cache.hotxOrigin, cache.hotyOrigin));
|
||||||
|
expect(cache.image.getBytes(), original.getBytes());
|
||||||
|
// Actual bundled pixels cover transparent, opaque and translucent colors.
|
||||||
|
for (final (x, y) in [(0, 0), (1, 8), (13, 22), (16, 16)]) {
|
||||||
|
final expected = original.getPixel(x, y);
|
||||||
|
final actual = decoded.getPixel(x, y);
|
||||||
|
expect(actual.a, expected.a);
|
||||||
|
for (final (got, want) in [
|
||||||
|
(actual.r, expected.r),
|
||||||
|
(actual.g, expected.g),
|
||||||
|
(actual.b, expected.b)
|
||||||
|
]) {
|
||||||
|
expect(got, closeTo(want, 1), reason: '${predefined.id} at ($x, $y)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user