mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 00:11:01 +03:00
Preserve the macOS cursor wire alpha format for older controllers
This commit is contained in:
@@ -3459,18 +3459,11 @@ class CursorModel with ChangeNotifier {
|
||||
throw FormatException('Invalid cursor pixel ratio: $pixelRatio');
|
||||
}
|
||||
List<dynamic> colors = json.decode(evt['colors']);
|
||||
var rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
||||
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
||||
final ui.Image? image;
|
||||
if (isWeb &&
|
||||
pixelRatio > 0 &&
|
||||
if (!isWeb &&
|
||||
parent.target?.ffiModel.pi.platform == kPeerPlatformMacOS) {
|
||||
// macOS cursors with density metadata use premultiplied alpha; older
|
||||
// hosts send straight alpha. Web needs a straight-alpha resize source.
|
||||
(rgba, image) = await _decodeWebMacCursor(rgba, width, height);
|
||||
} else if (!isWeb &&
|
||||
pixelRatio == 0 &&
|
||||
parent.target?.ffiModel.pi.platform == kPeerPlatformMacOS) {
|
||||
image = await _decodeLegacyMacCursor(rgba, width, height);
|
||||
image = await _decodeMacCursor(rgba, width, height);
|
||||
} else {
|
||||
image = await img.decodeImageFromPixels(
|
||||
rgba, width, height, ui.PixelFormat.rgba8888);
|
||||
@@ -3491,10 +3484,10 @@ class CursorModel with ChangeNotifier {
|
||||
_updateCurData();
|
||||
}
|
||||
|
||||
Future<ui.Image?> _decodeLegacyMacCursor(
|
||||
Future<ui.Image?> _decodeMacCursor(
|
||||
Uint8List rgba, int width, int height) async {
|
||||
// Old macOS packets are straight alpha. Premultiply a copy for native
|
||||
// ui.Image, retaining the original colors for the separate byte cache.
|
||||
// macOS packets keep straight alpha, regardless of density metadata.
|
||||
// Premultiply only for native ui.Image; the PNG cache needs straight colors.
|
||||
final source = img2.Image.fromBytes(
|
||||
width: width, height: height, bytes: rgba.buffer, order: img2.ChannelOrder.rgba);
|
||||
for (final pixel in source) {
|
||||
@@ -3507,30 +3500,6 @@ class CursorModel with ChangeNotifier {
|
||||
source.getBytes(), width, height, ui.PixelFormat.rgba8888);
|
||||
}
|
||||
|
||||
Future<(Uint8List, ui.Image)> _decodeWebMacCursor(
|
||||
Uint8List rgba, int width, int height) async {
|
||||
final source = img2.Image.fromBytes(
|
||||
width: width, height: height, bytes: rgba.buffer, order: img2.ChannelOrder.rgba);
|
||||
for (final pixel in source) {
|
||||
final alpha = pixel.a;
|
||||
if (alpha == 0) continue;
|
||||
final maxChannel = pixel.maxChannelValue;
|
||||
// RGB and alpha rounding in capture can differ by one channel value.
|
||||
pixel.r = min(maxChannel, (pixel.r * maxChannel / alpha).round());
|
||||
pixel.g = min(maxChannel, (pixel.g * maxChannel / alpha).round());
|
||||
pixel.b = min(maxChannel, (pixel.b * maxChannel / alpha).round());
|
||||
}
|
||||
// Web ImageDescriptor.raw treats RGBA as straight alpha. Decode a PNG so
|
||||
// both the painted cursor and the resize source use the correct colors.
|
||||
final codec = await ui.instantiateImageCodec(
|
||||
Uint8List.fromList(img2.encodePng(source)));
|
||||
try {
|
||||
return (source.getBytes(), (await codec.getNextFrame()).image);
|
||||
} finally {
|
||||
codec.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _updateCache(
|
||||
Uint8List rgba,
|
||||
ui.Image image,
|
||||
|
||||
@@ -67,8 +67,8 @@ void main() {
|
||||
for (final testCase in <(String, String?, List<int>)>[
|
||||
(kPeerPlatformMacOS, null, [64, 32, 16, 128]),
|
||||
(kPeerPlatformMacOS, '0', [64, 32, 16, 128]),
|
||||
(kPeerPlatformMacOS, '1', [32, 16, 8, 128]),
|
||||
(kPeerPlatformMacOS, '2', [32, 16, 8, 128]),
|
||||
(kPeerPlatformMacOS, '1', [64, 32, 16, 128]),
|
||||
(kPeerPlatformMacOS, '2', [64, 32, 16, 128]),
|
||||
(kPeerPlatformLinux, '0', [32, 16, 8, 128]),
|
||||
]) {
|
||||
for (final dpr in [1.0, 2.0]) {
|
||||
|
||||
@@ -225,9 +225,9 @@ Future<void> _checkPolicy(WidgetTester tester, String style, double dpr) async {
|
||||
|
||||
void _alphaTests() {
|
||||
for (final (density, pixel, expected) in [
|
||||
(1.0, [128, 128, 128, 128], [255, 255, 255, 128]),
|
||||
(2.0, [128, 64, 32, 128], [255, 128, 64, 128]),
|
||||
(1.0, [113, 56, 28, 112], [255, 128, 64, 112]),
|
||||
(1.0, [255, 255, 255, 128], [255, 255, 255, 128]),
|
||||
(2.0, [255, 128, 64, 128], [255, 128, 64, 128]),
|
||||
(1.0, [255, 128, 64, 112], [255, 128, 64, 112]),
|
||||
(1.0, [0, 0, 0, 0], [0, 0, 0, 0]),
|
||||
(1.0, [255, 255, 255, 255], [255, 255, 255, 255]),
|
||||
// Old macOS hosts send straight alpha without density metadata.
|
||||
|
||||
@@ -677,9 +677,10 @@ fn unsafe_get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
let g: f64 = msg_send![color, greenComponent];
|
||||
let b: f64 = msg_send![color, blueComponent];
|
||||
let a: f64 = msg_send![color, alphaComponent];
|
||||
colors.push((r * a * 255.).round() as _);
|
||||
colors.push((g * a * 255.).round() as _);
|
||||
colors.push((b * a * 255.).round() as _);
|
||||
// Keep straight RGBA on the wire for older Web/Sciter receivers.
|
||||
colors.push((r * 255.) as _);
|
||||
colors.push((g * 255.) as _);
|
||||
colors.push((b * 255.) as _);
|
||||
colors.push((a * 255.) as _);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ pub(super) unsafe fn data(cursor: id, id: u64, scale: f64) -> ResultType<CursorD
|
||||
let hotspot: NSPoint = msg_send![cursor, hotSpot];
|
||||
Ok(CursorData {
|
||||
id,
|
||||
colors: slice::from_raw_parts(pixels, length).to_vec().into(),
|
||||
colors: straight_rgba(slice::from_raw_parts(pixels, length)).into(),
|
||||
hotx: (hotspot.x * size.width / logical.width).round() as _,
|
||||
hoty: (hotspot.y * size.height / logical.height).round() as _,
|
||||
width: size.width as _,
|
||||
@@ -117,6 +117,24 @@ pub(super) unsafe fn data(cursor: id, id: u64, scale: f64) -> ResultType<CursorD
|
||||
})
|
||||
}
|
||||
|
||||
fn straight_rgba(pixels: &[u8]) -> Vec<u8> {
|
||||
// AppKit renders premultiplied pixels, but macOS cursor packets have always
|
||||
// used straight alpha. Density metadata does not negotiate a new format.
|
||||
const MAX_CHANNEL: u16 = u8::MAX as u16;
|
||||
let mut colors = pixels.to_vec();
|
||||
for pixel in colors.chunks_exact_mut(CHANNELS) {
|
||||
let alpha = u16::from(pixel[CHANNELS - 1]);
|
||||
if alpha == 0 {
|
||||
continue;
|
||||
}
|
||||
for channel in &mut pixel[..CHANNELS - 1] {
|
||||
*channel =
|
||||
((u16::from(*channel) * MAX_CHANNEL + alpha / 2) / alpha).min(MAX_CHANNEL) as u8;
|
||||
}
|
||||
}
|
||||
colors
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -168,6 +186,55 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retina_cursor_keeps_straight_alpha_for_legacy_receivers() {
|
||||
const SIDE: usize = 4;
|
||||
const SCALE: f64 = 2.0;
|
||||
const PREMULTIPLIED: [[u8; CHANNELS]; SIDE] = [
|
||||
[128, 128, 128, 128],
|
||||
[64, 32, 16, 128],
|
||||
[240, 100, 20, 255],
|
||||
[0, 0, 0, 0],
|
||||
];
|
||||
const STRAIGHT: [[u8; CHANNELS]; SIDE] = [
|
||||
[255, 255, 255, 128],
|
||||
[128, 64, 32, 128],
|
||||
[240, 100, 20, 255],
|
||||
[0, 0, 0, 0],
|
||||
];
|
||||
autoreleasepool(|| unsafe {
|
||||
let logical = NSSize::new(SIDE as f64 / SCALE, SIDE as f64 / SCALE);
|
||||
let image: id = msg_send![class!(NSImage), alloc];
|
||||
let image = StrongPtr::new(msg_send![image, initWithSize: logical]);
|
||||
let rep = bitmap(NSSize::new(SIDE as f64, SIDE as f64)).unwrap();
|
||||
let pixels: Vec<u8> = (0..SIDE * SIDE)
|
||||
.flat_map(|index| PREMULTIPLIED[index % SIDE])
|
||||
.collect();
|
||||
let buffer: *mut u8 = msg_send![*rep, bitmapData];
|
||||
ptr::copy_nonoverlapping(pixels.as_ptr(), buffer, pixels.len());
|
||||
let (): () = msg_send![*rep, setSize: logical];
|
||||
let (): () = msg_send![*image, addRepresentation: *rep];
|
||||
let cursor: id = msg_send![class!(NSCursor), alloc];
|
||||
let cursor = StrongPtr::new(
|
||||
msg_send![cursor, initWithImage: *image hotSpot: NSPoint::new(1.0, 1.0)],
|
||||
);
|
||||
let result = data(*cursor, 1, SCALE).unwrap();
|
||||
// Older Sciter receivers encode the received bytes directly as PNG.
|
||||
let mut png = Vec::new();
|
||||
repng::encode(
|
||||
&mut png,
|
||||
result.width as _,
|
||||
result.height as _,
|
||||
&result.colors,
|
||||
)
|
||||
.unwrap();
|
||||
let decoded = image::load_from_memory(&png).unwrap().to_rgba8();
|
||||
for (index, pixel) in decoded.pixels().enumerate() {
|
||||
assert_eq!(pixel.0, STRAIGHT[index % SIDE]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_cache_changes_with_display_scale() {
|
||||
assert_ne!(cache_id(123, 1.0), cache_id(123, 2.0));
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
sync::{atomic::AtomicUsize, Arc, Mutex, RwLock},
|
||||
};
|
||||
|
||||
use sciter::{
|
||||
@@ -31,8 +28,6 @@ use crate::{
|
||||
|
||||
type Video = AssetPtr<video_destination>;
|
||||
|
||||
mod cursor;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref VIDEO: Arc<Mutex<Option<Video>>> = Default::default();
|
||||
}
|
||||
@@ -44,8 +39,6 @@ lazy_static::lazy_static! {
|
||||
pub struct SciterHandler {
|
||||
element: Arc<Mutex<Option<Element>>>,
|
||||
close_state: HashMap<String, String>,
|
||||
// The I/O session clones its UI handler, so share the peer's pixel format.
|
||||
peer_is_macos: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl SciterHandler {
|
||||
@@ -129,7 +122,7 @@ impl SciterHandler {
|
||||
|
||||
impl InvokeUiSession for SciterHandler {
|
||||
fn set_cursor_data(&self, cd: CursorData) {
|
||||
let mut colors = cursor::png_colors(&cd, self.peer_is_macos.load(Ordering::Relaxed));
|
||||
let mut colors = hbb_common::compress::decompress(&cd.colors);
|
||||
if colors.iter().filter(|x| **x != 0).next().is_none() {
|
||||
log::info!("Fix transparent");
|
||||
// somehow all 0 images shows black rect, here is a workaround
|
||||
@@ -308,8 +301,6 @@ impl InvokeUiSession for SciterHandler {
|
||||
}
|
||||
|
||||
fn set_peer_info(&self, pi: &PeerInfo) {
|
||||
self.peer_is_macos
|
||||
.store(pi.platform == "Mac OS", Ordering::Relaxed);
|
||||
let mut pi_sciter = Value::map();
|
||||
pi_sciter.set_item("username", pi.username.clone());
|
||||
pi_sciter.set_item("hostname", pi.hostname.clone());
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
use base::message_proto::CursorData;
|
||||
|
||||
const CHANNELS: usize = 4;
|
||||
const ALPHA: usize = CHANNELS - 1;
|
||||
const MAX_CHANNEL: u16 = u8::MAX as u16;
|
||||
|
||||
pub(super) fn png_colors(cursor: &CursorData, is_macos: bool) -> Vec<u8> {
|
||||
let mut colors = hbb_common::compress::decompress(&cursor.colors);
|
||||
// New macOS packets are premultiplied for Flutter, but PNG stores straight
|
||||
// colors. Legacy macOS packets (scale zero) already contain straight RGBA.
|
||||
if !(is_macos && cursor.scale > 0.0) {
|
||||
return colors;
|
||||
}
|
||||
for pixel in colors.chunks_exact_mut(CHANNELS) {
|
||||
let alpha = u16::from(pixel[ALPHA]);
|
||||
if alpha == 0 {
|
||||
continue;
|
||||
}
|
||||
for channel in &mut pixel[..ALPHA] {
|
||||
// Capture can round RGB and alpha differently by one channel value.
|
||||
*channel =
|
||||
((u16::from(*channel) * MAX_CHANNEL + alpha / 2) / alpha).min(MAX_CHANNEL) as u8;
|
||||
}
|
||||
}
|
||||
colors
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const STRAIGHT: [u8; 16] = [
|
||||
255, 255, 255, 128, 128, 64, 32, 128, 240, 100, 20, 255, 0, 0, 0, 0,
|
||||
];
|
||||
const PREMULTIPLIED: [u8; 16] = [
|
||||
128, 128, 128, 128, 64, 32, 16, 128, 240, 100, 20, 255, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
fn encode_packet(pixels: &[u8], scale: f64, is_macos: bool) -> Vec<u8> {
|
||||
let cursor = CursorData {
|
||||
width: 4,
|
||||
height: 1,
|
||||
colors: hbb_common::compress::compress(pixels).into(),
|
||||
scale,
|
||||
..Default::default()
|
||||
};
|
||||
let colors = png_colors(&cursor, is_macos);
|
||||
let mut png = Vec::new();
|
||||
repng::encode(&mut png, cursor.width as _, cursor.height as _, &colors).unwrap();
|
||||
assert_eq!(hbb_common::compress::decompress(&cursor.colors), pixels);
|
||||
image::load_from_memory_with_format(&png, image::ImageFormat::Png)
|
||||
.unwrap()
|
||||
.to_rgba8()
|
||||
.into_raw()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_density_packets_encode_straight_png_colors() {
|
||||
for scale in [1.0, 2.0] {
|
||||
let actual = encode_packet(&PREMULTIPLIED, scale, true);
|
||||
for (got, expected) in actual
|
||||
.chunks_exact(CHANNELS)
|
||||
.zip(STRAIGHT.chunks_exact(CHANNELS))
|
||||
{
|
||||
assert_eq!(got[ALPHA], expected[ALPHA]);
|
||||
for channel in 0..ALPHA {
|
||||
assert!(got[channel].abs_diff(expected[channel]) <= 1, "{actual:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_and_non_macos_packets_keep_their_colors() {
|
||||
for (scale, is_macos) in [(0.0, true), (0.0, false), (2.0, false)] {
|
||||
assert_eq!(encode_packet(&STRAIGHT, scale, is_macos), STRAIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user