mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
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:
Generated
+28
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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.
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user