mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 00:11:01 +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;
|
||||
|
||||
init() {
|
||||
_image2 = img2.decodePng(base64Decode(png));
|
||||
final pngBytes = base64Decode(png);
|
||||
_image2 = img2.decodePng(pngBytes);
|
||||
if (_image2 != null) {
|
||||
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
||||
if (id == kPreForbiddenCursorId) {
|
||||
@@ -2993,18 +2994,18 @@ class PredefinedCursor {
|
||||
}
|
||||
|
||||
() 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();
|
||||
final nativeImage = await img.decodeImageFromPixels(
|
||||
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
||||
_image = nativeImage;
|
||||
if (nativeImage == null) {
|
||||
print("decodeImageFromPixels failed, pre-defined cursor $id");
|
||||
return;
|
||||
// PNG stores straight alpha; let the codec prepare the pixels for ui.Image.
|
||||
final codec = await ui.instantiateImageCodec(pngBytes);
|
||||
final ui.Image nativeImage;
|
||||
try {
|
||||
nativeImage = (await codec.getNextFrame()).image;
|
||||
} finally {
|
||||
codec.dispose();
|
||||
}
|
||||
_image = nativeImage;
|
||||
double scale = 1.0;
|
||||
final Uint8List data;
|
||||
if (isWindows) {
|
||||
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
@@ -142,6 +143,13 @@ void main() {
|
||||
view.resetDevicePixelRatio();
|
||||
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 [
|
||||
(kRemoteViewStyleAdaptive, 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();
|
||||
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