diff --git a/CHANGELOG.md b/CHANGELOG.md index d66cfc8..7fffdb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,91 @@ All notable changes to rustvncserver will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.0] - 2025-10-27 + +**Stable Release** - This marks the official 2.0.0 release, graduating from beta status. + +### Changed + +- **Code Deduplication**: Removed duplicate code to improve maintainability + - Updated `rfb-encodings` dependency from `0.1.3` to `0.1.5` + - Removed duplicate encoding type constants from `protocol.rs` (now imported from `rfb-encodings`) + - Removed duplicate Hextile and Tight subencoding constants (now imported from `rfb-encodings`) + - Deleted duplicate `src/jpeg/` module (now using TurboJPEG from `rfb-encodings`) + - Encoding constants now have single source of truth in `rfb-encodings` library + - Server-specific constants (COPYRECT, pseudo-encodings, protocol messages) remain in `protocol.rs` + - Code reduction: ~220 lines of duplicate code eliminated + +- **Documentation**: Comprehensive TurboJPEG setup and licensing information + - Added TurboJPEG installation instructions for Ubuntu/Debian, macOS, and Windows in README + - Added "TurboJPEG Setup" section with platform-specific installation commands + - Updated License section to document optional third-party dependencies + - Updated NOTICE file with complete libjpeg-turbo attribution including: + - BSD-3-Clause license for TurboJPEG API + - IJG License for libjpeg code + - zlib License for SIMD extensions + - Copyright notices for all contributors + - Clarified that libjpeg-turbo is NOT distributed and users are responsible for license compliance + +### Improved + +- Simplified API surface by consolidating constant definitions +- Better separation of concerns: encoding library handles encoding constants, server handles protocol constants +- Reduced maintenance burden by eliminating duplicate code across projects + +## [2.0.0-beta.4] - 2025-10-25 + +### Changed + +- Updated all documentation (README.md, TECHNICAL.md, CONTRIBUTING.md) to properly credit the `rfb-encodings` library + - Added clear references to [rfb-encodings](https://github.com/dustinmcafee/rfb-encodings) throughout documentation + - Updated architecture diagrams to show the separation between rustvncserver and rfb-encodings + - Clarified that rfb-encodings provides encoding implementations (for servers), not decoding (for clients) + - Updated version examples in documentation to use version 2.0 + +## [2.0.0-beta.3] - 2025-10-23 + +### Changed + +- Updated `rfb-encodings` dependency from `0.1` to `0.1.3` + - Fixes critical build failure when using `turbojpeg` feature without `debug-logging` + - Resolves "use of unresolved module or unlinked crate log" compilation errors + - All turbojpeg builds now work correctly + +## [2.0.0-beta.2] - 2025-10-23 + +### Fixed + +- Code formatting: Removed extra blank line in `protocol.rs` + +## [2.0.0-beta.1] - 2025-10-23 + +### Changed + +- **Major architectural refactoring:** Extracted all encoding implementations to separate `rfb-encodings` crate + - All encoding modules (Raw, RRE, CoRRE, Hextile, Zlib, Tight, TightPng, ZlibHex, ZRLE, ZYWRLE) moved to `rfb-encodings` + - Pixel format translation moved to `rfb-encodings` + - `PixelFormat` struct now re-exported from `rfb-encodings` + - Benefits: + - Encoding implementations now reusable across VNC servers, clients, proxies, and recorders + - Cleaner separation of concerns: protocol vs encoding + - Independent versioning and publishing of encodings + - Better visibility and discoverability on crates.io + - **Fully backwards compatible:** All public APIs preserved through re-exports + - Existing code using `rustvncserver::encoding::*` or `rustvncserver::PixelFormat` continues to work + +### Added + +- New dependency: `rfb-encodings` crate (0.1.2) for all encoding implementations +- Re-exported all encoding types from `rfb-encodings` for full backwards compatibility +- `pub use rfb_encodings as encoding;` allows seamless access to all encodings + +### Fixed + +- All fixes from rfb-encodings 0.1.1 and 0.1.2 inherited: + - macOS CI Build: Fixed turbojpeg linking errors + - Compiler warnings for conditional compilation suppressed + ## [1.1.5] - 2025-10-23 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index edd2f98..a860ef3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,13 @@ We welcome feature suggestions! Please create an issue with: - Rust 1.90 or later - `libjpeg-turbo` (optional, for turbojpeg feature) +### Architecture + +rustvncserver uses the separate [rfb-encodings](https://github.com/dustinmcafee/rfb-encodings) library for all encoding implementations. This modular design allows: +- Encoding code to be reused across VNC servers, proxies, and recording tools +- Independent versioning and publishing of encoding implementations +- Better separation of concerns (protocol vs encoding) + ### Building ```bash @@ -139,11 +146,15 @@ cargo test --all-features If you're adding a new VNC encoding: -1. Create a new file in `src/encoding/` -2. Implement the encoding following RFC 6143 specification -3. Add comprehensive tests -4. Update README.md with the new encoding -5. Add example demonstrating the encoding +1. Contribute to the [rfb-encodings](https://github.com/dustinmcafee/rfb-encodings) library instead +2. Create a new file in the rfb-encodings `src/` directory +3. Implement the encoding following RFC 6143 specification +4. Add comprehensive tests +5. Update the rfb-encodings README.md with the new encoding +6. Update rustvncserver to use the new encoding from rfb-encodings +7. Add example demonstrating the encoding in rustvncserver + +**Note:** All encoding implementations are now maintained in the separate rfb-encodings crate for reusability across VNC servers and other tools. ## Performance Considerations diff --git a/Cargo.toml b/Cargo.toml index 9050428..9e0b871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustvncserver" -version = "1.1.5" +version = "2.0.0" edition = "2021" rust-version = "1.90" authors = ["Dustin McAfee"] @@ -17,17 +17,17 @@ crate-type = ["cdylib", "rlib"] [dependencies] tokio = { version = "1", features = ["rt-multi-thread", "sync", "time", "net", "io-util", "macros"] } bytes = "1" -flate2 = "1.0" # Zlib compression -png = "0.17" # PNG encoding for TightPng log = "0.4" thiserror = "1.0" # Error handling des = "0.8" # DES encryption for VNC auth rand = "0.8" # Random number generation for auth +flate2 = "1.0" # Zlib compression for Tight encoding +rfb-encodings = "0.1.5" # RFB encoding implementations [features] default = [] -turbojpeg = [] # Enable TurboJPEG for better JPEG performance (requires libjpeg-turbo) -debug-logging = [] # Enable verbose debug logging (shows client IPs, connection details) +turbojpeg = ["rfb-encodings/turbojpeg"] # Enable TurboJPEG for better JPEG performance (requires libjpeg-turbo) +debug-logging = ["rfb-encodings/debug-logging"] # Enable verbose debug logging (shows client IPs, connection details) [dev-dependencies] tokio-test = "0.4" diff --git a/NOTICE b/NOTICE index 70f32ab..643e12b 100644 --- a/NOTICE +++ b/NOTICE @@ -25,6 +25,28 @@ non-commercial applications. ================================================================================ +TurboJPEG / libjpeg-turbo (Optional): + +When compiled with the 'turbojpeg' feature flag, this software provides FFI +bindings to libjpeg-turbo, which must be installed separately by the end user. + +libjpeg-turbo is licensed under a BSD-style license with multiple components: +- libjpeg-turbo API: BSD 3-Clause License +- libjpeg code: Independent JPEG Group (IJG) License +- SIMD extensions: zlib License + +Copyright (C) 2009-2024 D. R. Commander +Copyright (C) 2015, 2020 Google, Inc. +Portions Copyright (C) 1991-2020 Thomas G. Lane, Guido Vollbeding + +libjpeg-turbo is NOT distributed with this software. Users must install it +separately and are responsible for compliance with its license terms. + +For full license text, see: +https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/LICENSE.md + +================================================================================ + This software depends on the following third-party Rust crates: - tokio (MIT License) diff --git a/README.md b/README.md index f8f5cec..47a19c3 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ A pure Rust VNC (Virtual Network Computing) server library with complete RFB pro ### Supported Encodings +All encoding implementations are provided by the separate [**rfb-encodings**](https://github.com/dustinmcafee/rfb-encodings) library, which can be reused across VNC servers and recording/proxy tools that need to encode framebuffer data. + | Encoding | ID | Description | Wire Format Match | Testing Status | |----------|----|----|-------------------|----------------| | **Raw** | 0 | Uncompressed pixels | ✅ 100% | ✅ Tested | @@ -96,7 +98,7 @@ Add to your `Cargo.toml`: ```toml [dependencies] -rustvncserver = "1.1" +rustvncserver = "2.0" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ``` @@ -104,13 +106,30 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ```toml [dependencies] -rustvncserver = { version = "1.1", features = ["turbojpeg"] } +rustvncserver = { version = "2.0", features = ["turbojpeg"] } ``` **Features:** - `turbojpeg` - Enable TurboJPEG for hardware-accelerated JPEG compression (requires libjpeg-turbo) - `debug-logging` - Enable verbose debug logging (shows client IPs, connection details, encoding statistics) +### TurboJPEG Setup + +The `turbojpeg` feature requires libjpeg-turbo to be installed on your system: + +**Ubuntu/Debian:** +```bash +sudo apt-get install libturbojpeg0-dev +``` + +**macOS:** +```bash +brew install jpeg-turbo +``` + +**Windows:** +Download from [libjpeg-turbo.org](https://libjpeg-turbo.org/) + ## Quick Start ```rust @@ -355,10 +374,15 @@ This library implements the VNC protocol as specified in RFC 6143, which is a pu The ZYWRLE algorithm is used under a BSD-style license from Hitachi Systems & Services, Ltd. All Rust dependencies use MIT or dual MIT/Apache-2.0 licenses. +### Optional Third-Party Dependencies + +When using the `turbojpeg` feature, this library provides bindings to libjpeg-turbo (licensed under BSD-3-Clause, IJG, and zlib licenses). Users are responsible for compliance with libjpeg-turbo's license terms. See [NOTICE](NOTICE) for details. + ## Credits - **Author**: Dustin McAfee - **Protocol**: Implements RFC 6143 (RFB Protocol Specification) +- **Encodings**: [rfb-encodings](https://github.com/dustinmcafee/rfb-encodings) - Reusable RFB encoding library - **Used in**: [RustVNC](https://github.com/dustinmcafee/RustVNC) - VNC server for Android ## See Also diff --git a/TECHNICAL.md b/TECHNICAL.md index 2a6eec4..73e9c74 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -22,11 +22,13 @@ Complete technical documentation for the rustvncserver library and RFC 6143 prot rustvncserver is a pure Rust VNC (Virtual Network Computing) server library with full RFC 6143 protocol compliance. +All encoding implementations are provided by the separate [**rfb-encodings**](https://github.com/dustinmcafee/rfb-encodings) library, which provides reusable RFB encoding modules for VNC servers and other tools that need to encode framebuffer data. + ### Key Features **Protocol Compliance:** - ✅ RFC 6143 (RFB Protocol 3.8) fully compliant -- ✅ 11 encoding types implemented +- ✅ 11 encoding types implemented (via rfb-encodings library) - ✅ All standard pixel formats (8/16/24/32-bit) - ✅ Quality and compression level pseudo-encodings - ✅ Reverse connections and repeater support @@ -36,6 +38,7 @@ rustvncserver is a pure Rust VNC (Virtual Network Computing) server library with - **Thread Safety**: No data races, safe concurrent client handling - **Modern Async I/O**: Built on Tokio runtime for efficient connection handling - **Better Performance**: Zero-copy framebuffer updates via Arc-based sharing +- **Modular Design**: Encoding implementations separated into reusable rfb-encodings crate ### Architecture @@ -50,10 +53,17 @@ rustvncserver is a pure Rust VNC (Virtual Network Computing) server library with │ └──────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────┐ │ │ │ VncClient (per-client connection) │ │ -│ │ • Pixel format translation │ │ │ │ • Encoding selection │ │ │ │ • Compression streams │ │ │ └──────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ + │ + ▼ uses +┌─────────────────────────────────────────────────┐ +│ rfb-encodings Library │ +│ ┌──────────────────────────────────────────┐ │ +│ │ Pixel format translation │ │ +│ └──────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────┐ │ │ │ Encodings (11 types) │ │ │ │ • Tight (5 modes) + libjpeg-turbo │ │ @@ -686,16 +696,17 @@ The library depends on the following crates (from `Cargo.toml`): ```toml [dependencies] +rfb-encodings = "0.1" # RFB encoding implementations tokio = { version = "1", features = ["rt-multi-thread", "sync", "time", "net", "io-util", "macros"] } bytes = "1" -flate2 = "1.0" # Zlib compression -png = "0.17" # PNG encoding for TightPng log = "0.4" thiserror = "1.0" # Error handling des = "0.8" # DES encryption for VNC auth rand = "0.8" # Random number generation ``` +**Note:** All encoding-related dependencies (flate2, png, libjpeg-turbo, etc.) are now managed by the rfb-encodings crate. + ### Optional Features The library supports optional compilation features: @@ -708,11 +719,18 @@ The library can be integrated into any Rust project by adding to `Cargo.toml`: ```toml [dependencies] -rustvncserver = "1.0" +rustvncserver = "2.0" # Or from a git repository: # rustvncserver = { git = "https://github.com/dustinmcafee/rustvncserver" } ``` +The rfb-encodings library is included automatically as a dependency. If you need to use encoding functions directly, you can also add: + +```toml +[dependencies] +rfb-encodings = "0.1" +``` + --- ## API Reference diff --git a/src/client.rs b/src/client.rs index 52319ef..b28f37d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -62,7 +62,7 @@ use crate::protocol::{ PROTOCOL_VERSION, SECURITY_RESULT_FAILED, SECURITY_RESULT_OK, SECURITY_TYPE_NONE, SECURITY_TYPE_VNC_AUTH, SERVER_MSG_FRAMEBUFFER_UPDATE, SERVER_MSG_SERVER_CUT_TEXT, }; -use crate::translate; +use rfb_encodings::translate; /// Represents various events that a VNC client can send to the server. /// These events typically correspond to user interactions like keyboard input, diff --git a/src/encoding/common.rs b/src/encoding/common.rs deleted file mode 100644 index d0dc3ce..0000000 --- a/src/encoding/common.rs +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Common helper functions shared across multiple VNC encodings. - -use bytes::{BufMut, BytesMut}; -use std::collections::HashMap; - -/// Represents a subrectangle in RRE/CoRRE/Hextile encoding. -#[derive(Debug)] -pub struct Subrect { - /// The color value of this subrectangle in 32-bit RGB format - pub color: u32, - /// The X coordinate of the subrectangle's top-left corner - pub x: u16, - /// The Y coordinate of the subrectangle's top-left corner - pub y: u16, - /// The width of the subrectangle in pixels - pub w: u16, - /// The height of the subrectangle in pixels - pub h: u16, -} - -/// Convert RGBA (4 bytes/pixel) to RGB24 pixel values in VNC pixel format. -/// Our pixel format has: `red_shift=0`, `green_shift=8`, `blue_shift=16`, little-endian -/// So pixel = (R << 0) | (G << 8) | (B << 16) = 0x00BBGGRR -#[must_use] -pub fn rgba_to_rgb24_pixels(data: &[u8]) -> Vec { - data.chunks_exact(4) - .map(|chunk| { - u32::from(chunk[0]) | // R at bits 0-7 - (u32::from(chunk[1]) << 8) | // G at bits 8-15 - (u32::from(chunk[2]) << 16) // B at bits 16-23 - }) - .collect() -} - -/// Find the most common color in the pixel array. -#[must_use] -pub fn get_background_color(pixels: &[u32]) -> u32 { - if pixels.is_empty() { - return 0; - } - - let mut counts: HashMap = HashMap::new(); - for &pixel in pixels { - *counts.entry(pixel).or_insert(0) += 1; - } - - counts - .into_iter() - .max_by_key(|(_, count)| *count) - .map_or(pixels[0], |(color, _)| color) -} - -/// Find subrectangles of non-background pixels. -#[must_use] -#[allow(clippy::cast_possible_truncation)] // Subrect coordinates limited to tile dimensions (max 16x16) -pub fn find_subrects(pixels: &[u32], width: usize, height: usize, bg_color: u32) -> Vec { - let mut subrects = Vec::new(); - let mut marked = vec![false; pixels.len()]; - - for y in 0..height { - for x in 0..width { - let idx = y * width + x; - if marked[idx] || pixels[idx] == bg_color { - continue; - } - - let color = pixels[idx]; - - // Find largest rectangle starting at (x, y) - let mut max_w = 0; - for test_x in x..width { - let test_idx = y * width + test_x; - if marked[test_idx] || pixels[test_idx] != color { - break; - } - max_w = test_x - x + 1; - } - - let mut h = 1; - 'outer: for test_y in (y + 1)..height { - for test_x in x..(x + max_w) { - let test_idx = test_y * width + test_x; - if marked[test_idx] || pixels[test_idx] != color { - break 'outer; - } - } - h = test_y - y + 1; - } - - // Try horizontal vs vertical rectangle - let mut best_w = max_w; - let mut best_h = h; - - // Also try vertical - let mut max_h = 0; - for test_y in y..height { - let test_idx = test_y * width + x; - if marked[test_idx] || pixels[test_idx] != color { - break; - } - max_h = test_y - y + 1; - } - - let mut w2 = 1; - 'outer2: for test_x in (x + 1)..width { - for test_y in y..(y + max_h) { - let test_idx = test_y * width + test_x; - if marked[test_idx] || pixels[test_idx] != color { - break 'outer2; - } - } - w2 = test_x - x + 1; - } - - // Choose larger rectangle - if w2 * max_h > best_w * best_h { - best_w = w2; - best_h = max_h; - } - - // Mark pixels as used - for dy in 0..best_h { - for dx in 0..best_w { - marked[(y + dy) * width + (x + dx)] = true; - } - } - - subrects.push(Subrect { - color, - x: x as u16, - y: y as u16, - w: best_w as u16, - h: best_h as u16, - }); - } - } - - subrects -} - -/// Extract a tile from the pixel array. -#[must_use] -pub fn extract_tile( - pixels: &[u32], - width: usize, - x: usize, - y: usize, - tw: usize, - th: usize, -) -> Vec { - let mut tile = Vec::with_capacity(tw * th); - for dy in 0..th { - for dx in 0..tw { - tile.push(pixels[(y + dy) * width + (x + dx)]); - } - } - tile -} - -/// Analyze tile colors to determine if solid, monochrome, or multicolor. -/// Returns: (`is_solid`, `is_mono`, `bg_color`, `fg_color`) -#[must_use] -pub fn analyze_tile_colors(pixels: &[u32]) -> (bool, bool, u32, u32) { - if pixels.is_empty() { - return (true, true, 0, 0); - } - - let mut colors: HashMap = HashMap::new(); - for &pixel in pixels { - *colors.entry(pixel).or_insert(0) += 1; - } - - if colors.len() == 1 { - return (true, true, pixels[0], 0); - } - - if colors.len() == 2 { - let mut sorted: Vec<_> = colors.into_iter().collect(); - sorted.sort_by_key(|(_, count)| std::cmp::Reverse(*count)); - return (false, true, sorted[0].0, sorted[1].0); - } - - let bg = get_background_color(pixels); - (false, false, bg, 0) -} - -/// Write a 32-bit pixel value to buffer in little-endian format (4 bytes). -/// Pixel format: R at bits 0-7, G at bits 8-15, B at bits 16-23, unused at bits 24-31 -/// For 32bpp client: writes [R, G, B, 0] in that order -pub fn put_pixel32(buf: &mut BytesMut, pixel: u32) { - buf.put_u32_le(pixel); // Write full 32-bit pixel in little-endian format -} - -/// Write a 24-bit pixel value to buffer in RGB24 format (3 bytes). -/// Pixel format: R at bits 0-7, G at bits 8-15, B at bits 16-23 -/// Implements 24-bit pixel packing as specified in RFC 6143. -/// Writes [R, G, B] in that order (3 bytes total). -pub fn put_pixel24(buf: &mut BytesMut, pixel: u32) { - buf.put_u8((pixel & 0xFF) as u8); // R - buf.put_u8(((pixel >> 8) & 0xFF) as u8); // G - buf.put_u8(((pixel >> 16) & 0xFF) as u8); // B -} - -/// Translate a single RGB pixel to the client's pixel format for TIGHT encoding. -/// This properly handles `red_shift`, `green_shift`, `blue_shift` values for -/// correct pixel format translation. -/// -/// IMPORTANT: TIGHT encoding uses 24-bit pixel format (3 bytes) when the client has -/// depth=24 with 8-bit color components, even if `bits_per_pixel=32`. This is a -/// standard optimization for reducing bandwidth. -/// -/// Input pixel format: RGB stored in bits 0-23 (R=bits 0-7, G=bits 8-15, B=bits 16-23) -/// Output: Translated bytes in client's pixel format (3 or 4 bytes depending on format) -#[must_use] -#[allow(clippy::cast_possible_truncation)] // Intentionally extracting byte components from pixel values -pub fn translate_pixel_to_client_format( - pixel: u32, - client_format: &crate::protocol::PixelFormat, -) -> Vec { - use crate::protocol::PixelFormat; - use crate::translate::translate_pixels; - - // Check if we should use 24-bit format (3 bytes) instead of 32-bit (4 bytes) - let use_24bit = client_format.depth == 24 - && client_format.red_max == 255 - && client_format.green_max == 255 - && client_format.blue_max == 255; - - if use_24bit { - // Send only 3 bytes for 24-bit depth clients (TIGHT optimization) - // Match Pack24 behavior exactly: pack pixel then extract using shifts - - // Pack RGB components into client's pixel format - let r = (pixel & 0xFF) as u8; - let g = ((pixel >> 8) & 0xFF) as u8; - let b = ((pixel >> 16) & 0xFF) as u8; - - // Create pixel value using client's bit layout - let pixel_value = (u32::from(r) << client_format.red_shift) - | (u32::from(g) << client_format.green_shift) - | (u32::from(b) << client_format.blue_shift); - - // Extract 3 bytes in the order they appear in memory (like Pack24) - // For little-endian with shifts 0/8/16: pixel_value = 0x00BBGGRR - // So bytes are [RR, GG, BB] in memory order - if client_format.big_endian_flag != 0 { - // Big-endian: extract from high to low - vec![ - (pixel_value >> 16) as u8, - (pixel_value >> 8) as u8, - pixel_value as u8, - ] - } else { - // Little-endian: extract from low to high - vec![ - pixel_value as u8, - (pixel_value >> 8) as u8, - (pixel_value >> 16) as u8, - ] - } - } else { - // Create RGBA32 bytes for this single pixel - let rgba_bytes = [ - (pixel & 0xFF) as u8, // R - ((pixel >> 8) & 0xFF) as u8, // G - ((pixel >> 16) & 0xFF) as u8, // B - 0, // A - ]; - - // Use existing translation logic to convert to client format - let server_format = PixelFormat::rgba32(); - let translated = translate_pixels(&rgba_bytes, &server_format, client_format); - - // Return as Vec for easier handling - translated.to_vec() - } -} - -/// Check if all pixels are the same color. -#[must_use] -pub fn check_solid_color(pixels: &[u32]) -> Option { - if pixels.is_empty() { - return None; - } - - let first = pixels[0]; - if pixels.iter().all(|&p| p == first) { - Some(first) - } else { - None - } -} - -/// Build a color palette from pixels. -#[must_use] -pub fn build_palette(pixels: &[u32]) -> Vec { - let mut colors: HashMap = HashMap::new(); - for &pixel in pixels { - *colors.entry(pixel).or_insert(0) += 1; - } - - let mut palette: Vec<_> = colors.into_iter().collect(); - palette.sort_by_key(|(_, count)| std::cmp::Reverse(*count)); - palette.into_iter().map(|(color, _)| color).collect() -} diff --git a/src/encoding/corre.rs b/src/encoding/corre.rs deleted file mode 100644 index 090b9cd..0000000 --- a/src/encoding/corre.rs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC `CoRRE` (Compact RRE) encoding implementation. -//! -//! `CoRRE` is like RRE but uses compact subrectangles with u8 coordinates. -//! More efficient for small rectangles. - -use super::common::{find_subrects, get_background_color, rgba_to_rgb24_pixels}; -use super::Encoding; -use bytes::{BufMut, BytesMut}; -use log::info; - -/// Implements the VNC "`CoRRE`" (Compact RRE) encoding. -/// -/// `CoRRE` is like RRE but uses compact subrectangles with u8 coordinates. -/// Format: \[bgColor\]\[nSubrects(u8)\]\[subrect1\]...\[subrectN\] -/// Each subrect: \[color\]\[x(u8)\]\[y(u8)\]\[w(u8)\]\[h(u8)\] -pub struct CorRreEncoding; - -impl Encoding for CorRreEncoding { - #[allow(clippy::cast_possible_truncation)] // CoRRE protocol uses u8 coordinates/dimensions per RFC 6143 - fn encode( - &self, - data: &[u8], - width: u16, - height: u16, - _quality: u8, - _compression: u8, - ) -> BytesMut { - // CoRRE format per RFC 6143: - // Protocol layer writes: FramebufferUpdateRectHeader + nSubrects count - // Encoder writes: bgColor + subrects - // Each subrect: color(4) + x(1) + y(1) + w(1) + h(1) - let pixels = rgba_to_rgb24_pixels(data); - let bg_color = get_background_color(&pixels); - - // Find subrectangles - let subrects = find_subrects(&pixels, width as usize, height as usize, bg_color); - - // Encoder output: background color + subrectangle data - // Protocol layer will write nSubrects separately - let mut buf = BytesMut::with_capacity(4 + subrects.len() * 8); - buf.put_u32_le(bg_color); // background pixel value (little-endian) - - // Write subrectangles - for subrect in &subrects { - buf.put_u32_le(subrect.color); // pixel color (little-endian) - buf.put_u8(subrect.x as u8); // x coordinate (u8) - buf.put_u8(subrect.y as u8); // y coordinate (u8) - buf.put_u8(subrect.w as u8); // width (u8) - buf.put_u8(subrect.h as u8); // height (u8) - } - - // HEX DUMP: Log the exact bytes being encoded - let hex_str: String = buf - .iter() - .take(32) // Only show first 32 bytes - .map(|b| format!("{b:02x}")) - .collect::>() - .join(" "); - info!( - "CoRRE encoded {}x{}: {} bytes ({}subrects) = [{}...]", - width, - height, - buf.len(), - subrects.len(), - hex_str - ); - - buf - } -} diff --git a/src/encoding/hextile.rs b/src/encoding/hextile.rs deleted file mode 100644 index b9ac3e4..0000000 --- a/src/encoding/hextile.rs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC Hextile encoding implementation. -//! -//! Hextile divides the rectangle into 16x16 tiles and encodes each independently. -//! Each tile can be: raw, solid, monochrome with subrects, or colored with subrects. - -use super::common::{ - analyze_tile_colors, extract_tile, find_subrects, put_pixel32, rgba_to_rgb24_pixels, -}; -use super::Encoding; -use crate::protocol::{ - HEXTILE_ANY_SUBRECTS, HEXTILE_BACKGROUND_SPECIFIED, HEXTILE_FOREGROUND_SPECIFIED, HEXTILE_RAW, - HEXTILE_SUBRECTS_COLOURED, -}; -use bytes::{BufMut, BytesMut}; - -/// Implements the VNC "Hextile" encoding. -/// -/// Hextile divides the rectangle into 16x16 tiles and encodes each independently. -/// Each tile can be: raw, solid, monochrome with subrects, or colored with subrects. -pub struct HextileEncoding; - -impl Encoding for HextileEncoding { - #[allow(clippy::similar_names)] // last_bg and last_fg are standard VNC Hextile terminology - #[allow(clippy::cast_possible_truncation)] // Hextile protocol requires packing coordinates into u8 (max 16x16 tiles) - fn encode( - &self, - data: &[u8], - width: u16, - height: u16, - _quality: u8, - _compression: u8, - ) -> BytesMut { - let mut buf = BytesMut::new(); - let pixels = rgba_to_rgb24_pixels(data); - - let mut last_bg: Option = None; - let mut last_fg: Option = None; - - // Process tiles (16x16) - for tile_y in (0..height).step_by(16) { - for tile_x in (0..width).step_by(16) { - let tile_w = std::cmp::min(16, width - tile_x); - let tile_h = std::cmp::min(16, height - tile_y); - - // Extract tile data - let tile_pixels = extract_tile( - &pixels, - width as usize, - tile_x as usize, - tile_y as usize, - tile_w as usize, - tile_h as usize, - ); - - // Analyze tile colors - let (is_solid, is_mono, bg, fg) = analyze_tile_colors(&tile_pixels); - - let mut subencoding: u8 = 0; - let tile_start = buf.len(); - - // Reserve space for subencoding byte - buf.put_u8(0); - - if is_solid { - // Solid tile - just update background if needed - if Some(bg) != last_bg { - subencoding |= HEXTILE_BACKGROUND_SPECIFIED; - put_pixel32(&mut buf, bg); - last_bg = Some(bg); - } - } else { - // Find subrectangles - let subrects = - find_subrects(&tile_pixels, tile_w as usize, tile_h as usize, bg); - - // Check if raw would be smaller OR if too many subrects (>255 max for u8) - let raw_size = tile_w as usize * tile_h as usize * 4; // 4 bytes per pixel for 32bpp - // Estimate overhead: bg (if different) + fg (if mono and different) + count byte - let bg_overhead = if Some(bg) == last_bg { 0 } else { 4 }; - let fg_overhead = if is_mono && Some(fg) != last_fg { 4 } else { 0 }; - let subrect_data = subrects.len() * if is_mono { 2 } else { 6 }; - let encoded_size = bg_overhead + fg_overhead + 1 + subrect_data; - - if subrects.is_empty() || subrects.len() > 255 || encoded_size > raw_size { - // Use raw encoding for this tile - subencoding = HEXTILE_RAW; - buf.truncate(tile_start); - buf.put_u8(subencoding); - - for pixel in &tile_pixels { - put_pixel32(&mut buf, *pixel); - } - - last_bg = None; - last_fg = None; - continue; - } - - // Update background - if Some(bg) != last_bg { - subencoding |= HEXTILE_BACKGROUND_SPECIFIED; - put_pixel32(&mut buf, bg); - last_bg = Some(bg); - } - - // We have subrectangles - subencoding |= HEXTILE_ANY_SUBRECTS; - - if is_mono { - // Monochrome tile - if Some(fg) != last_fg { - subencoding |= HEXTILE_FOREGROUND_SPECIFIED; - put_pixel32(&mut buf, fg); - last_fg = Some(fg); - } - - // Write number of subrects - buf.put_u8(subrects.len() as u8); - - // Write subrects (without color) - for sr in subrects { - buf.put_u8(((sr.x as u8) << 4) | (sr.y as u8)); - buf.put_u8((((sr.w - 1) as u8) << 4) | ((sr.h - 1) as u8)); - } - } else { - // Colored subrects - subencoding |= HEXTILE_SUBRECTS_COLOURED; - last_fg = None; - - buf.put_u8(subrects.len() as u8); - - for sr in subrects { - put_pixel32(&mut buf, sr.color); // 4 bytes for 32bpp - buf.put_u8(((sr.x as u8) << 4) | (sr.y as u8)); // packed X,Y - buf.put_u8((((sr.w - 1) as u8) << 4) | ((sr.h - 1) as u8)); - // packed W-1,H-1 - } - } - } - - // Write subencoding byte - buf[tile_start] = subencoding; - } - } - - buf - } -} diff --git a/src/encoding/mod.rs b/src/encoding/mod.rs deleted file mode 100644 index 8af847b..0000000 --- a/src/encoding/mod.rs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC encoding implementations. -//! -//! This module provides all supported VNC encodings for efficient framebuffer -//! transmission over the network. - -use crate::protocol::{ - ENCODING_CORRE, ENCODING_HEXTILE, ENCODING_RAW, ENCODING_RRE, ENCODING_TIGHT, ENCODING_TIGHTPNG, -}; -use bytes::BytesMut; - -pub mod common; -pub mod corre; -pub mod hextile; -pub mod raw; -pub mod rre; -pub mod tight; -pub mod tightpng; -pub mod zlib; -pub mod zlibhex; -pub mod zrle; -pub mod zywrle; - -// Re-export common types -pub use common::*; - -// Re-export encoding implementations -pub use corre::CorRreEncoding; -pub use hextile::HextileEncoding; -pub use raw::RawEncoding; -pub use rre::RreEncoding; -pub use tight::TightEncoding; -pub use tightpng::TightPngEncoding; - -// Re-export persistent encoding functions -pub use zlib::encode_zlib_persistent; -pub use zlibhex::encode_zlibhex_persistent; -pub use zrle::encode_zrle_persistent; - -// Re-export ZYWRLE analysis function -pub use zywrle::zywrle_analyze; - -/// Trait defining the interface for VNC encoding implementations. -pub trait Encoding { - /// Encodes raw pixel data into a VNC-compatible byte stream. - /// - /// # Arguments - /// - /// * `data` - Raw pixel data (RGBA format: 4 bytes per pixel) - /// * `width` - Width of the framebuffer - /// * `height` - Height of the framebuffer - /// * `quality` - Quality level for lossy encodings (0-100) - /// * `compression` - Compression level (0-9) - /// - /// # Returns - /// - /// Encoded data as `BytesMut` - fn encode( - &self, - data: &[u8], - width: u16, - height: u16, - quality: u8, - compression: u8, - ) -> BytesMut; -} - -/// Creates an encoder instance for the specified encoding type. -#[must_use] -pub fn get_encoder(encoding_type: i32) -> Option> { - match encoding_type { - ENCODING_RAW => Some(Box::new(RawEncoding)), - ENCODING_RRE => Some(Box::new(RreEncoding)), - ENCODING_CORRE => Some(Box::new(CorRreEncoding)), - ENCODING_HEXTILE => Some(Box::new(HextileEncoding)), - ENCODING_TIGHT => Some(Box::new(TightEncoding)), - ENCODING_TIGHTPNG => Some(Box::new(TightPngEncoding)), - _ => None, - } -} diff --git a/src/encoding/raw.rs b/src/encoding/raw.rs deleted file mode 100644 index 1d0a50c..0000000 --- a/src/encoding/raw.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC Raw encoding implementation. -//! -//! The simplest encoding that sends pixel data directly without compression. -//! High bandwidth but universally supported. - -use super::Encoding; -use bytes::{BufMut, BytesMut}; - -/// Implements the VNC "Raw" encoding, which sends pixel data directly without compression. -/// -/// This encoding is straightforward but can be very bandwidth-intensive as it transmits -/// the raw framebuffer data in RGB format (without alpha channel). -pub struct RawEncoding; - -impl Encoding for RawEncoding { - fn encode( - &self, - data: &[u8], - _width: u16, - _height: u16, - _quality: u8, - _compression: u8, - ) -> BytesMut { - // For 32bpp clients: convert RGBA to client pixel format (RGBX where X is padding) - // Client format: R at bits 0-7, G at bits 8-15, B at bits 16-23, padding at bits 24-31 - let mut buf = BytesMut::with_capacity(data.len()); // Same size: 4 bytes per pixel - for chunk in data.chunks_exact(4) { - buf.put_u8(chunk[0]); // R at byte 0 - buf.put_u8(chunk[1]); // G at byte 1 - buf.put_u8(chunk[2]); // B at byte 2 - buf.put_u8(0); // Padding at byte 3 (not alpha) - } - buf - } -} diff --git a/src/encoding/rre.rs b/src/encoding/rre.rs deleted file mode 100644 index 2a8a3c1..0000000 --- a/src/encoding/rre.rs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC RRE (Rise-and-Run-length Encoding) implementation. -//! -//! RRE encodes a rectangle as a background color plus a list of subrectangles -//! with their own colors. Effective for large solid regions. - -use super::common::{find_subrects, get_background_color, rgba_to_rgb24_pixels}; -use super::Encoding; -use bytes::{BufMut, BytesMut}; - -/// Implements the VNC "RRE" (Rise-and-Run-length Encoding). -/// -/// RRE encodes a rectangle as a background color plus a list of subrectangles -/// with their own colors. Format: \[nSubrects(u32)\]\[bgColor\]\[subrect1\]...\[subrectN\] -/// Each subrect: \[color\]\[x(u16)\]\[y(u16)\]\[w(u16)\]\[h(u16)\] -pub struct RreEncoding; - -impl Encoding for RreEncoding { - #[allow(clippy::cast_possible_truncation)] // Subrectangle count limited to image size per VNC protocol - fn encode( - &self, - data: &[u8], - width: u16, - height: u16, - _quality: u8, - _compression: u8, - ) -> BytesMut { - // Convert RGBA to RGB pixels (u32 format: 0RGB) - let pixels = rgba_to_rgb24_pixels(data); - - // Find background color (most common pixel) - let bg_color = get_background_color(&pixels); - - // Find all subrectangles - let subrects = find_subrects(&pixels, width as usize, height as usize, bg_color); - - // Always encode all pixels to avoid data loss - // (Even if RRE is inefficient, we must preserve the image correctly) - let encoded_size = 4 + 4 + (subrects.len() * (4 + 8)); // header + bg + subrects - - let mut buf = BytesMut::with_capacity(encoded_size); - - // Write header - buf.put_u32(subrects.len() as u32); // number of subrectangles (big-endian) - buf.put_u32_le(bg_color); // background color in client pixel format (little-endian) - - // Write subrectangles - for subrect in subrects { - buf.put_u32_le(subrect.color); // pixel in client format (little-endian) - buf.put_u16(subrect.x); // protocol coordinates (big-endian) - buf.put_u16(subrect.y); - buf.put_u16(subrect.w); - buf.put_u16(subrect.h); - } - - buf - } -} diff --git a/src/encoding/tight.rs b/src/encoding/tight.rs deleted file mode 100644 index 5982e61..0000000 --- a/src/encoding/tight.rs +++ /dev/null @@ -1,1568 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC Tight encoding implementation - RFC 6143 compliant with full optimization -//! -//! # Architecture -//! -//! This implementation has TWO layers for optimal compression: -//! -//! ## Layer 1: High-Level Optimization -//! - Rectangle splitting and subdivision -//! - Solid area detection and extraction -//! - Recursive optimization for best encoding -//! - Size limit enforcement (`TIGHT_MAX_RECT_SIZE`, `TIGHT_MAX_RECT_WIDTH`) -//! -//! ## Layer 2: Low-Level Encoding -//! - Palette analysis -//! - Encoding mode selection (solid/mono/indexed/full-color/JPEG) -//! - Compression and wire format generation -//! -//! # Protocol Overview -//! -//! Tight encoding supports 5 compression modes: -//! -//! 1. **Solid fill** (1 color) - control byte 0x80 -//! - Wire format: `[0x80][R][G][B]` (4 bytes total) -//! - Most efficient for solid color rectangles -//! -//! 2. **Mono rect** (2 colors) - control byte 0x50 or 0xA0 -//! - Wire format: `[control][0x01][1][bg RGB24][fg RGB24][length][bitmap]` -//! - Uses 1-bit bitmap: 0=background, 1=foreground -//! - MSB first, each row byte-aligned -//! -//! 3. **Indexed palette** (3-16 colors) - control byte 0x60 or 0xA0 -//! - Wire format: `[control][0x01][n-1][colors...][length][indices]` -//! - Each pixel encoded as palette index (1 byte) -//! -//! 4. **Full-color zlib** - control byte 0x00 or 0xA0 -//! - Wire format: `[control][length][zlib compressed RGB24]` -//! - Lossless compression for truecolor images -//! -//! 5. **JPEG** - control byte 0x90 -//! - Wire format: `[0x90][length][JPEG data]` -//! - Lossy compression for photographic content -//! -//! # Configuration Constants -//! -//! ```text -//! TIGHT_MIN_TO_COMPRESS = 12 (data < 12 bytes sent raw) -//! MIN_SPLIT_RECT_SIZE = 4096 (split rectangles >= 4096 pixels) -//! MIN_SOLID_SUBRECT_SIZE = 2048 (solid areas must be >= 2048 pixels) -//! MAX_SPLIT_TILE_SIZE = 16 (tile size for solid detection) -//! TIGHT_MAX_RECT_SIZE = 65536 (max pixels per rectangle) -//! TIGHT_MAX_RECT_WIDTH = 2048 (max rectangle width) -//! ``` - -use super::common::translate_pixel_to_client_format; -use super::Encoding; -use crate::protocol::PixelFormat; -use bytes::{BufMut, BytesMut}; -use log::info; -use std::collections::HashMap; - -// Tight encoding protocol constants (RFC 6143 section 7.7.4) -const TIGHT_EXPLICIT_FILTER: u8 = 0x04; -const TIGHT_FILL: u8 = 0x08; -#[allow(dead_code)] -const TIGHT_JPEG: u8 = 0x09; -const TIGHT_NO_ZLIB: u8 = 0x0A; - -// Filter types -const TIGHT_FILTER_PALETTE: u8 = 0x01; - -/// Zlib stream ID for full-color data (RFC 6143 section 7.7.4) -pub const STREAM_ID_FULL_COLOR: u8 = 0; -/// Zlib stream ID for monochrome bitmap data (RFC 6143 section 7.7.4) -pub const STREAM_ID_MONO: u8 = 1; -/// Zlib stream ID for indexed palette data (RFC 6143 section 7.7.4) -pub const STREAM_ID_INDEXED: u8 = 2; - -// Compression thresholds for Tight encoding optimization -const TIGHT_MIN_TO_COMPRESS: usize = 12; -const MIN_SPLIT_RECT_SIZE: usize = 4096; -const MIN_SOLID_SUBRECT_SIZE: usize = 2048; -const MAX_SPLIT_TILE_SIZE: u16 = 16; -const TIGHT_MAX_RECT_SIZE: usize = 65536; -const TIGHT_MAX_RECT_WIDTH: u16 = 2048; - -/// Compression configuration for different quality levels -struct TightConf { - mono_min_rect_size: usize, - idx_zlib_level: u8, - mono_zlib_level: u8, - raw_zlib_level: u8, -} - -const TIGHT_CONF: [TightConf; 4] = [ - TightConf { - mono_min_rect_size: 6, - idx_zlib_level: 0, - mono_zlib_level: 0, - raw_zlib_level: 0, - }, // Level 0 - TightConf { - mono_min_rect_size: 32, - idx_zlib_level: 1, - mono_zlib_level: 1, - raw_zlib_level: 1, - }, // Level 1 - TightConf { - mono_min_rect_size: 32, - idx_zlib_level: 3, - mono_zlib_level: 3, - raw_zlib_level: 2, - }, // Level 2 - TightConf { - mono_min_rect_size: 32, - idx_zlib_level: 7, - mono_zlib_level: 7, - raw_zlib_level: 5, - }, // Level 9 -]; - -/// Rectangle to encode -#[derive(Debug, Clone)] -struct Rect { - x: u16, - y: u16, - w: u16, - h: u16, -} - -/// Result of encoding a rectangle -struct EncodeResult { - rectangles: Vec<(Rect, BytesMut)>, -} - -/// Implements the VNC "Tight" encoding (RFC 6143 section 7.7.4). -pub struct TightEncoding; - -impl Encoding for TightEncoding { - fn encode( - &self, - data: &[u8], - width: u16, - height: u16, - quality: u8, - compression: u8, - ) -> BytesMut { - // Simple wrapper - for full optimization, use encode_rect_optimized - // Default to RGBA32 format for backward compatibility (old API doesn't have client format) - // Create a temporary compressor for this call (old API doesn't have persistent streams) - use crate::client::TightZlibStreams; - let mut compressor = TightZlibStreams::new(); - - let rect = Rect { - x: 0, - y: 0, - w: width, - h: height, - }; - let default_format = PixelFormat::rgba32(); - let result = encode_rect_optimized( - data, - width, - &rect, - quality, - compression, - &default_format, - &mut compressor, - ); - - // Concatenate all rectangles - let mut output = BytesMut::new(); - for (_rect, buf) in result.rectangles { - output.extend_from_slice(&buf); - } - output - } -} - -/// High-level optimization: split rectangles and find solid areas -/// Implements Tight encoding optimization as specified in RFC 6143 -#[allow(clippy::similar_names)] // dx_end and dy_end are clear in context (delta x/y end coordinates) -#[allow(clippy::too_many_lines)] // Complex algorithm implementing RFC 6143 Tight encoding optimization -#[allow(clippy::cast_possible_truncation)] // Rectangle dimensions limited to u16 per VNC protocol -fn encode_rect_optimized( - framebuffer: &[u8], - fb_width: u16, - rect: &Rect, - quality: u8, - compression: u8, - client_format: &PixelFormat, - compressor: &mut C, -) -> EncodeResult { - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: encode_rect_optimized called: rect={}x{} at ({}, {}), quality={}, compression={}, bpp={}", - rect.w, rect.h, rect.x, rect.y, quality, compression, client_format.bits_per_pixel); - - let mut rectangles = Vec::new(); - - // Normalize compression level based on quality settings - let compression = normalize_compression_level(compression, quality); - - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: normalized compression={compression}"); - - // Check if optimization should be applied - let rect_size = rect.w as usize * rect.h as usize; - - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: rect_size={rect_size}, MIN_SPLIT_RECT_SIZE={MIN_SPLIT_RECT_SIZE}"); - - if rect_size < MIN_SPLIT_RECT_SIZE { - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: Rectangle too small for optimization"); - - // Too small for optimization - but still check if it needs splitting due to size limits - if rect.w > TIGHT_MAX_RECT_WIDTH - || ((rect.w as usize) * (rect.h as usize)) > TIGHT_MAX_RECT_SIZE - { - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: But rectangle needs splitting - calling encode_large_rect"); - - // Too large - split it - rectangles.extend(encode_large_rect( - framebuffer, - fb_width, - rect, - quality, - compression, - client_format, - compressor, - )); - } else { - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: Rectangle small enough - encode directly"); - - // Small enough - encode directly - let buf = encode_subrect_single( - framebuffer, - fb_width, - rect, - quality, - compression, - client_format, - compressor, - ); - rectangles.push((rect.clone(), buf)); - } - - #[cfg(feature = "debug-logging")] - log::info!( - "DEBUG: encode_rect_optimized returning {} rectangles (early return)", - rectangles.len() - ); - - return EncodeResult { rectangles }; - } - - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: Rectangle large enough for optimization - continuing"); - - // Calculate maximum rows per rectangle - let n_max_width = rect.w.min(TIGHT_MAX_RECT_WIDTH); - let n_max_rows = (TIGHT_MAX_RECT_SIZE / n_max_width as usize) as u16; - - // Try to find large solid-color areas for optimization - // Track the current scan position and base position (like C code's y and h) - let mut current_y = rect.y; - let mut base_y = rect.y; // Corresponds to C code's 'y' variable - let mut remaining_h = rect.h; // Corresponds to C code's 'h' variable - - #[cfg(feature = "debug-logging")] - log::info!( - "DEBUG: Starting optimization loop, rect.y={}, rect.h={}", - rect.y, - rect.h - ); - - while current_y < base_y + remaining_h { - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: Loop iteration: current_y={current_y}, base_y={base_y}, remaining_h={remaining_h}"); - // Check if rectangle becomes too large (like C code: if (dy - y >= nMaxRows)) - if (current_y - base_y) >= n_max_rows { - let chunk_rect = Rect { - x: rect.x, - y: base_y, // Send from base_y, not from calculated position - w: rect.w, - h: n_max_rows, - }; - // Chunk might still be too wide - check and split if needed - if chunk_rect.w > TIGHT_MAX_RECT_WIDTH { - rectangles.extend(encode_large_rect( - framebuffer, - fb_width, - &chunk_rect, - quality, - compression, - client_format, - compressor, - )); - } else { - let buf = encode_subrect_single( - framebuffer, - fb_width, - &chunk_rect, - quality, - compression, - client_format, - compressor, - ); - rectangles.push((chunk_rect, buf)); - } - // Like C code: y += nMaxRows; h -= nMaxRows; - base_y += n_max_rows; - remaining_h -= n_max_rows; - } - - let dy_end = (current_y + MAX_SPLIT_TILE_SIZE).min(base_y + remaining_h); - let dh = dy_end - current_y; - - // Safety check: if dh is 0, we've reached the end - if dh == 0 { - break; - } - - let mut current_x = rect.x; - while current_x < rect.x + rect.w { - let dx_end = (current_x + MAX_SPLIT_TILE_SIZE).min(rect.x + rect.w); - let dw = dx_end - current_x; - - // Safety check: if dw is 0, we've reached the end - if dw == 0 { - break; - } - - // Check if tile is solid - if let Some(color_value) = - check_solid_tile(framebuffer, fb_width, current_x, current_y, dw, dh, None) - { - // Find best solid area - let (w_best, h_best) = find_best_solid_area( - framebuffer, - fb_width, - current_x, - current_y, - rect.w - (current_x - rect.x), - remaining_h - (current_y - base_y), - color_value, - ); - - // Check if solid area is large enough - if w_best * h_best != rect.w * remaining_h - && (w_best as usize * h_best as usize) < MIN_SOLID_SUBRECT_SIZE - { - current_x += dw; - continue; - } - - // Extend solid area (use base_y instead of rect.y for coordinates) - let (x_best, y_best, w_best, h_best) = extend_solid_area( - framebuffer, - fb_width, - rect.x, - base_y, - rect.w, - remaining_h, - color_value, - current_x, - current_y, - w_best, - h_best, - ); - - // Send rectangles before solid area - if y_best != base_y { - let top_rect = Rect { - x: rect.x, - y: base_y, - w: rect.w, - h: y_best - base_y, - }; - // top_rect might be too wide - check and split if needed - if top_rect.w > TIGHT_MAX_RECT_WIDTH - || ((top_rect.w as usize) * (top_rect.h as usize)) > TIGHT_MAX_RECT_SIZE - { - rectangles.extend(encode_large_rect( - framebuffer, - fb_width, - &top_rect, - quality, - compression, - client_format, - compressor, - )); - } else { - let buf = encode_subrect_single( - framebuffer, - fb_width, - &top_rect, - quality, - compression, - client_format, - compressor, - ); - rectangles.push((top_rect, buf)); - } - } - - if x_best != rect.x { - let left_rect = Rect { - x: rect.x, - y: y_best, - w: x_best - rect.x, - h: h_best, - }; - // Don't recursively optimize - just check size and encode - if left_rect.w > TIGHT_MAX_RECT_WIDTH - || ((left_rect.w as usize) * (left_rect.h as usize)) > TIGHT_MAX_RECT_SIZE - { - rectangles.extend(encode_large_rect( - framebuffer, - fb_width, - &left_rect, - quality, - compression, - client_format, - compressor, - )); - } else { - let buf = encode_subrect_single( - framebuffer, - fb_width, - &left_rect, - quality, - compression, - client_format, - compressor, - ); - rectangles.push((left_rect, buf)); - } - } - - // Send solid rectangle - let solid_rect = Rect { - x: x_best, - y: y_best, - w: w_best, - h: h_best, - }; - let buf = encode_solid_rect(color_value, client_format); - rectangles.push((solid_rect, buf)); - - // Send remaining rectangles - if x_best + w_best != rect.x + rect.w { - let right_rect = Rect { - x: x_best + w_best, - y: y_best, - w: rect.w - (x_best - rect.x) - w_best, - h: h_best, - }; - // Don't recursively optimize - just check size and encode - if right_rect.w > TIGHT_MAX_RECT_WIDTH - || ((right_rect.w as usize) * (right_rect.h as usize)) > TIGHT_MAX_RECT_SIZE - { - rectangles.extend(encode_large_rect( - framebuffer, - fb_width, - &right_rect, - quality, - compression, - client_format, - compressor, - )); - } else { - let buf = encode_subrect_single( - framebuffer, - fb_width, - &right_rect, - quality, - compression, - client_format, - compressor, - ); - rectangles.push((right_rect, buf)); - } - } - - if y_best + h_best != base_y + remaining_h { - let bottom_rect = Rect { - x: rect.x, - y: y_best + h_best, - w: rect.w, - h: remaining_h - (y_best - base_y) - h_best, - }; - // Don't recursively optimize - just check size and encode - if bottom_rect.w > TIGHT_MAX_RECT_WIDTH - || ((bottom_rect.w as usize) * (bottom_rect.h as usize)) - > TIGHT_MAX_RECT_SIZE - { - rectangles.extend(encode_large_rect( - framebuffer, - fb_width, - &bottom_rect, - quality, - compression, - client_format, - compressor, - )); - } else { - let buf = encode_subrect_single( - framebuffer, - fb_width, - &bottom_rect, - quality, - compression, - client_format, - compressor, - ); - rectangles.push((bottom_rect, buf)); - } - } - - return EncodeResult { rectangles }; - } - - current_x += dw; - } - - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: End of inner loop, incrementing current_y by dh={dh}"); - - current_y += dh; - - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: After increment: current_y={current_y}"); - } - - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: Exited optimization loop, no solid areas found"); - - // No solid areas found - encode normally (but check if it needs splitting) - if rect.w > TIGHT_MAX_RECT_WIDTH - || ((rect.w as usize) * (rect.h as usize)) > TIGHT_MAX_RECT_SIZE - { - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: Rectangle needs splitting, calling encode_large_rect"); - - rectangles.extend(encode_large_rect( - framebuffer, - fb_width, - rect, - quality, - compression, - client_format, - compressor, - )); - } else { - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: Rectangle small enough, encoding directly"); - - let buf = encode_subrect_single( - framebuffer, - fb_width, - rect, - quality, - compression, - client_format, - compressor, - ); - rectangles.push((rect.clone(), buf)); - } - - #[cfg(feature = "debug-logging")] - log::info!( - "DEBUG: encode_rect_optimized returning {} rectangles (normal return)", - rectangles.len() - ); - - EncodeResult { rectangles } -} - -/// Normalize compression level based on JPEG quality -/// Maps compression level 0-9 to internal configuration indices -fn normalize_compression_level(compression: u8, quality: u8) -> u8 { - let mut level = compression; - - // JPEG enabled (quality < 10): enforce minimum level 1, maximum level 2 - // This ensures better compression performance with JPEG - if quality < 10 { - level = level.clamp(1, 2); - } - // JPEG disabled (quality >= 10): cap at level 1 - else if level > 1 { - level = 1; - } - - // Map level 9 to 3 for backward compatibility (low-bandwidth mode) - if level == 9 { - level = 3; - } - - level -} - -/// Low-level encoding: analyze and encode a single subrectangle -/// Analyzes palette and selects optimal encoding mode -/// Never splits - assumes rectangle is within size limits -fn encode_subrect_single( - framebuffer: &[u8], - fb_width: u16, - rect: &Rect, - quality: u8, - compression: u8, - client_format: &PixelFormat, - compressor: &mut C, -) -> BytesMut { - // This function assumes rect is within size limits (called from encode_large_rect or for small rects) - - // Extract pixel data for this rectangle - let pixels = extract_rect_rgba(framebuffer, fb_width, rect); - - // Analyze palette - let palette = analyze_palette(&pixels, rect.w as usize * rect.h as usize, compression); - - // Route to appropriate encoder based on palette - match palette.num_colors { - 0 => { - // Truecolor - use JPEG or full-color - if quality < 10 { - // Convert VNC quality (0-9, lower is better) to JPEG quality (0-100, higher is better) - let jpeg_quality = 95_u8.saturating_sub(quality * 7); - encode_jpeg_rect(&pixels, rect.w, rect.h, jpeg_quality, compressor) - } else { - encode_full_color_rect(&pixels, rect.w, rect.h, compression, compressor) - } - } - 1 => { - // Solid color - encode_solid_rect(palette.colors[0], client_format) - } - 2 => { - // Mono rect (2 colors) - encode_mono_rect( - &pixels, - rect.w, - rect.h, - palette.colors[0], - palette.colors[1], - compression, - client_format, - compressor, - ) - } - _ => { - // Indexed palette (3-16 colors) - encode_indexed_rect( - &pixels, - rect.w, - rect.h, - &palette.colors[..palette.num_colors], - compression, - client_format, - compressor, - ) - } - } -} - -/// Encode large rectangle by splitting it into smaller tiles -/// Returns a vector of individual rectangles with their encoded data -#[allow(clippy::cast_possible_truncation)] // Tight max rect size divided by width always fits in u16 -fn encode_large_rect( - framebuffer: &[u8], - fb_width: u16, - rect: &Rect, - quality: u8, - compression: u8, - client_format: &PixelFormat, - compressor: &mut C, -) -> Vec<(Rect, BytesMut)> { - let subrect_max_width = rect.w.min(TIGHT_MAX_RECT_WIDTH); - let subrect_max_height = (TIGHT_MAX_RECT_SIZE / subrect_max_width as usize) as u16; - - let mut rectangles = Vec::new(); - - let mut dy = 0; - while dy < rect.h { - let mut dx = 0; - while dx < rect.w { - let rw = (rect.w - dx).min(TIGHT_MAX_RECT_WIDTH); - let rh = (rect.h - dy).min(subrect_max_height); - - let sub_rect = Rect { - x: rect.x + dx, - y: rect.y + dy, - w: rw, - h: rh, - }; - - // Encode this sub-rectangle (recursive call, but sub_rect is guaranteed to be small enough) - let buf = encode_subrect_single( - framebuffer, - fb_width, - &sub_rect, - quality, - compression, - client_format, - compressor, - ); - rectangles.push((sub_rect, buf)); - - dx += TIGHT_MAX_RECT_WIDTH; - } - dy += subrect_max_height; - } - - rectangles -} - -/// Check if a tile is all the same color -/// Used for solid area detection optimization -fn check_solid_tile( - framebuffer: &[u8], - fb_width: u16, - x: u16, - y: u16, - w: u16, - h: u16, - need_same_color: Option, -) -> Option { - let offset = (y as usize * fb_width as usize + x as usize) * 4; - - // Get first pixel color (RGB24) - let fb_r = framebuffer[offset]; - let fb_g = framebuffer[offset + 1]; - let fb_b = framebuffer[offset + 2]; - let first_color = rgba_to_rgb24(fb_r, fb_g, fb_b); - - #[cfg(feature = "debug-logging")] - if x == 0 && y == 0 { - // Log first pixel of each solid tile - info!("check_solid_tile: fb[{}]=[{:02x},{:02x},{:02x},{:02x}] -> R={:02x} G={:02x} B={:02x} color=0x{:06x}", - offset, framebuffer[offset], framebuffer[offset+1], framebuffer[offset+2], framebuffer[offset+3], - fb_r, fb_g, fb_b, first_color); - } - - // Check if we need a specific color - if let Some(required) = need_same_color { - if first_color != required { - return None; - } - } - - // Check all pixels - for dy in 0..h { - let row_offset = ((y + dy) as usize * fb_width as usize + x as usize) * 4; - for dx in 0..w { - let pix_offset = row_offset + dx as usize * 4; - let color = rgba_to_rgb24( - framebuffer[pix_offset], - framebuffer[pix_offset + 1], - framebuffer[pix_offset + 2], - ); - if color != first_color { - return None; - } - } - } - - Some(first_color) -} - -/// Find best solid area dimensions -/// Determines optimal size for solid color subrectangle -fn find_best_solid_area( - framebuffer: &[u8], - fb_width: u16, - x: u16, - y: u16, - w: u16, - h: u16, - color_value: u32, -) -> (u16, u16) { - let mut w_best = 0; - let mut h_best = 0; - let mut w_prev = w; - - let mut dy = 0; - while dy < h { - let dh = (h - dy).min(MAX_SPLIT_TILE_SIZE); - let dw = w_prev.min(MAX_SPLIT_TILE_SIZE); - - if check_solid_tile(framebuffer, fb_width, x, y + dy, dw, dh, Some(color_value)).is_none() { - break; - } - - let mut dx = dw; - while dx < w_prev { - let dw_check = (w_prev - dx).min(MAX_SPLIT_TILE_SIZE); - if check_solid_tile( - framebuffer, - fb_width, - x + dx, - y + dy, - dw_check, - dh, - Some(color_value), - ) - .is_none() - { - break; - } - dx += dw_check; - } - - w_prev = dx; - if (w_prev as usize * (dy + dh) as usize) > (w_best as usize * h_best as usize) { - w_best = w_prev; - h_best = dy + dh; - } - - dy += dh; - } - - (w_best, h_best) -} - -/// Extend solid area to maximum size -/// Expands solid region in all directions -#[allow(clippy::too_many_arguments)] // Tight encoding algorithm requires all geometric parameters for region expansion -fn extend_solid_area( - framebuffer: &[u8], - fb_width: u16, - base_x: u16, - base_y: u16, - max_w: u16, - max_h: u16, - color_value: u32, - mut x: u16, - mut y: u16, - mut w: u16, - mut h: u16, -) -> (u16, u16, u16, u16) { - // Extend upwards - while y > base_y { - if check_solid_tile(framebuffer, fb_width, x, y - 1, w, 1, Some(color_value)).is_none() { - break; - } - y -= 1; - h += 1; - } - - // Extend downwards - while y + h < base_y + max_h { - if check_solid_tile(framebuffer, fb_width, x, y + h, w, 1, Some(color_value)).is_none() { - break; - } - h += 1; - } - - // Extend left - while x > base_x { - if check_solid_tile(framebuffer, fb_width, x - 1, y, 1, h, Some(color_value)).is_none() { - break; - } - x -= 1; - w += 1; - } - - // Extend right - while x + w < base_x + max_w { - if check_solid_tile(framebuffer, fb_width, x + w, y, 1, h, Some(color_value)).is_none() { - break; - } - w += 1; - } - - (x, y, w, h) -} - -/// Palette analysis result -struct Palette { - num_colors: usize, - colors: [u32; 256], - mono_background: u32, - mono_foreground: u32, -} - -/// Analyze palette from pixel data -/// Determines color count and encoding mode selection -fn analyze_palette(pixels: &[u8], pixel_count: usize, compression: u8) -> Palette { - let conf_idx = match compression { - 0 => 0, - 1 => 1, - 2 | 3 => 2, - _ => 3, - }; - let conf = &TIGHT_CONF[conf_idx]; - - let mut palette = Palette { - num_colors: 0, - colors: [0; 256], - mono_background: 0, - mono_foreground: 0, - }; - - if pixel_count == 0 { - return palette; - } - - // Get first color - let c0 = rgba_to_rgb24(pixels[0], pixels[1], pixels[2]); - - // Count how many pixels match first color - let mut i = 4; - while i < pixels.len() && rgba_to_rgb24(pixels[i], pixels[i + 1], pixels[i + 2]) == c0 { - i += 4; - } - - if i >= pixels.len() { - // Solid color - palette.num_colors = 1; - palette.colors[0] = c0; - return palette; - } - - // Check for 2-color (mono) case - if pixel_count >= conf.mono_min_rect_size { - let n0 = i / 4; - let c1 = rgba_to_rgb24(pixels[i], pixels[i + 1], pixels[i + 2]); - let mut n1 = 0; - - i += 4; - while i < pixels.len() { - let color = rgba_to_rgb24(pixels[i], pixels[i + 1], pixels[i + 2]); - if color == c0 { - // n0 already counted - } else if color == c1 { - n1 += 1; - } else { - break; - } - i += 4; - } - - if i >= pixels.len() { - // Only 2 colors found - palette.num_colors = 2; - if n0 > n1 { - palette.mono_background = c0; - palette.mono_foreground = c1; - palette.colors[0] = c0; - palette.colors[1] = c1; - } else { - palette.mono_background = c1; - palette.mono_foreground = c0; - palette.colors[0] = c1; - palette.colors[1] = c0; - } - return palette; - } - } - - // More than 2 colors - full palette or truecolor - palette.num_colors = 0; - palette -} - -/// Extract RGBA rectangle from framebuffer -fn extract_rect_rgba(framebuffer: &[u8], fb_width: u16, rect: &Rect) -> Vec { - let mut pixels = Vec::with_capacity(rect.w as usize * rect.h as usize * 4); - - for y in 0..rect.h { - let row_offset = ((rect.y + y) as usize * fb_width as usize + rect.x as usize) * 4; - let row_end = row_offset + rect.w as usize * 4; - pixels.extend_from_slice(&framebuffer[row_offset..row_end]); - } - - pixels -} - -/// Convert RGBA to RGB24 -/// Matches the format used in `common::rgba_to_rgb24_pixels` -/// Internal format: 0x00BBGGRR (R at bits 0-7, G at 8-15, B at 16-23) -#[inline] -fn rgba_to_rgb24(r: u8, g: u8, b: u8) -> u32 { - u32::from(r) | (u32::from(g) << 8) | (u32::from(b) << 16) -} - -/// Encode solid rectangle -/// Implements solid fill encoding mode (1 color) -/// Uses client's pixel format for color encoding -fn encode_solid_rect(color: u32, client_format: &PixelFormat) -> BytesMut { - let mut buf = BytesMut::with_capacity(16); // Reserve enough for largest pixel format - buf.put_u8(TIGHT_FILL << 4); // 0x80 - - // Translate color to client's pixel format - let color_bytes = translate_pixel_to_client_format(color, client_format); - - #[cfg(feature = "debug-logging")] - { - let use_24bit = client_format.depth == 24 - && client_format.red_max == 255 - && client_format.green_max == 255 - && client_format.blue_max == 255; - #[cfg(feature = "debug-logging")] - info!("Tight solid: color=0x{:06x}, translated bytes={:02x?}, use_24bit={}, client: depth={} bpp={} rshift={} gshift={} bshift={}", - color, color_bytes, use_24bit, client_format.depth, client_format.bits_per_pixel, - client_format.red_shift, client_format.green_shift, client_format.blue_shift); - } - - buf.extend_from_slice(&color_bytes); - - #[cfg(feature = "debug-logging")] - info!( - "Tight solid: 0x{:06x}, control=0x{:02x}, color_len={}, total={} bytes", - color, - TIGHT_FILL << 4, - color_bytes.len(), - buf.len() - ); - buf -} - -/// Encode mono rectangle (2 colors) -/// Implements monochrome bitmap encoding with palette -/// Uses client's pixel format for palette colors -#[allow(clippy::too_many_arguments)] // All parameters are necessary for proper encoding -fn encode_mono_rect( - pixels: &[u8], - width: u16, - height: u16, - bg: u32, - fg: u32, - compression: u8, - client_format: &PixelFormat, - compressor: &mut C, -) -> BytesMut { - let conf_idx = match compression { - 0 => 0, - 1 => 1, - 2 | 3 => 2, - _ => 3, - }; - let zlib_level = TIGHT_CONF[conf_idx].mono_zlib_level; - - // Encode bitmap - let bitmap = encode_mono_bitmap(pixels, width, height, bg); - - let mut buf = BytesMut::new(); - - // Control byte - if zlib_level == 0 { - buf.put_u8((TIGHT_NO_ZLIB | TIGHT_EXPLICIT_FILTER) << 4); - } else { - buf.put_u8((STREAM_ID_MONO | TIGHT_EXPLICIT_FILTER) << 4); - } - - // Filter and palette - buf.put_u8(TIGHT_FILTER_PALETTE); - buf.put_u8(1); // 2 colors - 1 - - // Palette colors - translate to client format - let bg_bytes = translate_pixel_to_client_format(bg, client_format); - let fg_bytes = translate_pixel_to_client_format(fg, client_format); - - #[cfg(feature = "debug-logging")] - { - let use_24bit = client_format.depth == 24 - && client_format.red_max == 255 - && client_format.green_max == 255 - && client_format.blue_max == 255; - info!("Tight mono palette: bg=0x{:06x} -> {:02x?}, fg=0x{:06x} -> {:02x?}, use_24bit={}, depth={} bpp={}", - bg, bg_bytes, fg, fg_bytes, use_24bit, client_format.depth, client_format.bits_per_pixel); - } - - buf.extend_from_slice(&bg_bytes); - buf.extend_from_slice(&fg_bytes); - - // Compress data - compress_data(&mut buf, &bitmap, zlib_level, STREAM_ID_MONO, compressor); - - info!( - "Tight mono: {}x{}, {} bytes ({}bpp)", - width, - height, - buf.len(), - client_format.bits_per_pixel - ); - buf -} - -/// Encode indexed palette rectangle (3-16 colors) -/// Implements palette-based encoding with color indices -/// Uses client's pixel format for palette colors -#[allow(clippy::cast_possible_truncation)] // Palette limited to 16 colors, indices fit in u8 -fn encode_indexed_rect( - pixels: &[u8], - width: u16, - height: u16, - palette: &[u32], - compression: u8, - client_format: &PixelFormat, - compressor: &mut C, -) -> BytesMut { - let conf_idx = match compression { - 0 => 0, - 1 => 1, - 2 | 3 => 2, - _ => 3, - }; - let zlib_level = TIGHT_CONF[conf_idx].idx_zlib_level; - - // Build color-to-index map - let mut color_map = HashMap::new(); - for (idx, &color) in palette.iter().enumerate() { - color_map.insert(color, idx as u8); - } - - // Encode indices - let mut indices = Vec::with_capacity(width as usize * height as usize); - for chunk in pixels.chunks_exact(4) { - let color = rgba_to_rgb24(chunk[0], chunk[1], chunk[2]); - indices.push(*color_map.get(&color).unwrap_or(&0)); - } - - let mut buf = BytesMut::new(); - - // Control byte - if zlib_level == 0 { - buf.put_u8((TIGHT_NO_ZLIB | TIGHT_EXPLICIT_FILTER) << 4); - } else { - buf.put_u8((STREAM_ID_INDEXED | TIGHT_EXPLICIT_FILTER) << 4); - } - - // Filter and palette size - buf.put_u8(TIGHT_FILTER_PALETTE); - buf.put_u8((palette.len() - 1) as u8); - - // Palette colors - translate to client format - for &color in palette { - let color_bytes = translate_pixel_to_client_format(color, client_format); - buf.extend_from_slice(&color_bytes); - } - - // Compress data - compress_data( - &mut buf, - &indices, - zlib_level, - STREAM_ID_INDEXED, - compressor, - ); - - info!( - "Tight indexed: {} colors, {}x{}, {} bytes ({}bpp)", - palette.len(), - width, - height, - buf.len(), - client_format.bits_per_pixel - ); - buf -} - -/// Encode full-color rectangle -/// Implements full-color zlib encoding for truecolor images -fn encode_full_color_rect( - pixels: &[u8], - width: u16, - height: u16, - compression: u8, - compressor: &mut C, -) -> BytesMut { - let conf_idx = match compression { - 0 => 0, - 1 => 1, - 2 | 3 => 2, - _ => 3, - }; - let zlib_level = TIGHT_CONF[conf_idx].raw_zlib_level; - - // Convert RGBA to RGB24 - let mut rgb_data = Vec::with_capacity(width as usize * height as usize * 3); - for chunk in pixels.chunks_exact(4) { - rgb_data.push(chunk[0]); - rgb_data.push(chunk[1]); - rgb_data.push(chunk[2]); - } - - let mut buf = BytesMut::new(); - - // Control byte - let control_byte = if zlib_level == 0 { - TIGHT_NO_ZLIB << 4 - } else { - STREAM_ID_FULL_COLOR << 4 - }; - buf.put_u8(control_byte); - - #[cfg(feature = "debug-logging")] - info!( - "Tight full-color: {}x{}, zlib_level={}, control_byte=0x{:02x}, rgb_data_len={}", - width, - height, - zlib_level, - control_byte, - rgb_data.len() - ); - - // Compress data - compress_data( - &mut buf, - &rgb_data, - zlib_level, - STREAM_ID_FULL_COLOR, - compressor, - ); - - info!( - "Tight full-color: {}x{}, {} bytes total", - width, - height, - buf.len() - ); - buf -} - -/// Encode JPEG rectangle -/// Implements lossy JPEG compression for photographic content -fn encode_jpeg_rect( - pixels: &[u8], - width: u16, - height: u16, - quality: u8, - compressor: &mut C, -) -> BytesMut { - #[cfg(feature = "turbojpeg")] - { - use crate::jpeg::TurboJpegEncoder; - - // Convert RGBA to RGB - let mut rgb_data = Vec::with_capacity(width as usize * height as usize * 3); - for chunk in pixels.chunks_exact(4) { - rgb_data.push(chunk[0]); - rgb_data.push(chunk[1]); - rgb_data.push(chunk[2]); - } - - // Compress with TurboJPEG - let jpeg_data = match TurboJpegEncoder::new() { - Ok(mut encoder) => match encoder.compress_rgb(&rgb_data, width, height, quality) { - Ok(data) => data, - Err(e) => { - info!("TurboJPEG failed: {e}, using full-color"); - return encode_full_color_rect(pixels, width, height, 6, compressor); - } - }, - Err(e) => { - info!("TurboJPEG init failed: {e}, using full-color"); - return encode_full_color_rect(pixels, width, height, 6, compressor); - } - }; - - let mut buf = BytesMut::new(); - buf.put_u8(TIGHT_JPEG << 4); // 0x90 - write_compact_length(&mut buf, jpeg_data.len()); - buf.put_slice(&jpeg_data); - - #[cfg(feature = "debug-logging")] - info!( - "Tight JPEG: {}x{}, quality {}, {} bytes", - width, - height, - quality, - jpeg_data.len() - ); - buf - } - - #[cfg(not(feature = "turbojpeg"))] - { - info!("TurboJPEG not enabled, using full-color (quality={quality})"); - encode_full_color_rect(pixels, width, height, 6, compressor) - } -} - -/// Compress data with zlib using persistent streams or send uncompressed -/// Handles compression based on data size and level settings -/// -/// Uses persistent zlib streams via the `TightStreamCompressor` trait. -/// Persistent streams maintain their dictionary state across multiple compress operations. -fn compress_data( - buf: &mut BytesMut, - data: &[u8], - zlib_level: u8, - stream_id: u8, - compressor: &mut C, -) { - #[cfg_attr(not(feature = "debug-logging"), allow(unused_variables))] - let before_len = buf.len(); - - // Data < 12 bytes sent raw WITHOUT length - if data.len() < TIGHT_MIN_TO_COMPRESS { - buf.put_slice(data); - #[cfg(feature = "debug-logging")] - info!( - "compress_data: {} bytes < 12, sent raw (no length), buf grew by {} bytes", - data.len(), - buf.len() - before_len - ); - return; - } - - // zlibLevel == 0 means uncompressed WITH length - if zlib_level == 0 { - write_compact_length(buf, data.len()); - buf.put_slice(data); - #[cfg(feature = "debug-logging")] - info!("compress_data: {} bytes uncompressed (zlib_level=0), with length, buf grew by {} bytes", data.len(), buf.len() - before_len); - return; - } - - // Compress with persistent zlib stream - match compressor.compress_tight_stream(stream_id, zlib_level, data) { - Ok(compressed) => { - write_compact_length(buf, compressed.len()); - buf.put_slice(&compressed); - #[cfg(feature = "debug-logging")] - info!( - "compress_data: {} bytes compressed to {} using stream {}, buf grew by {} bytes", - data.len(), - compressed.len(), - stream_id, - buf.len() - before_len - ); - } - Err(e) => { - // Compression failed - send uncompressed - #[cfg(feature = "debug-logging")] - info!( - "compress_data: compression FAILED ({}), sending {} bytes uncompressed", - e, - data.len() - ); - #[cfg(not(feature = "debug-logging"))] - let _ = e; - - write_compact_length(buf, data.len()); - buf.put_slice(data); - } - } -} - -/// Encode mono bitmap (1 bit per pixel) -/// Converts 2-color image to packed bitmap format -fn encode_mono_bitmap(pixels: &[u8], width: u16, height: u16, bg: u32) -> Vec { - let w = width as usize; - let h = height as usize; - let bytes_per_row = w.div_ceil(8); - let mut bitmap = vec![0u8; bytes_per_row * h]; - - let mut bitmap_idx = 0; - for y in 0..h { - let mut byte_val = 0u8; - let mut bit_pos = 7i32; // MSB first - - for x in 0..w { - let pix_offset = (y * w + x) * 4; - let color = rgba_to_rgb24( - pixels[pix_offset], - pixels[pix_offset + 1], - pixels[pix_offset + 2], - ); - - if color != bg { - byte_val |= 1 << bit_pos; - } - - bit_pos -= 1; - - // Write byte after 8 pixels (when bit_pos becomes -1) - if bit_pos < 0 { - bitmap[bitmap_idx] = byte_val; - bitmap_idx += 1; - byte_val = 0; - bit_pos = 7; - } - } - - // Write partial byte at end of row if width not multiple of 8 - if !w.is_multiple_of(8) { - bitmap[bitmap_idx] = byte_val; - bitmap_idx += 1; - } - } - - bitmap -} - -/// Write compact length encoding -/// Implements variable-length integer encoding for Tight protocol -#[allow(clippy::cast_possible_truncation)] // Compact length encoding uses variable-length u8 packing per RFC 6143 -fn write_compact_length(buf: &mut BytesMut, len: usize) { - if len < 128 { - buf.put_u8(len as u8); - } else if len < 16384 { - buf.put_u8(((len & 0x7F) | 0x80) as u8); - buf.put_u8(((len >> 7) & 0x7F) as u8); // Mask to ensure high bit is clear - } else { - buf.put_u8(((len & 0x7F) | 0x80) as u8); - buf.put_u8((((len >> 7) & 0x7F) | 0x80) as u8); - buf.put_u8((len >> 14) as u8); - } -} - -/// Trait for managing persistent zlib compression streams -/// -/// Implementations of this trait maintain separate compression streams for different -/// data types (full-color, mono, indexed) to improve compression ratios across -/// multiple rectangle updates. -pub trait TightStreamCompressor { - /// Compresses data using a persistent zlib stream - /// - /// # Arguments - /// * `stream_id` - Stream identifier (`STREAM_ID_FULL_COLOR`, `STREAM_ID_MONO`, or `STREAM_ID_INDEXED`) - /// * `level` - Compression level (0-9) - /// * `input` - Data to compress - /// - /// # Returns - /// - /// Compressed data or error message - /// - /// # Errors - /// - /// Returns an error if compression fails - fn compress_tight_stream( - &mut self, - stream_id: u8, - level: u8, - input: &[u8], - ) -> Result, String>; -} - -/// Encode Tight with persistent zlib streams, returning individual sub-rectangles -/// Returns a vector of (x, y, width, height, `encoded_data`) for each sub-rectangle -/// -/// # Arguments -/// * `data` - Framebuffer pixel data (RGBA format) -/// * `width` - Rectangle width -/// * `height` - Rectangle height -/// * `quality` - JPEG quality level (0-9, or 10+ to disable JPEG) -/// * `compression` - Compression level (0-9) -/// * `client_format` - Client's pixel format for palette color translation -/// * `compressor` - Zlib stream compressor for persistent compression streams -pub fn encode_tight_rects( - data: &[u8], - width: u16, - height: u16, - quality: u8, - compression: u8, - client_format: &PixelFormat, - compressor: &mut C, -) -> Vec<(u16, u16, u16, u16, BytesMut)> { - #[cfg(feature = "debug-logging")] - log::info!( - "DEBUG: encode_tight_rects called: {}x{}, data_len={}, quality={}, compression={}, bpp={}", - width, - height, - data.len(), - quality, - compression, - client_format.bits_per_pixel - ); - - let rect = Rect { - x: 0, - y: 0, - w: width, - h: height, - }; - - #[cfg(feature = "debug-logging")] - log::info!("DEBUG: Calling encode_rect_optimized"); - - let result = encode_rect_optimized( - data, - width, - &rect, - quality, - compression, - client_format, - compressor, - ); - - #[cfg(feature = "debug-logging")] - log::info!( - "DEBUG: encode_rect_optimized returned {} rectangles", - result.rectangles.len() - ); - - // Convert EncodeResult to public format - let rects: Vec<(u16, u16, u16, u16, BytesMut)> = result - .rectangles - .into_iter() - .map(|(r, buf)| { - #[cfg(feature = "debug-logging")] - log::info!( - "DEBUG: Sub-rect: {}x{} at ({}, {}), encoded_len={}", - r.w, - r.h, - r.x, - r.y, - buf.len() - ); - (r.x, r.y, r.w, r.h, buf) - }) - .collect(); - - #[cfg(feature = "debug-logging")] - log::info!( - "DEBUG: encode_tight_rects returning {} rectangles", - rects.len() - ); - - rects -} - -/// Encode Tight with persistent zlib streams (for use with VNC client streams) -/// Returns concatenated data (legacy API - consider using `encode_tight_rects` instead) -pub fn encode_tight_with_streams( - data: &[u8], - width: u16, - height: u16, - quality: u8, - compression: u8, - client_format: &PixelFormat, - compressor: &mut C, -) -> BytesMut { - // Concatenate all sub-rectangles - let rects = encode_tight_rects( - data, - width, - height, - quality, - compression, - client_format, - compressor, - ); - let mut output = BytesMut::new(); - for (_x, _y, _w, _h, buf) in rects { - output.extend_from_slice(&buf); - } - output -} diff --git a/src/encoding/tightpng.rs b/src/encoding/tightpng.rs deleted file mode 100644 index c1d000c..0000000 --- a/src/encoding/tightpng.rs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC `TightPng` encoding implementation. -//! -//! `TightPng` encoding uses PNG compression exclusively for all rectangles. -//! Unlike standard Tight encoding which supports multiple compression modes -//! (solid fill, palette, zlib, JPEG), `TightPng` ONLY uses PNG mode. -//! -//! This design is optimized for browser-based VNC clients like noVNC, -//! which can decode PNG data natively in hardware without needing to -//! handle zlib decompression or palette operations. - -use super::Encoding; -use crate::protocol::TIGHT_PNG; -use bytes::{BufMut, BytesMut}; - -/// Implements the VNC "`TightPng`" encoding (encoding -260). -/// -/// `TightPng` sends all pixel data as PNG-compressed images, regardless of -/// content. This differs from standard Tight encoding which uses multiple -/// compression strategies. -pub struct TightPngEncoding; - -impl Encoding for TightPngEncoding { - fn encode( - &self, - data: &[u8], - width: u16, - height: u16, - _quality: u8, - compression: u8, - ) -> BytesMut { - // TightPng ONLY uses PNG mode - no solid fill, no palette modes - // This is the key difference from standard Tight encoding - // Browser-based clients like noVNC expect only PNG data - encode_tightpng_png(data, width, height, compression) - } -} - -/// Encode as `TightPng` using PNG compression. -/// -/// This is the only compression mode used by `TightPng` encoding. -#[allow(clippy::cast_possible_truncation)] // TightPng compact length encoding uses variable-length u8 packing per RFC 6143 -fn encode_tightpng_png(data: &[u8], width: u16, height: u16, compression: u8) -> BytesMut { - use png::{BitDepth, ColorType, Encoder}; - - // Convert RGBA to RGB (PNG encoder will handle this) - let mut rgb_data = Vec::with_capacity((width as usize) * (height as usize) * 3); - for chunk in data.chunks_exact(4) { - rgb_data.push(chunk[0]); - rgb_data.push(chunk[1]); - rgb_data.push(chunk[2]); - } - - // Create PNG encoder - let mut png_data = Vec::new(); - { - let mut encoder = Encoder::new(&mut png_data, u32::from(width), u32::from(height)); - encoder.set_color(ColorType::Rgb); - encoder.set_depth(BitDepth::Eight); - - // Map TightVNC compression level (0-9) to PNG compression (0-9 maps to Fast/Default/Best) - let png_compression = match compression { - 0..=2 => png::Compression::Fast, - 3..=6 => png::Compression::Default, - _ => png::Compression::Best, - }; - encoder.set_compression(png_compression); - - let mut writer = match encoder.write_header() { - Ok(w) => w, - Err(e) => { - log::error!("PNG header write failed: {e}, falling back to basic encoding"); - // Fall back to basic tight encoding - let mut buf = BytesMut::with_capacity(1 + data.len()); - buf.put_u8(0x00); // Basic tight encoding, no compression - for chunk in data.chunks_exact(4) { - buf.put_u8(chunk[0]); // R - buf.put_u8(chunk[1]); // G - buf.put_u8(chunk[2]); // B - buf.put_u8(0); // Padding - } - return buf; - } - }; - - if let Err(e) = writer.write_image_data(&rgb_data) { - log::error!("PNG data write failed: {e}, falling back to basic encoding"); - // Fall back to basic tight encoding - let mut buf = BytesMut::with_capacity(1 + data.len()); - buf.put_u8(0x00); // Basic tight encoding, no compression - for chunk in data.chunks_exact(4) { - buf.put_u8(chunk[0]); // R - buf.put_u8(chunk[1]); // G - buf.put_u8(chunk[2]); // B - buf.put_u8(0); // Padding - } - return buf; - } - } - - let mut buf = BytesMut::new(); - buf.put_u8(TIGHT_PNG << 4); // PNG subencoding - - // Compact length - let len = png_data.len(); - if len < 128 { - buf.put_u8(len as u8); - } else if len < 16384 { - buf.put_u8(((len & 0x7F) | 0x80) as u8); - buf.put_u8((len >> 7) as u8); - } else { - buf.put_u8(((len & 0x7F) | 0x80) as u8); - buf.put_u8((((len >> 7) & 0x7F) | 0x80) as u8); - buf.put_u8((len >> 14) as u8); - } - - buf.put_slice(&png_data); - buf -} diff --git a/src/encoding/zlib.rs b/src/encoding/zlib.rs deleted file mode 100644 index 4da192d..0000000 --- a/src/encoding/zlib.rs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC Zlib encoding implementation. -//! -//! Simple zlib compression on raw pixel data using the client's pixel format. - -use bytes::{BufMut, BytesMut}; -use flate2::{Compress, FlushCompress}; -use std::io; - -/// Encodes pixel data using Zlib with a persistent compressor (RFC 6143 compliant). -/// -/// This maintains compression state across rectangles as required by RFC 6143. -/// The implementation matches standard VNC protocol's approach: single `deflate()` call per rectangle. -/// -/// # Arguments -/// * `data` - RGBA pixel data (4 bytes per pixel) -/// * `compressor` - Persistent zlib compressor maintaining state across rectangles -/// -/// # Returns -/// -/// 4-byte length header + compressed data -/// -/// # Errors -/// -/// Returns an error if zlib compression fails -#[allow(clippy::cast_possible_truncation)] // Zlib total_in/total_out limited to buffer size -pub fn encode_zlib_persistent(data: &[u8], compressor: &mut Compress) -> io::Result> { - // Convert RGBA to RGBX (client pixel format for 32bpp) - // R at byte 0, G at byte 1, B at byte 2, padding at byte 3 - let mut pixel_data = Vec::with_capacity(data.len()); - for chunk in data.chunks_exact(4) { - pixel_data.push(chunk[0]); // R - pixel_data.push(chunk[1]); // G - pixel_data.push(chunk[2]); // B - pixel_data.push(0); // Padding - } - - // Calculate maximum compressed size (zlib overhead formula) - // From zlib.h: compressed size ≤ uncompressed + (uncompressed/1000) + 12 - let max_compressed_size = pixel_data.len() + (pixel_data.len() / 1000) + 12; - let mut compressed_output = vec![0u8; max_compressed_size]; - - // Track total_in and total_out before compression - let previous_in = compressor.total_in(); - let previous_out = compressor.total_out(); - - // Single deflate() call with Z_SYNC_FLUSH (RFC 6143 Section 7.7.2) - compressor.compress(&pixel_data, &mut compressed_output, FlushCompress::Sync)?; - - // Calculate actual compressed length and consumed input - let compressed_len = (compressor.total_out() - previous_out) as usize; - let total_consumed = (compressor.total_in() - previous_in) as usize; - - // Verify all input was consumed - if total_consumed < pixel_data.len() { - return Err(io::Error::other(format!( - "Zlib: incomplete compression {}/{}", - total_consumed, - pixel_data.len() - ))); - } - - // Build result: 4-byte big-endian length + compressed data - let mut result = BytesMut::with_capacity(4 + compressed_len); - result.put_u32(compressed_len as u32); - result.extend_from_slice(&compressed_output[..compressed_len]); - - Ok(result.to_vec()) -} diff --git a/src/encoding/zlibhex.rs b/src/encoding/zlibhex.rs deleted file mode 100644 index 3919edc..0000000 --- a/src/encoding/zlibhex.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC `ZlibHex` encoding implementation. -//! -//! `ZlibHex` combines Hextile encoding with zlib compression for improved -//! bandwidth efficiency while maintaining the tile-based structure. - -use super::Encoding; -use super::HextileEncoding; -use bytes::{BufMut, BytesMut}; -use flate2::{Compress, FlushCompress}; -use std::io; - -/// Encodes pixel data using `ZlibHex` with a persistent compressor (RFC 6143 compliant). -/// -/// This encoding first applies Hextile encoding to the pixel data, then compresses -/// the result using zlib. The compressor maintains state across rectangles for better -/// compression ratios. -/// -/// # Arguments -/// * `data` - RGBA pixel data (4 bytes per pixel) -/// * `width` - Width of the rectangle in pixels -/// * `height` - Height of the rectangle in pixels -/// * `compressor` - Persistent zlib compressor maintaining state across rectangles -/// -/// # Returns -/// -/// 4-byte length header + compressed Hextile data -/// -/// # Errors -/// -/// Returns an error if zlib compression fails -#[allow(clippy::cast_possible_truncation)] // Zlib total_in/total_out limited to buffer size -pub fn encode_zlibhex_persistent( - data: &[u8], - width: u16, - height: u16, - compressor: &mut Compress, -) -> io::Result> { - // First, encode using Hextile - let hextile_encoder = HextileEncoding; - let hextile_data = hextile_encoder.encode(data, width, height, 0, 0); - - // Calculate maximum compressed size (zlib overhead formula) - // From zlib.h: compressed size ≤ uncompressed + (uncompressed/1000) + 12 - let max_compressed_size = hextile_data.len() + (hextile_data.len() / 1000) + 12; - let mut compressed_output = vec![0u8; max_compressed_size]; - - // Track total_in and total_out before compression - let previous_in = compressor.total_in(); - let previous_out = compressor.total_out(); - - // Single deflate() call with Z_SYNC_FLUSH (RFC 6143 Section 7.7.2) - compressor.compress(&hextile_data, &mut compressed_output, FlushCompress::Sync)?; - - // Calculate actual compressed length - let compressed_len = (compressor.total_out() - previous_out) as usize; - let consumed_len = (compressor.total_in() - previous_in) as usize; - - // Verify all input was consumed - if consumed_len < hextile_data.len() { - return Err(io::Error::other(format!( - "ZlibHex: incomplete compression {}/{}", - consumed_len, - hextile_data.len() - ))); - } - - // Build result: 4-byte big-endian length + compressed data - let mut result = BytesMut::with_capacity(4 + compressed_len); - result.put_u32(compressed_len as u32); - result.extend_from_slice(&compressed_output[..compressed_len]); - - Ok(result.to_vec()) -} diff --git a/src/encoding/zrle.rs b/src/encoding/zrle.rs deleted file mode 100644 index 8d81ed0..0000000 --- a/src/encoding/zrle.rs +++ /dev/null @@ -1,521 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! ZRLE (Zlib Run-Length Encoding) implementation for VNC. -//! -//! ZRLE is a highly efficient encoding that combines tiling, palette-based compression, -//! run-length encoding, and zlib compression. It is effective for a wide range of -//! screen content. -//! -//! # Encoding Process -//! -//! 1. The framebuffer region is divided into 64x64 pixel tiles. -//! 2. Each tile is compressed independently. -//! 3. The compressed data for all tiles is concatenated and then compressed as a whole -//! using zlib. -//! -//! # Tile Sub-encodings -//! -//! Each tile is analyzed and compressed using one of the following methods: -//! - **Raw:** If not otherwise compressible, sent as uncompressed RGBA data. -//! - **Solid Color:** If the tile contains only one color. -//! - **Packed Palette:** If the tile contains 2-16 unique colors. Pixels are sent as -//! palette indices, which can be run-length encoded. -//! - **Plain RLE:** If the tile has more than 16 colors but is still compressible with RLE. -//! - -use bytes::{BufMut, BytesMut}; -use flate2::write::ZlibEncoder; -use flate2::{Compress, Compression, FlushCompress}; -use std::collections::HashMap; -use std::io::Write; - -use super::Encoding; -use crate::protocol::PixelFormat; - -const TILE_SIZE: usize = 64; - -/// Analyzes pixel data to count RLE runs, single pixels, and unique colors. -/// Returns: (runs, `single_pixels`, `palette_vec`) -/// CRITICAL: The palette Vec must preserve insertion order (order colors first appear) -/// as required by RFC 6143 for proper ZRLE palette encoding. -/// Optimized: uses inline array for small palettes to avoid `HashMap` allocation. -fn analyze_runs_and_palette(pixels: &[u32]) -> (usize, usize, Vec) { - let mut runs = 0; - let mut single_pixels = 0; - let mut palette: Vec = Vec::with_capacity(16); // Most tiles have <= 16 colors - - if pixels.is_empty() { - return (0, 0, palette); - } - - let mut i = 0; - while i < pixels.len() { - let color = pixels[i]; - - // For small palettes (common case), linear search is faster than HashMap - if palette.len() < 256 && !palette.contains(&color) { - palette.push(color); - } - - let mut run_len = 1; - while i + run_len < pixels.len() && pixels[i + run_len] == color { - run_len += 1; - } - - if run_len == 1 { - single_pixels += 1; - } else { - runs += 1; - } - i += run_len; - } - (runs, single_pixels, palette) -} - -/// Encodes a rectangle of pixel data using ZRLE with a persistent compressor. -/// This maintains compression state across rectangles as required by RFC 6143. -/// -/// # Errors -/// -/// Returns an error if zlib compression fails -#[allow(dead_code)] -#[allow(clippy::cast_possible_truncation)] // ZRLE protocol requires u8/u16/u32 packing of pixel data -pub fn encode_zrle_persistent( - data: &[u8], - width: u16, - height: u16, - _pixel_format: &PixelFormat, - compressor: &mut Compress, -) -> std::io::Result> { - let width = width as usize; - let height = height as usize; - let mut uncompressed_data = BytesMut::new(); - - for y in (0..height).step_by(TILE_SIZE) { - for x in (0..width).step_by(TILE_SIZE) { - let tile_w = (width - x).min(TILE_SIZE); - let tile_h = (height - y).min(TILE_SIZE); - - // Extract tile pixel data - let tile_data = extract_tile(data, width, x, y, tile_w, tile_h); - - // Analyze and encode the tile - encode_tile(&mut uncompressed_data, &tile_data, tile_w, tile_h); - } - } - - // Compress using persistent compressor with Z_SYNC_FLUSH - // RFC 6143: use persistent zlib stream with dictionary for compression continuity - let input = &uncompressed_data[..]; - let mut output_buf = vec![0u8; input.len() * 2 + 1024]; // Generous buffer - - let before_out = compressor.total_out(); - - // Single compress call with Z_SYNC_FLUSH - this should handle all input - compressor.compress(input, &mut output_buf, FlushCompress::Sync)?; - - let produced = (compressor.total_out() - before_out) as usize; - let compressed_output = &output_buf[..produced]; - - // Build result with length prefix (big-endian) + compressed data - let mut result = BytesMut::with_capacity(4 + compressed_output.len()); - result.put_u32(compressed_output.len() as u32); - result.extend_from_slice(compressed_output); - - #[cfg(feature = "debug-logging")] - log::info!( - "ZRLE: compressed {}->{} bytes ({}x{} tiles)", - uncompressed_data.len(), - compressed_output.len(), - width, - height - ); - - Ok(result.to_vec()) -} - -/// Encodes a rectangle of pixel data using the ZRLE encoding. -/// This creates a new compressor for each rectangle (non-RFC compliant, deprecated). -/// -/// # Errors -/// -/// Returns an error if zlib compression fails -#[allow(clippy::cast_possible_truncation)] // ZRLE protocol requires u8/u16/u32 packing of pixel data -pub fn encode_zrle( - data: &[u8], - width: u16, - height: u16, - _pixel_format: &PixelFormat, // Assuming RGBA32 - compression: u8, -) -> std::io::Result> { - let compression_level = match compression { - 0 => Compression::fast(), - 1..=3 => Compression::new(u32::from(compression)), - 4..=6 => Compression::default(), - _ => Compression::best(), - }; - let mut zlib_encoder = ZlibEncoder::new(Vec::new(), compression_level); - let mut uncompressed_data = BytesMut::new(); - - let width = width as usize; - let height = height as usize; - - for y in (0..height).step_by(TILE_SIZE) { - for x in (0..width).step_by(TILE_SIZE) { - let tile_w = (width - x).min(TILE_SIZE); - let tile_h = (height - y).min(TILE_SIZE); - - // Extract tile pixel data - let tile_data = extract_tile(data, width, x, y, tile_w, tile_h); - - // Analyze and encode the tile - encode_tile(&mut uncompressed_data, &tile_data, tile_w, tile_h); - } - } - - zlib_encoder.write_all(&uncompressed_data)?; - let compressed = zlib_encoder.finish()?; - - // ZRLE requires a 4-byte big-endian length prefix before the zlib data - let mut result = BytesMut::with_capacity(4 + compressed.len()); - result.put_u32(compressed.len() as u32); // big-endian length - result.extend_from_slice(&compressed); - - Ok(result.to_vec()) -} - -/// Encodes a single tile, choosing the best sub-encoding. -/// Optimized to minimize allocations by working directly with RGBA data where possible. -#[allow(clippy::cast_possible_truncation)] // ZRLE palette indices and run lengths limited to u8 per RFC 6143 -fn encode_tile(buf: &mut BytesMut, tile_data: &[u8], width: usize, height: usize) { - const CPIXEL_SIZE: usize = 3; // CPIXEL is 3 bytes for depth=24 - - // Quick check for solid color by scanning RGBA data directly (avoid allocation) - if tile_data.len() >= 4 { - let first_r = tile_data[0]; - let first_g = tile_data[1]; - let first_b = tile_data[2]; - let mut is_solid = true; - - for chunk in tile_data.chunks_exact(4).skip(1) { - if chunk[0] != first_r || chunk[1] != first_g || chunk[2] != first_b { - is_solid = false; - break; - } - } - - if is_solid { - let color = u32::from(first_r) | (u32::from(first_g) << 8) | (u32::from(first_b) << 16); - encode_solid_color_tile(buf, color); - return; - } - } - - // Convert RGBA to RGB24 pixels (still needed for analysis) - let pixels = rgba_to_rgb24_pixels(tile_data); - let (runs, single_pixels, palette) = analyze_runs_and_palette(&pixels); - - let mut use_rle = false; - let mut use_palette = false; - - // Start assuming raw encoding size - let mut estimated_bytes = width * height * CPIXEL_SIZE; - - let plain_rle_bytes = (CPIXEL_SIZE + 1) * (runs + single_pixels); - - if plain_rle_bytes < estimated_bytes { - use_rle = true; - estimated_bytes = plain_rle_bytes; - } - - if palette.len() < 128 { - let palette_size = palette.len(); - - // Palette RLE encoding - let palette_rle_bytes = CPIXEL_SIZE * palette_size + 2 * runs + single_pixels; - - if palette_rle_bytes < estimated_bytes { - use_rle = true; - use_palette = true; - estimated_bytes = palette_rle_bytes; - } - - // Packed palette encoding (no RLE) - if palette_size < 17 { - let bits_per_packed_pixel = match palette_size { - 2 => 1, - 3..=4 => 2, - _ => 4, // 5-16 colors - }; - // Round up: (bits + 7) / 8 to match actual encoding - let packed_bytes = - CPIXEL_SIZE * palette_size + (width * height * bits_per_packed_pixel).div_ceil(8); - - if packed_bytes < estimated_bytes { - use_rle = false; - use_palette = true; - // No need to update estimated_bytes, this is the last check - } - } - } - - if use_palette { - // Palette (Packed Palette or Packed Palette RLE) - // Build index lookup from palette (preserves insertion order) - let color_to_idx: HashMap<_, _> = palette - .iter() - .enumerate() - .map(|(i, &c)| (c, i as u8)) - .collect(); - - if use_rle { - // Packed Palette RLE - encode_packed_palette_rle_tile(buf, &pixels, &palette, &color_to_idx); - } else { - // Packed Palette (no RLE) - encode_packed_palette_tile(buf, &pixels, width, height, &palette, &color_to_idx); - } - } else { - // Raw or Plain RLE - if use_rle { - // Plain RLE - encode directly to buffer (avoid intermediate Vec) - buf.put_u8(128); - encode_rle_to_buf(buf, &pixels); - } else { - // Raw - encode_raw_tile(buf, tile_data); - } - } -} - -/// Extracts a tile from the full framebuffer. -/// Optimized to use a single allocation and bulk copy operations. -#[allow(clippy::uninit_vec)] // Performance optimization: all bytes written via bulk copy before return -fn extract_tile( - full_frame: &[u8], - frame_width: usize, - x: usize, - y: usize, - width: usize, - height: usize, -) -> Vec { - let tile_size = width * height * 4; - let mut tile_data = Vec::with_capacity(tile_size); - - // Use unsafe for performance - we know the capacity is correct - unsafe { - tile_data.set_len(tile_size); - } - - let row_bytes = width * 4; - for row in 0..height { - let src_start = ((y + row) * frame_width + x) * 4; - let dst_start = row * row_bytes; - tile_data[dst_start..dst_start + row_bytes] - .copy_from_slice(&full_frame[src_start..src_start + row_bytes]); - } - tile_data -} - -/// Converts RGBA to 32-bit RGB pixels (0x00BBGGRR format for VNC). -fn rgba_to_rgb24_pixels(data: &[u8]) -> Vec { - data.chunks_exact(4) - .map(|c| u32::from(c[0]) | (u32::from(c[1]) << 8) | (u32::from(c[2]) << 16)) - .collect() -} - -/// Writes a CPIXEL (3 bytes for depth=24) in little-endian format. -/// CPIXEL format: R at byte 0, G at byte 1, B at byte 2 -fn put_cpixel(buf: &mut BytesMut, pixel: u32) { - buf.put_u8((pixel & 0xFF) as u8); // R at bits 0-7 - buf.put_u8(((pixel >> 8) & 0xFF) as u8); // G at bits 8-15 - buf.put_u8(((pixel >> 16) & 0xFF) as u8); // B at bits 16-23 -} - -/// Sub-encoding for a tile with a single color. -fn encode_solid_color_tile(buf: &mut BytesMut, color: u32) { - buf.put_u8(1); // Solid color sub-encoding - put_cpixel(buf, color); // Write 3-byte CPIXEL -} - -/// Sub-encoding for raw pixel data. -fn encode_raw_tile(buf: &mut BytesMut, tile_data: &[u8]) { - buf.put_u8(0); // Raw sub-encoding - // Convert RGBA (4 bytes) to CPIXEL (3 bytes) for each pixel - for chunk in tile_data.chunks_exact(4) { - buf.put_u8(chunk[0]); // R - buf.put_u8(chunk[1]); // G - buf.put_u8(chunk[2]); // B (skip alpha channel) - } -} - -/// Sub-encoding for a tile with a small palette. -#[allow(clippy::cast_possible_truncation)] // ZRLE palette size limited to 16 colors (u8) per RFC 6143 -fn encode_packed_palette_tile( - buf: &mut BytesMut, - pixels: &[u32], - width: usize, - height: usize, - palette: &[u32], - color_to_idx: &HashMap, -) { - let palette_size = palette.len(); - let bits_per_pixel = match palette_size { - 2 => 1, - 3..=4 => 2, - _ => 4, - }; - - buf.put_u8(palette_size as u8); // Packed palette sub-encoding - - // Write palette as CPIXEL (3 bytes each) - in insertion order - for &color in palette { - put_cpixel(buf, color); - } - - // Write packed pixel data ROW BY ROW per RFC 6143 ZRLE specification - // Critical: Each row must be byte-aligned - for row in 0..height { - let mut packed_byte = 0; - let mut nbits = 0; - let row_start = row * width; - let row_end = row_start + width; - - for &pixel in &pixels[row_start..row_end] { - let idx = color_to_idx[&pixel]; - // Pack from MSB: byte = (byte << bppp) | index - packed_byte = (packed_byte << bits_per_pixel) | idx; - nbits += bits_per_pixel; - - if nbits >= 8 { - buf.put_u8(packed_byte); - packed_byte = 0; - nbits = 0; - } - } - - // Pad remaining bits to MSB at end of row per RFC 6143 - if nbits > 0 { - packed_byte <<= 8 - nbits; - buf.put_u8(packed_byte); - } - } -} - -/// Sub-encoding for a tile with a small palette and RLE. -#[allow(clippy::cast_possible_truncation)] // ZRLE palette size limited to 16 colors (u8) per RFC 6143 -fn encode_packed_palette_rle_tile( - buf: &mut BytesMut, - pixels: &[u32], - palette: &[u32], - color_to_idx: &HashMap, -) { - let palette_size = palette.len(); - buf.put_u8(128 | (palette_size as u8)); // Packed palette RLE sub-encoding - - // Write palette as CPIXEL (3 bytes each) - for &color in palette { - put_cpixel(buf, color); - } - - // Write RLE data using palette indices per RFC 6143 specification - let mut i = 0; - while i < pixels.len() { - let color = pixels[i]; - let index = color_to_idx[&color]; - - let mut run_len = 1; - while i + run_len < pixels.len() && pixels[i + run_len] == color { - run_len += 1; - } - - // Short runs (1-2 pixels) are written WITHOUT RLE marker per RFC 6143 - if run_len <= 2 { - // Write index once for length 1, twice for length 2 - if run_len == 2 { - buf.put_u8(index); - } - buf.put_u8(index); - } else { - // RLE encoding for runs >= 3 per RFC 6143 - buf.put_u8(index | 128); // Set bit 7 to indicate RLE follows - // Encode run length - 1 using variable-length encoding - let mut remaining_len = run_len - 1; - while remaining_len >= 255 { - buf.put_u8(255); - remaining_len -= 255; - } - buf.put_u8(remaining_len as u8); - } - i += run_len; - } -} - -/// Encodes pixel data using run-length encoding directly to buffer (optimized). -#[allow(clippy::cast_possible_truncation)] // ZRLE run lengths encoded as u8 per RFC 6143 -fn encode_rle_to_buf(buf: &mut BytesMut, pixels: &[u32]) { - let mut i = 0; - while i < pixels.len() { - let color = pixels[i]; - let mut run_len = 1; - while i + run_len < pixels.len() && pixels[i + run_len] == color { - run_len += 1; - } - // Write CPIXEL (3 bytes) - put_cpixel(buf, color); - - // Encode run length - 1 per RFC 6143 ZRLE specification - // Length encoding: write 255 for each full 255-length chunk, then remainder - // NO continuation bits - just plain bytes where 255 means "add 255 to length" - let mut len_to_encode = run_len - 1; - while len_to_encode >= 255 { - buf.put_u8(255); - len_to_encode -= 255; - } - buf.put_u8(len_to_encode as u8); - - i += run_len; - } -} - -/// Implements the VNC "ZRLE" (Zlib Run-Length Encoding). -pub struct ZrleEncoding; - -impl Encoding for ZrleEncoding { - fn encode( - &self, - data: &[u8], - width: u16, - height: u16, - _quality: u8, - compression: u8, - ) -> BytesMut { - // ZRLE doesn't use quality, but it does use compression. - let pixel_format = PixelFormat::rgba32(); // Assuming RGBA32 for now - if let Ok(encoded_data) = encode_zrle(data, width, height, &pixel_format, compression) { - BytesMut::from(&encoded_data[..]) - } else { - // Fallback to Raw encoding if ZRLE fails. - let mut buf = BytesMut::with_capacity(data.len()); - for chunk in data.chunks_exact(4) { - buf.put_u8(chunk[0]); // R - buf.put_u8(chunk[1]); // G - buf.put_u8(chunk[2]); // B - buf.put_u8(0); // Padding - } - buf - } - } -} diff --git a/src/encoding/zywrle.rs b/src/encoding/zywrle.rs deleted file mode 100644 index 6736e1d..0000000 --- a/src/encoding/zywrle.rs +++ /dev/null @@ -1,496 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! VNC ZYWRLE (Zlib+Wavelet+Run-Length Encoding) implementation. -//! -//! ZYWRLE is a wavelet-based lossy compression encoding for low-bandwidth scenarios. -//! It uses: -//! - Piecewise-Linear Haar (`PLHarr`) wavelet transform -//! - RCT (Reversible Color Transform) for RGB to YUV conversion -//! - Non-linear quantization filtering -//! - ZRLE encoding on the transformed coefficients -//! -//! # Algorithm Attribution -//! The ZYWRLE algorithm is Copyright 2006 by Hitachi Systems & Services, Ltd. -//! (Noriaki Yamazaki, Research & Development Center). -//! -//! This implementation is based on the ZYWRLE specification and is distributed -//! under the terms compatible with the original BSD-style license granted by -//! Hitachi Systems & Services, Ltd. for use of the ZYWRLE codec. -//! -//! # References -//! - `PLHarr`: Senecal, J. G., et al., "An Improved N-Bit to N-Bit Reversible Haar-Like Transform" -//! - EZW: Shapiro, JM: "Embedded Image Coding Using Zerotrees of Wavelet Coefficients" -//! - ZYWRLE specification and reference implementation - -/// Non-linear quantization filter lookup tables. -/// These tables implement r=2.0 non-linear quantization (quantize is x^2, dequantize is sqrt(x)). -/// The tables map input coefficient values [0..255] to quantized-dequantized (filtered) values. -/// -/// Table selection based on quality level: -/// - `zywrle_conv`[0]: bi=5, bo=5 r=0.0:PSNR=24.849 (zero everything, highest compression) -/// - `zywrle_conv`[1]: bi=5, bo=5 r=2.0:PSNR=74.031 (good quality) -/// - `zywrle_conv`[2]: bi=5, bo=4 r=2.0:PSNR=64.441 (medium quality) -/// - `zywrle_conv`[3]: bi=5, bo=2 r=2.0:PSNR=43.175 (low quality, highest compression) -const ZYWRLE_CONV: [[i8; 256]; 4] = [ - [ - // bi=5, bo=5 r=0.0:PSNR=24.849 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - [ - // bi=5, bo=5 r=2.0:PSNR=74.031 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 32, 32, 32, 32, 32, - 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 56, - 56, 56, 56, 56, 56, 56, 56, 56, 64, 64, 64, 64, 64, 64, 64, 64, 72, 72, 72, 72, 72, 72, 72, - 72, 80, 80, 80, 80, 80, 80, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 96, 96, 96, 96, - 96, 104, 104, 104, 104, 104, 104, 104, 104, 104, 104, 112, 112, 112, 112, 112, 112, 112, - 112, 112, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 0, -120, -120, -120, -120, - -120, -120, -120, -120, -120, -120, -112, -112, -112, -112, -112, -112, -112, -112, -112, - -104, -104, -104, -104, -104, -104, -104, -104, -104, -104, -96, -96, -96, -96, -96, -88, - -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -80, -80, -80, -80, -80, -80, -72, - -72, -72, -72, -72, -72, -72, -72, -64, -64, -64, -64, -64, -64, -64, -64, -56, -56, -56, - -56, -56, -56, -56, -56, -56, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -32, - -32, -32, -32, -32, -32, -32, -32, -32, -32, -32, -32, -32, -32, -32, -32, -32, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - [ - // bi=5, bo=4 r=2.0:PSNR=64.441 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, - 48, 48, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 80, 80, 80, 80, 80, - 80, 80, 80, 80, 80, 80, 80, 80, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 104, 104, 104, - 104, 104, 104, 104, 104, 104, 104, 104, 112, 112, 112, 112, 112, 112, 112, 112, 112, 120, - 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 0, -120, -120, -120, -120, -120, - -120, -120, -120, -120, -120, -120, -120, -112, -112, -112, -112, -112, -112, -112, -112, - -112, -104, -104, -104, -104, -104, -104, -104, -104, -104, -104, -104, -88, -88, -88, -88, - -88, -88, -88, -88, -88, -88, -88, -80, -80, -80, -80, -80, -80, -80, -80, -80, -80, -80, - -80, -80, -64, -64, -64, -64, -64, -64, -64, -64, -64, -64, -64, -64, -64, -64, -64, -64, - -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, - -48, -48, -48, -48, -48, -48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - [ - // bi=5, bo=2 r=2.0:PSNR=43.175 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, - 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, - 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 0, -88, - -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, - -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, - -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, -88, - -88, -88, -88, -88, -88, -88, -88, -88, -88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], -]; - -/// Filter parameter tables indexed by [level-1][l][channel]. -/// Maps quality level and wavelet level to the appropriate quantization filter. -const ZYWRLE_PARAM: [[[usize; 3]; 3]; 3] = [ - [[0, 2, 0], [0, 0, 0], [0, 0, 0]], // level 1 - [[0, 3, 0], [1, 1, 1], [0, 0, 0]], // level 2 - [[0, 3, 0], [2, 2, 2], [1, 1, 1]], // level 3 -]; - -/// Piecewise-Linear Haar (`PLHarr`) transform on two signed bytes. -/// -/// This is the core wavelet transform operation. It's an improved N-bit to N-bit -/// reversible Haar-like transform that handles signed values correctly. -/// -/// # Arguments -/// * `x0` - First coefficient (modified in place to contain Low component) -/// * `x1` - Second coefficient (modified in place to contain High component) -#[inline] -#[allow(clippy::cast_possible_truncation)] // Piecewise-Linear Haar transform uses i32 math, results fit in i8 -fn harr(x0: &mut i8, x1: &mut i8) { - let orig_x0 = i32::from(*x0); - let orig_x1 = i32::from(*x1); - let mut x0_val = orig_x0; - let mut x1_val = orig_x1; - - if (x0_val ^ x1_val) & 0x80 != 0 { - // Different signs - x1_val += x0_val; - if ((x1_val ^ orig_x1) & 0x80) == 0 { - // |X1| > |X0| - x0_val -= x1_val; // H = -B - } - } else { - // Same sign - x0_val -= x1_val; - if ((x0_val ^ orig_x0) & 0x80) == 0 { - // |X0| > |X1| - x1_val += x0_val; // L = A - } - } - - *x0 = x1_val as i8; - *x1 = x0_val as i8; -} - -/// Performs one level of wavelet transform on a 1D array. -/// -/// Uses interleave decomposition instead of pyramid decomposition to avoid -/// needing line buffers. In interleave mode, H/L and X0/X1 are always in -/// the same position. -/// -/// # Arguments -/// * `data` - Pointer to coefficient array (as i8 slice) -/// * `size` - Size of the dimension being transformed -/// * `level` - Current wavelet level (0-based) -/// * `skip_pixel` - Number of pixels to skip between elements (1 for horizontal, width for vertical) -#[inline] -fn wavelet_level(data: &mut [i8], size: usize, level: usize, skip_pixel: usize) { - let s = (8 << level) * skip_pixel; - let end_offset = (size >> (level + 1)) * s; - let ofs = (4 << level) * skip_pixel; - - let mut offset = 0; - while offset < end_offset { - // Process 3 bytes (RGB channels) - if offset + ofs + 2 < data.len() { - let (slice1, slice2) = data.split_at_mut(offset + ofs); - harr(&mut slice1[offset], &mut slice2[0]); - harr(&mut slice1[offset + 1], &mut slice2[1]); - harr(&mut slice1[offset + 2], &mut slice2[2]); - } - offset += s; - } -} - -/// Apply wavelet transform and quantization filtering to a coefficient buffer. -/// -/// This implements the complete wavelet analysis pipeline: -/// 1. Horizontal wavelet transform at each level -/// 2. Vertical wavelet transform at each level -/// 3. Quantization filtering after each level -/// -/// # Arguments -/// * `buf` - Coefficient buffer (i32 values reinterpreted as i8 arrays) -/// * `width` - Image width -/// * `height` - Image height -/// * `level` - Number of wavelet levels to apply (1-3) -fn wavelet(buf: &mut [i32], width: usize, height: usize, level: usize) { - let bytes = - unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr().cast::(), buf.len() * 4) }; - - for l in 0..level { - // Horizontal transform - let s = width << l; - for row in 0..(height >> l) { - let row_offset = row * s * 4; - wavelet_level(&mut bytes[row_offset..], width, l, 1); - } - - // Vertical transform - let s = 1 << l; - for col in 0..(width >> l) { - let col_offset = col * s * 4; - wavelet_level(&mut bytes[col_offset..], height, l, width); - } - - // Apply quantization filter - filter_wavelet_square(buf, width, height, level, l); - } -} - -/// Apply non-linear quantization filtering to wavelet coefficients. -/// -/// This filters the high-frequency subbands using the quantization lookup tables. -/// The filter preserves low-frequency coefficients (subband 0) and quantizes -/// high-frequency subbands (1, 2, 3) based on the selected quality level. -/// -/// # Arguments -/// * `buf` - Coefficient buffer -/// * `width` - Image width -/// * `height` - Image height -/// * `level` - Total number of wavelet levels -/// * `l` - Current level being filtered -/// -/// # Performance Note -/// This function contains bounds checks in nested loops which add ~2-3% overhead. -/// The checks are necessary for safety but could be optimized with `debug_assert`! -/// and unsafe indexing if profiling shows this as a bottleneck. -#[allow(clippy::cast_sign_loss)] // Quantization filter applies i8 lookup table to u8 bytes -fn filter_wavelet_square(buf: &mut [i32], width: usize, height: usize, level: usize, l: usize) { - let param = &ZYWRLE_PARAM[level - 1][l]; - let s = 2 << l; - - // Process subbands 1, 2, 3 (skip subband 0 which is low-frequency) - for r in 1..4 { - let mut row_start = 0; - if (r & 0x01) != 0 { - row_start += s >> 1; - } - if (r & 0x02) != 0 { - row_start += (s >> 1) * width; - } - - for y in 0..(height / s) { - for x in 0..(width / s) { - let idx = row_start + y * s * width + x * s; - if idx < buf.len() { - let pixel = &mut buf[idx]; - let mut bytes = pixel.to_le_bytes(); - - // Apply filter to each channel (V, Y, U stored in bytes 2, 1, 0) - bytes[2] = ZYWRLE_CONV[param[2]][bytes[2] as usize] as u8; - bytes[1] = ZYWRLE_CONV[param[1]][bytes[1] as usize] as u8; - bytes[0] = ZYWRLE_CONV[param[0]][bytes[0] as usize] as u8; - - *pixel = i32::from_le_bytes(bytes); - } - } - } - } -} - -/// Convert RGB to YUV using RCT (Reversible Color Transform). -/// -/// RCT is described in JPEG-2000 specification: -/// Y = (R + 2G + B)/4 -/// U = B - G -/// V = R - G -/// -/// The U and V components are further processed to reduce to odd range for `PLHarr`. -/// -/// # Arguments -/// * `buf` - Output coefficient buffer (YUV as i32) -/// * `data` - Input RGBA pixel data -/// * `width` - Image width -/// * `height` - Image height -#[allow(clippy::many_single_char_names)] // r, g, b, y, u, v are standard color component names -#[allow(clippy::cast_sign_loss)] // RCT transform stores signed YUV as unsigned bytes in i32 -#[allow(clippy::cast_possible_truncation)] // YUV color components limited to i8 range (-128..127) per ZYWRLE spec -fn rgb_to_yuv(buf: &mut [i32], data: &[u8], width: usize, height: usize) { - let mut buf_idx = 0; - let mut data_idx = 0; - - for _ in 0..height { - for _ in 0..width { - if data_idx + 2 < data.len() && buf_idx < buf.len() { - let r = i32::from(data[data_idx]); - let g = i32::from(data[data_idx + 1]); - let b = i32::from(data[data_idx + 2]); - - // RCT transform - let mut y = (r + (g << 1) + b) >> 2; - let mut u = b - g; - let mut v = r - g; - - // Center around 0 - y -= 128; - u >>= 1; - v >>= 1; - - // Mask to ensure proper bit depth (32-bit: no masking) - // For 15/16-bit, standard VNC protocol masks here, but we're always 32-bit RGBA - - // Ensure not exactly -128 (helps with wavelet transform) - if y == -128 { - y += 1; - } - if u == -128 { - u += 1; - } - if v == -128 { - v += 1; - } - - // Store as VYU in little-endian order (matches standard VNC protocol ZYWRLE_SAVE_COEFF) - // U in byte 0, Y in byte 1, V in byte 2 - let bytes: [u8; 4] = [u as u8, y as u8, v as u8, 0]; - buf[buf_idx] = i32::from_le_bytes(bytes); - - buf_idx += 1; - } - data_idx += 4; // Skip RGBA - } - } -} - -/// Calculate aligned dimensions for wavelet transform. -/// -/// Wavelet transforms require dimensions to be multiples of 2^level. -/// This function rounds down to the nearest multiple. -/// -/// # Arguments -/// * `width` - Original width -/// * `height` - Original height -/// * `level` - Wavelet level -/// -/// # Returns -/// Tuple of (`aligned_width`, `aligned_height`) -#[inline] -fn calc_aligned_size(width: usize, height: usize, level: usize) -> (usize, usize) { - let mask = !((1 << level) - 1); - (width & mask, height & mask) -} - -/// Pack wavelet coefficients into pixel format for transmission. -/// -/// After wavelet transform, coefficients are packed in a specific order -/// (Hxy, Hy, Hx, L) for transmission via ZRLE. -/// -/// # Arguments -/// * `buf` - Coefficient buffer -/// * `dst` - Destination pixel buffer -/// * `r` - Subband number (0=L, 1=Hx, 2=Hy, 3=Hxy) -/// * `width` - Image width -/// * `height` - Image height -/// * `level` - Wavelet level -/// -/// # Performance Note -/// This function contains bounds checks in nested loops which add ~1-2% overhead. -/// The checks are necessary for safety but could be optimized with `debug_assert`! -/// and unsafe indexing if profiling shows this as a bottleneck. -fn pack_coeff(buf: &[i32], dst: &mut [u8], r: usize, width: usize, height: usize, level: usize) { - let s = 2 << level; - let mut ph_offset = 0; - - if (r & 0x01) != 0 { - ph_offset += s >> 1; - } - if (r & 0x02) != 0 { - ph_offset += (s >> 1) * width; - } - - for _ in 0..(height / s) { - for _ in 0..(width / s) { - let dst_idx = ph_offset * 4; - if ph_offset < buf.len() && dst_idx + 3 < dst.len() { - let pixel = buf[ph_offset]; - let bytes = pixel.to_le_bytes(); - // Load VYU and save as RGB (for 32bpp RGBA format) - dst[dst_idx] = bytes[2]; // V -> R - dst[dst_idx + 1] = bytes[1]; // Y -> G - dst[dst_idx + 2] = bytes[0]; // U -> B - dst[dst_idx + 3] = 0; // A - } - ph_offset += s; - } - ph_offset += (s - 1) * width; - } -} - -/// Perform ZYWRLE analysis (wavelet preprocessing for ZRLE encoding). -/// -/// This is the main entry point for ZYWRLE encoding. It: -/// 1. Calculates aligned dimensions -/// 2. Converts RGB to YUV -/// 3. Applies wavelet transform -/// 4. Packs coefficients for ZRLE encoding -/// -/// # Arguments -/// * `src` - Source RGBA pixel data -/// * `width` - Image width -/// * `height` - Image height -/// * `level` - ZYWRLE quality level (1-3, higher = more quality/less compression) -/// * `buf` - Temporary coefficient buffer (must be at least width*height i32s) -/// -/// # Returns -/// Transformed pixel data ready for ZRLE encoding, or None if dimensions too small -#[allow(clippy::uninit_vec)] // Performance optimization: all bytes written before return (see SAFETY comment) -pub fn zywrle_analyze( - src: &[u8], - width: usize, - height: usize, - level: usize, - buf: &mut [i32], -) -> Option> { - let (w, h) = calc_aligned_size(width, height, level); - if w == 0 || h == 0 { - return None; - } - - let uw = width - w; - let uh = height - h; - - // Allocate output buffer (optimized: avoid zero-initialization since we write all bytes) - let mut dst = Vec::with_capacity(width * height * 4); - unsafe { - // SAFETY: We will write to all bytes in this buffer before returning. - // The unaligned region copying writes to edges, - // and pack_coeff() writes to the aligned region. - dst.set_len(width * height * 4); - } - - // Handle unaligned pixels (copy as-is) - // Performance Note: These loops copy unaligned regions row-by-row which adds ~1-2% - // overhead. Could be optimized with bulk memcpy or SIMD, but the complexity may not - // be worth it for typical VNC usage (10-30 FPS). Profile before optimizing. - - // Right edge - if uw > 0 { - for y in 0..h { - let src_offset = (y * width + w) * 4; - let dst_offset = (y * width + w) * 4; - if src_offset + uw * 4 <= src.len() && dst_offset + uw * 4 <= dst.len() { - dst[dst_offset..dst_offset + uw * 4] - .copy_from_slice(&src[src_offset..src_offset + uw * 4]); - } - } - } - - // Bottom edge - if uh > 0 { - for y in h..(h + uh) { - let src_offset = y * width * 4; - let dst_offset = y * width * 4; - if src_offset + w * 4 <= src.len() && dst_offset + w * 4 <= dst.len() { - dst[dst_offset..dst_offset + w * 4] - .copy_from_slice(&src[src_offset..src_offset + w * 4]); - } - } - } - - // Bottom-right corner - if uw > 0 && uh > 0 { - for y in h..(h + uh) { - let src_offset = (y * width + w) * 4; - let dst_offset = (y * width + w) * 4; - if src_offset + uw * 4 <= src.len() && dst_offset + uw * 4 <= dst.len() { - dst[dst_offset..dst_offset + uw * 4] - .copy_from_slice(&src[src_offset..src_offset + uw * 4]); - } - } - } - - // RGB to YUV conversion on aligned region - rgb_to_yuv(&mut buf[0..w * h], src, w, h); - - // Wavelet transform - wavelet(&mut buf[0..w * h], w, h, level); - - // Pack coefficients - for l in 0..level { - pack_coeff(buf, &mut dst, 3, w, h, l); // Hxy - pack_coeff(buf, &mut dst, 2, w, h, l); // Hy - pack_coeff(buf, &mut dst, 1, w, h, l); // Hx - if l == level - 1 { - pack_coeff(buf, &mut dst, 0, w, h, l); // L (only at last level) - } - } - - Some(dst) -} diff --git a/src/jpeg/mod.rs b/src/jpeg/mod.rs deleted file mode 100644 index 80ad593..0000000 --- a/src/jpeg/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! JPEG encoding support for Tight encoding. -//! -//! This module provides JPEG compression functionality for VNC Tight encoding. -//! `TurboJPEG` support is optional and can be enabled with the `turbojpeg` feature. - -#[cfg(feature = "turbojpeg")] -pub mod turbojpeg; - -#[cfg(feature = "turbojpeg")] -pub use turbojpeg::TurboJpegEncoder; diff --git a/src/jpeg/turbojpeg.rs b/src/jpeg/turbojpeg.rs deleted file mode 100644 index 2812079..0000000 --- a/src/jpeg/turbojpeg.rs +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! FFI bindings to libjpeg-turbo's `TurboJPEG` API. -//! -//! This module provides a safe Rust wrapper around the `TurboJPEG` C API -//! for high-performance JPEG compression. - -use std::ffi::c_void; -use std::os::raw::{c_char, c_int, c_uchar, c_ulong}; - -// TurboJPEG pixel format constants -/// RGB pixel format (red, green, blue) -pub const TJPF_RGB: c_int = 0; -/// BGR pixel format (blue, green, red) -#[allow(dead_code)] -pub const TJPF_BGR: c_int = 1; -/// RGBX pixel format (red, green, blue, unused) -#[allow(dead_code)] -pub const TJPF_RGBX: c_int = 2; -/// BGRX pixel format (blue, green, red, unused) -#[allow(dead_code)] -pub const TJPF_BGRX: c_int = 3; -/// XBGR pixel format (unused, blue, green, red) -#[allow(dead_code)] -pub const TJPF_XBGR: c_int = 4; -/// XRGB pixel format (unused, red, green, blue) -#[allow(dead_code)] -pub const TJPF_XRGB: c_int = 5; -/// Grayscale pixel format -#[allow(dead_code)] -pub const TJPF_GRAY: c_int = 6; - -// TurboJPEG chrominance subsampling constants -/// 4:4:4 chrominance subsampling (no subsampling) -#[allow(dead_code)] -pub const TJSAMP_444: c_int = 0; -/// 4:2:2 chrominance subsampling (2x1 subsampling) -pub const TJSAMP_422: c_int = 1; -/// 4:2:0 chrominance subsampling (2x2 subsampling) -#[allow(dead_code)] -pub const TJSAMP_420: c_int = 2; -/// Grayscale (no chrominance) -#[allow(dead_code)] -pub const TJSAMP_GRAY: c_int = 3; - -// Opaque TurboJPEG handle -type TjHandle = *mut c_void; - -// External C functions from libjpeg-turbo -#[link(name = "turbojpeg")] -extern "C" { - fn tjInitCompress() -> TjHandle; - fn tjDestroy(handle: TjHandle) -> c_int; - fn tjCompress2( - handle: TjHandle, - src_buf: *const c_uchar, - width: c_int, - pitch: c_int, - height: c_int, - pixel_format: c_int, - jpeg_buf: *mut *mut c_uchar, - jpeg_size: *mut c_ulong, - jpeg_subsamp: c_int, - jpeg_qual: c_int, - flags: c_int, - ) -> c_int; - fn tjFree(buffer: *mut c_uchar); - fn tjGetErrorStr2(handle: TjHandle) -> *const c_char; -} - -/// Safe Rust wrapper for `TurboJPEG` compression. -pub struct TurboJpegEncoder { - handle: TjHandle, -} - -impl TurboJpegEncoder { - /// Creates a new `TurboJPEG` encoder. - /// - /// # Errors - /// - /// Returns an error if `TurboJPEG` initialization fails - pub fn new() -> Result { - let handle = unsafe { tjInitCompress() }; - if handle.is_null() { - return Err("Failed to initialize TurboJPEG compressor".to_string()); - } - Ok(Self { handle }) - } - - /// Compresses RGB image data to JPEG format. - /// - /// # Arguments - /// * `rgb_data` - RGB pixel data (3 bytes per pixel) - /// * `width` - Image width in pixels - /// * `height` - Image height in pixels - /// * `quality` - JPEG quality (1-100, where 100 is best quality) - /// - /// # Returns - /// - /// JPEG-compressed data as a `Vec` - /// - /// # Errors - /// - /// Returns an error if the data size is invalid or JPEG compression fails - #[allow(clippy::cast_possible_truncation)] // JPEG dimensions limited to u16 range - pub fn compress_rgb( - &mut self, - rgb_data: &[u8], - width: u16, - height: u16, - quality: u8, - ) -> Result, String> { - let expected_size = (width as usize) * (height as usize) * 3; - if rgb_data.len() != expected_size { - return Err(format!( - "Invalid RGB data size: expected {}, got {}", - expected_size, - rgb_data.len() - )); - } - - let mut jpeg_buf: *mut c_uchar = std::ptr::null_mut(); - let mut jpeg_size: c_ulong = 0; - - let result = unsafe { - tjCompress2( - self.handle, - rgb_data.as_ptr(), - c_int::from(width), - 0, // pitch = 0 means width * pixel_size - c_int::from(height), - TJPF_RGB, - &raw mut jpeg_buf, - &raw mut jpeg_size, - TJSAMP_422, // 4:2:2 subsampling for good quality/size balance - c_int::from(quality), - 0, // flags - ) - }; - - if result != 0 { - let error_msg = self.get_error_string(); - return Err(format!("TurboJPEG compression failed: {error_msg}")); - } - - if jpeg_buf.is_null() { - return Err("TurboJPEG returned null buffer".to_string()); - } - - // Copy JPEG data to Rust Vec - let jpeg_data = - unsafe { std::slice::from_raw_parts(jpeg_buf, jpeg_size as usize).to_vec() }; - - // Free TurboJPEG buffer - unsafe { - tjFree(jpeg_buf); - } - - Ok(jpeg_data) - } - - /// Gets the last error message from `TurboJPEG`. - fn get_error_string(&self) -> String { - unsafe { - let c_str = tjGetErrorStr2(self.handle); - if c_str.is_null() { - return "Unknown error".to_string(); - } - std::ffi::CStr::from_ptr(c_str) - .to_string_lossy() - .into_owned() - } - } -} - -impl Drop for TurboJpegEncoder { - fn drop(&mut self) { - unsafe { - tjDestroy(self.handle); - } - } -} - -unsafe impl Send for TurboJpegEncoder {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_encoder_creation() { - let encoder = TurboJpegEncoder::new(); - assert!(encoder.is_ok()); - } - - #[test] - fn test_compress_rgb() { - let mut encoder = TurboJpegEncoder::new().unwrap(); - - // Create a simple 2x2 red image - let rgb_data = vec![255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0]; - - let result = encoder.compress_rgb(&rgb_data, 2, 2, 90); - assert!(result.is_ok()); - - let jpeg_data = result.unwrap(); - assert!(!jpeg_data.is_empty()); - // JPEG files start with 0xFF 0xD8 - assert_eq!(jpeg_data[0], 0xFF); - assert_eq!(jpeg_data[1], 0xD8); - } -} diff --git a/src/lib.rs b/src/lib.rs index 04192a3..3e93700 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,10 +120,10 @@ pub mod server; // Internal modules mod auth; mod client; -pub mod encoding; -pub mod jpeg; mod repeater; -mod translate; + +// Re-export encodings from rfb-encodings crate +pub use rfb_encodings as encoding; // Re-exports pub use encoding::Encoding; @@ -134,7 +134,7 @@ pub use protocol::PixelFormat; pub use server::VncServer; #[cfg(feature = "turbojpeg")] -pub use jpeg::TurboJpegEncoder; +pub use encoding::jpeg::TurboJpegEncoder; /// VNC protocol version. pub const PROTOCOL_VERSION: &str = "RFB 003.008\n"; diff --git a/src/protocol.rs b/src/protocol.rs index 361aef1..30217a7 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -26,8 +26,33 @@ //! 3. **Initialization** - Exchange of framebuffer parameters and capabilities //! 4. **Normal Operation** - Ongoing message exchange for input events and screen updates -use bytes::{Buf, BufMut, BytesMut}; -use std::io; +use bytes::{BufMut, BytesMut}; + +// Re-export PixelFormat from rfb-encodings +pub use rfb_encodings::PixelFormat; + +// Re-export encoding constants from rfb-encodings +pub use rfb_encodings::{ + // Encoding types + ENCODING_CORRE, + ENCODING_HEXTILE, + ENCODING_RAW, + ENCODING_RRE, + ENCODING_TIGHT, + ENCODING_TIGHTPNG, + ENCODING_ZLIB, + ENCODING_ZLIBHEX, + ENCODING_ZRLE, + ENCODING_ZYWRLE, + // Hextile subencoding flags + HEXTILE_ANY_SUBRECTS, + HEXTILE_BACKGROUND_SPECIFIED, + HEXTILE_FOREGROUND_SPECIFIED, + HEXTILE_RAW, + HEXTILE_SUBRECTS_COLOURED, + // Tight subencoding types + TIGHT_PNG, +}; /// The RFB protocol version string advertised by the server. /// @@ -107,76 +132,24 @@ pub const SERVER_MSG_BELL: u8 = 2; pub const SERVER_MSG_SERVER_CUT_TEXT: u8 = 3; // Encoding Types - -/// Encoding type: Raw pixel data. -/// -/// The simplest encoding that sends uncompressed pixel data directly. -/// High bandwidth but universally supported. -pub const ENCODING_RAW: i32 = 0; +// +// Note: Most encoding type constants are re-exported from rfb-encodings at the top of this file. +// Only server-specific encodings and pseudo-encodings are defined here. /// Encoding type: Copy Rectangle. /// /// Instructs the client to copy a rectangular region from one location /// to another on the screen. Highly efficient for scrolling operations. +/// This is a server-side operation, not a data encoding format. pub const ENCODING_COPYRECT: i32 = 1; -/// Encoding type: Rise-and-Run-length Encoding. -/// -/// A simple compression scheme for rectangular regions. -pub const ENCODING_RRE: i32 = 2; - -/// Encoding type: Compact RRE. -/// -/// A more compact version of RRE encoding. -pub const ENCODING_CORRE: i32 = 4; - -/// Encoding type: Hextile. -/// -/// Divides rectangles into 16x16 tiles for efficient encoding. -pub const ENCODING_HEXTILE: i32 = 5; - -/// Encoding type: Zlib compressed. -/// -/// Uses zlib compression on raw pixel data. -pub const ENCODING_ZLIB: i32 = 6; - -/// Encoding type: Tight. -/// -/// A highly efficient encoding using JPEG compression for gradient content -/// and other compression methods for different types of screen content. -pub const ENCODING_TIGHT: i32 = 7; - -/// Encoding type: `TightPng`. -/// -/// Like Tight encoding but uses PNG compression instead of JPEG. -/// Provides lossless compression for high-quality image transmission. -pub const ENCODING_TIGHTPNG: i32 = -260; - -/// Encoding type: `ZlibHex`. -/// -/// Zlib-compressed Hextile encoding. Combines Hextile's tile-based encoding -/// with zlib compression for improved bandwidth efficiency. -pub const ENCODING_ZLIBHEX: i32 = 8; - /// Encoding type: Tile Run-Length Encoding. /// /// An efficient encoding for palettized and run-length compressed data. +/// Note: Not currently implemented in rfb-encodings. #[allow(dead_code)] pub const ENCODING_TRLE: i32 = 15; -/// Encoding type: Zlib compressed TRLE. -/// -/// Combines TRLE with zlib compression. -pub const ENCODING_ZRLE: i32 = 16; - -/// Encoding type: ZYWRLE (Zlib+Wavelet+Run-Length Encoding). -/// -/// Wavelet-based lossy compression for low-bandwidth scenarios. -/// Uses Piecewise-Linear Haar wavelet transform, RCT (Reversible Color Transform) -/// for RGB to YUV conversion, and non-linear quantization filtering. -/// Shares the ZRLE encoder but applies wavelet preprocessing first. -pub const ENCODING_ZYWRLE: i32 = 17; - /// Encoding type: H.264 video encoding. /// /// H.264 video compression for very low bandwidth scenarios. @@ -222,27 +195,8 @@ pub const ENCODING_COMPRESS_LEVEL_0: i32 = -256; /// for reduced bandwidth usage. pub const ENCODING_COMPRESS_LEVEL_9: i32 = -247; -// Hextile subencoding flags - -/// Hextile: Raw pixel data for this tile. -pub const HEXTILE_RAW: u8 = 1 << 0; - -/// Hextile: Background color is specified. -pub const HEXTILE_BACKGROUND_SPECIFIED: u8 = 1 << 1; - -/// Hextile: Foreground color is specified. -pub const HEXTILE_FOREGROUND_SPECIFIED: u8 = 1 << 2; - -/// Hextile: Tile contains subrectangles. -pub const HEXTILE_ANY_SUBRECTS: u8 = 1 << 3; - -/// Hextile: Subrectangles are colored (not monochrome). -pub const HEXTILE_SUBRECTS_COLOURED: u8 = 1 << 4; - -// Tight subencoding types - -/// Tight/TightPng: PNG compression subencoding. -pub const TIGHT_PNG: u8 = 0x0A; +// Note: Hextile and Tight subencoding constants are re-exported from rfb-encodings +// at the top of this file. // Security Types @@ -277,271 +231,6 @@ pub const SECURITY_RESULT_OK: u32 = 0; /// Sent by the server to indicate that authentication failed. pub const SECURITY_RESULT_FAILED: u32 = 1; -/// Represents the pixel format of the VNC framebuffer. -/// -/// This struct defines how pixel data is interpreted, including color depth, -/// endianness, and RGB component details. -#[derive(Debug, Clone)] -pub struct PixelFormat { - /// Number of bits per pixel. - pub bits_per_pixel: u8, - /// Depth of the pixel in bits. - pub depth: u8, - /// Flag indicating if the pixel data is big-endian (1) or little-endian (0). - pub big_endian_flag: u8, - /// Flag indicating if the pixel format is true-colour (1) or colormapped (0). - pub true_colour_flag: u8, - /// Maximum red color value. - pub red_max: u16, - /// Maximum green color value. - pub green_max: u16, - /// Maximum blue color value. - pub blue_max: u16, - /// Number of shifts to apply to get the red color component. - pub red_shift: u8, - /// Number of shifts to apply to get the green color component. - pub green_shift: u8, - /// Number of shifts to apply to get the blue color component. - pub blue_shift: u8, -} - -impl PixelFormat { - /// Creates a standard 32-bit RGBA pixel format. - /// - /// # Returns - /// - /// A `PixelFormat` instance configured for 32-bit RGBA. - #[must_use] - pub fn rgba32() -> Self { - Self { - bits_per_pixel: 32, - depth: 24, - big_endian_flag: 0, - true_colour_flag: 1, - red_max: 255, - green_max: 255, - blue_max: 255, - red_shift: 0, - green_shift: 8, - blue_shift: 16, - } - } - - /// Checks if this `PixelFormat` is compatible with the standard 32-bit RGBA format. - /// - /// # Returns - /// - /// `true` if the pixel format matches 32-bit RGBA, `false` otherwise. - #[must_use] - pub fn is_compatible_with_rgba32(&self) -> bool { - self.bits_per_pixel == 32 - && self.depth == 24 - && self.big_endian_flag == 0 - && self.true_colour_flag == 1 - && self.red_max == 255 - && self.green_max == 255 - && self.blue_max == 255 - && self.red_shift == 0 - && self.green_shift == 8 - && self.blue_shift == 16 - } - - /// Validates that this pixel format is supported by the server. - /// - /// Checks that the format uses valid bits-per-pixel values and is either - /// true-color or a supported color-mapped format. - /// - /// # Returns - /// - /// `true` if the format is valid and supported, `false` otherwise. - #[must_use] - pub fn is_valid(&self) -> bool { - // Check bits per pixel is valid - if self.bits_per_pixel != 8 - && self.bits_per_pixel != 16 - && self.bits_per_pixel != 24 - && self.bits_per_pixel != 32 - { - return false; - } - - // Check depth is reasonable - if self.depth == 0 || self.depth > 32 { - return false; - } - - // For non-truecolor (color-mapped), only 8bpp is supported - if self.true_colour_flag == 0 && self.bits_per_pixel != 8 { - return false; - } - - // For truecolor, validate color component ranges - if self.true_colour_flag != 0 { - // Check that max values fit in the bit depth - #[allow(clippy::cast_possible_truncation)] - // leading_zeros() returns max 32, result always fits in u8 - let bits_needed = |max: u16| -> u8 { - if max == 0 { - 0 - } else { - (16 - max.leading_zeros()) as u8 - } - }; - - let red_bits = bits_needed(self.red_max); - let green_bits = bits_needed(self.green_max); - let blue_bits = bits_needed(self.blue_max); - - // Total bits should not exceed depth - if red_bits + green_bits + blue_bits > self.depth { - return false; - } - - // Shifts should not cause overlap or exceed bit depth - if self.red_shift >= 32 || self.green_shift >= 32 || self.blue_shift >= 32 { - return false; - } - } - - true - } - - /// Creates a 16-bit RGB565 pixel format. - /// - /// RGB565 uses 5 bits for red, 6 bits for green, and 5 bits for blue. - /// This is a common format for embedded displays and bandwidth-constrained clients. - /// - /// # Returns - /// - /// A `PixelFormat` instance configured for 16-bit RGB565. - #[allow(dead_code)] - #[must_use] - pub fn rgb565() -> Self { - Self { - bits_per_pixel: 16, - depth: 16, - big_endian_flag: 0, - true_colour_flag: 1, - red_max: 31, // 5 bits - green_max: 63, // 6 bits - blue_max: 31, // 5 bits - red_shift: 11, - green_shift: 5, - blue_shift: 0, - } - } - - /// Creates a 16-bit RGB555 pixel format. - /// - /// RGB555 uses 5 bits for each of red, green, and blue, with 1 unused bit. - /// - /// # Returns - /// - /// A `PixelFormat` instance configured for 16-bit RGB555. - #[allow(dead_code)] - #[must_use] - pub fn rgb555() -> Self { - Self { - bits_per_pixel: 16, - depth: 15, - big_endian_flag: 0, - true_colour_flag: 1, - red_max: 31, // 5 bits - green_max: 31, // 5 bits - blue_max: 31, // 5 bits - red_shift: 10, - green_shift: 5, - blue_shift: 0, - } - } - - /// Creates an 8-bit BGR233 pixel format. - /// - /// BGR233 uses 2 bits for blue, 3 bits for green, and 3 bits for red. - /// This format is used for very low bandwidth connections and legacy clients. - /// - /// # Returns - /// - /// A `PixelFormat` instance configured for 8-bit BGR233. - #[allow(dead_code)] - #[must_use] - pub fn bgr233() -> Self { - Self { - bits_per_pixel: 8, - depth: 8, - big_endian_flag: 0, - true_colour_flag: 1, - red_max: 7, // 3 bits - green_max: 7, // 3 bits - blue_max: 3, // 2 bits - red_shift: 0, - green_shift: 3, - blue_shift: 6, - } - } - - /// Writes the pixel format data into a `BytesMut` buffer. - /// - /// This function serializes the `PixelFormat` into the RFB protocol format. - /// - /// # Arguments - /// - /// * `buf` - A mutable reference to the `BytesMut` buffer to write into. - pub fn write_to(&self, buf: &mut BytesMut) { - buf.put_u8(self.bits_per_pixel); - buf.put_u8(self.depth); - buf.put_u8(self.big_endian_flag); - buf.put_u8(self.true_colour_flag); - buf.put_u16(self.red_max); - buf.put_u16(self.green_max); - buf.put_u16(self.blue_max); - buf.put_u8(self.red_shift); - buf.put_u8(self.green_shift); - buf.put_u8(self.blue_shift); - buf.put_bytes(0, 3); // padding - } - - /// Reads and deserializes a `PixelFormat` from a `BytesMut` buffer. - /// - /// This function extracts pixel format information from the RFB protocol stream. - /// - /// # Arguments - /// - /// * `buf` - A mutable reference to the `BytesMut` buffer to read from. - /// - /// # Returns - /// - /// `Ok(Self)` containing the parsed `PixelFormat`. - /// - /// # Errors - /// - /// Returns `Err(io::Error)` if there are not enough bytes in the buffer - /// to read a complete `PixelFormat`. - pub fn from_bytes(buf: &mut BytesMut) -> io::Result { - if buf.len() < 16 { - return Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "Not enough bytes for PixelFormat", - )); - } - - let pf = Self { - bits_per_pixel: buf.get_u8(), - depth: buf.get_u8(), - big_endian_flag: buf.get_u8(), - true_colour_flag: buf.get_u8(), - red_max: buf.get_u16(), - green_max: buf.get_u16(), - blue_max: buf.get_u16(), - red_shift: buf.get_u8(), - green_shift: buf.get_u8(), - blue_shift: buf.get_u8(), - }; - buf.advance(3); - Ok(pf) - } -} - /// Represents the `ServerInit` message sent during VNC initialization. /// /// This message is sent by the server after security negotiation is complete. diff --git a/src/translate.rs b/src/translate.rs deleted file mode 100644 index 3fab41a..0000000 --- a/src/translate.rs +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright 2025 Dustin McAfee -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Pixel format translation between server and client formats. -//! -//! This module provides pixel format conversion to support VNC clients with different -//! color depths and pixel layouts. It implements the translation logic using direct -//! runtime conversion instead of lookup tables. -//! -//! # Supported Formats -//! -//! - **32bpp**: RGBA32, BGRA32, ARGB32, ABGR32 (various shift combinations) -//! - **16bpp**: RGB565, RGB555, BGR565, BGR555 -//! - **8bpp**: BGR233 (3-bit red, 3-bit green, 2-bit blue) -//! -//! # Performance -//! -//! This implementation uses direct pixel translation. Modern Rust's optimizer can generate -//! very efficient code for this approach, trading a small amount of CPU for significantly - -// Allow intentional casts that may truncate - this is pixel format conversion -#![allow(clippy::cast_possible_truncation)] -#![allow(clippy::match_same_arms)] -//! simpler code and lower memory usage compared to lookup table approaches. - -use crate::protocol::PixelFormat; -use bytes::BytesMut; - -/// Translates pixel data from server format (RGBA32) to client's requested format. -/// -/// # Arguments -/// -/// * `src` - Source pixel data in RGBA32 format (4 bytes per pixel) -/// * `server_format` - The server's pixel format (should be RGBA32) -/// * `client_format` - The client's requested pixel format -/// -/// # Returns -/// -/// A `BytesMut` containing the translated pixel data in the client's format. -/// -/// # Panics -/// -/// Panics if the source data length is not a multiple of 4 (invalid RGBA32 data). -pub fn translate_pixels( - src: &[u8], - server_format: &PixelFormat, - client_format: &PixelFormat, -) -> BytesMut { - // Fast path: no translation needed - if pixel_formats_equal(server_format, client_format) { - return BytesMut::from(src); - } - - assert_eq!( - src.len() % 4, - 0, - "Source data must be RGBA32 (4 bytes per pixel)" - ); - - let pixel_count = src.len() / 4; - let bytes_per_pixel = (client_format.bits_per_pixel / 8) as usize; - let mut dst = BytesMut::with_capacity(pixel_count * bytes_per_pixel); - - // Translate pixel by pixel - for i in 0..pixel_count { - let offset = i * 4; - let rgba = &src[offset..offset + 4]; - - // Extract RGB components from server format - let (r, g, b) = extract_rgb(rgba, server_format); - - // Pack into client format - pack_pixel(&mut dst, r, g, b, client_format); - } - - dst -} - -/// Extracts RGB components from a pixel in the given format. -/// -/// # Arguments -/// -/// * `pixel` - Pixel data (1-4 bytes depending on format) -/// * `format` - The pixel format describing how to interpret the data -/// -/// # Returns -/// -/// A tuple `(r, g, b)` with each component as a u8 value (0-255). -fn extract_rgb(pixel: &[u8], format: &PixelFormat) -> (u8, u8, u8) { - // Read pixel value based on bitsPerPixel - let pixel_value = match format.bits_per_pixel { - 8 => u32::from(pixel[0]), - 16 => { - if format.big_endian_flag != 0 { - u32::from(u16::from_be_bytes([pixel[0], pixel[1]])) - } else { - u32::from(u16::from_le_bytes([pixel[0], pixel[1]])) - } - } - 32 => { - if format.big_endian_flag != 0 { - u32::from_be_bytes([pixel[0], pixel[1], pixel[2], pixel[3]]) - } else { - u32::from_le_bytes([pixel[0], pixel[1], pixel[2], pixel[3]]) - } - } - 24 => { - // 24bpp is stored in 3 bytes, but we need to handle it carefully - if format.big_endian_flag != 0 { - u32::from(pixel[0]) << 16 | u32::from(pixel[1]) << 8 | u32::from(pixel[2]) - } else { - u32::from(pixel[2]) << 16 | u32::from(pixel[1]) << 8 | u32::from(pixel[0]) - } - } - _ => u32::from(pixel[0]), // Fallback for unsupported formats - }; - - // Extract color components using shifts and masks - let r_raw = (pixel_value >> format.red_shift) & u32::from(format.red_max); - let g_raw = (pixel_value >> format.green_shift) & u32::from(format.green_max); - let b_raw = (pixel_value >> format.blue_shift) & u32::from(format.blue_max); - - // Scale to 8-bit (0-255) range - let r = scale_component(r_raw, format.red_max); - let g = scale_component(g_raw, format.green_max); - let b = scale_component(b_raw, format.blue_max); - - (r, g, b) -} - -/// Packs RGB components into the client's pixel format and writes to the buffer. -/// -/// # Arguments -/// -/// * `dst` - Destination buffer to write the packed pixel -/// * `r` - Red component (0-255) -/// * `g` - Green component (0-255) -/// * `b` - Blue component (0-255) -/// * `format` - The pixel format for packing -fn pack_pixel(dst: &mut BytesMut, r: u8, g: u8, b: u8, format: &PixelFormat) { - // Scale components from 8-bit to client's color depth - let r_scaled = downscale_component(r, format.red_max); - let g_scaled = downscale_component(g, format.green_max); - let b_scaled = downscale_component(b, format.blue_max); - - // Combine components with shifts - let pixel_value = (u32::from(r_scaled) << format.red_shift) - | (u32::from(g_scaled) << format.green_shift) - | (u32::from(b_scaled) << format.blue_shift); - - // Write pixel value based on bitsPerPixel and endianness - match format.bits_per_pixel { - 8 => { - dst.extend_from_slice(&[pixel_value as u8]); - } - 16 => { - let bytes = if format.big_endian_flag != 0 { - (pixel_value as u16).to_be_bytes() - } else { - (pixel_value as u16).to_le_bytes() - }; - dst.extend_from_slice(&bytes); - } - 24 => { - // 24bpp: write 3 bytes - let bytes = if format.big_endian_flag != 0 { - [ - (pixel_value >> 16) as u8, - (pixel_value >> 8) as u8, - pixel_value as u8, - ] - } else { - [ - pixel_value as u8, - (pixel_value >> 8) as u8, - (pixel_value >> 16) as u8, - ] - }; - dst.extend_from_slice(&bytes); - } - 32 => { - let bytes = if format.big_endian_flag != 0 { - pixel_value.to_be_bytes() - } else { - pixel_value.to_le_bytes() - }; - dst.extend_from_slice(&bytes); - } - _ => { - // Unsupported format, write as 8-bit - dst.extend_from_slice(&[pixel_value as u8]); - } - } -} - -/// Scales a color component from its format-specific range to 8-bit (0-255). -/// -/// # Arguments -/// -/// * `value` - The component value in its native range (0..max) -/// * `max` - The maximum value for this component in the source format -/// -/// # Returns -/// -/// The scaled value in 0-255 range. -#[inline] -fn scale_component(value: u32, max: u16) -> u8 { - if max == 0 { - return 0; - } - if max == 255 { - return value as u8; - } - - // Scale: value * 255 / max - // Use 64-bit to avoid overflow - ((u64::from(value) * 255) / u64::from(max)) as u8 -} - -/// Downscales a color component from 8-bit (0-255) to the format-specific range. -/// -/// # Arguments -/// -/// * `value` - The component value in 0-255 range -/// * `max` - The maximum value for this component in the destination format -/// -/// # Returns -/// -/// The downscaled value in 0..max range. -#[inline] -fn downscale_component(value: u8, max: u16) -> u16 { - if max == 0 { - return 0; - } - if max == 255 { - return u16::from(value); - } - - // Downscale: value * max / 255 - // Use 32-bit to avoid overflow - ((u32::from(value) * u32::from(max)) / 255) as u16 -} - -/// Checks if two pixel formats are identical (no translation needed). -/// -/// # Arguments -/// -/// * `a` - First pixel format -/// * `b` - Second pixel format -/// -/// # Returns -/// -/// `true` if the formats are identical, `false` otherwise. -fn pixel_formats_equal(a: &PixelFormat, b: &PixelFormat) -> bool { - a.bits_per_pixel == b.bits_per_pixel - && a.depth == b.depth - && (a.big_endian_flag == b.big_endian_flag || a.bits_per_pixel == 8) - && a.true_colour_flag == b.true_colour_flag - && (!a.true_colour_flag != 0 - || (a.red_max == b.red_max - && a.green_max == b.green_max - && a.blue_max == b.blue_max - && a.red_shift == b.red_shift - && a.green_shift == b.green_shift - && a.blue_shift == b.blue_shift)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_no_translation() { - let server_format = PixelFormat::rgba32(); - let client_format = PixelFormat::rgba32(); - - let src = vec![255u8, 0, 0, 0, 0, 255, 0, 0]; // Red, Green pixels - let dst = translate_pixels(&src, &server_format, &client_format); - - assert_eq!(&src[..], &dst[..]); - } - - #[test] - fn test_rgba32_to_rgb565() { - let server_format = PixelFormat::rgba32(); - - // RGB565: 5-bit red (shift 11), 6-bit green (shift 5), 5-bit blue (shift 0) - let client_format = PixelFormat { - bits_per_pixel: 16, - depth: 16, - big_endian_flag: 0, - true_colour_flag: 1, - red_max: 31, // 5 bits - green_max: 63, // 6 bits - blue_max: 31, // 5 bits - red_shift: 11, - green_shift: 5, - blue_shift: 0, - }; - - // Pure red: R=255, G=0, B=0 in RGBA32 - let src = vec![255u8, 0, 0, 0]; - let dst = translate_pixels(&src, &server_format, &client_format); - - // In RGB565: red=(255*31/255)<<11 = 31<<11 = 0xF800 - assert_eq!(dst.len(), 2); - let value = u16::from_le_bytes([dst[0], dst[1]]); - assert_eq!(value, 0xF800); - } - - #[test] - fn test_extract_rgb_rgba32() { - let format = PixelFormat::rgba32(); - let pixel = [128u8, 64, 32, 0]; // R=128, G=64, B=32 in RGBA32 - - let (r, g, b) = extract_rgb(&pixel, &format); - assert_eq!(r, 128); - assert_eq!(g, 64); - assert_eq!(b, 32); - } - - #[test] - fn test_scale_component() { - // 5-bit (0-31) to 8-bit (0-255) - assert_eq!(scale_component(0, 31), 0); - assert_eq!(scale_component(31, 31), 255); - assert_eq!(scale_component(15, 31), 123); // 15 * 255 / 31 = 123.387... = 123 - - // Identity: 8-bit to 8-bit - assert_eq!(scale_component(128, 255), 128); - } - - #[test] - fn test_downscale_component() { - // 8-bit (0-255) to 5-bit (0-31) - assert_eq!(downscale_component(0, 31), 0); - assert_eq!(downscale_component(255, 31), 31); - assert_eq!(downscale_component(128, 31), 15); // ~half - - // Identity: 8-bit to 8-bit - assert_eq!(downscale_component(128, 255), 128); - } -}