1use bytes::{BufMut, BytesMut};
39use flate2::write::ZlibEncoder;
40use flate2::{Compress, Compression, FlushCompress};
41use std::collections::HashMap;
42use std::io::Write;
43
44use crate::{Encoding, PixelFormat};
45
46const TILE_SIZE: usize = 64;
47
48fn analyze_runs_and_palette(pixels: &[u32]) -> (usize, usize, Vec<u32>) {
54 let mut runs = 0;
55 let mut single_pixels = 0;
56 let mut palette: Vec<u32> = Vec::with_capacity(16); if pixels.is_empty() {
59 return (0, 0, palette);
60 }
61
62 let mut i = 0;
63 while i < pixels.len() {
64 let color = pixels[i];
65
66 if palette.len() < 256 && !palette.contains(&color) {
68 palette.push(color);
69 }
70
71 let mut run_len = 1;
72 while i + run_len < pixels.len() && pixels[i + run_len] == color {
73 run_len += 1;
74 }
75
76 if run_len == 1 {
77 single_pixels += 1;
78 } else {
79 runs += 1;
80 }
81 i += run_len;
82 }
83 (runs, single_pixels, palette)
84}
85
86#[allow(dead_code)]
93#[allow(clippy::cast_possible_truncation)] pub fn encode_zrle_persistent(
95 data: &[u8],
96 width: u16,
97 height: u16,
98 _pixel_format: &PixelFormat,
99 compressor: &mut Compress,
100) -> std::io::Result<Vec<u8>> {
101 let width = width as usize;
102 let height = height as usize;
103 let mut uncompressed_data = BytesMut::new();
104
105 for y in (0..height).step_by(TILE_SIZE) {
106 for x in (0..width).step_by(TILE_SIZE) {
107 let tile_w = (width - x).min(TILE_SIZE);
108 let tile_h = (height - y).min(TILE_SIZE);
109
110 let tile_data = extract_tile(data, width, x, y, tile_w, tile_h);
112
113 encode_tile(&mut uncompressed_data, &tile_data, tile_w, tile_h);
115 }
116 }
117
118 let input = &uncompressed_data[..];
121 let mut output_buf = vec![0u8; input.len() * 2 + 1024]; let before_out = compressor.total_out();
124
125 compressor.compress(input, &mut output_buf, FlushCompress::Sync)?;
127
128 let produced = (compressor.total_out() - before_out) as usize;
129 let compressed_output = &output_buf[..produced];
130
131 let mut result = BytesMut::with_capacity(4 + compressed_output.len());
133 result.put_u32(compressed_output.len() as u32);
134 result.extend_from_slice(compressed_output);
135
136 #[cfg(feature = "debug-logging")]
137 log::info!(
138 "ZRLE: compressed {}->{} bytes ({}x{} tiles)",
139 uncompressed_data.len(),
140 compressed_output.len(),
141 width,
142 height
143 );
144
145 Ok(result.to_vec())
146}
147
148#[allow(clippy::cast_possible_truncation)] pub fn encode_zrle(
156 data: &[u8],
157 width: u16,
158 height: u16,
159 _pixel_format: &PixelFormat, compression: u8,
161) -> std::io::Result<Vec<u8>> {
162 let compression_level = match compression {
163 0 => Compression::fast(),
164 1..=3 => Compression::new(u32::from(compression)),
165 4..=6 => Compression::default(),
166 _ => Compression::best(),
167 };
168 let mut zlib_encoder = ZlibEncoder::new(Vec::new(), compression_level);
169 let mut uncompressed_data = BytesMut::new();
170
171 let width = width as usize;
172 let height = height as usize;
173
174 for y in (0..height).step_by(TILE_SIZE) {
175 for x in (0..width).step_by(TILE_SIZE) {
176 let tile_w = (width - x).min(TILE_SIZE);
177 let tile_h = (height - y).min(TILE_SIZE);
178
179 let tile_data = extract_tile(data, width, x, y, tile_w, tile_h);
181
182 encode_tile(&mut uncompressed_data, &tile_data, tile_w, tile_h);
184 }
185 }
186
187 zlib_encoder.write_all(&uncompressed_data)?;
188 let compressed = zlib_encoder.finish()?;
189
190 let mut result = BytesMut::with_capacity(4 + compressed.len());
192 result.put_u32(compressed.len() as u32); result.extend_from_slice(&compressed);
194
195 Ok(result.to_vec())
196}
197
198#[allow(clippy::cast_possible_truncation)] fn encode_tile(buf: &mut BytesMut, tile_data: &[u8], width: usize, height: usize) {
202 const CPIXEL_SIZE: usize = 3; if tile_data.len() >= 4 {
206 let first_r = tile_data[0];
207 let first_g = tile_data[1];
208 let first_b = tile_data[2];
209 let mut is_solid = true;
210
211 for chunk in tile_data.chunks_exact(4).skip(1) {
212 if chunk[0] != first_r || chunk[1] != first_g || chunk[2] != first_b {
213 is_solid = false;
214 break;
215 }
216 }
217
218 if is_solid {
219 let color = u32::from(first_r) | (u32::from(first_g) << 8) | (u32::from(first_b) << 16);
220 encode_solid_color_tile(buf, color);
221 return;
222 }
223 }
224
225 let pixels = rgba_to_rgb24_pixels(tile_data);
227 let (runs, single_pixels, palette) = analyze_runs_and_palette(&pixels);
228
229 let mut use_rle = false;
230 let mut use_palette = false;
231
232 let mut estimated_bytes = width * height * CPIXEL_SIZE;
234
235 let plain_rle_bytes = (CPIXEL_SIZE + 1) * (runs + single_pixels);
236
237 if plain_rle_bytes < estimated_bytes {
238 use_rle = true;
239 estimated_bytes = plain_rle_bytes;
240 }
241
242 if palette.len() < 128 {
243 let palette_size = palette.len();
244
245 let palette_rle_bytes = CPIXEL_SIZE * palette_size + 2 * runs + single_pixels;
247
248 if palette_rle_bytes < estimated_bytes {
249 use_rle = true;
250 use_palette = true;
251 estimated_bytes = palette_rle_bytes;
252 }
253
254 if palette_size < 17 {
256 let bits_per_packed_pixel = match palette_size {
257 2 => 1,
258 3..=4 => 2,
259 _ => 4, };
261 let packed_bytes =
263 CPIXEL_SIZE * palette_size + (width * height * bits_per_packed_pixel).div_ceil(8);
264
265 if packed_bytes < estimated_bytes {
266 use_rle = false;
267 use_palette = true;
268 }
270 }
271 }
272
273 if use_palette {
274 let color_to_idx: HashMap<_, _> = palette
277 .iter()
278 .enumerate()
279 .map(|(i, &c)| (c, i as u8))
280 .collect();
281
282 if use_rle {
283 encode_packed_palette_rle_tile(buf, &pixels, &palette, &color_to_idx);
285 } else {
286 encode_packed_palette_tile(buf, &pixels, width, height, &palette, &color_to_idx);
288 }
289 } else {
290 if use_rle {
292 buf.put_u8(128);
294 encode_rle_to_buf(buf, &pixels);
295 } else {
296 encode_raw_tile(buf, tile_data);
298 }
299 }
300}
301
302#[allow(clippy::uninit_vec)] fn extract_tile(
306 full_frame: &[u8],
307 frame_width: usize,
308 x: usize,
309 y: usize,
310 width: usize,
311 height: usize,
312) -> Vec<u8> {
313 let tile_size = width * height * 4;
314 let mut tile_data = Vec::with_capacity(tile_size);
315
316 unsafe {
318 tile_data.set_len(tile_size);
319 }
320
321 let row_bytes = width * 4;
322 for row in 0..height {
323 let src_start = ((y + row) * frame_width + x) * 4;
324 let dst_start = row * row_bytes;
325 tile_data[dst_start..dst_start + row_bytes]
326 .copy_from_slice(&full_frame[src_start..src_start + row_bytes]);
327 }
328 tile_data
329}
330
331fn rgba_to_rgb24_pixels(data: &[u8]) -> Vec<u32> {
333 data.chunks_exact(4)
334 .map(|c| u32::from(c[0]) | (u32::from(c[1]) << 8) | (u32::from(c[2]) << 16))
335 .collect()
336}
337
338fn put_cpixel(buf: &mut BytesMut, pixel: u32) {
341 buf.put_u8((pixel & 0xFF) as u8); buf.put_u8(((pixel >> 8) & 0xFF) as u8); buf.put_u8(((pixel >> 16) & 0xFF) as u8); }
345
346fn encode_solid_color_tile(buf: &mut BytesMut, color: u32) {
348 buf.put_u8(1); put_cpixel(buf, color); }
351
352fn encode_raw_tile(buf: &mut BytesMut, tile_data: &[u8]) {
354 buf.put_u8(0); for chunk in tile_data.chunks_exact(4) {
357 buf.put_u8(chunk[0]); buf.put_u8(chunk[1]); buf.put_u8(chunk[2]); }
361}
362
363#[allow(clippy::cast_possible_truncation)] fn encode_packed_palette_tile(
366 buf: &mut BytesMut,
367 pixels: &[u32],
368 width: usize,
369 height: usize,
370 palette: &[u32],
371 color_to_idx: &HashMap<u32, u8>,
372) {
373 let palette_size = palette.len();
374 let bits_per_pixel = match palette_size {
375 2 => 1,
376 3..=4 => 2,
377 _ => 4,
378 };
379
380 buf.put_u8(palette_size as u8); for &color in palette {
384 put_cpixel(buf, color);
385 }
386
387 for row in 0..height {
390 let mut packed_byte = 0;
391 let mut nbits = 0;
392 let row_start = row * width;
393 let row_end = row_start + width;
394
395 for &pixel in &pixels[row_start..row_end] {
396 let idx = color_to_idx[&pixel];
397 packed_byte = (packed_byte << bits_per_pixel) | idx;
399 nbits += bits_per_pixel;
400
401 if nbits >= 8 {
402 buf.put_u8(packed_byte);
403 packed_byte = 0;
404 nbits = 0;
405 }
406 }
407
408 if nbits > 0 {
410 packed_byte <<= 8 - nbits;
411 buf.put_u8(packed_byte);
412 }
413 }
414}
415
416#[allow(clippy::cast_possible_truncation)] fn encode_packed_palette_rle_tile(
419 buf: &mut BytesMut,
420 pixels: &[u32],
421 palette: &[u32],
422 color_to_idx: &HashMap<u32, u8>,
423) {
424 let palette_size = palette.len();
425 buf.put_u8(128 | (palette_size as u8)); for &color in palette {
429 put_cpixel(buf, color);
430 }
431
432 let mut i = 0;
434 while i < pixels.len() {
435 let color = pixels[i];
436 let index = color_to_idx[&color];
437
438 let mut run_len = 1;
439 while i + run_len < pixels.len() && pixels[i + run_len] == color {
440 run_len += 1;
441 }
442
443 if run_len <= 2 {
445 if run_len == 2 {
447 buf.put_u8(index);
448 }
449 buf.put_u8(index);
450 } else {
451 buf.put_u8(index | 128); let mut remaining_len = run_len - 1;
455 while remaining_len >= 255 {
456 buf.put_u8(255);
457 remaining_len -= 255;
458 }
459 buf.put_u8(remaining_len as u8);
460 }
461 i += run_len;
462 }
463}
464
465#[allow(clippy::cast_possible_truncation)] fn encode_rle_to_buf(buf: &mut BytesMut, pixels: &[u32]) {
468 let mut i = 0;
469 while i < pixels.len() {
470 let color = pixels[i];
471 let mut run_len = 1;
472 while i + run_len < pixels.len() && pixels[i + run_len] == color {
473 run_len += 1;
474 }
475 put_cpixel(buf, color);
477
478 let mut len_to_encode = run_len - 1;
482 while len_to_encode >= 255 {
483 buf.put_u8(255);
484 len_to_encode -= 255;
485 }
486 buf.put_u8(len_to_encode as u8);
487
488 i += run_len;
489 }
490}
491
492pub struct ZrleEncoding;
494
495impl Encoding for ZrleEncoding {
496 fn encode(
497 &self,
498 data: &[u8],
499 width: u16,
500 height: u16,
501 _quality: u8,
502 compression: u8,
503 ) -> BytesMut {
504 let pixel_format = PixelFormat::rgba32(); if let Ok(encoded_data) = encode_zrle(data, width, height, &pixel_format, compression) {
507 BytesMut::from(&encoded_data[..])
508 } else {
509 let mut buf = BytesMut::with_capacity(data.len());
511 for chunk in data.chunks_exact(4) {
512 buf.put_u8(chunk[0]); buf.put_u8(chunk[1]); buf.put_u8(chunk[2]); buf.put_u8(0); }
517 buf
518 }
519 }
520}