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}