1use super::common::translate_pixel_to_client_format;
69use crate::{Encoding, PixelFormat};
70use bytes::{BufMut, BytesMut};
71use std::collections::HashMap;
72
73const 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
80const TIGHT_FILTER_PALETTE: u8 = 0x01;
82
83pub const STREAM_ID_FULL_COLOR: u8 = 0;
85pub const STREAM_ID_MONO: u8 = 1;
87pub const STREAM_ID_INDEXED: u8 = 2;
89
90const 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
98struct 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 }, TightConf {
114 mono_min_rect_size: 32,
115 idx_zlib_level: 1,
116 mono_zlib_level: 1,
117 raw_zlib_level: 1,
118 }, TightConf {
120 mono_min_rect_size: 32,
121 idx_zlib_level: 3,
122 mono_zlib_level: 3,
123 raw_zlib_level: 2,
124 }, TightConf {
126 mono_min_rect_size: 32,
127 idx_zlib_level: 7,
128 mono_zlib_level: 7,
129 raw_zlib_level: 5,
130 }, ];
132
133#[derive(Debug, Clone)]
135struct Rect {
136 x: u16,
137 y: u16,
138 w: u16,
139 h: u16,
140}
141
142struct EncodeResult {
144 rectangles: Vec<(Rect, BytesMut)>,
145}
146
147pub 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 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 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#[allow(clippy::similar_names)] #[allow(clippy::too_many_lines)] #[allow(clippy::cast_possible_truncation)] fn 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 let compression = normalize_compression_level(compression, quality);
212
213 #[cfg(feature = "debug-logging")]
214 log::info!("DEBUG: normalized compression={compression}");
215
216 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 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 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 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 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 let mut current_y = rect.y;
279 let mut base_y = rect.y; let mut remaining_h = rect.h; #[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 if (current_y - base_y) >= n_max_rows {
294 let chunk_rect = Rect {
295 x: rect.x,
296 y: base_y, w: rect.w,
298 h: n_max_rows,
299 };
300 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 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 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 if dw == 0 {
343 break;
344 }
345
346 if let Some(color_value) =
348 check_solid_tile(framebuffer, fb_width, current_x, current_y, dw, dh, None)
349 {
350 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 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 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 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 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 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 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 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 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 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 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
592fn normalize_compression_level(compression: u8, quality: u8) -> u8 {
595 let mut level = compression;
596
597 if quality < 10 {
600 level = level.clamp(1, 2);
601 }
602 else if level > 1 {
604 level = 1;
605 }
606
607 if level == 9 {
609 level = 3;
610 }
611
612 level
613}
614
615fn 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 let pixels = extract_rect_rgba(framebuffer, fb_width, rect);
631
632 let palette = analyze_palette(&pixels, rect.w as usize * rect.h as usize, compression);
634
635 match palette.num_colors {
637 0 => {
638 if quality < 10 {
640 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 encode_solid_rect(palette.colors[0], client_format)
650 }
651 2 => {
652 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 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#[allow(clippy::cast_possible_truncation)] fn 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 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
730fn 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 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::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 if let Some(required) = need_same_color {
759 if first_color != required {
760 return None;
761 }
762 }
763
764 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
783fn 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#[allow(clippy::too_many_arguments)] fn 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 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 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 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 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
891struct Palette {
893 num_colors: usize,
894 colors: [u32; 256],
895 mono_background: u32,
896 mono_foreground: u32,
897}
898
899fn 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 let c0 = rgba_to_rgb24(pixels[0], pixels[1], pixels[2]);
923
924 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 palette.num_colors = 1;
933 palette.colors[0] = c0;
934 return palette;
935 }
936
937 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 } else if color == c1 {
949 n1 += 1;
950 } else {
951 break;
952 }
953 i += 4;
954 }
955
956 if i >= pixels.len() {
957 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 palette.num_colors = 0;
976 palette
977}
978
979fn 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#[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
1000fn encode_solid_rect(color: u32, client_format: &PixelFormat) -> BytesMut {
1004 let mut buf = BytesMut::with_capacity(16); buf.put_u8(TIGHT_FILL << 4); 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#[allow(clippy::too_many_arguments)] fn 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 let bitmap = encode_mono_bitmap(pixels, width, height, bg);
1059
1060 let mut buf = BytesMut::new();
1061
1062 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 buf.put_u8(TIGHT_FILTER_PALETTE);
1071 buf.put_u8(1); 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(&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#[allow(clippy::cast_possible_truncation)] fn 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 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 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 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 buf.put_u8(TIGHT_FILTER_PALETTE);
1149 buf.put_u8((palette.len() - 1) as u8);
1150
1151 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(
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
1178fn 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 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 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(
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
1242fn 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 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 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); 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
1306fn 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 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 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 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 #[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
1373fn 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; 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 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 if !w.is_multiple_of(8) {
1411 bitmap[bitmap_idx] = byte_val;
1412 bitmap_idx += 1;
1413 }
1414 }
1415
1416 bitmap
1417}
1418
1419#[allow(clippy::cast_possible_truncation)] fn 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); } 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
1435pub trait TightStreamCompressor {
1441 fn compress_tight_stream(
1456 &mut self,
1457 stream_id: u8,
1458 level: u8,
1459 input: &[u8],
1460 ) -> Result<Vec<u8>, String>;
1461}
1462
1463pub struct SimpleTightCompressor {
1468 streams: [Option<flate2::Compress>; 4],
1469 level: u8,
1470}
1471
1472impl SimpleTightCompressor {
1473 #[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)] 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 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
1522pub 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 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
1606pub 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 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}