ironrdp_graphics/rdp6/bitmap_stream/
decoder.rs

1use ironrdp_core::{decode, DecodeError};
2use ironrdp_pdu::bitmap::rdp6::{BitmapStream as BitmapStreamPdu, ColorPlaneDefinition};
3
4use crate::color_conversion::Rgb;
5use crate::rdp6::rle::{decompress_8bpp_plane, RleDecodeError};
6
7#[derive(Debug)]
8pub enum BitmapDecodeError {
9    Decode(DecodeError),
10    Rle(RleDecodeError),
11    InvalidUncompressedDataSize,
12}
13
14impl core::fmt::Display for BitmapDecodeError {
15    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
16        match self {
17            BitmapDecodeError::Decode(_error) => write!(f, "failed to decode RDP6 bitmap stream PDU"),
18            BitmapDecodeError::Rle(_error) => {
19                write!(f, "failed to perform RLE decompression of RDP6 bitmap stream")
20            }
21            BitmapDecodeError::InvalidUncompressedDataSize => write!(
22                f,
23                "color plane data size provided in PDU is not sufficient to reconstruct the bitmap"
24            ),
25        }
26    }
27}
28
29impl core::error::Error for BitmapDecodeError {
30    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
31        match self {
32            BitmapDecodeError::Decode(err) => Some(err),
33            BitmapDecodeError::Rle(err) => Some(err),
34            BitmapDecodeError::InvalidUncompressedDataSize => None,
35        }
36    }
37}
38
39impl From<DecodeError> for BitmapDecodeError {
40    fn from(err: DecodeError) -> Self {
41        BitmapDecodeError::Decode(err)
42    }
43}
44
45impl From<RleDecodeError> for BitmapDecodeError {
46    fn from(err: RleDecodeError) -> Self {
47        BitmapDecodeError::Rle(err)
48    }
49}
50
51/// Implements decoding of RDP6 bitmap stream PDU (see [`BitmapStreamPdu`])
52#[derive(Debug, Default)]
53pub struct BitmapStreamDecoder {
54    /// Optimization to avoid reallocations, re-use this buffer for all bitmaps in the session
55    planes_buffer: Vec<u8>,
56}
57
58/// Internal implementation of RDP6 bitmap stream PDU decoder for specific image size and format
59struct BitmapStreamDecoderImpl<'a> {
60    bitmap: BitmapStreamPdu<'a>,
61    image_width: usize,
62    image_height: usize,
63    chroma_width: usize,
64    chroma_height: usize,
65    full_plane_size: usize,
66    chroma_plane_size: usize,
67    uncompressed_planes_size: usize,
68    color_plane_offsets: [usize; 3],
69}
70
71struct AYCoCgParams {
72    color_loss_level: u8,
73    chroma_subsampling: bool,
74    alpha: bool,
75}
76
77impl<'a> BitmapStreamDecoderImpl<'a> {
78    fn init(bitmap: BitmapStreamPdu<'a>, image_width: usize, image_height: usize) -> Self {
79        let (chroma_width, chroma_height) = if bitmap.has_subsampled_chroma() {
80            // When image is subsampled, chroma plane has half the size of the luma plane, however
81            // its size is rounded up to the nearest greater integer, to take into account odd image
82            // size (e.g. if width is 3, then chroma plane width is 2, not 1, to take into account
83            // the odd column which expands to 1 pixel instead of 2 during supersampling)
84            (image_width.div_ceil(2), image_height.div_ceil(2))
85        } else {
86            (image_width, image_height)
87        };
88
89        let full_plane_size = image_width * image_height;
90        let chroma_plane_size = chroma_width * chroma_height;
91
92        let uncompressed_planes_size = if bitmap.has_subsampled_chroma() {
93            full_plane_size + chroma_plane_size * 2
94        } else {
95            full_plane_size * 3
96        };
97
98        let color_plane_offsets = [0, full_plane_size, full_plane_size + chroma_plane_size];
99
100        Self {
101            bitmap,
102            image_width,
103            image_height,
104            chroma_width,
105            chroma_height,
106            full_plane_size,
107            chroma_plane_size,
108            uncompressed_planes_size,
109            color_plane_offsets,
110        }
111    }
112
113    fn decompress_planes(&'a self, aux_buffer: &'a mut Vec<u8>) -> Result<&'a [u8], BitmapDecodeError> {
114        let planes = if self.bitmap.header.enable_rle_compression {
115            // We don't care for the previous content, just resize it to fit the data
116            aux_buffer.resize(self.uncompressed_planes_size, 0);
117            let uncompressed_planes_buffer = &mut aux_buffer[..self.uncompressed_planes_size];
118
119            let compressed = self.bitmap.color_panes_data();
120            let mut src_offset = 0;
121
122            // Decompress Alpha plane
123            if self.bitmap.header.use_alpha {
124                // Decompress alpha alpha, but discard it (always 0xFF)
125                src_offset += decompress_8bpp_plane(
126                    &compressed[src_offset..],
127                    uncompressed_planes_buffer,
128                    self.image_width,
129                    self.image_height,
130                )?;
131            }
132
133            // Decompress R/Y plane
134            src_offset += decompress_8bpp_plane(
135                &compressed[src_offset..],
136                &mut uncompressed_planes_buffer[self.color_plane_offsets[0]..],
137                self.image_width,
138                self.image_height,
139            )?;
140
141            // Decompress G/Co plane
142            src_offset += decompress_8bpp_plane(
143                &compressed[src_offset..],
144                &mut uncompressed_planes_buffer[self.color_plane_offsets[1]..],
145                self.chroma_width,
146                self.chroma_height,
147            )?;
148
149            // Decompress B/Cg plane
150            decompress_8bpp_plane(
151                &compressed[src_offset..],
152                &mut uncompressed_planes_buffer[self.color_plane_offsets[2]..],
153                self.chroma_width,
154                self.chroma_height,
155            )?;
156
157            &uncompressed_planes_buffer[..self.uncompressed_planes_size]
158        } else {
159            // Discard alpha plane
160            let color_planes_offset = if self.bitmap.header.use_alpha {
161                self.full_plane_size
162            } else {
163                0
164            };
165
166            let expected_data_size = color_planes_offset + self.uncompressed_planes_size;
167
168            if self.bitmap.color_panes_data().len() < expected_data_size {
169                return Err(BitmapDecodeError::InvalidUncompressedDataSize);
170            }
171
172            &self.bitmap.color_panes_data()[color_planes_offset..]
173        };
174
175        Ok(planes)
176    }
177
178    fn write_argb_planes_to_rgb24(&self, planes: &[u8], dst: &mut Vec<u8>) {
179        // For ARGB conversion is simple - just copy data in correct order
180        let (r_offset, g_offset, b_offset) = (
181            self.color_plane_offsets[0],
182            self.color_plane_offsets[1],
183            self.color_plane_offsets[2],
184        );
185
186        let r_plane = &planes[r_offset..r_offset + self.full_plane_size];
187        let g_plane = &planes[g_offset..g_offset + self.full_plane_size];
188        let b_plane = &planes[b_offset..b_offset + self.full_plane_size];
189
190        for i in 0..self.full_plane_size {
191            let (r, g, b) = (r_plane[i], g_plane[i], b_plane[i]);
192
193            dst.extend_from_slice(&[r, g, b]);
194        }
195    }
196
197    fn write_aycocg_planes_to_rgb24(&self, params: AYCoCgParams, planes: &[u8], dst: &mut Vec<u8>) {
198        #![allow(clippy::similar_names, reason = "it’s hard to find better names for co, cg, etc")]
199
200        let sample_shift = usize::from(params.chroma_subsampling);
201
202        let (y_offset, co_offset, cg_offset) = (
203            self.color_plane_offsets[0],
204            self.color_plane_offsets[1],
205            self.color_plane_offsets[2],
206        );
207
208        let y_plane = &planes[y_offset..y_offset + self.full_plane_size];
209        let co_plane = &planes[co_offset..co_offset + self.chroma_plane_size];
210        let cg_plane = &planes[cg_offset..cg_offset + self.chroma_plane_size];
211
212        for (idx, y) in y_plane.iter().copied().enumerate() {
213            let chroma_row = (idx / self.image_width) >> sample_shift;
214            let chroma_col = (idx % self.image_width) >> sample_shift;
215            let chroma_idx = chroma_row * self.chroma_width + chroma_col;
216
217            let co = co_plane[chroma_idx];
218            let cg = cg_plane[chroma_idx];
219
220            let Rgb { r, g, b } = ycocg_with_cll_to_rgb(params.color_loss_level, y, co, cg);
221
222            // As described in 3.1.9.1.2 [MS-RDPEGDI], R and B channels are swapped for
223            // AYCoCg when 24-bit image is used (no alpha). We swap them back here
224            if params.alpha {
225                dst.extend_from_slice(&[r, g, b]);
226            } else {
227                dst.extend_from_slice(&[b, g, r]);
228            }
229        }
230    }
231
232    fn decode(self, dst: &mut Vec<u8>, aux_buffer: &'a mut Vec<u8>) -> Result<(), BitmapDecodeError> {
233        // Reserve enough space for decoded RGB channels data
234        dst.reserve(self.image_height * self.image_width * 3);
235
236        match self.bitmap.header.color_plane_definition {
237            ColorPlaneDefinition::Argb => {
238                let color_planes = self.decompress_planes(aux_buffer)?;
239                self.write_argb_planes_to_rgb24(color_planes, dst);
240            }
241            ColorPlaneDefinition::AYCoCg {
242                color_loss_level,
243                use_chroma_subsampling,
244                ..
245            } => {
246                let params: AYCoCgParams = AYCoCgParams {
247                    color_loss_level,
248                    chroma_subsampling: use_chroma_subsampling,
249                    alpha: self.bitmap.header.use_alpha,
250                };
251                let color_planes = self.decompress_planes(aux_buffer)?;
252                self.write_aycocg_planes_to_rgb24(params, color_planes, dst);
253            }
254        }
255
256        Ok(())
257    }
258}
259
260/// Perform YCoCg -> RGB conversion with color loss reduction (CLL) correction.
261fn ycocg_with_cll_to_rgb(cll: u8, y: u8, co: u8, cg: u8) -> Rgb {
262    #![allow(clippy::similar_names)] // It’s hard to find better names for co, cg, etc.
263
264    // We decrease CLL by 1 to skip division by 2 for co & cg components during computation of
265    // the following color conversion matrix:
266    // |R|   |1   1/2   -1/2|   |Y |
267    // |G| = |1    0     1/2| * |Co|
268    // |B|   |1  -1/2   -1/2|   |Cg|
269    let chroma_shift = cll - 1;
270
271    let clip_i16 =
272        |v: i16| u8::try_from(v.clamp(0, 255)).expect("fits into u8 because the value is clamped to [0..256]");
273
274    let co_signed = (co << chroma_shift).cast_signed();
275    let cg_signed = (cg << chroma_shift).cast_signed();
276
277    let y = i16::from(y);
278    let co = i16::from(co_signed);
279    let cg = i16::from(cg_signed);
280
281    let t = y - cg;
282    let r = clip_i16(t + co);
283    let g = clip_i16(y + cg);
284    let b = clip_i16(t - co);
285
286    Rgb { r, g, b }
287}
288
289impl BitmapStreamDecoder {
290    /// Performs decoding of bitmap stream PDU from `bitmap_data` and writes decoded rgb24
291    /// image to `dst` buffer.
292    pub fn decode_bitmap_stream_to_rgb24(
293        &mut self,
294        bitmap_data: &[u8],
295        dst: &mut Vec<u8>,
296        image_width: usize,
297        image_height: usize,
298    ) -> Result<(), BitmapDecodeError> {
299        let bitmap = decode::<BitmapStreamPdu<'_>>(bitmap_data)?;
300
301        let decoder = BitmapStreamDecoderImpl::init(bitmap, image_width, image_height);
302
303        decoder.decode(dst, &mut self.planes_buffer)
304    }
305}