rfb_encodings/tightpng.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 `TightPng` encoding implementation.
16//!
17//! `TightPng` encoding uses PNG compression exclusively for all rectangles.
18//! Unlike standard Tight encoding which supports multiple compression modes
19//! (solid fill, palette, zlib, JPEG), `TightPng` ONLY uses PNG mode.
20//!
21//! This design is optimized for browser-based VNC clients like noVNC,
22//! which can decode PNG data natively in hardware without needing to
23//! handle zlib decompression or palette operations.
24
25use crate::{Encoding, TIGHT_PNG};
26use bytes::{BufMut, BytesMut};
27
28/// Implements the VNC "`TightPng`" encoding (encoding -260).
29///
30/// `TightPng` sends all pixel data as PNG-compressed images, regardless of
31/// content. This differs from standard Tight encoding which uses multiple
32/// compression strategies.
33pub struct TightPngEncoding;
34
35impl Encoding for TightPngEncoding {
36 fn encode(
37 &self,
38 data: &[u8],
39 width: u16,
40 height: u16,
41 _quality: u8,
42 compression: u8,
43 ) -> BytesMut {
44 // TightPng ONLY uses PNG mode - no solid fill, no palette modes
45 // This is the key difference from standard Tight encoding
46 // Browser-based clients like noVNC expect only PNG data
47 encode_tightpng_png(data, width, height, compression)
48 }
49}
50
51/// Encode as `TightPng` using PNG compression.
52///
53/// This is the only compression mode used by `TightPng` encoding.
54#[allow(clippy::cast_possible_truncation)] // TightPng compact length encoding uses variable-length u8 packing per RFC 6143
55fn encode_tightpng_png(data: &[u8], width: u16, height: u16, compression: u8) -> BytesMut {
56 use png::{BitDepth, ColorType, Encoder};
57
58 // Convert RGBA to RGB (PNG encoder will handle this)
59 let mut rgb_data = Vec::with_capacity((width as usize) * (height as usize) * 3);
60 for chunk in data.chunks_exact(4) {
61 rgb_data.push(chunk[0]);
62 rgb_data.push(chunk[1]);
63 rgb_data.push(chunk[2]);
64 }
65
66 // Create PNG encoder
67 let mut png_data = Vec::new();
68 {
69 let mut encoder = Encoder::new(&mut png_data, u32::from(width), u32::from(height));
70 encoder.set_color(ColorType::Rgb);
71 encoder.set_depth(BitDepth::Eight);
72
73 // Map TightVNC compression level (0-9) to PNG compression (0-9 maps to Fast/Default/Best)
74 let png_compression = match compression {
75 0..=2 => png::Compression::Fast,
76 3..=6 => png::Compression::Default,
77 _ => png::Compression::Best,
78 };
79 encoder.set_compression(png_compression);
80
81 let mut writer = match encoder.write_header() {
82 Ok(w) => w,
83 #[allow(unused_variables)]
84 Err(e) => {
85 #[cfg(feature = "debug-logging")]
86 log::error!("PNG header write failed: {e}, falling back to basic encoding");
87 // Fall back to basic tight encoding
88 let mut buf = BytesMut::with_capacity(1 + data.len());
89 buf.put_u8(0x00); // Basic tight encoding, no compression
90 for chunk in data.chunks_exact(4) {
91 buf.put_u8(chunk[0]); // R
92 buf.put_u8(chunk[1]); // G
93 buf.put_u8(chunk[2]); // B
94 buf.put_u8(0); // Padding
95 }
96 return buf;
97 }
98 };
99
100 #[allow(unused_variables)]
101 if let Err(e) = writer.write_image_data(&rgb_data) {
102 #[cfg(feature = "debug-logging")]
103 log::error!("PNG data write failed: {e}, falling back to basic encoding");
104 // Fall back to basic tight encoding
105 let mut buf = BytesMut::with_capacity(1 + data.len());
106 buf.put_u8(0x00); // Basic tight encoding, no compression
107 for chunk in data.chunks_exact(4) {
108 buf.put_u8(chunk[0]); // R
109 buf.put_u8(chunk[1]); // G
110 buf.put_u8(chunk[2]); // B
111 buf.put_u8(0); // Padding
112 }
113 return buf;
114 }
115 }
116
117 let mut buf = BytesMut::new();
118 buf.put_u8(TIGHT_PNG << 4); // PNG subencoding
119
120 // Compact length
121 let len = png_data.len();
122 if len < 128 {
123 buf.put_u8(len as u8);
124 } else if len < 16384 {
125 buf.put_u8(((len & 0x7F) | 0x80) as u8);
126 buf.put_u8((len >> 7) as u8);
127 } else {
128 buf.put_u8(((len & 0x7F) | 0x80) as u8);
129 buf.put_u8((((len >> 7) & 0x7F) | 0x80) as u8);
130 buf.put_u8((len >> 14) as u8);
131 }
132
133 buf.put_slice(&png_data);
134 buf
135}