roxify 1.10.1 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "roxify_native"
3
- version = "1.10.1"
3
+ version = "1.11.1"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
Binary file
Binary file
package/native/archive.rs CHANGED
@@ -4,7 +4,12 @@ use rayon::prelude::*;
4
4
  use tar::{Archive, Builder, Header};
5
5
  use walkdir::WalkDir;
6
6
 
7
- pub fn tar_pack_directory(dir_path: &Path) -> Result<Vec<u8>, String> {
7
+ pub struct TarPackResult {
8
+ pub data: Vec<u8>,
9
+ pub file_list: Vec<(String, u64)>,
10
+ }
11
+
12
+ pub fn tar_pack_directory_with_list(dir_path: &Path) -> Result<TarPackResult, String> {
8
13
  let base = dir_path;
9
14
 
10
15
  let entries: Vec<_> = WalkDir::new(dir_path)
@@ -27,6 +32,10 @@ pub fn tar_pack_directory(dir_path: &Path) -> Result<Vec<u8>, String> {
27
32
  })
28
33
  .collect();
29
34
 
35
+ let file_list: Vec<(String, u64)> = file_data.iter()
36
+ .map(|(name, data)| (name.clone(), data.len() as u64))
37
+ .collect();
38
+
30
39
  let total_estimate: usize = file_data.iter().map(|(n, d)| 512 + d.len() + 512 + n.len()).sum();
31
40
  let mut buf = Vec::with_capacity(total_estimate + 1024);
32
41
  {
@@ -42,20 +51,42 @@ pub fn tar_pack_directory(dir_path: &Path) -> Result<Vec<u8>, String> {
42
51
  }
43
52
  builder.finish().map_err(|e| format!("tar finish: {}", e))?;
44
53
  }
45
- Ok(buf)
54
+ Ok(TarPackResult { data: buf, file_list })
55
+ }
56
+
57
+ pub fn tar_pack_directory(dir_path: &Path) -> Result<Vec<u8>, String> {
58
+ tar_pack_directory_with_list(dir_path).map(|r| r.data)
59
+ }
60
+
61
+ pub fn tar_file_list_fast(tar_data: &[u8]) -> Vec<(String, u64)> {
62
+ let mut list = Vec::new();
63
+ let mut pos = 0;
64
+ while pos + 512 <= tar_data.len() {
65
+ let header = &tar_data[pos..pos + 512];
66
+ if header.iter().all(|&b| b == 0) {
67
+ break;
68
+ }
69
+ let name_end = header[..100].iter().position(|&b| b == 0).unwrap_or(100);
70
+ let name = String::from_utf8_lossy(&header[..name_end]).to_string();
71
+ let size_str = String::from_utf8_lossy(&header[124..136]);
72
+ let size = u64::from_str_radix(size_str.trim().trim_matches('\0'), 8).unwrap_or(0);
73
+ if !name.is_empty() {
74
+ list.push((name, size));
75
+ }
76
+ let data_blocks = (size as usize + 511) / 512;
77
+ pos += 512 + data_blocks * 512;
78
+ }
79
+ list
46
80
  }
47
81
 
48
82
  pub fn tar_unpack(tar_data: &[u8], output_dir: &Path) -> Result<Vec<String>, String> {
49
83
  let mut archive = Archive::new(Cursor::new(tar_data));
50
- let mut written = Vec::new();
84
+ let mut entries_data: Vec<(std::path::PathBuf, Vec<u8>)> = Vec::new();
51
85
 
52
86
  let entries = archive.entries().map_err(|e| format!("tar entries: {}", e))?;
53
87
  for entry in entries {
54
88
  let mut entry = entry.map_err(|e| format!("tar entry: {}", e))?;
55
- let path = entry
56
- .path()
57
- .map_err(|e| format!("tar entry path: {}", e))?
58
- .to_path_buf();
89
+ let path = entry.path().map_err(|e| format!("tar entry path: {}", e))?.to_path_buf();
59
90
 
60
91
  let mut safe = std::path::PathBuf::new();
61
92
  for comp in path.components() {
@@ -63,23 +94,36 @@ pub fn tar_unpack(tar_data: &[u8], output_dir: &Path) -> Result<Vec<String>, Str
63
94
  safe.push(osstr);
64
95
  }
65
96
  }
66
-
67
97
  if safe.as_os_str().is_empty() {
68
98
  continue;
69
99
  }
70
100
 
71
- let dest = output_dir.join(&safe);
72
- if let Some(parent) = dest.parent() {
73
- std::fs::create_dir_all(parent)
74
- .map_err(|e| format!("mkdir {:?}: {}", parent, e))?;
75
- }
101
+ let mut data = Vec::with_capacity(entry.size() as usize);
102
+ std::io::Read::read_to_end(&mut entry, &mut data)
103
+ .map_err(|e| format!("tar read {:?}: {}", safe, e))?;
104
+ entries_data.push((safe, data));
105
+ }
76
106
 
77
- entry
78
- .unpack(&dest)
79
- .map_err(|e| format!("tar unpack {:?}: {}", dest, e))?;
80
- written.push(safe.to_string_lossy().to_string());
107
+ let dirs: std::collections::HashSet<_> = entries_data.iter()
108
+ .filter_map(|(p, _)| {
109
+ let dest = output_dir.join(p);
110
+ dest.parent().map(|d| d.to_path_buf())
111
+ })
112
+ .collect();
113
+ for dir in &dirs {
114
+ std::fs::create_dir_all(dir).map_err(|e| format!("mkdir {:?}: {}", dir, e))?;
81
115
  }
82
116
 
117
+ let written: Vec<String> = entries_data.par_iter()
118
+ .filter_map(|(safe, data)| {
119
+ let dest = output_dir.join(safe);
120
+ match std::fs::write(&dest, data) {
121
+ Ok(_) => Some(safe.to_string_lossy().to_string()),
122
+ Err(_) => None,
123
+ }
124
+ })
125
+ .collect();
126
+
83
127
  Ok(written)
84
128
  }
85
129
 
@@ -1,5 +1,3 @@
1
- use rayon::prelude::*;
2
-
3
1
  #[derive(Clone, Copy, Debug)]
4
2
  pub struct ProbabilityEstimate {
5
3
  pub p0: u32,
@@ -91,26 +89,25 @@ impl ContextMixer {
91
89
  }
92
90
 
93
91
  pub fn analyze_entropy(data: &[u8]) -> f32 {
94
- let freq: Vec<u32> = {
95
- let mut f = vec![0u32; 256];
96
- for &byte in data {
97
- f[byte as usize] += 1;
98
- }
99
- f
100
- };
92
+ let mut freq = [0u32; 256];
93
+ for &byte in data {
94
+ freq[byte as usize] += 1;
95
+ }
101
96
 
102
- let total: u32 = freq.iter().sum();
103
- if total == 0 {
97
+ let total = data.len() as f32;
98
+ if total == 0.0 {
104
99
  return 0.0;
105
100
  }
106
101
 
107
- freq.par_iter()
108
- .filter(|&&f| f > 0)
109
- .map(|&f| {
110
- let p = (f as f32) / (total as f32);
111
- -p * p.log2()
112
- })
113
- .sum()
102
+ let inv_total = 1.0 / total;
103
+ let mut entropy = 0.0f32;
104
+ for &f in &freq {
105
+ if f > 0 {
106
+ let p = f as f32 * inv_total;
107
+ entropy -= p * p.log2();
108
+ }
109
+ }
110
+ entropy
114
111
  }
115
112
 
116
113
  pub fn estimate_compression_gain(original: &[u8], entropy_bits: f32) -> f64 {
package/native/core.rs CHANGED
@@ -85,15 +85,55 @@ pub fn crc32_bytes(buf: &[u8]) -> u32 {
85
85
 
86
86
  pub fn adler32_bytes(buf: &[u8]) -> u32 {
87
87
  const MOD: u32 = 65521;
88
+ const NMAX: usize = 5552;
89
+
90
+ if buf.len() > 4 * 1024 * 1024 {
91
+ return adler32_parallel(buf);
92
+ }
93
+
88
94
  let mut a: u32 = 1;
89
95
  let mut b: u32 = 0;
90
- for &v in buf {
91
- a = (a + v as u32) % MOD;
92
- b = (b + a) % MOD;
96
+
97
+ for chunk in buf.chunks(NMAX) {
98
+ for &v in chunk {
99
+ a += v as u32;
100
+ b += a;
101
+ }
102
+ a %= MOD;
103
+ b %= MOD;
93
104
  }
105
+
94
106
  (b << 16) | a
95
107
  }
96
108
 
109
+ fn adler32_parallel(buf: &[u8]) -> u32 {
110
+ use rayon::prelude::*;
111
+ const MOD: u32 = 65521;
112
+ const CHUNK: usize = 1024 * 1024;
113
+
114
+ let chunks: Vec<&[u8]> = buf.chunks(CHUNK).collect();
115
+ let partials: Vec<(u32, u32, usize)> = chunks.par_iter().map(|chunk| {
116
+ let mut a: u32 = 0;
117
+ let mut b: u32 = 0;
118
+ for &v in *chunk {
119
+ a += v as u32;
120
+ b += a;
121
+ }
122
+ a %= MOD;
123
+ b %= MOD;
124
+ (a, b, chunk.len())
125
+ }).collect();
126
+
127
+ let mut a: u64 = 1;
128
+ let mut b: u64 = 0;
129
+ for (pa, pb, len) in partials {
130
+ b = (b + pb as u64 + a * len as u64) % MOD as u64;
131
+ a = (a + pa as u64) % MOD as u64;
132
+ }
133
+
134
+ ((b as u32) << 16) | (a as u32)
135
+ }
136
+
97
137
  pub fn delta_encode_bytes(buf: &[u8]) -> Vec<u8> {
98
138
  let len = buf.len();
99
139
  if len == 0 {
@@ -173,40 +213,85 @@ pub fn train_zstd_dictionary(sample_paths: &[PathBuf], dict_size: usize) -> Resu
173
213
  /// For large buffers (>50 MiB) without a dictionary, multiple chunk sizes
174
214
  /// are benchmarked on a sample and the best is selected automatically.
175
215
  pub fn zstd_compress_bytes(buf: &[u8], level: i32, dict: Option<&[u8]>) -> std::result::Result<Vec<u8>, String> {
216
+ zstd_compress_with_prefix(buf, level, dict, &[])
217
+ }
218
+
219
+ pub fn zstd_compress_with_prefix(buf: &[u8], level: i32, dict: Option<&[u8]>, prefix: &[u8]) -> std::result::Result<Vec<u8>, String> {
176
220
  use std::io::Write;
177
221
 
178
222
  let actual_level = level.min(22).max(1);
223
+ let total_len = prefix.len() + buf.len();
224
+
225
+ let adaptive_level = if total_len > 2 * 1024 * 1024 * 1024 {
226
+ actual_level.min(1)
227
+ } else if total_len > 1024 * 1024 * 1024 {
228
+ actual_level.min(3)
229
+ } else if total_len > 256 * 1024 * 1024 {
230
+ actual_level.min(6)
231
+ } else if total_len > 64 * 1024 * 1024 {
232
+ actual_level.min(12)
233
+ } else {
234
+ actual_level
235
+ };
236
+
237
+ if dict.is_none() && total_len < 4 * 1024 * 1024 {
238
+ if prefix.is_empty() {
239
+ return zstd::bulk::compress(buf, adaptive_level)
240
+ .map_err(|e| format!("zstd bulk compress error: {}", e));
241
+ }
242
+ let mut combined = Vec::with_capacity(total_len);
243
+ combined.extend_from_slice(prefix);
244
+ combined.extend_from_slice(buf);
245
+ return zstd::bulk::compress(&combined, adaptive_level)
246
+ .map_err(|e| format!("zstd bulk compress error: {}", e));
247
+ }
248
+
179
249
  let mut encoder = if let Some(d) = dict {
180
- zstd::stream::Encoder::with_dictionary(Vec::new(), actual_level, d)
250
+ zstd::stream::Encoder::with_dictionary(Vec::with_capacity(total_len / 2), adaptive_level, d)
181
251
  .map_err(|e| format!("zstd encoder init error: {}", e))?
182
252
  } else {
183
- zstd::stream::Encoder::new(Vec::new(), actual_level)
253
+ zstd::stream::Encoder::new(Vec::with_capacity(total_len / 2), adaptive_level)
184
254
  .map_err(|e| format!("zstd encoder init error: {}", e))?
185
255
  };
186
256
 
187
257
  let threads = num_cpus::get() as u32;
188
258
  if threads > 1 {
189
- let max_threads = if actual_level >= 20 { threads.min(4) } else { threads };
259
+ let max_threads = if adaptive_level >= 20 { threads.min(4) } else { threads };
190
260
  let _ = encoder.multithread(max_threads);
191
261
  }
192
262
 
193
- if buf.len() > 1024 * 1024 {
263
+ if total_len > 256 * 1024 && adaptive_level >= 3 {
194
264
  let _ = encoder.long_distance_matching(true);
195
- let wlog = if buf.len() > 512 * 1024 * 1024 { 28 }
196
- else if buf.len() > 64 * 1024 * 1024 { 27 }
265
+ }
266
+ if total_len > 256 * 1024 {
267
+ let wlog = if total_len > 1024 * 1024 * 1024 { 30 }
268
+ else if total_len > 512 * 1024 * 1024 { 29 }
269
+ else if total_len > 64 * 1024 * 1024 { 28 }
270
+ else if total_len > 8 * 1024 * 1024 { 27 }
197
271
  else { 26 };
198
272
  let _ = encoder.window_log(wlog);
199
273
  }
200
274
 
201
- let _ = encoder.set_pledged_src_size(Some(buf.len() as u64));
275
+ let _ = encoder.set_pledged_src_size(Some(total_len as u64));
276
+
277
+ if !prefix.is_empty() {
278
+ encoder.write_all(prefix).map_err(|e| format!("zstd write prefix error: {}", e))?;
279
+ }
280
+
281
+ let chunk_size = if total_len > 256 * 1024 * 1024 { 16 * 1024 * 1024 }
282
+ else if total_len > 64 * 1024 * 1024 { 8 * 1024 * 1024 }
283
+ else { buf.len() };
284
+
285
+ for chunk in buf.chunks(chunk_size) {
286
+ encoder.write_all(chunk).map_err(|e| format!("zstd write error: {}", e))?;
287
+ }
202
288
 
203
- encoder.write_all(buf).map_err(|e| format!("zstd write error: {}", e))?;
204
289
  encoder.finish().map_err(|e| format!("zstd finish error: {}", e))
205
290
  }
206
291
 
207
292
  pub fn zstd_decompress_bytes(buf: &[u8], dict: Option<&[u8]>) -> std::result::Result<Vec<u8>, String> {
208
293
  use std::io::Read;
209
- let mut out = Vec::new();
294
+ let mut out = Vec::with_capacity(buf.len() * 2);
210
295
  if let Some(d) = dict {
211
296
  let mut decoder = zstd::stream::Decoder::with_dictionary(std::io::Cursor::new(buf), d)
212
297
  .map_err(|e| format!("zstd decoder init error: {}", e))?;
package/native/crypto.rs CHANGED
@@ -10,7 +10,7 @@ use sha2::Sha256;
10
10
  const ENC_NONE: u8 = 0x00;
11
11
  const ENC_AES: u8 = 0x01;
12
12
  const ENC_XOR: u8 = 0x02;
13
- const PBKDF2_ITERS: u32 = 1_000_000;
13
+ const PBKDF2_ITERS: u32 = 600_000;
14
14
 
15
15
  pub fn encrypt_xor(data: &[u8], passphrase: &str) -> Vec<u8> {
16
16
  let key = passphrase.as_bytes();
package/native/encoder.rs CHANGED
@@ -2,7 +2,6 @@ use anyhow::Result;
2
2
  use std::process::{Command, Stdio};
3
3
 
4
4
  const MAGIC: &[u8] = b"ROX1";
5
- const ENC_NONE: u8 = 0x00;
6
5
  const PIXEL_MAGIC: &[u8] = b"PXL1";
7
6
  const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
8
7
 
@@ -112,9 +111,7 @@ pub fn encode_to_wav_with_encryption_name_and_filelist(
112
111
  file_list: Option<&str>,
113
112
  ) -> Result<Vec<u8>> {
114
113
  // Same compression + encryption pipeline as PNG
115
- let payload_input = [MAGIC, data].concat();
116
-
117
- let compressed = crate::core::zstd_compress_bytes(&payload_input, compression_level, None)
114
+ let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, None, MAGIC)
118
115
  .map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
119
116
 
120
117
  let encrypted = if let Some(pass) = passphrase {
@@ -180,9 +177,7 @@ fn encode_to_png_with_encryption_name_and_filelist_internal(
180
177
  file_list: Option<&str>,
181
178
  dict: Option<&[u8]>,
182
179
  ) -> Result<Vec<u8>> {
183
- let payload_input = [MAGIC, data].concat();
184
-
185
- let compressed = crate::core::zstd_compress_bytes(&payload_input, compression_level, dict)
180
+ let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, dict, MAGIC)
186
181
  .map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
187
182
 
188
183
  let encrypted = if let Some(pass) = passphrase {
@@ -194,65 +189,73 @@ fn encode_to_png_with_encryption_name_and_filelist_internal(
194
189
  } else {
195
190
  crate::crypto::no_encryption(&compressed)
196
191
  };
192
+ drop(compressed);
197
193
 
198
194
  let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
199
- let data_without_markers = [PIXEL_MAGIC, &meta_pixel].concat();
200
-
201
- let padding_needed = (3 - (data_without_markers.len() % 3)) % 3;
202
- let padded_data = if padding_needed > 0 {
203
- [&data_without_markers[..], &vec![0u8; padding_needed]].concat()
204
- } else {
205
- data_without_markers
206
- };
195
+ drop(encrypted);
207
196
 
208
- let mut marker_bytes = Vec::with_capacity(12);
209
- for m in &MARKER_START {
210
- marker_bytes.extend_from_slice(&[m.0, m.1, m.2]);
211
- }
212
- marker_bytes.extend_from_slice(&[MARKER_ZSTD.0, MARKER_ZSTD.1, MARKER_ZSTD.2]);
197
+ let raw_payload_len = PIXEL_MAGIC.len() + meta_pixel.len();
198
+ let padding_needed = (3 - (raw_payload_len % 3)) % 3;
199
+ let padded_len = raw_payload_len + padding_needed;
213
200
 
214
- let data_with_markers = [&marker_bytes[..], &padded_data[..]].concat();
201
+ let marker_start_len = 12;
202
+ let marker_end_len = 9;
215
203
 
216
- let mut marker_end_bytes = Vec::with_capacity(9);
217
- for m in &MARKER_END {
218
- marker_end_bytes.extend_from_slice(&[m.0, m.1, m.2]);
219
- }
220
-
221
- let data_pixels = (data_with_markers.len() + 2) / 3;
204
+ let data_with_markers_len = marker_start_len + padded_len;
205
+ let data_pixels = (data_with_markers_len + 2) / 3;
222
206
  let end_marker_pixels = 3;
223
207
  let total_pixels = data_pixels + end_marker_pixels;
224
208
 
225
209
  let side = (total_pixels as f64).sqrt().ceil() as usize;
226
210
  let side = side.max(end_marker_pixels);
227
-
228
211
  let width = side;
229
212
  let height = side;
230
213
 
231
214
  let total_data_bytes = width * height * 3;
232
- let mut full_data = vec![0u8; total_data_bytes];
215
+ let marker_end_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
233
216
 
234
- let marker_start_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
217
+ let flat = build_flat_pixel_buffer(&meta_pixel, padding_needed, marker_end_pos, marker_end_len, total_data_bytes);
218
+ drop(meta_pixel);
235
219
 
236
- let copy_len = data_with_markers.len().min(marker_start_pos);
237
- full_data[..copy_len].copy_from_slice(&data_with_markers[..copy_len]);
220
+ let row_bytes = width * 3;
221
+ let idat_data = create_raw_deflate_from_rows(&flat, row_bytes, height);
222
+ drop(flat);
238
223
 
239
- let end_len = marker_end_bytes.len().min(total_data_bytes - marker_start_pos);
240
- full_data[marker_start_pos..marker_start_pos + end_len]
241
- .copy_from_slice(&marker_end_bytes[..end_len]);
242
-
243
- let stride = width * 3 + 1;
244
- let mut scanlines = Vec::with_capacity(height * stride);
224
+ build_png(width, height, &idat_data, file_list)
225
+ }
245
226
 
246
- for row in 0..height {
247
- scanlines.push(0u8);
248
- let src_start = row * width * 3;
249
- let src_end = (row + 1) * width * 3;
250
- scanlines.extend_from_slice(&full_data[src_start..src_end]);
227
+ fn build_flat_pixel_buffer(
228
+ meta_pixel: &[u8],
229
+ padding_needed: usize,
230
+ marker_end_pos: usize,
231
+ marker_end_len: usize,
232
+ total_data_bytes: usize,
233
+ ) -> Vec<u8> {
234
+ let header_len = 12 + PIXEL_MAGIC.len() + meta_pixel.len() + padding_needed;
235
+ let mut flat = vec![0u8; total_data_bytes];
236
+
237
+ let mut pos = 0;
238
+ for m in &MARKER_START {
239
+ flat[pos] = m.0; flat[pos + 1] = m.1; flat[pos + 2] = m.2;
240
+ pos += 3;
251
241
  }
242
+ flat[pos] = MARKER_ZSTD.0; flat[pos + 1] = MARKER_ZSTD.1; flat[pos + 2] = MARKER_ZSTD.2;
243
+ pos += 3;
252
244
 
253
- let idat_data = create_raw_deflate(&scanlines);
245
+ flat[pos..pos + PIXEL_MAGIC.len()].copy_from_slice(PIXEL_MAGIC);
246
+ pos += PIXEL_MAGIC.len();
254
247
 
255
- build_png(width, height, &idat_data, file_list)
248
+ flat[pos..pos + meta_pixel.len()].copy_from_slice(meta_pixel);
249
+ pos += meta_pixel.len();
250
+
251
+ if marker_end_pos + marker_end_len <= total_data_bytes {
252
+ for (i, m) in MARKER_END.iter().enumerate() {
253
+ let off = marker_end_pos + i * 3;
254
+ flat[off] = m.0; flat[off + 1] = m.1; flat[off + 2] = m.2;
255
+ }
256
+ }
257
+
258
+ flat
256
259
  }
257
260
 
258
261
  fn build_meta_pixel(payload: &[u8]) -> Result<Vec<u8>> {
@@ -323,33 +326,37 @@ fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) -> Result<(
323
326
  out.extend_from_slice(chunk_type);
324
327
  out.extend_from_slice(data);
325
328
 
326
- let mut crc_data = Vec::with_capacity(chunk_type.len() + data.len());
327
- crc_data.extend_from_slice(chunk_type);
328
- crc_data.extend_from_slice(data);
329
- let crc = crate::core::crc32_bytes(&crc_data);
329
+ let mut hasher = crc32fast::Hasher::new();
330
+ hasher.update(chunk_type);
331
+ hasher.update(data);
332
+ let crc = hasher.finalize();
330
333
 
331
334
  out.extend_from_slice(&crc.to_be_bytes());
332
335
  Ok(())
333
336
  }
334
337
 
335
338
  fn create_raw_deflate(data: &[u8]) -> Vec<u8> {
336
- let mut result = Vec::with_capacity(data.len() + 6 + (data.len() / 65535 + 1) * 5);
339
+ const MAX_BLOCK: usize = 65535;
340
+ let num_blocks = (data.len() + MAX_BLOCK - 1) / MAX_BLOCK;
341
+ let total_size = 2 + num_blocks * 5 + data.len() + 4;
342
+ let mut result = Vec::with_capacity(total_size);
337
343
 
338
344
  result.push(0x78);
339
345
  result.push(0x01);
340
346
 
341
347
  let mut offset = 0;
342
348
  while offset < data.len() {
343
- let chunk_size = (data.len() - offset).min(65535);
349
+ let chunk_size = (data.len() - offset).min(MAX_BLOCK);
344
350
  let is_last = offset + chunk_size >= data.len();
345
351
 
346
- result.push(if is_last { 0x01 } else { 0x00 });
347
-
348
- result.push(chunk_size as u8);
349
- result.push((chunk_size >> 8) as u8);
350
- result.push(!chunk_size as u8);
351
- result.push((!(chunk_size >> 8)) as u8);
352
-
352
+ let header = [
353
+ if is_last { 0x01 } else { 0x00 },
354
+ chunk_size as u8,
355
+ (chunk_size >> 8) as u8,
356
+ !chunk_size as u8,
357
+ (!(chunk_size >> 8)) as u8,
358
+ ];
359
+ result.extend_from_slice(&header);
353
360
  result.extend_from_slice(&data[offset..offset + chunk_size]);
354
361
  offset += chunk_size;
355
362
  }
@@ -360,6 +367,51 @@ fn create_raw_deflate(data: &[u8]) -> Vec<u8> {
360
367
  result
361
368
  }
362
369
 
370
+ fn create_raw_deflate_from_rows(flat: &[u8], row_bytes: usize, height: usize) -> Vec<u8> {
371
+ let stride = row_bytes + 1;
372
+ let scanlines_total = height * stride;
373
+
374
+ let mut scanlines = vec![0u8; scanlines_total];
375
+ for row in 0..height {
376
+ let flat_start = row * row_bytes;
377
+ let flat_end = (flat_start + row_bytes).min(flat.len());
378
+ let copy_len = flat_end.saturating_sub(flat_start);
379
+ if copy_len > 0 {
380
+ let dst_start = row * stride + 1;
381
+ scanlines[dst_start..dst_start + copy_len].copy_from_slice(&flat[flat_start..flat_end]);
382
+ }
383
+ }
384
+
385
+ const MAX_BLOCK: usize = 65535;
386
+ let num_blocks = (scanlines_total + MAX_BLOCK - 1) / MAX_BLOCK;
387
+ let total_size = 2 + num_blocks * 5 + scanlines_total + 4;
388
+ let mut result = Vec::with_capacity(total_size);
389
+
390
+ result.push(0x78);
391
+ result.push(0x01);
392
+
393
+ let mut offset = 0;
394
+ while offset < scanlines.len() {
395
+ let chunk_size = (scanlines.len() - offset).min(MAX_BLOCK);
396
+ let is_last = offset + chunk_size >= scanlines.len();
397
+ let header = [
398
+ if is_last { 0x01 } else { 0x00 },
399
+ chunk_size as u8,
400
+ (chunk_size >> 8) as u8,
401
+ !chunk_size as u8,
402
+ (!(chunk_size >> 8)) as u8,
403
+ ];
404
+ result.extend_from_slice(&header);
405
+ result.extend_from_slice(&scanlines[offset..offset + chunk_size]);
406
+ offset += chunk_size;
407
+ }
408
+
409
+ let adler = crate::core::adler32_bytes(&scanlines);
410
+ result.extend_from_slice(&adler.to_be_bytes());
411
+
412
+ result
413
+ }
414
+
363
415
  fn predict_best_format(data: &[u8]) -> ImageFormat {
364
416
  if data.len() < 2048 {
365
417
  return ImageFormat::Png;