rfb_encodings/
tight.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 Tight encoding implementation - RFC 6143 compliant with full optimization
16//!
17//! # Architecture
18//!
19//! This implementation has TWO layers for optimal compression:
20//!
21//! ## Layer 1: High-Level Optimization
22//! - Rectangle splitting and subdivision
23//! - Solid area detection and extraction
24//! - Recursive optimization for best encoding
25//! - Size limit enforcement (`TIGHT_MAX_RECT_SIZE`, `TIGHT_MAX_RECT_WIDTH`)
26//!
27//! ## Layer 2: Low-Level Encoding
28//! - Palette analysis
29//! - Encoding mode selection (solid/mono/indexed/full-color/JPEG)
30//! - Compression and wire format generation
31//!
32//! # Protocol Overview
33//!
34//! Tight encoding supports 5 compression modes:
35//!
36//! 1. **Solid fill** (1 color) - control byte 0x80
37//!    - Wire format: `[0x80][R][G][B]` (4 bytes total)
38//!    - Most efficient for solid color rectangles
39//!
40//! 2. **Mono rect** (2 colors) - control byte 0x50 or 0xA0
41//!    - Wire format: `[control][0x01][1][bg RGB24][fg RGB24][length][bitmap]`
42//!    - Uses 1-bit bitmap: 0=background, 1=foreground
43//!    - MSB first, each row byte-aligned
44//!
45//! 3. **Indexed palette** (3-16 colors) - control byte 0x60 or 0xA0
46//!    - Wire format: `[control][0x01][n-1][colors...][length][indices]`
47//!    - Each pixel encoded as palette index (1 byte)
48//!
49//! 4. **Full-color zlib** - control byte 0x00 or 0xA0
50//!    - Wire format: `[control][length][zlib compressed RGB24]`
51//!    - Lossless compression for truecolor images
52//!
53//! 5. **JPEG** - control byte 0x90
54//!    - Wire format: `[0x90][length][JPEG data]`
55//!    - Lossy compression for photographic content
56//!
57//! # Configuration Constants
58//!
59//! ```text
60//! TIGHT_MIN_TO_COMPRESS = 12      (data < 12 bytes sent raw)
61//! MIN_SPLIT_RECT_SIZE = 4096      (split rectangles >= 4096 pixels)
62//! MIN_SOLID_SUBRECT_SIZE = 2048   (solid areas must be >= 2048 pixels)
63//! MAX_SPLIT_TILE_SIZE = 16        (tile size for solid detection)
64//! TIGHT_MAX_RECT_SIZE = 65536     (max pixels per rectangle)
65//! TIGHT_MAX_RECT_WIDTH = 2048     (max rectangle width)
66//! ```
67
68use super::common::translate_pixel_to_client_format;
69use crate::{Encoding, PixelFormat};
70use bytes::{BufMut, BytesMut};
71use std::collections::HashMap;
72
73// Tight encoding protocol constants (RFC 6143 section 7.7.4)
74const TIGHT_EXPLICIT_FILTER: u8 = 0x04;
75const TIGHT_FILL: u8 = 0x08;
76#[allow(dead_code)]
77const TIGHT_JPEG: u8 = 0x09;
78const TIGHT_NO_ZLIB: u8 = 0x0A;
79
80// Filter types
81const TIGHT_FILTER_PALETTE: u8 = 0x01;
82
83/// Zlib stream ID for full-color data (RFC 6143 section 7.7.4)
84pub const STREAM_ID_FULL_COLOR: u8 = 0;
85/// Zlib stream ID for monochrome bitmap data (RFC 6143 section 7.7.4)
86pub const STREAM_ID_MONO: u8 = 1;
87/// Zlib stream ID for indexed palette data (RFC 6143 section 7.7.4)
88pub const STREAM_ID_INDEXED: u8 = 2;
89
90// Compression thresholds for Tight encoding optimization
91const TIGHT_MIN_TO_COMPRESS: usize = 12;
92const MIN_SPLIT_RECT_SIZE: usize = 4096;
93const MIN_SOLID_SUBRECT_SIZE: usize = 2048;
94const MAX_SPLIT_TILE_SIZE: u16 = 16;
95const TIGHT_MAX_RECT_SIZE: usize = 65536;
96const TIGHT_MAX_RECT_WIDTH: u16 = 2048;
97
98/// Compression configuration for different quality levels
99struct TightConf {
100    mono_min_rect_size: usize,
101    idx_zlib_level: u8,
102    mono_zlib_level: u8,
103    raw_zlib_level: u8,
104}
105
106const TIGHT_CONF: [TightConf; 4] = [
107    TightConf {
108        mono_min_rect_size: 6,
109        idx_zlib_level: 0,
110        mono_zlib_level: 0,
111        raw_zlib_level: 0,
112    }, // Level 0
113    TightConf {
114        mono_min_rect_size: 32,
115        idx_zlib_level: 1,
116        mono_zlib_level: 1,
117        raw_zlib_level: 1,
118    }, // Level 1
119    TightConf {
120        mono_min_rect_size: 32,
121        idx_zlib_level: 3,
122        mono_zlib_level: 3,
123        raw_zlib_level: 2,
124    }, // Level 2
125    TightConf {
126        mono_min_rect_size: 32,
127        idx_zlib_level: 7,
128        mono_zlib_level: 7,
129        raw_zlib_level: 5,
130    }, // Level 9
131];
132
133/// Rectangle to encode
134#[derive(Debug, Clone)]
135struct Rect {
136    x: u16,
137    y: u16,
138    w: u16,
139    h: u16,
140}
141
142/// Result of encoding a rectangle
143struct EncodeResult {
144    rectangles: Vec<(Rect, BytesMut)>,
145}
146
147/// Implements the VNC "Tight" encoding (RFC 6143 section 7.7.4).
148pub struct TightEncoding;
149
150impl Encoding for TightEncoding {
151    fn encode(
152        &self,
153        data: &[u8],
154        width: u16,
155        height: u16,
156        quality: u8,
157        compression: u8,
158    ) -> BytesMut {
159        // Simple wrapper - for full optimization, use encode_rect_optimized
160        // Default to RGBA32 format for backward compatibility (old API doesn't have client format)
161        // Create a temporary compressor for this call (old API doesn't have persistent streams)
162        let mut compressor = SimpleTightCompressor::new(compression);
163
164        let rect = Rect {
165            x: 0,
166            y: 0,
167            w: width,
168            h: height,
169        };
170        let default_format = PixelFormat::rgba32();
171        let result = encode_rect_optimized(
172            data,
173            width,
174            &rect,
175            quality,
176            compression,
177            &default_format,
178            &mut compressor,
179        );
180
181        // Concatenate all rectangles
182        let mut output = BytesMut::new();
183        for (_rect, buf) in result.rectangles {
184            output.extend_from_slice(&buf);
185        }
186        output
187    }
188}
189
190/// High-level optimization: split rectangles and find solid areas
191/// Implements Tight encoding optimization as specified in RFC 6143
192#[allow(clippy::similar_names)] // dx_end and dy_end are clear in context (delta x/y end coordinates)
193#[allow(clippy::too_many_lines)] // Complex algorithm implementing RFC 6143 Tight encoding optimization
194#[allow(clippy::cast_possible_truncation)] // Rectangle dimensions limited to u16 per VNC protocol
195fn encode_rect_optimized<C: TightStreamCompressor>(
196    framebuffer: &[u8],
197    fb_width: u16,
198    rect: &Rect,
199    quality: u8,
200    compression: u8,
201    client_format: &PixelFormat,
202    compressor: &mut C,
203) -> EncodeResult {
204    #[cfg(feature = "debug-logging")]
205    log::info!("DEBUG: encode_rect_optimized called: rect={}x{} at ({}, {}), quality={}, compression={}, bpp={}",
206        rect.w, rect.h, rect.x, rect.y, quality, compression, client_format.bits_per_pixel);
207
208    let mut rectangles = Vec::new();
209
210    // Normalize compression level based on quality settings
211    let compression = normalize_compression_level(compression, quality);
212
213    #[cfg(feature = "debug-logging")]
214    log::info!("DEBUG: normalized compression={compression}");
215
216    // Check if optimization should be applied
217    let rect_size = rect.w as usize * rect.h as usize;
218
219    #[cfg(feature = "debug-logging")]
220    log::info!("DEBUG: rect_size={rect_size}, MIN_SPLIT_RECT_SIZE={MIN_SPLIT_RECT_SIZE}");
221
222    if rect_size < MIN_SPLIT_RECT_SIZE {
223        #[cfg(feature = "debug-logging")]
224        log::info!("DEBUG: Rectangle too small for optimization");
225
226        // Too small for optimization - but still check if it needs splitting due to size limits
227        if rect.w > TIGHT_MAX_RECT_WIDTH
228            || ((rect.w as usize) * (rect.h as usize)) > TIGHT_MAX_RECT_SIZE
229        {
230            #[cfg(feature = "debug-logging")]
231            log::info!("DEBUG: But rectangle needs splitting - calling encode_large_rect");
232
233            // Too large - split it
234            rectangles.extend(encode_large_rect(
235                framebuffer,
236                fb_width,
237                rect,
238                quality,
239                compression,
240                client_format,
241                compressor,
242            ));
243        } else {
244            #[cfg(feature = "debug-logging")]
245            log::info!("DEBUG: Rectangle small enough - encode directly");
246
247            // Small enough - encode directly
248            let buf = encode_subrect_single(
249                framebuffer,
250                fb_width,
251                rect,
252                quality,
253                compression,
254                client_format,
255                compressor,
256            );
257            rectangles.push((rect.clone(), buf));
258        }
259
260        #[cfg(feature = "debug-logging")]
261        log::info!(
262            "DEBUG: encode_rect_optimized returning {} rectangles (early return)",
263            rectangles.len()
264        );
265
266        return EncodeResult { rectangles };
267    }
268
269    #[cfg(feature = "debug-logging")]
270    log::info!("DEBUG: Rectangle large enough for optimization - continuing");
271
272    // Calculate maximum rows per rectangle
273    let n_max_width = rect.w.min(TIGHT_MAX_RECT_WIDTH);
274    let n_max_rows = (TIGHT_MAX_RECT_SIZE / n_max_width as usize) as u16;
275
276    // Try to find large solid-color areas for optimization
277    // Track the current scan position and base position (like C code's y and h)
278    let mut current_y = rect.y;
279    let mut base_y = rect.y; // Corresponds to C code's 'y' variable
280    let mut remaining_h = rect.h; // Corresponds to C code's 'h' variable
281
282    #[cfg(feature = "debug-logging")]
283    log::info!(
284        "DEBUG: Starting optimization loop, rect.y={}, rect.h={}",
285        rect.y,
286        rect.h
287    );
288
289    while current_y < base_y + remaining_h {
290        #[cfg(feature = "debug-logging")]
291        log::info!("DEBUG: Loop iteration: current_y={current_y}, base_y={base_y}, remaining_h={remaining_h}");
292        // Check if rectangle becomes too large (like C code: if (dy - y >= nMaxRows))
293        if (current_y - base_y) >= n_max_rows {
294            let chunk_rect = Rect {
295                x: rect.x,
296                y: base_y, // Send from base_y, not from calculated position
297                w: rect.w,
298                h: n_max_rows,
299            };
300            // Chunk might still be too wide - check and split if needed
301            if chunk_rect.w > TIGHT_MAX_RECT_WIDTH {
302                rectangles.extend(encode_large_rect(
303                    framebuffer,
304                    fb_width,
305                    &chunk_rect,
306                    quality,
307                    compression,
308                    client_format,
309                    compressor,
310                ));
311            } else {
312                let buf = encode_subrect_single(
313                    framebuffer,
314                    fb_width,
315                    &chunk_rect,
316                    quality,
317                    compression,
318                    client_format,
319                    compressor,
320                );
321                rectangles.push((chunk_rect, buf));
322            }
323            // Like C code: y += nMaxRows; h -= nMaxRows;
324            base_y += n_max_rows;
325            remaining_h -= n_max_rows;
326        }
327
328        let dy_end = (current_y + MAX_SPLIT_TILE_SIZE).min(base_y + remaining_h);
329        let dh = dy_end - current_y;
330
331        // Safety check: if dh is 0, we've reached the end
332        if dh == 0 {
333            break;
334        }
335
336        let mut current_x = rect.x;
337        while current_x < rect.x + rect.w {
338            let dx_end = (current_x + MAX_SPLIT_TILE_SIZE).min(rect.x + rect.w);
339            let dw = dx_end - current_x;
340
341            // Safety check: if dw is 0, we've reached the end
342            if dw == 0 {
343                break;
344            }
345
346            // Check if tile is solid
347            if let Some(color_value) =
348                check_solid_tile(framebuffer, fb_width, current_x, current_y, dw, dh, None)
349            {
350                // Find best solid area
351                let (w_best, h_best) = find_best_solid_area(
352                    framebuffer,
353                    fb_width,
354                    current_x,
355                    current_y,
356                    rect.w - (current_x - rect.x),
357                    remaining_h - (current_y - base_y),
358                    color_value,
359                );
360
361                // Check if solid area is large enough
362                if w_best * h_best != rect.w * remaining_h
363                    && (w_best as usize * h_best as usize) < MIN_SOLID_SUBRECT_SIZE
364                {
365                    current_x += dw;
366                    continue;
367                }
368
369                // Extend solid area (use base_y instead of rect.y for coordinates)
370                let (x_best, y_best, w_best, h_best) = extend_solid_area(
371                    framebuffer,
372                    fb_width,
373                    rect.x,
374                    base_y,
375                    rect.w,
376                    remaining_h,
377                    color_value,
378                    current_x,
379                    current_y,
380                    w_best,
381                    h_best,
382                );
383
384                // Send rectangles before solid area
385                if y_best != base_y {
386                    let top_rect = Rect {
387                        x: rect.x,
388                        y: base_y,
389                        w: rect.w,
390                        h: y_best - base_y,
391                    };
392                    // top_rect might be too wide - check and split if needed
393                    if top_rect.w > TIGHT_MAX_RECT_WIDTH
394                        || ((top_rect.w as usize) * (top_rect.h as usize)) > TIGHT_MAX_RECT_SIZE
395                    {
396                        rectangles.extend(encode_large_rect(
397                            framebuffer,
398                            fb_width,
399                            &top_rect,
400                            quality,
401                            compression,
402                            client_format,
403                            compressor,
404                        ));
405                    } else {
406                        let buf = encode_subrect_single(
407                            framebuffer,
408                            fb_width,
409                            &top_rect,
410                            quality,
411                            compression,
412                            client_format,
413                            compressor,
414                        );
415                        rectangles.push((top_rect, buf));
416                    }
417                }
418
419                if x_best != rect.x {
420                    let left_rect = Rect {
421                        x: rect.x,
422                        y: y_best,
423                        w: x_best - rect.x,
424                        h: h_best,
425                    };
426                    // Don't recursively optimize - just check size and encode
427                    if left_rect.w > TIGHT_MAX_RECT_WIDTH
428                        || ((left_rect.w as usize) * (left_rect.h as usize)) > TIGHT_MAX_RECT_SIZE
429                    {
430                        rectangles.extend(encode_large_rect(
431                            framebuffer,
432                            fb_width,
433                            &left_rect,
434                            quality,
435                            compression,
436                            client_format,
437                            compressor,
438                        ));
439                    } else {
440                        let buf = encode_subrect_single(
441                            framebuffer,
442                            fb_width,
443                            &left_rect,
444                            quality,
445                            compression,
446                            client_format,
447                            compressor,
448                        );
449                        rectangles.push((left_rect, buf));
450                    }
451                }
452
453                // Send solid rectangle
454                let solid_rect = Rect {
455                    x: x_best,
456                    y: y_best,
457                    w: w_best,
458                    h: h_best,
459                };
460                let buf = encode_solid_rect(color_value, client_format);
461                rectangles.push((solid_rect, buf));
462
463                // Send remaining rectangles
464                if x_best + w_best != rect.x + rect.w {
465                    let right_rect = Rect {
466                        x: x_best + w_best,
467                        y: y_best,
468                        w: rect.w - (x_best - rect.x) - w_best,
469                        h: h_best,
470                    };
471                    // Don't recursively optimize - just check size and encode
472                    if right_rect.w > TIGHT_MAX_RECT_WIDTH
473                        || ((right_rect.w as usize) * (right_rect.h as usize)) > TIGHT_MAX_RECT_SIZE
474                    {
475                        rectangles.extend(encode_large_rect(
476                            framebuffer,
477                            fb_width,
478                            &right_rect,
479                            quality,
480                            compression,
481                            client_format,
482                            compressor,
483                        ));
484                    } else {
485                        let buf = encode_subrect_single(
486                            framebuffer,
487                            fb_width,
488                            &right_rect,
489                            quality,
490                            compression,
491                            client_format,
492                            compressor,
493                        );
494                        rectangles.push((right_rect, buf));
495                    }
496                }
497
498                if y_best + h_best != base_y + remaining_h {
499                    let bottom_rect = Rect {
500                        x: rect.x,
501                        y: y_best + h_best,
502                        w: rect.w,
503                        h: remaining_h - (y_best - base_y) - h_best,
504                    };
505                    // Don't recursively optimize - just check size and encode
506                    if bottom_rect.w > TIGHT_MAX_RECT_WIDTH
507                        || ((bottom_rect.w as usize) * (bottom_rect.h as usize))
508                            > TIGHT_MAX_RECT_SIZE
509                    {
510                        rectangles.extend(encode_large_rect(
511                            framebuffer,
512                            fb_width,
513                            &bottom_rect,
514                            quality,
515                            compression,
516                            client_format,
517                            compressor,
518                        ));
519                    } else {
520                        let buf = encode_subrect_single(
521                            framebuffer,
522                            fb_width,
523                            &bottom_rect,
524                            quality,
525                            compression,
526                            client_format,
527                            compressor,
528                        );
529                        rectangles.push((bottom_rect, buf));
530                    }
531                }
532
533                return EncodeResult { rectangles };
534            }
535
536            current_x += dw;
537        }
538
539        #[cfg(feature = "debug-logging")]
540        log::info!("DEBUG: End of inner loop, incrementing current_y by dh={dh}");
541
542        current_y += dh;
543
544        #[cfg(feature = "debug-logging")]
545        log::info!("DEBUG: After increment: current_y={current_y}");
546    }
547
548    #[cfg(feature = "debug-logging")]
549    log::info!("DEBUG: Exited optimization loop, no solid areas found");
550
551    // No solid areas found - encode normally (but check if it needs splitting)
552    if rect.w > TIGHT_MAX_RECT_WIDTH
553        || ((rect.w as usize) * (rect.h as usize)) > TIGHT_MAX_RECT_SIZE
554    {
555        #[cfg(feature = "debug-logging")]
556        log::info!("DEBUG: Rectangle needs splitting, calling encode_large_rect");
557
558        rectangles.extend(encode_large_rect(
559            framebuffer,
560            fb_width,
561            rect,
562            quality,
563            compression,
564            client_format,
565            compressor,
566        ));
567    } else {
568        #[cfg(feature = "debug-logging")]
569        log::info!("DEBUG: Rectangle small enough, encoding directly");
570
571        let buf = encode_subrect_single(
572            framebuffer,
573            fb_width,
574            rect,
575            quality,
576            compression,
577            client_format,
578            compressor,
579        );
580        rectangles.push((rect.clone(), buf));
581    }
582
583    #[cfg(feature = "debug-logging")]
584    log::info!(
585        "DEBUG: encode_rect_optimized returning {} rectangles (normal return)",
586        rectangles.len()
587    );
588
589    EncodeResult { rectangles }
590}
591
592/// Normalize compression level based on JPEG quality
593/// Maps compression level 0-9 to internal configuration indices
594fn normalize_compression_level(compression: u8, quality: u8) -> u8 {
595    let mut level = compression;
596
597    // JPEG enabled (quality < 10): enforce minimum level 1, maximum level 2
598    // This ensures better compression performance with JPEG
599    if quality < 10 {
600        level = level.clamp(1, 2);
601    }
602    // JPEG disabled (quality >= 10): cap at level 1
603    else if level > 1 {
604        level = 1;
605    }
606
607    // Map level 9 to 3 for backward compatibility (low-bandwidth mode)
608    if level == 9 {
609        level = 3;
610    }
611
612    level
613}
614
615/// Low-level encoding: analyze and encode a single subrectangle
616/// Analyzes palette and selects optimal encoding mode
617/// Never splits - assumes rectangle is within size limits
618fn encode_subrect_single<C: TightStreamCompressor>(
619    framebuffer: &[u8],
620    fb_width: u16,
621    rect: &Rect,
622    quality: u8,
623    compression: u8,
624    client_format: &PixelFormat,
625    compressor: &mut C,
626) -> BytesMut {
627    // This function assumes rect is within size limits (called from encode_large_rect or for small rects)
628
629    // Extract pixel data for this rectangle
630    let pixels = extract_rect_rgba(framebuffer, fb_width, rect);
631
632    // Analyze palette
633    let palette = analyze_palette(&pixels, rect.w as usize * rect.h as usize, compression);
634
635    // Route to appropriate encoder based on palette
636    match palette.num_colors {
637        0 => {
638            // Truecolor - use JPEG or full-color
639            if quality < 10 {
640                // Convert VNC quality (0-9, lower is better) to JPEG quality (0-100, higher is better)
641                let jpeg_quality = 95_u8.saturating_sub(quality * 7);
642                encode_jpeg_rect(&pixels, rect.w, rect.h, jpeg_quality, compressor)
643            } else {
644                encode_full_color_rect(&pixels, rect.w, rect.h, compression, compressor)
645            }
646        }
647        1 => {
648            // Solid color
649            encode_solid_rect(palette.colors[0], client_format)
650        }
651        2 => {
652            // Mono rect (2 colors)
653            encode_mono_rect(
654                &pixels,
655                rect.w,
656                rect.h,
657                palette.colors[0],
658                palette.colors[1],
659                compression,
660                client_format,
661                compressor,
662            )
663        }
664        _ => {
665            // Indexed palette (3-16 colors)
666            encode_indexed_rect(
667                &pixels,
668                rect.w,
669                rect.h,
670                &palette.colors[..palette.num_colors],
671                compression,
672                client_format,
673                compressor,
674            )
675        }
676    }
677}
678
679/// Encode large rectangle by splitting it into smaller tiles
680/// Returns a vector of individual rectangles with their encoded data
681#[allow(clippy::cast_possible_truncation)] // Tight max rect size divided by width always fits in u16
682fn encode_large_rect<C: TightStreamCompressor>(
683    framebuffer: &[u8],
684    fb_width: u16,
685    rect: &Rect,
686    quality: u8,
687    compression: u8,
688    client_format: &PixelFormat,
689    compressor: &mut C,
690) -> Vec<(Rect, BytesMut)> {
691    let subrect_max_width = rect.w.min(TIGHT_MAX_RECT_WIDTH);
692    let subrect_max_height = (TIGHT_MAX_RECT_SIZE / subrect_max_width as usize) as u16;
693
694    let mut rectangles = Vec::new();
695
696    let mut dy = 0;
697    while dy < rect.h {
698        let mut dx = 0;
699        while dx < rect.w {
700            let rw = (rect.w - dx).min(TIGHT_MAX_RECT_WIDTH);
701            let rh = (rect.h - dy).min(subrect_max_height);
702
703            let sub_rect = Rect {
704                x: rect.x + dx,
705                y: rect.y + dy,
706                w: rw,
707                h: rh,
708            };
709
710            // Encode this sub-rectangle (recursive call, but sub_rect is guaranteed to be small enough)
711            let buf = encode_subrect_single(
712                framebuffer,
713                fb_width,
714                &sub_rect,
715                quality,
716                compression,
717                client_format,
718                compressor,
719            );
720            rectangles.push((sub_rect, buf));
721
722            dx += TIGHT_MAX_RECT_WIDTH;
723        }
724        dy += subrect_max_height;
725    }
726
727    rectangles
728}
729
730/// Check if a tile is all the same color
731/// Used for solid area detection optimization
732fn check_solid_tile(
733    framebuffer: &[u8],
734    fb_width: u16,
735    x: u16,
736    y: u16,
737    w: u16,
738    h: u16,
739    need_same_color: Option<u32>,
740) -> Option<u32> {
741    let offset = (y as usize * fb_width as usize + x as usize) * 4;
742
743    // Get first pixel color (RGB24)
744    let fb_r = framebuffer[offset];
745    let fb_g = framebuffer[offset + 1];
746    let fb_b = framebuffer[offset + 2];
747    let first_color = rgba_to_rgb24(fb_r, fb_g, fb_b);
748
749    #[cfg(feature = "debug-logging")]
750    if x == 0 && y == 0 {
751        // Log first pixel of each solid tile
752        log::info!("check_solid_tile: fb[{}]=[{:02x},{:02x},{:02x},{:02x}] -> R={:02x} G={:02x} B={:02x} color=0x{:06x}",
753            offset, framebuffer[offset], framebuffer[offset+1], framebuffer[offset+2], framebuffer[offset+3],
754            fb_r, fb_g, fb_b, first_color);
755    }
756
757    // Check if we need a specific color
758    if let Some(required) = need_same_color {
759        if first_color != required {
760            return None;
761        }
762    }
763
764    // Check all pixels
765    for dy in 0..h {
766        let row_offset = ((y + dy) as usize * fb_width as usize + x as usize) * 4;
767        for dx in 0..w {
768            let pix_offset = row_offset + dx as usize * 4;
769            let color = rgba_to_rgb24(
770                framebuffer[pix_offset],
771                framebuffer[pix_offset + 1],
772                framebuffer[pix_offset + 2],
773            );
774            if color != first_color {
775                return None;
776            }
777        }
778    }
779
780    Some(first_color)
781}
782
783/// Find best solid area dimensions
784/// Determines optimal size for solid color subrectangle
785fn find_best_solid_area(
786    framebuffer: &[u8],
787    fb_width: u16,
788    x: u16,
789    y: u16,
790    w: u16,
791    h: u16,
792    color_value: u32,
793) -> (u16, u16) {
794    let mut w_best = 0;
795    let mut h_best = 0;
796    let mut w_prev = w;
797
798    let mut dy = 0;
799    while dy < h {
800        let dh = (h - dy).min(MAX_SPLIT_TILE_SIZE);
801        let dw = w_prev.min(MAX_SPLIT_TILE_SIZE);
802
803        if check_solid_tile(framebuffer, fb_width, x, y + dy, dw, dh, Some(color_value)).is_none() {
804            break;
805        }
806
807        let mut dx = dw;
808        while dx < w_prev {
809            let dw_check = (w_prev - dx).min(MAX_SPLIT_TILE_SIZE);
810            if check_solid_tile(
811                framebuffer,
812                fb_width,
813                x + dx,
814                y + dy,
815                dw_check,
816                dh,
817                Some(color_value),
818            )
819            .is_none()
820            {
821                break;
822            }
823            dx += dw_check;
824        }
825
826        w_prev = dx;
827        if (w_prev as usize * (dy + dh) as usize) > (w_best as usize * h_best as usize) {
828            w_best = w_prev;
829            h_best = dy + dh;
830        }
831
832        dy += dh;
833    }
834
835    (w_best, h_best)
836}
837
838/// Extend solid area to maximum size
839/// Expands solid region in all directions
840#[allow(clippy::too_many_arguments)] // Tight encoding algorithm requires all geometric parameters for region expansion
841fn extend_solid_area(
842    framebuffer: &[u8],
843    fb_width: u16,
844    base_x: u16,
845    base_y: u16,
846    max_w: u16,
847    max_h: u16,
848    color_value: u32,
849    mut x: u16,
850    mut y: u16,
851    mut w: u16,
852    mut h: u16,
853) -> (u16, u16, u16, u16) {
854    // Extend upwards
855    while y > base_y {
856        if check_solid_tile(framebuffer, fb_width, x, y - 1, w, 1, Some(color_value)).is_none() {
857            break;
858        }
859        y -= 1;
860        h += 1;
861    }
862
863    // Extend downwards
864    while y + h < base_y + max_h {
865        if check_solid_tile(framebuffer, fb_width, x, y + h, w, 1, Some(color_value)).is_none() {
866            break;
867        }
868        h += 1;
869    }
870
871    // Extend left
872    while x > base_x {
873        if check_solid_tile(framebuffer, fb_width, x - 1, y, 1, h, Some(color_value)).is_none() {
874            break;
875        }
876        x -= 1;
877        w += 1;
878    }
879
880    // Extend right
881    while x + w < base_x + max_w {
882        if check_solid_tile(framebuffer, fb_width, x + w, y, 1, h, Some(color_value)).is_none() {
883            break;
884        }
885        w += 1;
886    }
887
888    (x, y, w, h)
889}
890
891/// Palette analysis result
892struct Palette {
893    num_colors: usize,
894    colors: [u32; 256],
895    mono_background: u32,
896    mono_foreground: u32,
897}
898
899/// Analyze palette from pixel data
900/// Determines color count and encoding mode selection
901fn analyze_palette(pixels: &[u8], pixel_count: usize, compression: u8) -> Palette {
902    let conf_idx = match compression {
903        0 => 0,
904        1 => 1,
905        2 | 3 => 2,
906        _ => 3,
907    };
908    let conf = &TIGHT_CONF[conf_idx];
909
910    let mut palette = Palette {
911        num_colors: 0,
912        colors: [0; 256],
913        mono_background: 0,
914        mono_foreground: 0,
915    };
916
917    if pixel_count == 0 {
918        return palette;
919    }
920
921    // Get first color
922    let c0 = rgba_to_rgb24(pixels[0], pixels[1], pixels[2]);
923
924    // Count how many pixels match first color
925    let mut i = 4;
926    while i < pixels.len() && rgba_to_rgb24(pixels[i], pixels[i + 1], pixels[i + 2]) == c0 {
927        i += 4;
928    }
929
930    if i >= pixels.len() {
931        // Solid color
932        palette.num_colors = 1;
933        palette.colors[0] = c0;
934        return palette;
935    }
936
937    // Check for 2-color (mono) case
938    if pixel_count >= conf.mono_min_rect_size {
939        let n0 = i / 4;
940        let c1 = rgba_to_rgb24(pixels[i], pixels[i + 1], pixels[i + 2]);
941        let mut n1 = 0;
942
943        i += 4;
944        while i < pixels.len() {
945            let color = rgba_to_rgb24(pixels[i], pixels[i + 1], pixels[i + 2]);
946            if color == c0 {
947                // n0 already counted
948            } else if color == c1 {
949                n1 += 1;
950            } else {
951                break;
952            }
953            i += 4;
954        }
955
956        if i >= pixels.len() {
957            // Only 2 colors found
958            palette.num_colors = 2;
959            if n0 > n1 {
960                palette.mono_background = c0;
961                palette.mono_foreground = c1;
962                palette.colors[0] = c0;
963                palette.colors[1] = c1;
964            } else {
965                palette.mono_background = c1;
966                palette.mono_foreground = c0;
967                palette.colors[0] = c1;
968                palette.colors[1] = c0;
969            }
970            return palette;
971        }
972    }
973
974    // More than 2 colors - full palette or truecolor
975    palette.num_colors = 0;
976    palette
977}
978
979/// Extract RGBA rectangle from framebuffer
980fn extract_rect_rgba(framebuffer: &[u8], fb_width: u16, rect: &Rect) -> Vec<u8> {
981    let mut pixels = Vec::with_capacity(rect.w as usize * rect.h as usize * 4);
982
983    for y in 0..rect.h {
984        let row_offset = ((rect.y + y) as usize * fb_width as usize + rect.x as usize) * 4;
985        let row_end = row_offset + rect.w as usize * 4;
986        pixels.extend_from_slice(&framebuffer[row_offset..row_end]);
987    }
988
989    pixels
990}
991
992/// Convert RGBA to RGB24
993/// Matches the format used in `common::rgba_to_rgb24_pixels`
994/// Internal format: 0x00BBGGRR (R at bits 0-7, G at 8-15, B at 16-23)
995#[inline]
996fn rgba_to_rgb24(r: u8, g: u8, b: u8) -> u32 {
997    u32::from(r) | (u32::from(g) << 8) | (u32::from(b) << 16)
998}
999
1000/// Encode solid rectangle
1001/// Implements solid fill encoding mode (1 color)
1002/// Uses client's pixel format for color encoding
1003fn encode_solid_rect(color: u32, client_format: &PixelFormat) -> BytesMut {
1004    let mut buf = BytesMut::with_capacity(16); // Reserve enough for largest pixel format
1005    buf.put_u8(TIGHT_FILL << 4); // 0x80
1006
1007    // Translate color to client's pixel format
1008    let color_bytes = translate_pixel_to_client_format(color, client_format);
1009
1010    #[cfg(feature = "debug-logging")]
1011    {
1012        let use_24bit = client_format.depth == 24
1013            && client_format.red_max == 255
1014            && client_format.green_max == 255
1015            && client_format.blue_max == 255;
1016        #[cfg(feature = "debug-logging")]
1017        log::info!("Tight solid: color=0x{:06x}, translated bytes={:02x?}, use_24bit={}, client: depth={} bpp={} rshift={} gshift={} bshift={}",
1018            color, color_bytes, use_24bit, client_format.depth, client_format.bits_per_pixel,
1019            client_format.red_shift, client_format.green_shift, client_format.blue_shift);
1020    }
1021
1022    buf.extend_from_slice(&color_bytes);
1023
1024    #[cfg(feature = "debug-logging")]
1025    log::info!(
1026        "Tight solid: 0x{:06x}, control=0x{:02x}, color_len={}, total={} bytes",
1027        color,
1028        TIGHT_FILL << 4,
1029        color_bytes.len(),
1030        buf.len()
1031    );
1032    buf
1033}
1034
1035/// Encode mono rectangle (2 colors)
1036/// Implements monochrome bitmap encoding with palette
1037/// Uses client's pixel format for palette colors
1038#[allow(clippy::too_many_arguments)] // All parameters are necessary for proper encoding
1039fn encode_mono_rect<C: TightStreamCompressor>(
1040    pixels: &[u8],
1041    width: u16,
1042    height: u16,
1043    bg: u32,
1044    fg: u32,
1045    compression: u8,
1046    client_format: &PixelFormat,
1047    compressor: &mut C,
1048) -> BytesMut {
1049    let conf_idx = match compression {
1050        0 => 0,
1051        1 => 1,
1052        2 | 3 => 2,
1053        _ => 3,
1054    };
1055    let zlib_level = TIGHT_CONF[conf_idx].mono_zlib_level;
1056
1057    // Encode bitmap
1058    let bitmap = encode_mono_bitmap(pixels, width, height, bg);
1059
1060    let mut buf = BytesMut::new();
1061
1062    // Control byte
1063    if zlib_level == 0 {
1064        buf.put_u8((TIGHT_NO_ZLIB | TIGHT_EXPLICIT_FILTER) << 4);
1065    } else {
1066        buf.put_u8((STREAM_ID_MONO | TIGHT_EXPLICIT_FILTER) << 4);
1067    }
1068
1069    // Filter and palette
1070    buf.put_u8(TIGHT_FILTER_PALETTE);
1071    buf.put_u8(1); // 2 colors - 1
1072
1073    // Palette colors - translate to client format
1074    let bg_bytes = translate_pixel_to_client_format(bg, client_format);
1075    let fg_bytes = translate_pixel_to_client_format(fg, client_format);
1076
1077    #[cfg(feature = "debug-logging")]
1078    {
1079        let use_24bit = client_format.depth == 24
1080            && client_format.red_max == 255
1081            && client_format.green_max == 255
1082            && client_format.blue_max == 255;
1083        log::info!("Tight mono palette: bg=0x{:06x} -> {:02x?}, fg=0x{:06x} -> {:02x?}, use_24bit={}, depth={} bpp={}",
1084            bg, bg_bytes, fg, fg_bytes, use_24bit, client_format.depth, client_format.bits_per_pixel);
1085    }
1086
1087    buf.extend_from_slice(&bg_bytes);
1088    buf.extend_from_slice(&fg_bytes);
1089
1090    // Compress data
1091    compress_data(&mut buf, &bitmap, zlib_level, STREAM_ID_MONO, compressor);
1092
1093    #[cfg(feature = "debug-logging")]
1094    log::info!(
1095        "Tight mono: {}x{}, {} bytes ({}bpp)",
1096        width,
1097        height,
1098        buf.len(),
1099        client_format.bits_per_pixel
1100    );
1101    buf
1102}
1103
1104/// Encode indexed palette rectangle (3-16 colors)
1105/// Implements palette-based encoding with color indices
1106/// Uses client's pixel format for palette colors
1107#[allow(clippy::cast_possible_truncation)] // Palette limited to 16 colors, indices fit in u8
1108fn encode_indexed_rect<C: TightStreamCompressor>(
1109    pixels: &[u8],
1110    width: u16,
1111    height: u16,
1112    palette: &[u32],
1113    compression: u8,
1114    client_format: &PixelFormat,
1115    compressor: &mut C,
1116) -> BytesMut {
1117    let conf_idx = match compression {
1118        0 => 0,
1119        1 => 1,
1120        2 | 3 => 2,
1121        _ => 3,
1122    };
1123    let zlib_level = TIGHT_CONF[conf_idx].idx_zlib_level;
1124
1125    // Build color-to-index map
1126    let mut color_map = HashMap::new();
1127    for (idx, &color) in palette.iter().enumerate() {
1128        color_map.insert(color, idx as u8);
1129    }
1130
1131    // Encode indices
1132    let mut indices = Vec::with_capacity(width as usize * height as usize);
1133    for chunk in pixels.chunks_exact(4) {
1134        let color = rgba_to_rgb24(chunk[0], chunk[1], chunk[2]);
1135        indices.push(*color_map.get(&color).unwrap_or(&0));
1136    }
1137
1138    let mut buf = BytesMut::new();
1139
1140    // Control byte
1141    if zlib_level == 0 {
1142        buf.put_u8((TIGHT_NO_ZLIB | TIGHT_EXPLICIT_FILTER) << 4);
1143    } else {
1144        buf.put_u8((STREAM_ID_INDEXED | TIGHT_EXPLICIT_FILTER) << 4);
1145    }
1146
1147    // Filter and palette size
1148    buf.put_u8(TIGHT_FILTER_PALETTE);
1149    buf.put_u8((palette.len() - 1) as u8);
1150
1151    // Palette colors - translate to client format
1152    for &color in palette {
1153        let color_bytes = translate_pixel_to_client_format(color, client_format);
1154        buf.extend_from_slice(&color_bytes);
1155    }
1156
1157    // Compress data
1158    compress_data(
1159        &mut buf,
1160        &indices,
1161        zlib_level,
1162        STREAM_ID_INDEXED,
1163        compressor,
1164    );
1165
1166    #[cfg(feature = "debug-logging")]
1167    log::info!(
1168        "Tight indexed: {} colors, {}x{}, {} bytes ({}bpp)",
1169        palette.len(),
1170        width,
1171        height,
1172        buf.len(),
1173        client_format.bits_per_pixel
1174    );
1175    buf
1176}
1177
1178/// Encode full-color rectangle
1179/// Implements full-color zlib encoding for truecolor images
1180fn encode_full_color_rect<C: TightStreamCompressor>(
1181    pixels: &[u8],
1182    width: u16,
1183    height: u16,
1184    compression: u8,
1185    compressor: &mut C,
1186) -> BytesMut {
1187    let conf_idx = match compression {
1188        0 => 0,
1189        1 => 1,
1190        2 | 3 => 2,
1191        _ => 3,
1192    };
1193    let zlib_level = TIGHT_CONF[conf_idx].raw_zlib_level;
1194
1195    // Convert RGBA to RGB24
1196    let mut rgb_data = Vec::with_capacity(width as usize * height as usize * 3);
1197    for chunk in pixels.chunks_exact(4) {
1198        rgb_data.push(chunk[0]);
1199        rgb_data.push(chunk[1]);
1200        rgb_data.push(chunk[2]);
1201    }
1202
1203    let mut buf = BytesMut::new();
1204
1205    // Control byte
1206    let control_byte = if zlib_level == 0 {
1207        TIGHT_NO_ZLIB << 4
1208    } else {
1209        STREAM_ID_FULL_COLOR << 4
1210    };
1211    buf.put_u8(control_byte);
1212
1213    #[cfg(feature = "debug-logging")]
1214    log::info!(
1215        "Tight full-color: {}x{}, zlib_level={}, control_byte=0x{:02x}, rgb_data_len={}",
1216        width,
1217        height,
1218        zlib_level,
1219        control_byte,
1220        rgb_data.len()
1221    );
1222
1223    // Compress data
1224    compress_data(
1225        &mut buf,
1226        &rgb_data,
1227        zlib_level,
1228        STREAM_ID_FULL_COLOR,
1229        compressor,
1230    );
1231
1232    #[cfg(feature = "debug-logging")]
1233    log::info!(
1234        "Tight full-color: {}x{}, {} bytes total",
1235        width,
1236        height,
1237        buf.len()
1238    );
1239    buf
1240}
1241
1242/// Encode JPEG rectangle
1243/// Implements lossy JPEG compression for photographic content
1244fn encode_jpeg_rect<C: TightStreamCompressor>(
1245    pixels: &[u8],
1246    width: u16,
1247    height: u16,
1248    #[allow(unused_variables)] quality: u8,
1249    compressor: &mut C,
1250) -> BytesMut {
1251    #[cfg(feature = "turbojpeg")]
1252    {
1253        use crate::jpeg::TurboJpegEncoder;
1254
1255        // Convert RGBA to RGB
1256        let mut rgb_data = Vec::with_capacity(width as usize * height as usize * 3);
1257        for chunk in pixels.chunks_exact(4) {
1258            rgb_data.push(chunk[0]);
1259            rgb_data.push(chunk[1]);
1260            rgb_data.push(chunk[2]);
1261        }
1262
1263        // Compress with TurboJPEG
1264        let jpeg_data = match TurboJpegEncoder::new() {
1265            Ok(mut encoder) => match encoder.compress_rgb(&rgb_data, width, height, quality) {
1266                Ok(data) => data,
1267                #[allow(unused_variables)]
1268                Err(e) => {
1269                    #[cfg(feature = "debug-logging")]
1270                    log::info!("TurboJPEG failed: {e}, using full-color");
1271                    return encode_full_color_rect(pixels, width, height, 6, compressor);
1272                }
1273            },
1274            #[allow(unused_variables)]
1275            Err(e) => {
1276                #[cfg(feature = "debug-logging")]
1277                log::info!("TurboJPEG init failed: {e}, using full-color");
1278                return encode_full_color_rect(pixels, width, height, 6, compressor);
1279            }
1280        };
1281
1282        let mut buf = BytesMut::new();
1283        buf.put_u8(TIGHT_JPEG << 4); // 0x90
1284        write_compact_length(&mut buf, jpeg_data.len());
1285        buf.put_slice(&jpeg_data);
1286
1287        #[cfg(feature = "debug-logging")]
1288        log::info!(
1289            "Tight JPEG: {}x{}, quality {}, {} bytes",
1290            width,
1291            height,
1292            quality,
1293            jpeg_data.len()
1294        );
1295        buf
1296    }
1297
1298    #[cfg(not(feature = "turbojpeg"))]
1299    {
1300        #[cfg(feature = "debug-logging")]
1301        log::info!("TurboJPEG not enabled, using full-color (quality={quality})");
1302        encode_full_color_rect(pixels, width, height, 6, compressor)
1303    }
1304}
1305
1306/// Compress data with zlib using persistent streams or send uncompressed
1307/// Handles compression based on data size and level settings
1308///
1309/// Uses persistent zlib streams via the `TightStreamCompressor` trait.
1310/// Persistent streams maintain their dictionary state across multiple compress operations.
1311fn compress_data<C: TightStreamCompressor>(
1312    buf: &mut BytesMut,
1313    data: &[u8],
1314    zlib_level: u8,
1315    stream_id: u8,
1316    compressor: &mut C,
1317) {
1318    #[cfg_attr(not(feature = "debug-logging"), allow(unused_variables))]
1319    let before_len = buf.len();
1320
1321    // Data < 12 bytes sent raw WITHOUT length
1322    if data.len() < TIGHT_MIN_TO_COMPRESS {
1323        buf.put_slice(data);
1324        #[cfg(feature = "debug-logging")]
1325        log::info!(
1326            "compress_data: {} bytes < 12, sent raw (no length), buf grew by {} bytes",
1327            data.len(),
1328            buf.len() - before_len
1329        );
1330        return;
1331    }
1332
1333    // zlibLevel == 0 means uncompressed WITH length
1334    if zlib_level == 0 {
1335        write_compact_length(buf, data.len());
1336        buf.put_slice(data);
1337        #[cfg(feature = "debug-logging")]
1338        log::info!("compress_data: {} bytes uncompressed (zlib_level=0), with length, buf grew by {} bytes", data.len(), buf.len() - before_len);
1339        return;
1340    }
1341
1342    // Compress with persistent zlib stream
1343    match compressor.compress_tight_stream(stream_id, zlib_level, data) {
1344        Ok(compressed) => {
1345            write_compact_length(buf, compressed.len());
1346            buf.put_slice(&compressed);
1347            #[cfg(feature = "debug-logging")]
1348            log::info!(
1349                "compress_data: {} bytes compressed to {} using stream {}, buf grew by {} bytes",
1350                data.len(),
1351                compressed.len(),
1352                stream_id,
1353                buf.len() - before_len
1354            );
1355        }
1356        Err(e) => {
1357            // Compression failed - send uncompressed
1358            #[cfg(feature = "debug-logging")]
1359            log::info!(
1360                "compress_data: compression FAILED ({}), sending {} bytes uncompressed",
1361                e,
1362                data.len()
1363            );
1364            #[cfg(not(feature = "debug-logging"))]
1365            let _ = e;
1366
1367            write_compact_length(buf, data.len());
1368            buf.put_slice(data);
1369        }
1370    }
1371}
1372
1373/// Encode mono bitmap (1 bit per pixel)
1374/// Converts 2-color image to packed bitmap format
1375fn encode_mono_bitmap(pixels: &[u8], width: u16, height: u16, bg: u32) -> Vec<u8> {
1376    let w = width as usize;
1377    let h = height as usize;
1378    let bytes_per_row = w.div_ceil(8);
1379    let mut bitmap = vec![0u8; bytes_per_row * h];
1380
1381    let mut bitmap_idx = 0;
1382    for y in 0..h {
1383        let mut byte_val = 0u8;
1384        let mut bit_pos = 7i32; // MSB first
1385
1386        for x in 0..w {
1387            let pix_offset = (y * w + x) * 4;
1388            let color = rgba_to_rgb24(
1389                pixels[pix_offset],
1390                pixels[pix_offset + 1],
1391                pixels[pix_offset + 2],
1392            );
1393
1394            if color != bg {
1395                byte_val |= 1 << bit_pos;
1396            }
1397
1398            bit_pos -= 1;
1399
1400            // Write byte after 8 pixels (when bit_pos becomes -1)
1401            if bit_pos < 0 {
1402                bitmap[bitmap_idx] = byte_val;
1403                bitmap_idx += 1;
1404                byte_val = 0;
1405                bit_pos = 7;
1406            }
1407        }
1408
1409        // Write partial byte at end of row if width not multiple of 8
1410        if !w.is_multiple_of(8) {
1411            bitmap[bitmap_idx] = byte_val;
1412            bitmap_idx += 1;
1413        }
1414    }
1415
1416    bitmap
1417}
1418
1419/// Write compact length encoding
1420/// Implements variable-length integer encoding for Tight protocol
1421#[allow(clippy::cast_possible_truncation)] // Compact length encoding uses variable-length u8 packing per RFC 6143
1422fn write_compact_length(buf: &mut BytesMut, len: usize) {
1423    if len < 128 {
1424        buf.put_u8(len as u8);
1425    } else if len < 16384 {
1426        buf.put_u8(((len & 0x7F) | 0x80) as u8);
1427        buf.put_u8(((len >> 7) & 0x7F) as u8); // Mask to ensure high bit is clear
1428    } else {
1429        buf.put_u8(((len & 0x7F) | 0x80) as u8);
1430        buf.put_u8((((len >> 7) & 0x7F) | 0x80) as u8);
1431        buf.put_u8((len >> 14) as u8);
1432    }
1433}
1434
1435/// Trait for managing persistent zlib compression streams
1436///
1437/// Implementations of this trait maintain separate compression streams for different
1438/// data types (full-color, mono, indexed) to improve compression ratios across
1439/// multiple rectangle updates.
1440pub trait TightStreamCompressor {
1441    /// Compresses data using a persistent zlib stream
1442    ///
1443    /// # Arguments
1444    /// * `stream_id` - Stream identifier (`STREAM_ID_FULL_COLOR`, `STREAM_ID_MONO`, or `STREAM_ID_INDEXED`)
1445    /// * `level` - Compression level (0-9)
1446    /// * `input` - Data to compress
1447    ///
1448    /// # Returns
1449    ///
1450    /// Compressed data or error message
1451    ///
1452    /// # Errors
1453    ///
1454    /// Returns an error if compression fails
1455    fn compress_tight_stream(
1456        &mut self,
1457        stream_id: u8,
1458        level: u8,
1459        input: &[u8],
1460    ) -> Result<Vec<u8>, String>;
1461}
1462
1463/// Simple implementation of `TightStreamCompressor` for standalone encoding.
1464///
1465/// This creates separate persistent zlib streams for each stream ID (full-color, mono, indexed).
1466/// Used when encoding without access to a VNC client's stream manager.
1467pub struct SimpleTightCompressor {
1468    streams: [Option<flate2::Compress>; 4],
1469    level: u8,
1470}
1471
1472impl SimpleTightCompressor {
1473    /// Creates a new `SimpleTightCompressor` with the specified compression level.
1474    #[must_use]
1475    pub fn new(level: u8) -> Self {
1476        Self {
1477            streams: [None, None, None, None],
1478            level,
1479        }
1480    }
1481}
1482
1483impl TightStreamCompressor for SimpleTightCompressor {
1484    #[allow(clippy::cast_possible_truncation)] // Zlib total_out limited to buffer size
1485    fn compress_tight_stream(
1486        &mut self,
1487        stream_id: u8,
1488        level: u8,
1489        input: &[u8],
1490    ) -> Result<Vec<u8>, String> {
1491        use flate2::{Compress, Compression, FlushCompress};
1492
1493        let stream_idx = stream_id as usize;
1494        if stream_idx >= 4 {
1495            return Err(format!("Invalid stream ID: {stream_id}"));
1496        }
1497
1498        // Initialize stream if needed
1499        if self.streams[stream_idx].is_none() {
1500            self.streams[stream_idx] = Some(Compress::new(
1501                Compression::new(u32::from(level.min(self.level))),
1502                true,
1503            ));
1504        }
1505
1506        let stream = self.streams[stream_idx].as_mut().unwrap();
1507        let mut output = vec![0u8; input.len() + 64];
1508        let before_out = stream.total_out();
1509
1510        match stream.compress(input, &mut output, FlushCompress::Sync) {
1511            Ok(flate2::Status::Ok | flate2::Status::StreamEnd) => {
1512                let total_out = (stream.total_out() - before_out) as usize;
1513                output.truncate(total_out);
1514                Ok(output)
1515            }
1516            Ok(flate2::Status::BufError) => Err("Compression buffer error".to_string()),
1517            Err(e) => Err(format!("Compression failed: {e}")),
1518        }
1519    }
1520}
1521
1522/// Encode Tight with persistent zlib streams, returning individual sub-rectangles
1523/// Returns a vector of (x, y, width, height, `encoded_data`) for each sub-rectangle
1524///
1525/// # Arguments
1526/// * `data` - Framebuffer pixel data (RGBA format)
1527/// * `width` - Rectangle width
1528/// * `height` - Rectangle height
1529/// * `quality` - JPEG quality level (0-9, or 10+ to disable JPEG)
1530/// * `compression` - Compression level (0-9)
1531/// * `client_format` - Client's pixel format for palette color translation
1532/// * `compressor` - Zlib stream compressor for persistent compression streams
1533pub fn encode_tight_rects<C: TightStreamCompressor>(
1534    data: &[u8],
1535    width: u16,
1536    height: u16,
1537    quality: u8,
1538    compression: u8,
1539    client_format: &PixelFormat,
1540    compressor: &mut C,
1541) -> Vec<(u16, u16, u16, u16, BytesMut)> {
1542    #[cfg(feature = "debug-logging")]
1543    log::info!(
1544        "DEBUG: encode_tight_rects called: {}x{}, data_len={}, quality={}, compression={}, bpp={}",
1545        width,
1546        height,
1547        data.len(),
1548        quality,
1549        compression,
1550        client_format.bits_per_pixel
1551    );
1552
1553    let rect = Rect {
1554        x: 0,
1555        y: 0,
1556        w: width,
1557        h: height,
1558    };
1559
1560    #[cfg(feature = "debug-logging")]
1561    log::info!("DEBUG: Calling encode_rect_optimized");
1562
1563    let result = encode_rect_optimized(
1564        data,
1565        width,
1566        &rect,
1567        quality,
1568        compression,
1569        client_format,
1570        compressor,
1571    );
1572
1573    #[cfg(feature = "debug-logging")]
1574    log::info!(
1575        "DEBUG: encode_rect_optimized returned {} rectangles",
1576        result.rectangles.len()
1577    );
1578
1579    // Convert EncodeResult to public format
1580    let rects: Vec<(u16, u16, u16, u16, BytesMut)> = result
1581        .rectangles
1582        .into_iter()
1583        .map(|(r, buf)| {
1584            #[cfg(feature = "debug-logging")]
1585            log::info!(
1586                "DEBUG: Sub-rect: {}x{} at ({}, {}), encoded_len={}",
1587                r.w,
1588                r.h,
1589                r.x,
1590                r.y,
1591                buf.len()
1592            );
1593            (r.x, r.y, r.w, r.h, buf)
1594        })
1595        .collect();
1596
1597    #[cfg(feature = "debug-logging")]
1598    log::info!(
1599        "DEBUG: encode_tight_rects returning {} rectangles",
1600        rects.len()
1601    );
1602
1603    rects
1604}
1605
1606/// Encode Tight with persistent zlib streams (for use with VNC client streams)
1607/// Returns concatenated data (legacy API - consider using `encode_tight_rects` instead)
1608pub fn encode_tight_with_streams<C: TightStreamCompressor>(
1609    data: &[u8],
1610    width: u16,
1611    height: u16,
1612    quality: u8,
1613    compression: u8,
1614    client_format: &PixelFormat,
1615    compressor: &mut C,
1616) -> BytesMut {
1617    // Concatenate all sub-rectangles
1618    let rects = encode_tight_rects(
1619        data,
1620        width,
1621        height,
1622        quality,
1623        compression,
1624        client_format,
1625        compressor,
1626    );
1627    let mut output = BytesMut::new();
1628    for (_x, _y, _w, _h, buf) in rects {
1629        output.extend_from_slice(&buf);
1630    }
1631    output
1632}