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 +1 -1
- package/dist/roxify_native +0 -0
- package/dist/roxify_native.exe +0 -0
- package/libroxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/native/archive.rs +61 -17
- package/native/context_mixing.rs +15 -18
- package/native/core.rs +97 -12
- package/native/crypto.rs +1 -1
- package/native/encoder.rs +110 -58
- package/native/hybrid.rs +133 -30
- package/native/lib.rs +1 -0
- package/native/main.rs +55 -29
- package/native/rans_byte.rs +117 -21
- package/native/streaming.rs +214 -0
- package/package.json +1 -1
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
package/Cargo.toml
CHANGED
|
Binary file
|
|
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
|
|
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
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
package/native/context_mixing.rs
CHANGED
|
@@ -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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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::
|
|
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::
|
|
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
|
|
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
|
|
263
|
+
if total_len > 256 * 1024 && adaptive_level >= 3 {
|
|
194
264
|
let _ = encoder.long_distance_matching(true);
|
|
195
|
-
|
|
196
|
-
|
|
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(
|
|
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::
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
201
|
+
let marker_start_len = 12;
|
|
202
|
+
let marker_end_len = 9;
|
|
215
203
|
|
|
216
|
-
let
|
|
217
|
-
|
|
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
|
|
215
|
+
let marker_end_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
|
|
233
216
|
|
|
234
|
-
let
|
|
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
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
245
|
+
flat[pos..pos + PIXEL_MAGIC.len()].copy_from_slice(PIXEL_MAGIC);
|
|
246
|
+
pos += PIXEL_MAGIC.len();
|
|
254
247
|
|
|
255
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let crc =
|
|
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
|
-
|
|
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(
|
|
349
|
+
let chunk_size = (data.len() - offset).min(MAX_BLOCK);
|
|
344
350
|
let is_last = offset + chunk_size >= data.len();
|
|
345
351
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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;
|