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}