rfb_encodings/
hextile.rs

1// Copyright 2025 Dustin McAfee
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! VNC Hextile encoding implementation.
16//!
17//! Hextile divides the rectangle into 16x16 tiles and encodes each independently.
18//! Each tile can be: raw, solid, monochrome with subrects, or colored with subrects.
19
20use super::common::{
21    analyze_tile_colors, extract_tile, find_subrects, put_pixel32, rgba_to_rgb24_pixels,
22};
23use crate::{
24    Encoding, HEXTILE_ANY_SUBRECTS, HEXTILE_BACKGROUND_SPECIFIED, HEXTILE_FOREGROUND_SPECIFIED,
25    HEXTILE_RAW, HEXTILE_SUBRECTS_COLOURED,
26};
27use bytes::{BufMut, BytesMut};
28
29/// Implements the VNC "Hextile" encoding.
30///
31/// Hextile divides the rectangle into 16x16 tiles and encodes each independently.
32/// Each tile can be: raw, solid, monochrome with subrects, or colored with subrects.
33pub struct HextileEncoding;
34
35impl Encoding for HextileEncoding {
36    #[allow(clippy::similar_names)] // last_bg and last_fg are standard VNC Hextile terminology
37    #[allow(clippy::cast_possible_truncation)] // Hextile protocol requires packing coordinates into u8 (max 16x16 tiles)
38    fn encode(
39        &self,
40        data: &[u8],
41        width: u16,
42        height: u16,
43        _quality: u8,
44        _compression: u8,
45    ) -> BytesMut {
46        let mut buf = BytesMut::new();
47        let pixels = rgba_to_rgb24_pixels(data);
48
49        let mut last_bg: Option<u32> = None;
50        let mut last_fg: Option<u32> = None;
51
52        // Process tiles (16x16)
53        for tile_y in (0..height).step_by(16) {
54            for tile_x in (0..width).step_by(16) {
55                let tile_w = std::cmp::min(16, width - tile_x);
56                let tile_h = std::cmp::min(16, height - tile_y);
57
58                // Extract tile data
59                let tile_pixels = extract_tile(
60                    &pixels,
61                    width as usize,
62                    tile_x as usize,
63                    tile_y as usize,
64                    tile_w as usize,
65                    tile_h as usize,
66                );
67
68                // Analyze tile colors
69                let (is_solid, is_mono, bg, fg) = analyze_tile_colors(&tile_pixels);
70
71                let mut subencoding: u8 = 0;
72                let tile_start = buf.len();
73
74                // Reserve space for subencoding byte
75                buf.put_u8(0);
76
77                if is_solid {
78                    // Solid tile - just update background if needed
79                    if Some(bg) != last_bg {
80                        subencoding |= HEXTILE_BACKGROUND_SPECIFIED;
81                        put_pixel32(&mut buf, bg);
82                        last_bg = Some(bg);
83                    }
84                } else {
85                    // Find subrectangles
86                    let subrects =
87                        find_subrects(&tile_pixels, tile_w as usize, tile_h as usize, bg);
88
89                    // Check if raw would be smaller OR if too many subrects (>255 max for u8)
90                    let raw_size = tile_w as usize * tile_h as usize * 4; // 4 bytes per pixel for 32bpp
91                                                                          // Estimate overhead: bg (if different) + fg (if mono and different) + count byte
92                    let bg_overhead = if Some(bg) == last_bg { 0 } else { 4 };
93                    let fg_overhead = if is_mono && Some(fg) != last_fg { 4 } else { 0 };
94                    let subrect_data = subrects.len() * if is_mono { 2 } else { 6 };
95                    let encoded_size = bg_overhead + fg_overhead + 1 + subrect_data;
96
97                    if subrects.is_empty() || subrects.len() > 255 || encoded_size > raw_size {
98                        // Use raw encoding for this tile
99                        subencoding = HEXTILE_RAW;
100                        buf.truncate(tile_start);
101                        buf.put_u8(subencoding);
102
103                        for pixel in &tile_pixels {
104                            put_pixel32(&mut buf, *pixel);
105                        }
106
107                        last_bg = None;
108                        last_fg = None;
109                        continue;
110                    }
111
112                    // Update background
113                    if Some(bg) != last_bg {
114                        subencoding |= HEXTILE_BACKGROUND_SPECIFIED;
115                        put_pixel32(&mut buf, bg);
116                        last_bg = Some(bg);
117                    }
118
119                    // We have subrectangles
120                    subencoding |= HEXTILE_ANY_SUBRECTS;
121
122                    if is_mono {
123                        // Monochrome tile
124                        if Some(fg) != last_fg {
125                            subencoding |= HEXTILE_FOREGROUND_SPECIFIED;
126                            put_pixel32(&mut buf, fg);
127                            last_fg = Some(fg);
128                        }
129
130                        // Write number of subrects
131                        buf.put_u8(subrects.len() as u8);
132
133                        // Write subrects (without color)
134                        for sr in subrects {
135                            buf.put_u8(((sr.x as u8) << 4) | (sr.y as u8));
136                            buf.put_u8((((sr.w - 1) as u8) << 4) | ((sr.h - 1) as u8));
137                        }
138                    } else {
139                        // Colored subrects
140                        subencoding |= HEXTILE_SUBRECTS_COLOURED;
141                        last_fg = None;
142
143                        buf.put_u8(subrects.len() as u8);
144
145                        for sr in subrects {
146                            put_pixel32(&mut buf, sr.color); // 4 bytes for 32bpp
147                            buf.put_u8(((sr.x as u8) << 4) | (sr.y as u8)); // packed X,Y
148                            buf.put_u8((((sr.w - 1) as u8) << 4) | ((sr.h - 1) as u8));
149                            // packed W-1,H-1
150                        }
151                    }
152                }
153
154                // Write subencoding byte
155                buf[tile_start] = subencoding;
156            }
157        }
158
159        buf
160    }
161}