Add support for RDP6 32bpp bitmaps (#118)

This adds support for RDP6 32bpp bitmaps.

- RDP6 RLE implementation
- `RDP6_BITMAP_STREAM` pdu parsing
- New CLI arguments to select color depth and color reduction
- Unit tests and fuzzing for implemented functionality

Test bpp32 mode without lossy compression (RGB  colorspace)
```
cargo run -p ironrdp-client -- -u vmbox -p password --color-depth=32 <IP_ADDRESS>
```

Test bpp32 mode with lossy compression (YCoCg colorspace, subsampling, color loss)
```
cargo run -p ironrdp-client -- -u vmbox -p password --color-depth=32 --lossy-bitmap-compression <IP_ADDRESS>
```

Issue: ARC-149
This commit is contained in:
Vladyslav Nikonov
2023-05-08 09:43:53 -04:00
committed by GitHub
parent f11131b18a
commit 137556013f
35 changed files with 1157 additions and 15 deletions
Generated
+28
View File
@@ -346,6 +346,15 @@ dependencies = [
"objc2-encode",
]
[[package]]
name = "bmp"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69985ff4f58085ac696454692d0b646a66ad1f9cc9be294c91dc51bb5df511ae"
dependencies = [
"byteorder",
]
[[package]]
name = "bufstream"
version = "0.1.4"
@@ -912,6 +921,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "dissimilar"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "210ec60ae7d710bed8683e333e9d2855a8a56a3e9892b38bad3bb0d4d29b0d5e"
[[package]]
name = "dlib"
version = "0.5.0"
@@ -969,6 +984,16 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193"
[[package]]
name = "expect-test"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3"
dependencies = [
"dissimilar",
"once_cell",
]
[[package]]
name = "fake-simd"
version = "0.1.2"
@@ -1543,7 +1568,9 @@ dependencies = [
"bit_field",
"bitflags 2.0.2",
"bitvec",
"bmp",
"byteorder",
"expect-test",
"hex-literal",
"ironrdp-pdu",
"lazy_static",
@@ -1575,6 +1602,7 @@ dependencies = [
"bitflags 2.0.2",
"byteorder",
"der-parser 8.2.0",
"expect-test",
"ironrdp-pdu-samples",
"lazy_static",
"md-5 0.10.5",
+9 -8
View File
@@ -59,22 +59,23 @@ keywords = ["rdp", "remote-desktop", "network", "client", "protocol"]
categories = ["network-programming"]
[workspace.dependencies]
expect-test = "1"
ironrdp = { version = "0.5", path = "." }
ironrdp-pdu = { version = "0.1", path = "crates/pdu" }
ironrdp-async = { version = "0.1", path = "crates/async" }
ironrdp-connector = { version = "0.1", path = "crates/connector" }
ironrdp-session = { version = "0.1", path = "crates/session" }
ironrdp-graphics = { version = "0.1", path = "crates/graphics" }
ironrdp-input = { version = "0.1", path = "crates/input" }
ironrdp-async = { version = "0.1", path = "crates/async" }
ironrdp-tls = { version = "0.1", path = "crates/tls" }
ironrdp-rdcleanpath = { version = "0.1", path = "crates/rdcleanpath" }
ironrdp-pdu-samples = { path = "crates/pdu-samples" }
ironrdp-pdu = { version = "0.1", path = "crates/pdu" }
ironrdp-pdu-generators = { path = "crates/pdu-generators" }
ironrdp-pdu-samples = { path = "crates/pdu-samples" }
ironrdp-rdcleanpath = { version = "0.1", path = "crates/rdcleanpath" }
ironrdp-session = { version = "0.1", path = "crates/session" }
ironrdp-session-generators = { path = "crates/session-generators" }
sspi = "0.8.1"
tracing = "0.1.37"
ironrdp-tls = { version = "0.1", path = "crates/tls" }
proptest = "1.1.0"
rstest = "0.17.0"
sspi = "0.8.1"
tracing = "0.1.37"
[profile.dev]
opt-level = 1
+27
View File
@@ -131,6 +131,15 @@ struct Args {
#[clap(long)]
small_cache: bool,
/// Enable RDP6 lossy bitmap compression algorithm. Please note that lossy compression
/// only works with 32 bit color depth
#[clap(long)]
lossy_bitmap_compression: bool,
/// Set required color depth. Currently only 32 and 16 bit color depths are supported
#[clap(long)]
color_depth: Option<u32>,
/// Enabled capability versions. Each bit represents enabling a capability version
/// starting from V8 to V10_7
#[clap(long, value_parser = parse_hex, default_value_t = 0)]
@@ -168,6 +177,23 @@ impl Config {
.context("Password prompt")?
};
let bitmap = if let Some(color_depth) = args.color_depth {
if color_depth != 16 && color_depth != 32 {
anyhow::bail!("Invalid color depth. Only 16 and 32 bit color depths are supported.");
}
if color_depth != 32 && args.lossy_bitmap_compression {
anyhow::bail!("Lossy bitmap compression only works with 32 bit color depth.");
}
Some(connector::BitmapConfig {
color_depth,
lossy_compression: args.lossy_bitmap_compression,
})
} else {
None
};
let graphics = if args.avc444 || args.h264 {
Some(connector::GraphicsConfig {
avc444: args.avc444,
@@ -195,6 +221,7 @@ impl Config {
height: DEFAULT_HEIGHT,
},
graphics,
bitmap,
client_build: semver::Version::parse(env!("CARGO_PKG_VERSION"))
.map(|version| version.major * 100 + version.minor * 10 + version.patch)
.unwrap_or(0)
+28 -2
View File
@@ -781,6 +781,18 @@ impl Sequence for ClientConnector {
fn create_gcc_blocks(config: &Config, selected_protocol: nego::SecurityProtocol) -> gcc::ClientGccBlocks {
use ironrdp_pdu::gcc::*;
let color_depth = config
.bitmap
.as_ref()
.map(|bitmap| match bitmap.color_depth {
15 => SupportedColorDepths::BPP15,
16 => SupportedColorDepths::BPP16,
24 => SupportedColorDepths::BPP24,
32 => SupportedColorDepths::BPP32,
_ => panic!("Unsupported color depth: {}", bitmap.color_depth),
})
.unwrap_or(SupportedColorDepths::BPP16);
ClientGccBlocks {
core: ClientCoreData {
version: RdpVersion::V5_PLUS,
@@ -800,7 +812,7 @@ fn create_gcc_blocks(config: &Config, selected_protocol: nego::SecurityProtocol)
client_product_id: Some(1),
serial_number: Some(0),
high_color_depth: Some(HighColorDepth::Bpp24),
supported_color_depths: Some(SupportedColorDepths::BPP16),
supported_color_depths: Some(color_depth),
early_capability_flags: {
let mut early_capability_flags = ClientEarlyCapabilityFlags::VALID_CONNECTION_TYPE
| ClientEarlyCapabilityFlags::SUPPORT_ERR_INFO_PDU;
@@ -893,6 +905,20 @@ fn create_client_confirm_active(
server_capability_sets.retain(|capability_set| matches!(capability_set, CapabilitySet::MultiFragmentUpdate(_)));
let lossy_bitmap_compression = config
.bitmap
.as_ref()
.map(|bitmap| bitmap.lossy_compression)
.unwrap_or(false);
let drawing_flags = if lossy_bitmap_compression {
BitmapDrawingFlags::ALLOW_SKIP_ALPHA
| BitmapDrawingFlags::ALLOW_DYNAMIC_COLOR_FIDELITY
| BitmapDrawingFlags::ALLOW_COLOR_SUBSAMPLING
} else {
BitmapDrawingFlags::ALLOW_SKIP_ALPHA
};
server_capability_sets.extend_from_slice(&[
CapabilitySet::General(General {
major_platform_type: config.platform,
@@ -906,7 +932,7 @@ fn create_client_confirm_active(
desktop_width: config.desktop_size.width,
desktop_height: config.desktop_size.height,
desktop_resize_flag: false,
drawing_flags: BitmapDrawingFlags::empty(),
drawing_flags,
}),
CapabilitySet::Order(Order::new(
OrderFlags::NEGOTIATE_ORDER_SUPPORT | OrderFlags::ZERO_BOUNDS_DELTAS_SUPPORT,
+8
View File
@@ -39,6 +39,13 @@ pub struct GraphicsConfig {
pub capabilities: u32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct BitmapConfig {
pub lossy_compression: bool,
pub color_depth: u32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Config {
@@ -56,6 +63,7 @@ pub struct Config {
pub keyboard_functional_keys_count: u32,
pub ime_file_name: String,
pub graphics: Option<GraphicsConfig>,
pub bitmap: Option<BitmapConfig>,
pub dig_product_id: String,
pub client_dir: String,
pub platform: capability_sets::MajorPlatformType,
+2
View File
@@ -23,6 +23,8 @@ bitflags = "2"
lazy_static = "1.4.0"
[dev-dependencies]
bmp = "0.5"
expect-test.workspace = true
hex-literal = "0.3.4"
proptest = "1.1.0"
rdp-rs = { git = "https://github.com/citronneur/rdp-rs", rev = "7ac880d7efb7f05efef3c84476f7c24f4053e0ea" }
+27
View File
@@ -18,6 +18,10 @@ fn clip(v: i32) -> u8 {
min(max(v, 0), 255) as u8
}
fn clip_i16(v: i16) -> u8 {
min(max(v, 0), 255) as u8
}
#[derive(Debug)]
pub struct YCbCrBuffer<'a> {
pub y: &'a [i16],
@@ -52,6 +56,13 @@ pub struct YCbCr {
pub cr: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct YCoCg {
pub y: u8,
pub co: i8,
pub cg: i8,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Rgb {
pub r: u8,
@@ -79,3 +90,19 @@ impl From<YCbCr> for Rgb {
Self { r, g, b }
}
}
impl From<YCoCg> for Rgb {
fn from(YCoCg { y, co, cg }: YCoCg) -> Self {
let y = i16::from(y);
let co = i16::from(co);
let cg = i16::from(cg);
let t = y - cg / 2;
let r = clip_i16(t + co / 2);
let g = clip_i16(y + cg / 2);
let b = clip_i16(t - co / 2);
Self { r, g, b }
}
}
+1
View File
@@ -2,6 +2,7 @@ pub mod color_conversion;
pub mod dwt;
pub mod image_processing;
pub mod quantization;
pub mod rdp6;
pub mod rectangle_processing;
pub mod rle;
pub mod rlgr;
+334
View File
@@ -0,0 +1,334 @@
use crate::{
color_conversion::{Rgb, YCoCg},
rdp6::rle::{decompress_8bpp_plane, RleError},
};
use ironrdp_pdu::{
bitmap::rdp6::{BitmapStream as BitmapStreamPdu, ColorPlanes},
decode, Error as PduError,
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum BitmapDecodeError {
#[error("Failed to decode RDP6 bitmap stream PDU: {0}")]
Pdu(#[from] PduError),
#[error("Failed to perform RLE decompression of RDP6 bitmap stream: {0}")]
Rle(#[from] RleError),
#[error("Color plane data size provided in PDU is not sufficient to reconstruct the bitmap")]
InvalidUncompressedDataSize,
}
/// Implements decoding of RDP6 bitmap stream PDU (see [`BitmapStreamPdu`])
#[derive(Debug, Default)]
pub struct BitmapStreamDecoder {
/// Optimization to avoid reallocations, re-use this buffer for all bitmaps in the session
planes_buffer: Vec<u8>,
}
/// Internal implementation of RDP6 bitmap stream PDU decoder for specific image size and format
struct BitmapStreamDecoderImpl<'a> {
bitmap: BitmapStreamPdu<'a>,
image_width: usize,
image_height: usize,
chroma_width: usize,
chroma_height: usize,
full_plane_size: usize,
chroma_plane_size: usize,
uncompressed_planes_size: usize,
color_plane_offsets: [usize; 3],
}
struct AYCoCgParams {
color_loss_level: u8,
chroma_subsampling: bool,
alpha: bool,
}
impl<'a> BitmapStreamDecoderImpl<'a> {
pub fn init(bitmap: BitmapStreamPdu<'a>, image_width: usize, image_height: usize) -> Self {
let (chroma_width, chroma_height) = if bitmap.has_subsampled_chroma() {
// When image is subsampled, chroma plane has half the size of the luma plane, however
// its size is rounded up to the nearest greater integer, to take into account odd image
// size (e.g. if width is 3, then chroma plane width is 2, not 1, to take into account
// the odd column which expands to 1 pixel instead of 2 during supersampling)
((image_width + 1) / 2, (image_height + 1) / 2)
} else {
(image_width, image_height)
};
let full_plane_size = image_width * image_height;
let chroma_plane_size = chroma_width * chroma_height;
let uncompressed_planes_size = if bitmap.has_subsampled_chroma() {
full_plane_size + chroma_plane_size * 2
} else {
full_plane_size * 3
};
let color_plane_offsets = [0, full_plane_size, full_plane_size + chroma_plane_size];
Self {
bitmap,
image_width,
image_height,
chroma_width,
chroma_height,
full_plane_size,
chroma_plane_size,
uncompressed_planes_size,
color_plane_offsets,
}
}
fn decompress_planes(&'a self, aux_buffer: &'a mut Vec<u8>) -> Result<&'a [u8], BitmapDecodeError> {
let planes = if self.bitmap.enable_rle_compression {
// We don't care for the previous content, just resize it to fit the data
aux_buffer.resize(self.uncompressed_planes_size, 0);
let uncompressed_planes_buffer = &mut aux_buffer[..self.uncompressed_planes_size];
let compressed = self.bitmap.color_panes_data();
let mut src_offset = 0;
// Decompress Alpha plane
if self.bitmap.use_alpha {
// Decompress alpha alpha, but discard it (always 0xFF)
src_offset += decompress_8bpp_plane(
&compressed[src_offset..],
uncompressed_planes_buffer,
self.image_width,
self.image_height,
)?;
}
// Decompress R/Y plane
src_offset += decompress_8bpp_plane(
&compressed[src_offset..],
&mut uncompressed_planes_buffer[self.color_plane_offsets[0]..],
self.image_width,
self.image_height,
)?;
// Decompress G/Co plane
src_offset += decompress_8bpp_plane(
&compressed[src_offset..],
&mut uncompressed_planes_buffer[self.color_plane_offsets[1]..],
self.chroma_width,
self.chroma_height,
)?;
// Decompress B/Cg plane
decompress_8bpp_plane(
&compressed[src_offset..],
&mut uncompressed_planes_buffer[self.color_plane_offsets[2]..],
self.chroma_width,
self.chroma_height,
)?;
&uncompressed_planes_buffer[..self.uncompressed_planes_size]
} else {
// Discard alpha plane
let color_planes_offset = if self.bitmap.use_alpha { self.full_plane_size } else { 0 };
let expected_data_size = color_planes_offset + self.uncompressed_planes_size;
if self.bitmap.color_panes_data().len() < expected_data_size {
return Err(BitmapDecodeError::InvalidUncompressedDataSize);
}
&self.bitmap.color_panes_data()[color_planes_offset..]
};
Ok(planes)
}
fn write_argb_planes_to_rgb24(&self, planes: &[u8], dst: &mut Vec<u8>) {
// For ARGB comversion is simple - just copy data in correct order
let (r_offset, g_offset, b_offset) = (
self.color_plane_offsets[0],
self.color_plane_offsets[1],
self.color_plane_offsets[2],
);
let r_plane = &planes[r_offset..r_offset + self.full_plane_size];
let g_plane = &planes[g_offset..g_offset + self.full_plane_size];
let b_plane = &planes[b_offset..b_offset + self.full_plane_size];
for i in 0..self.full_plane_size {
let (r, g, b) = (r_plane[i], g_plane[i], b_plane[i]);
dst.extend_from_slice(&[r, g, b]);
}
}
fn write_aycocg_planes_to_rgb24(&self, params: AYCoCgParams, planes: &[u8], dst: &mut Vec<u8>) {
// For AYCoCg we need to take color loss level and subsampling into account
let chroma_shift = (params.color_loss_level - 1) as usize;
let sample_shift = params.chroma_subsampling as usize;
let (y_offset, co_offset, cg_offset) = (
self.color_plane_offsets[0],
self.color_plane_offsets[1],
self.color_plane_offsets[2],
);
let y_plane = &planes[y_offset..y_offset + self.full_plane_size];
let co_plane = &planes[co_offset..co_offset + self.chroma_plane_size];
let cg_plane = &planes[cg_offset..cg_offset + self.chroma_plane_size];
for (idx, y) in y_plane.iter().copied().enumerate() {
let chroma_row = (idx / self.image_width) >> sample_shift;
let chroma_col = (idx % self.image_width) >> sample_shift;
let chroma_idx = chroma_row * self.chroma_width + chroma_col;
let co = (co_plane[chroma_idx] << chroma_shift) as i8;
let cg = (cg_plane[chroma_idx] << chroma_shift) as i8;
let Rgb { r, g, b } = YCoCg { y, co, cg }.into();
// As described in 3.1.9.1.2 [MS-RDPEGDI], R and B channels are swapped for
// AYCoCg when 24-bit image is used (no alpha). We swap them back here
if params.alpha {
dst.extend_from_slice(&[r, g, b]);
} else {
dst.extend_from_slice(&[b, g, r]);
}
}
}
fn decode(self, dst: &mut Vec<u8>, aux_buffer: &'a mut Vec<u8>) -> Result<(), BitmapDecodeError> {
// Reserve enough space for decoded RGB channels data
dst.reserve(self.image_height * self.image_width * 3);
match self.bitmap.color_planes {
ColorPlanes::Argb { .. } => {
let color_planes = self.decompress_planes(aux_buffer)?;
self.write_argb_planes_to_rgb24(color_planes, dst);
}
ColorPlanes::AYCoCg {
color_loss_level,
use_chroma_subsampling,
..
} => {
let params = AYCoCgParams {
color_loss_level,
chroma_subsampling: use_chroma_subsampling,
alpha: self.bitmap.use_alpha,
};
let color_planes = self.decompress_planes(aux_buffer)?;
self.write_aycocg_planes_to_rgb24(params, color_planes, dst);
}
}
Ok(())
}
}
impl BitmapStreamDecoder {
/// Performs decoding of bitmap stream PDU from `bitmap_data` and writes decoded rgb24
/// image to `dst` buffer.
pub fn decode_bitmap_stream_to_rgb24(
&mut self,
bitmap_data: &[u8],
dst: &mut Vec<u8>,
image_width: usize,
image_height: usize,
) -> Result<(), BitmapDecodeError> {
let bitmap = decode::<BitmapStreamPdu>(bitmap_data)?;
let decoder = BitmapStreamDecoderImpl::init(bitmap, image_width, image_height);
decoder.decode(dst, &mut self.planes_buffer)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_decoded_image(pdu: &[u8], expected_bmp: &[u8], width: usize, height: usize) {
let expected_bmp = bmp::from_reader(&mut std::io::Cursor::new(expected_bmp)).unwrap();
let mut expected_buffer = vec![0; width * height * 3];
for (idx, (x, y)) in expected_bmp.coordinates().enumerate() {
let pixel = expected_bmp.get_pixel(x, y);
let offset = idx * 3;
expected_buffer[offset] = pixel.r;
expected_buffer[offset + 1] = pixel.g;
expected_buffer[offset + 2] = pixel.b;
}
let mut actual = Vec::new();
BitmapStreamDecoder::default()
.decode_bitmap_stream_to_rgb24(pdu, &mut actual, width, height)
.unwrap();
assert_eq!(actual.as_slice(), expected_buffer.as_slice());
}
#[test]
fn decode_32x64_rgb_raw() {
// RGB (No alpha), no RLE
assert_decoded_image(
include_bytes!("test_assets/32x64_rgb_raw.bin"),
include_bytes!("test_assets/32x64_rgb_raw.bmp"),
32,
64,
);
}
#[test]
fn decode_64x24_argb_rle() {
// ARGB (With alpha), RLE
assert_decoded_image(
include_bytes!("test_assets/64x24_argb_rle.bin"),
include_bytes!("test_assets/64x24_argb_rle.bmp"),
64,
24,
);
}
#[test]
fn decode_64x24_aycocg_rle() {
// AYCoCg (With alpha), RLE, no chroma subsampling
assert_decoded_image(
include_bytes!("test_assets/64x24_aycocg_rle.bin"),
include_bytes!("test_assets/64x24_aycocg_rle.bmp"),
64,
24,
);
}
#[test]
fn decode_64x24_ycocg_rle_ss() {
// AYCoCg (No alpha), RLE, with chroma subsampling
assert_decoded_image(
include_bytes!("test_assets/64x24_ycocg_rle_ss.bin"),
include_bytes!("test_assets/64x24_ycocg_rle_ss.bmp"),
64,
24,
);
}
#[test]
fn decode_64x57_ycocg_rle_ss() {
// AYCoCg (No alpha), RLE, with chroma subsampling + odd resolution
assert_decoded_image(
include_bytes!("test_assets/64x57_ycocg_rle_ss.bin"),
include_bytes!("test_assets/64x57_ycocg_rle_ss.bmp"),
64,
57,
);
}
#[test]
fn decode_64x64_ycocg_raw_ss() {
// AYCoCg (No alpha), no RLE, with chroma subsampling
assert_decoded_image(
include_bytes!("test_assets/64x64_ycocg_raw_ss.bin"),
include_bytes!("test_assets/64x64_ycocg_raw_ss.bmp"),
64,
64,
);
}
}
+7
View File
@@ -0,0 +1,7 @@
//! This module provides the RDP6 bitmap decoder implementation
pub(crate) mod bitmap_stream;
pub(crate) mod rle;
pub use bitmap_stream::{BitmapDecodeError, BitmapStreamDecoder};
pub use rle::RleError;
+334
View File
@@ -0,0 +1,334 @@
use byteorder::ReadBytesExt;
use std::io::{Read, Write};
use thiserror::Error;
/// Maximum possible segment size is 47 (run_length = 2, raw_bytes_count = 15), which is treated as
/// special mode segment, which repeats last decoded byte in scanline 32 + raw_bytes_count times
const MAX_DECODED_SEGMENT_SIZE: usize = 47;
#[derive(Debug, Error)]
pub enum RleError {
#[error("Failed to read RLE-compressed data: {0}")]
ReadCompressedData(#[source] std::io::Error),
#[error("Failed to write decompressed data: {0}")]
WriteDecompressedData(#[source] std::io::Error),
#[error("Invalid RLE segment header")]
InvalidSegmentHeader,
#[error("Decoded scanline segments length exceeds scanline length")]
SegmentDoNotFitScanline,
}
/// RLE-encoded color plane decoder implementation for RDP6 bitmap stream
#[derive(Debug)]
struct RlePlaneDecoder {
/// RDP6 performs per-scanline encoding, therefore segment decoder require state reset
/// for when each scanline is started (e.g. resetting last decoded byte value to 0)
last_decoded_byte: u8,
width: usize,
height: usize,
decoded_data: [u8; MAX_DECODED_SEGMENT_SIZE],
decoded_data_len: usize,
}
impl RlePlaneDecoder {
pub fn new(width: usize, height: usize) -> Self {
Self {
last_decoded_byte: 0,
width,
height,
decoded_data: [0; MAX_DECODED_SEGMENT_SIZE],
decoded_data_len: 0,
}
}
fn decompress_next_segment(&mut self, mut src: &[u8]) -> Result<usize, RleError> {
let control_byte = src.read_u8().map_err(RleError::ReadCompressedData)?;
if control_byte == 0 {
return Err(RleError::InvalidSegmentHeader);
}
let rle_bytes_field = control_byte & 0x0F;
let raw_bytes_field = (control_byte >> 4) & 0x0F;
let (run_length, raw_bytes_count) = match rle_bytes_field {
1 => (16 + raw_bytes_field as usize, 0),
2 => (32 + raw_bytes_field as usize, 0),
rle_control => (rle_control as usize, raw_bytes_field as usize),
};
self.decoded_data_len = raw_bytes_count + run_length;
src.read_exact(&mut self.decoded_data[..raw_bytes_count])
.map_err(RleError::ReadCompressedData)?;
if raw_bytes_count > 0 {
// save last decoded byte for the next segments decoding
self.last_decoded_byte = self.decoded_data[raw_bytes_count - 1];
}
self.decoded_data[raw_bytes_count..self.decoded_data_len].fill(self.last_decoded_byte);
Ok(raw_bytes_count + 1)
}
/// Decodes single RLE-encoded scanline, without performing delta transformation
fn decode_scanline(&mut self, src: &[u8], mut dst: &mut [u8]) -> Result<usize, RleError> {
let mut decoded_columns = 0;
let mut read_bytes = 0;
self.last_decoded_byte = 0;
while decoded_columns < self.width {
read_bytes += self.decompress_next_segment(&src[read_bytes..])?;
if decoded_columns + self.decoded_data_len > self.width {
return Err(RleError::SegmentDoNotFitScanline);
}
dst.write_all(&self.decoded_data[..self.decoded_data_len])
.map_err(RleError::WriteDecompressedData)?;
decoded_columns += self.decoded_data_len;
}
Ok(read_bytes)
}
/// Performs delta transformation as described in 3.1.9.2.3 of [MS-RDPEGDI]
fn resolve_scanline_delta(prev_line: &[u8], current_scanline: &mut [u8]) {
assert!(prev_line.len() == current_scanline.len());
current_scanline
.iter_mut()
.zip(prev_line.iter())
.for_each(|(dst, src)| {
let delta = *dst;
let value_above = *src;
let transformed_delta = if delta % 2 == 1 {
255u8.wrapping_sub((delta.wrapping_sub(1)) >> 1)
} else {
delta >> 1
};
*dst = value_above.wrapping_add(transformed_delta);
});
}
pub fn decode(mut self, src: &[u8], dst: &mut [u8]) -> Result<usize, RleError> {
let mut read_bytes = 0;
read_bytes += self.decode_scanline(src, dst)?;
let (mut prev_scanline, mut dst) = dst.split_at_mut(self.width);
for _ in 1..self.height {
let current_scanline = &mut dst[..self.width];
read_bytes += self.decode_scanline(&src[read_bytes..], current_scanline)?;
Self::resolve_scanline_delta(prev_scanline, current_scanline);
(prev_scanline, dst) = dst.split_at_mut(self.width);
}
Ok(read_bytes)
}
}
/// Performs decompression of 8bpp color plane into slice.
/// Slice must have enough space for decompressed data.
/// Size of data written to dst buffer is exactly equal to `width * height`.
///
/// Returns number of bytes consumed from src buffer.
pub fn decompress_8bpp_plane(
src: &[u8],
dst: &mut [u8],
width: impl Into<usize>,
height: impl Into<usize>,
) -> Result<usize, RleError> {
let width = width.into();
let height = height.into();
RlePlaneDecoder::new(width, height).decode(src, dst)
}
#[cfg(test)]
mod tests {
use super::*;
use expect_test::expect;
/// Performs decompression of 8bpp color plane into vector. Vector will be resized to fit decompressed data.
pub fn decompress(
src: &[u8],
dst: &mut Vec<u8>,
width: impl Into<usize>,
height: impl Into<usize>,
) -> Result<usize, RleError> {
let width = width.into();
let height = height.into();
// Ensure dest buffer have enough space for decompressed data
dst.resize(width * height, 0);
decompress_8bpp_plane(src, dst.as_mut_slice(), width, height)
}
#[test]
fn long_sequence_decode() {
// Example from 3.1.9.2.2 of [MS-RDPEGDI].
let src = [0x1F, 0x41, 0xF2, 0x52];
let width = 100usize;
let height = 1usize;
let expected = &[0x41u8; 100];
let mut actual = Vec::new();
decompress(&src, &mut actual, width, height).unwrap();
assert_eq!(actual, expected);
}
#[test]
fn multiline_decode() {
// Example from 3.1.9.2.3 of [MS-RDPEGDI].
let src = [
0x13, 0xFF, 0x20, 0xFE, 0xFD, 0x60, 0x01, 0x7D, 0xF5, 0xC2, 0x9A, 0x38, 0x60, 0x01, 0x67, 0x8B, 0xA3, 0x78,
0xAF,
];
let width = 6usize;
let height = 3usize;
let expected = &[
255, 255, 255, 255, 254, 253, 254, 192, 132, 96, 75, 25, 253, 140, 62, 14, 135, 193,
];
let mut actual = Vec::new();
decompress(&src, &mut actual, width, height).unwrap();
assert_eq!(actual, expected);
}
#[test]
fn each_scanline_resets_last_decoded_byte() {
let src = [0x17, 0xFF, 0x04, 0x40, 0x01, 0x02, 0x03, 0x04];
let width = 8usize;
let height = 2usize;
let mut actual = Vec::new();
let expected = &[
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 0, 253, 1,
];
decompress(&src, &mut actual, width, height).unwrap();
assert_eq!(actual, expected);
}
#[test]
fn segments_out_of_scanline_produce_error() {
let src = [
0x18, 0xFF, // Will produce 9 bytes which is out of bounds for 8x2 image
0x04, 0x40, 0x01, 0x02, 0x03, 0x04,
];
let width = 8usize;
let height = 2usize;
let mut actual = Vec::new();
expect![[r#"
Err(
SegmentDoNotFitScanline,
)
"#]]
.assert_debug_eq(&decompress(&src, &mut actual, width, height));
// Same test, but fail on non-first line
let src = [
0x17, 0xFF, 0x18, 0xFF, // Will produce 9 bytes which is out of bounds for 8x2 image
];
let width = 8usize;
let height = 2usize;
let mut actual = Vec::new();
expect![[r#"
Err(
SegmentDoNotFitScanline,
)
"#]]
.assert_debug_eq(&decompress(&src, &mut actual, width, height));
}
#[test]
fn insufficient_raw_bytes_handled() {
let src = [0x18]; // Actually require 1 more byte
let width = 8usize;
let height = 2usize;
let mut actual = Vec::new();
expect![[r#"
Err(
ReadCompressedData(
Error {
kind: UnexpectedEof,
message: "failed to fill whole buffer",
},
),
)
"#]]
.assert_debug_eq(&decompress(&src, &mut actual, width, height));
}
#[test]
fn empty_buffer_handled() {
let src = [];
let width = 8usize;
let height = 2usize;
let mut actual = Vec::new();
expect![[r#"
Err(
ReadCompressedData(
Error {
kind: UnexpectedEof,
message: "failed to fill whole buffer",
},
),
)
"#]]
.assert_debug_eq(&decompress(&src, &mut actual, width, height));
}
#[test]
fn too_small_dest_buffer_handled() {
let src = [0x17, 0xFF, 0x04, 0x40, 0x01, 0x02, 0x03, 0x04];
let width = 8usize;
let height = 2usize;
let mut actual = vec![0u8; 7];
expect![[r#"
Err(
WriteDecompressedData(
Error {
kind: WriteZero,
message: "failed to write whole buffer",
},
),
)
"#]]
.assert_debug_eq(&decompress_8bpp_plane(&src, &mut actual, width, height));
// Check same failure mode, but on non-first line
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Some files were not shown because too many files have changed in this diff Show More