ironrdp_graphics/
pointer.rs

1//! This module implements logic to decode pointer PDUs into RGBA bitmaps ready for rendering.
2//!
3//! # References:
4//! - Drawing pointers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/display/pointer-drawing>
5//! - Drawing color pointers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/display/drawing-color-pointers>
6//! - Drawing monochrome pointers <https://learn.microsoft.com/en-us/windows-hardware/drivers/display/drawing-monochrome-pointers>
7//!
8//!
9//! # Notes on xor/and masks encoding:
10//! RDP's pointer representation is a bit weird. It uses two masks to represent a pointer -
11//! andMask and xorMask. Xor mask is used as a base color for a pointer pixel, and andMask
12//! mask is used co control pixel's full transparency (`src_color.a = 0`), full opacity
13//! (`src_color.a = 255`) or pixel inversion (`dst_color.rgb = vec3(255) - dst_color.rgb`).
14//!
15//! Xor basks could be 1, 8, 16, 24 or 32 bits per pixel, and andMask is always 1 bit per pixel.
16//!
17//! Rules for decoding masks:
18//! - `andMask == 0` -> dst_color Copy pixel from xorMask
19//! - andMask == 1, xorMask == 0(black color) -> Transparent pixel
20//! - andMask == 1, xorMask == 1(white color) -> Pixel is inverted
21
22use ironrdp_core::ReadCursor;
23use ironrdp_pdu::pointer::{ColorPointerAttribute, LargePointerAttribute, PointerAttribute};
24
25use crate::color_conversion::rdp_16bit_to_rgb;
26
27const SUPPORTED_COLOR_BPP: [u16; 4] = [1, 16, 24, 32];
28
29#[derive(Debug)]
30pub enum PointerError {
31    InvalidXorMaskSize { expected: usize, actual: usize },
32    InvalidAndMaskSize { expected: usize, actual: usize },
33    NotSupportedBpp { bpp: u16 },
34    Pdu(ironrdp_pdu::PduError),
35}
36
37impl core::fmt::Display for PointerError {
38    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39        match self {
40            PointerError::InvalidXorMaskSize { expected, actual } => {
41                write!(
42                    f,
43                    "invalid pointer xorMask size. Expected: {expected}, actual: {actual}"
44                )
45            }
46            PointerError::InvalidAndMaskSize { expected, actual } => {
47                write!(
48                    f,
49                    "invalid pointer andMask size. Expected: {expected}, actual: {actual}"
50                )
51            }
52            PointerError::NotSupportedBpp { bpp } => {
53                write!(f, "not supported pointer bpp: {bpp}")
54            }
55            PointerError::Pdu(err) => err.fmt(f),
56        }
57    }
58}
59
60impl core::error::Error for PointerError {
61    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
62        match self {
63            PointerError::InvalidXorMaskSize { .. } => None,
64            PointerError::InvalidAndMaskSize { .. } => None,
65            PointerError::NotSupportedBpp { .. } => None,
66            PointerError::Pdu(error) => error.source(),
67        }
68    }
69}
70
71impl From<ironrdp_pdu::PduError> for PointerError {
72    fn from(error: ironrdp_pdu::PduError) -> Self {
73        PointerError::Pdu(error)
74    }
75}
76
77/// Represents RDP pointer in decoded form (color channels stored as RGBA pre-multiplied values)
78#[derive(Debug)]
79pub struct DecodedPointer {
80    pub width: u16,
81    pub height: u16,
82    pub hotspot_x: u16,
83    pub hotspot_y: u16,
84    pub bitmap_data: Vec<u8>,
85}
86
87/// Pointer bitmap rendering target. Defines properties and format of the decoded bitmap.
88#[derive(Clone, Copy, Debug)]
89pub enum PointerBitmapTarget {
90    /// Software rendering target will produce RGBA bitmaps with premultiplied alpha.
91    ///
92    /// Colors with alpha channel set to 0x00 are always invisible no matter their color
93    /// component. We could take advantage of that, and use a special color to represent
94    /// inverted pixels. [0xFF, 0xFF, 0xFF, 0x00] is used for such purpose in software
95    /// rendering mode.
96    Software,
97    /// Accelerated rendering target will produce RGBA bitmaps with non-premultiplied alpha.
98    /// Inverted pixels will be rendered following the check pattern.
99    Accelerated,
100}
101
102impl PointerBitmapTarget {
103    fn should_premultiply_alpha(self) -> bool {
104        match self {
105            Self::Software => true,
106            Self::Accelerated => false,
107        }
108    }
109
110    fn should_invert_pixels_using_check_pattern(self) -> bool {
111        match self {
112            Self::Software => false,
113            Self::Accelerated => true,
114        }
115    }
116}
117
118impl DecodedPointer {
119    pub fn new_invisible() -> Self {
120        Self {
121            width: 0,
122            height: 0,
123            bitmap_data: Vec::new(),
124            hotspot_x: 0,
125            hotspot_y: 0,
126        }
127    }
128
129    pub fn decode_pointer_attribute(
130        src: &PointerAttribute<'_>,
131        target: PointerBitmapTarget,
132    ) -> Result<Self, PointerError> {
133        Self::decode_pointer(
134            PointerData {
135                width: src.color_pointer.width,
136                height: src.color_pointer.height,
137                xor_bpp: src.xor_bpp,
138                xor_mask: src.color_pointer.xor_mask,
139                and_mask: src.color_pointer.and_mask,
140                hot_spot_x: src.color_pointer.hot_spot.x,
141                hot_spot_y: src.color_pointer.hot_spot.y,
142            },
143            target,
144        )
145    }
146
147    pub fn decode_color_pointer_attribute(
148        src: &ColorPointerAttribute<'_>,
149        target: PointerBitmapTarget,
150    ) -> Result<Self, PointerError> {
151        Self::decode_pointer(
152            PointerData {
153                width: src.width,
154                height: src.height,
155                xor_bpp: 24,
156                xor_mask: src.xor_mask,
157                and_mask: src.and_mask,
158                hot_spot_x: src.hot_spot.x,
159                hot_spot_y: src.hot_spot.y,
160            },
161            target,
162        )
163    }
164
165    pub fn decode_large_pointer_attribute(
166        src: &LargePointerAttribute<'_>,
167        target: PointerBitmapTarget,
168    ) -> Result<Self, PointerError> {
169        Self::decode_pointer(
170            PointerData {
171                width: src.width,
172                height: src.height,
173                xor_bpp: src.xor_bpp,
174                xor_mask: src.xor_mask,
175                and_mask: src.and_mask,
176                hot_spot_x: src.hot_spot.x,
177                hot_spot_y: src.hot_spot.y,
178            },
179            target,
180        )
181    }
182
183    fn decode_pointer(data: PointerData<'_>, target: PointerBitmapTarget) -> Result<Self, PointerError> {
184        if data.width == 0 || data.height == 0 {
185            return Ok(Self::new_invisible());
186        }
187
188        if !SUPPORTED_COLOR_BPP.contains(&data.xor_bpp) {
189            // 8bpp indexed colors are not supported yet (palette messages are not implemented)
190            // Other unknown bpps are not supported either
191            return Err(PointerError::NotSupportedBpp { bpp: data.xor_bpp });
192        }
193
194        let flip_vertical = data.xor_bpp != 1;
195
196        let and_stride = Stride::from_bits(data.width.into());
197        let xor_stride = Stride::from_bits(usize::from(data.width) * usize::from(data.xor_bpp));
198
199        if data.xor_mask.len() != xor_stride.length * usize::from(data.height) {
200            return Err(PointerError::InvalidXorMaskSize {
201                expected: xor_stride.length * usize::from(data.height),
202                actual: data.xor_mask.len(),
203            });
204        }
205
206        let default_and_mask = vec![0x00; and_stride.length * usize::from(data.height)];
207        let mut and_mask = data.and_mask;
208        if and_mask.is_empty() {
209            and_mask = &default_and_mask;
210        } else if and_mask.len() != and_stride.length * usize::from(data.height) {
211            return Err(PointerError::InvalidAndMaskSize {
212                expected: and_stride.length * usize::from(data.height),
213                actual: data.and_mask.len(),
214            });
215        }
216
217        let mut bitmap_data = Vec::new();
218
219        for row_idx in 0..data.height {
220            // For non-monochrome cursors we read strides from bottom to top
221            let (mut xor_stride_cursor, mut and_stride_cursor) = if flip_vertical {
222                let xor_stride_cursor =
223                    ReadCursor::new(&data.xor_mask[usize::from(data.height - row_idx - 1) * xor_stride.length..]);
224                let and_stride_cursor =
225                    ReadCursor::new(&and_mask[usize::from(data.height - row_idx - 1) * and_stride.length..]);
226                (xor_stride_cursor, and_stride_cursor)
227            } else {
228                let xor_stride_cursor = ReadCursor::new(&data.xor_mask[usize::from(row_idx) * xor_stride.length..]);
229                let and_stride_cursor = ReadCursor::new(&and_mask[usize::from(row_idx) * and_stride.length..]);
230                (xor_stride_cursor, and_stride_cursor)
231            };
232
233            let mut color_reader = ColorStrideReader::new(data.xor_bpp, xor_stride)?;
234            let mut bitmask_reader = BitmaskStrideReader::new(and_stride);
235
236            let compute_inverted_pixel = if target.should_invert_pixels_using_check_pattern() {
237                |row_idx: u16, col_idx: u16| -> [u8; 4] {
238                    // Checkered pattern is used to represent inverted pixels.
239                    if (row_idx + col_idx) % 2 == 0 {
240                        [0xff, 0xff, 0xff, 0xff]
241                    } else {
242                        [0x00, 0x00, 0x00, 0xff]
243                    }
244                }
245            } else {
246                |_, _| [0xFF, 0xFF, 0xFF, 0x00]
247            };
248
249            for col_idx in 0..data.width {
250                let and_bit = bitmask_reader.next_bit(&mut and_stride_cursor);
251                let color = color_reader.next_pixel(&mut xor_stride_cursor);
252
253                if and_bit == 1 && color == [0, 0, 0, 0xff] {
254                    // Force transparent pixel (The only way to get a transparent pixel with
255                    // non-32-bit cursors)
256                    bitmap_data.extend_from_slice(&[0, 0, 0, 0]);
257                } else if and_bit == 1 && color == [0xff, 0xff, 0xff, 0xff] {
258                    // Inverted pixel.
259                    bitmap_data.extend_from_slice(&compute_inverted_pixel(row_idx, col_idx));
260                } else if target.should_premultiply_alpha() {
261                    // Calculate premultiplied alpha via integer arithmetic
262                    let with_premultiplied_alpha = [
263                        ((color[0] as u16 * color[0] as u16) >> 8) as u8,
264                        ((color[1] as u16 * color[1] as u16) >> 8) as u8,
265                        ((color[2] as u16 * color[2] as u16) >> 8) as u8,
266                        color[3],
267                    ];
268                    bitmap_data.extend_from_slice(&with_premultiplied_alpha);
269                } else {
270                    bitmap_data.extend_from_slice(&color);
271                }
272            }
273        }
274
275        Ok(Self {
276            width: data.width,
277            height: data.height,
278            bitmap_data,
279            hotspot_x: data.hot_spot_x,
280            hotspot_y: data.hot_spot_y,
281        })
282    }
283}
284
285#[derive(Clone, Copy)]
286struct Stride {
287    length: usize,
288    data_bytes: usize,
289    padding: usize,
290}
291
292impl Stride {
293    fn from_bits(bits: usize) -> Stride {
294        let length = bit_stride_size_align_u16(bits);
295        let data_bytes = bit_stride_size_align_u8(bits);
296        Stride {
297            length,
298            data_bytes,
299            padding: length - data_bytes,
300        }
301    }
302}
303
304struct BitmaskStrideReader {
305    current_byte: u8,
306    read_bits: usize,
307    read_stide_bytes: usize,
308    stride_data_bytes: usize,
309    stride_padding: usize,
310}
311
312impl BitmaskStrideReader {
313    fn new(stride: Stride) -> Self {
314        Self {
315            current_byte: 0,
316            read_bits: 8,
317            read_stide_bytes: 0,
318            stride_data_bytes: stride.data_bytes,
319            stride_padding: stride.padding,
320        }
321    }
322
323    fn next_bit(&mut self, cursor: &mut ReadCursor<'_>) -> u8 {
324        if self.read_bits == 8 {
325            self.read_bits = 0;
326
327            if self.read_stide_bytes == self.stride_data_bytes {
328                self.read_stide_bytes = 0;
329                cursor.read_slice(self.stride_padding);
330            }
331
332            self.current_byte = cursor.read_u8();
333        }
334
335        let bit = (self.current_byte >> (7 - self.read_bits)) & 1;
336        self.read_bits += 1;
337        bit
338    }
339}
340
341enum ColorStrideReader {
342    Color {
343        /// INVARIANT: `bpp == 16 || bpp == 24 || bpp == 32`
344        bpp: u16,
345        read_stide_bytes: usize,
346        stride_data_bytes: usize,
347        stride_padding: usize,
348    },
349    Bitmask(BitmaskStrideReader),
350}
351
352impl ColorStrideReader {
353    fn new(bpp: u16, stride: Stride) -> Result<Self, PointerError> {
354        Ok(match bpp {
355            1 => Self::Bitmask(BitmaskStrideReader::new(stride)),
356            bpp => Self::Color {
357                bpp: {
358                    // Enforce the bpp == 16 || bpp == 24 || bpp == 32 invariant.
359                    if !SUPPORTED_COLOR_BPP[1..].contains(&bpp) {
360                        return Err(PointerError::NotSupportedBpp { bpp });
361                    }
362
363                    bpp
364                },
365                read_stide_bytes: 0,
366                stride_data_bytes: stride.data_bytes,
367                stride_padding: stride.padding,
368            },
369        })
370    }
371
372    fn next_pixel(&mut self, cursor: &mut ReadCursor<'_>) -> [u8; 4] {
373        match self {
374            ColorStrideReader::Color {
375                bpp,
376                read_stide_bytes,
377                stride_data_bytes,
378                stride_padding,
379            } => {
380                if read_stide_bytes == stride_data_bytes {
381                    *read_stide_bytes = 0;
382                    cursor.read_slice(*stride_padding);
383                }
384
385                match bpp {
386                    16 => {
387                        *read_stide_bytes += 2;
388                        let color_16bit = cursor.read_u16();
389                        let [r, g, b] = rdp_16bit_to_rgb(color_16bit);
390                        [r, g, b, 0xff]
391                    }
392                    24 => {
393                        *read_stide_bytes += 3;
394
395                        let color_24bit = cursor.read_array::<3>();
396                        [color_24bit[2], color_24bit[1], color_24bit[0], 0xff]
397                    }
398                    32 => {
399                        *read_stide_bytes += 4;
400                        let color_32bit = cursor.read_array::<4>();
401                        [color_32bit[2], color_32bit[1], color_32bit[0], color_32bit[3]]
402                    }
403                    _ => unreachable!("per the invariant on self.bpp, this path is unreachable"),
404                }
405            }
406            ColorStrideReader::Bitmask(bitask) => {
407                if bitask.next_bit(cursor) == 1 {
408                    [0xff, 0xff, 0xff, 0xff]
409                } else {
410                    [0, 0, 0, 0xff]
411                }
412            }
413        }
414    }
415}
416
417fn bit_stride_size_align_u8(size_bits: usize) -> usize {
418    size_bits.div_ceil(8)
419}
420
421fn bit_stride_size_align_u16(size_bits: usize) -> usize {
422    size_bits.div_ceil(16) * 2
423}
424
425/// Message-agnostic pointer data.
426struct PointerData<'a> {
427    width: u16,
428    height: u16,
429    xor_bpp: u16,
430    xor_mask: &'a [u8],
431    and_mask: &'a [u8],
432    hot_spot_x: u16,
433    hot_spot_y: u16,
434}