roxify 1.13.1 → 1.13.3
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 +2 -11
- package/README.md +22 -144
- package/dist/cli.js +15 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/rox-macos-universal +0 -0
- package/dist/roxify_native +0 -0
- package/dist/roxify_native-macos-arm64 +0 -0
- package/dist/roxify_native-macos-x64 +0 -0
- package/dist/roxify_native.exe +0 -0
- package/dist/utils/rust-cli-wrapper.d.ts +3 -2
- package/dist/utils/rust-cli-wrapper.js +26 -6
- package/native/bench_hybrid.rs +1 -1
- package/native/core.rs +3 -48
- package/native/crypto.rs +5 -0
- package/native/encoder.rs +10 -228
- package/native/hybrid.rs +17 -7
- package/native/lib.rs +54 -65
- package/native/main.rs +20 -3
- package/native/progress.rs +117 -18
- package/native/streaming_decode.rs +91 -7
- package/native/streaming_encode.rs +77 -50
- package/package.json +1 -1
- package/roxify_native-aarch64-apple-darwin.node +0 -0
- package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
- package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
- package/roxify_native-i686-pc-windows-msvc.node +0 -0
- package/roxify_native-i686-unknown-linux-gnu.node +0 -0
- package/roxify_native-x86_64-apple-darwin.node +0 -0
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/dist/roxify-cli +0 -0
- package/libroxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/native/gpu.rs +0 -116
package/native/encoder.rs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
|
-
use std::process::{Command, Stdio};
|
|
3
2
|
|
|
4
3
|
const MAGIC: &[u8] = b"ROX1";
|
|
5
4
|
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
@@ -12,54 +11,23 @@ const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
|
|
|
12
11
|
#[derive(Debug, Clone, Copy)]
|
|
13
12
|
pub enum ImageFormat {
|
|
14
13
|
Png,
|
|
15
|
-
WebP,
|
|
16
|
-
JpegXL,
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
pub fn encode_to_png(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
|
|
20
|
-
|
|
21
|
-
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, None, None, format, None, None, None)
|
|
17
|
+
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, None, None, None)
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
|
|
25
21
|
pub fn encode_to_png_with_name(data: &[u8], compression_level: i32, name: Option<&str>) -> Result<Vec<u8>> {
|
|
26
|
-
|
|
27
|
-
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, None, None, format, name, None, None)
|
|
22
|
+
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, name, None, None)
|
|
28
23
|
}
|
|
29
24
|
|
|
30
25
|
pub fn encode_to_png_with_name_and_filelist(data: &[u8], compression_level: i32, name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
fn predict_best_format_raw(data: &[u8]) -> ImageFormat {
|
|
35
|
-
if data.len() < 512 {
|
|
36
|
-
return ImageFormat::Png;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let sample_size = data.len().min(4096);
|
|
40
|
-
let sample = &data[..sample_size];
|
|
41
|
-
|
|
42
|
-
let entropy = calculate_shannon_entropy(sample);
|
|
43
|
-
let repetition_score = detect_repetition_patterns(sample);
|
|
44
|
-
let unique_bytes = count_unique_bytes(sample);
|
|
45
|
-
let unique_ratio = unique_bytes as f64 / 256.0;
|
|
46
|
-
let is_sequential = detect_sequential_pattern(sample);
|
|
47
|
-
|
|
48
|
-
if entropy > 7.8 {
|
|
49
|
-
ImageFormat::Png
|
|
50
|
-
} else if is_sequential || repetition_score > 0.15 {
|
|
51
|
-
ImageFormat::JpegXL
|
|
52
|
-
} else if unique_ratio < 0.4 && entropy < 6.5 {
|
|
53
|
-
ImageFormat::JpegXL
|
|
54
|
-
} else if entropy < 5.0 {
|
|
55
|
-
ImageFormat::JpegXL
|
|
56
|
-
} else {
|
|
57
|
-
ImageFormat::Png
|
|
58
|
-
}
|
|
26
|
+
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, name, file_list, None)
|
|
59
27
|
}
|
|
60
28
|
|
|
61
29
|
pub fn encode_to_png_raw(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
|
|
62
|
-
|
|
30
|
+
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, None, None, None)
|
|
63
31
|
}
|
|
64
32
|
|
|
65
33
|
pub fn encode_to_png_with_encryption_and_name(
|
|
@@ -69,8 +37,7 @@ pub fn encode_to_png_with_encryption_and_name(
|
|
|
69
37
|
encrypt_type: Option<&str>,
|
|
70
38
|
name: Option<&str>,
|
|
71
39
|
) -> Result<Vec<u8>> {
|
|
72
|
-
|
|
73
|
-
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, passphrase, encrypt_type, format, name, None, None)
|
|
40
|
+
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, None, None)
|
|
74
41
|
}
|
|
75
42
|
|
|
76
43
|
pub fn encode_to_png_with_encryption_name_and_filelist(
|
|
@@ -81,7 +48,7 @@ pub fn encode_to_png_with_encryption_name_and_filelist(
|
|
|
81
48
|
name: Option<&str>,
|
|
82
49
|
file_list: Option<&str>,
|
|
83
50
|
) -> Result<Vec<u8>> {
|
|
84
|
-
|
|
51
|
+
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, file_list, None)
|
|
85
52
|
}
|
|
86
53
|
|
|
87
54
|
// ─── WAV container encoding ─────────────────────────────────────────────────
|
|
@@ -121,15 +88,13 @@ pub fn encode_to_wav_with_encryption_name_and_filelist(
|
|
|
121
88
|
_ => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
122
89
|
}
|
|
123
90
|
} else {
|
|
124
|
-
crate::crypto::
|
|
91
|
+
crate::crypto::no_encryption_in_place(compressed)
|
|
125
92
|
};
|
|
126
93
|
|
|
127
94
|
let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
|
|
128
95
|
|
|
129
|
-
// Prepend PIXEL_MAGIC so decoder can validate the payload
|
|
130
96
|
let wav_payload = [PIXEL_MAGIC, &meta_pixel].concat();
|
|
131
97
|
|
|
132
|
-
// Wrap in WAV container (44 bytes overhead, constant)
|
|
133
98
|
Ok(crate::audio::bytes_to_wav(&wav_payload))
|
|
134
99
|
}
|
|
135
100
|
|
|
@@ -144,28 +109,12 @@ pub fn encode_to_png_with_encryption_name_and_format_and_filelist(
|
|
|
144
109
|
compression_level: i32,
|
|
145
110
|
passphrase: Option<&str>,
|
|
146
111
|
encrypt_type: Option<&str>,
|
|
147
|
-
|
|
112
|
+
_format: ImageFormat,
|
|
148
113
|
name: Option<&str>,
|
|
149
114
|
file_list: Option<&str>,
|
|
150
115
|
dict: Option<&[u8]>,
|
|
151
116
|
) -> Result<Vec<u8>> {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
match format {
|
|
155
|
-
ImageFormat::Png => Ok(png),
|
|
156
|
-
ImageFormat::WebP => {
|
|
157
|
-
match optimize_to_webp(&png) {
|
|
158
|
-
Ok(optimized) => reconvert_to_png(&optimized, "webp").or_else(|_| Ok(png)),
|
|
159
|
-
Err(_) => Ok(png),
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
ImageFormat::JpegXL => {
|
|
163
|
-
match optimize_to_jxl(&png) {
|
|
164
|
-
Ok(optimized) => reconvert_to_png(&optimized, "jxl").or_else(|_| Ok(png)),
|
|
165
|
-
Err(_) => Ok(png),
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
}
|
|
117
|
+
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, file_list, dict)
|
|
169
118
|
}
|
|
170
119
|
|
|
171
120
|
fn encode_to_png_with_encryption_name_and_filelist_internal(
|
|
@@ -187,9 +136,8 @@ fn encode_to_png_with_encryption_name_and_filelist_internal(
|
|
|
187
136
|
_ => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
188
137
|
}
|
|
189
138
|
} else {
|
|
190
|
-
crate::crypto::
|
|
139
|
+
crate::crypto::no_encryption_in_place(compressed)
|
|
191
140
|
};
|
|
192
|
-
drop(compressed);
|
|
193
141
|
|
|
194
142
|
let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
|
|
195
143
|
drop(encrypted);
|
|
@@ -369,51 +317,6 @@ fn create_raw_deflate_from_rows(flat: &[u8], row_bytes: usize, height: usize) ->
|
|
|
369
317
|
result
|
|
370
318
|
}
|
|
371
319
|
|
|
372
|
-
fn detect_sequential_pattern(data: &[u8]) -> bool {
|
|
373
|
-
if data.len() < 256 {
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
let check_len = data.len().min(256);
|
|
378
|
-
let mut sequential = 0;
|
|
379
|
-
|
|
380
|
-
for i in 0..check_len - 1 {
|
|
381
|
-
let diff = (data[i + 1] as i16 - data[i] as i16).abs();
|
|
382
|
-
if diff <= 1 {
|
|
383
|
-
sequential += 1;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
sequential as f64 / (check_len - 1) as f64 > 0.6
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
fn count_unique_bytes(data: &[u8]) -> usize {
|
|
391
|
-
let mut seen = [false; 256];
|
|
392
|
-
for &byte in data {
|
|
393
|
-
seen[byte as usize] = true;
|
|
394
|
-
}
|
|
395
|
-
seen.iter().filter(|&&x| x).count()
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
fn calculate_shannon_entropy(data: &[u8]) -> f64 {
|
|
399
|
-
let mut freq = [0u32; 256];
|
|
400
|
-
for &byte in data {
|
|
401
|
-
freq[byte as usize] += 1;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
let len = data.len() as f64;
|
|
405
|
-
let mut entropy = 0.0;
|
|
406
|
-
|
|
407
|
-
for &count in &freq {
|
|
408
|
-
if count > 0 {
|
|
409
|
-
let p = count as f64 / len;
|
|
410
|
-
entropy -= p * p.log2();
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
entropy
|
|
415
|
-
}
|
|
416
|
-
|
|
417
320
|
#[cfg(test)]
|
|
418
321
|
mod tests {
|
|
419
322
|
use super::*;
|
|
@@ -512,124 +415,3 @@ mod tests {
|
|
|
512
415
|
}
|
|
513
416
|
}
|
|
514
417
|
}
|
|
515
|
-
|
|
516
|
-
fn detect_repetition_patterns(data: &[u8]) -> f64 {
|
|
517
|
-
if data.len() < 4 {
|
|
518
|
-
return 0.0;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
let mut repetitions = 0;
|
|
522
|
-
let mut total_checks = 0;
|
|
523
|
-
|
|
524
|
-
for i in 0..data.len().min(1024) {
|
|
525
|
-
if i + 3 < data.len() {
|
|
526
|
-
let byte = data[i];
|
|
527
|
-
if data[i + 1] == byte && data[i + 2] == byte && data[i + 3] == byte {
|
|
528
|
-
repetitions += 1;
|
|
529
|
-
}
|
|
530
|
-
total_checks += 1;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if total_checks > 0 {
|
|
535
|
-
repetitions as f64 / total_checks as f64
|
|
536
|
-
} else {
|
|
537
|
-
0.0
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
fn optimize_to_webp(png_data: &[u8]) -> Result<Vec<u8>> {
|
|
542
|
-
use std::fs;
|
|
543
|
-
|
|
544
|
-
let tmp_dir = std::env::temp_dir();
|
|
545
|
-
let id = rand::random::<u64>();
|
|
546
|
-
let tmp_in = tmp_dir.join(format!("roxify_{}_in.png", id));
|
|
547
|
-
let tmp_out = tmp_dir.join(format!("roxify_{}_out.webp", id));
|
|
548
|
-
|
|
549
|
-
fs::write(&tmp_in, png_data)?;
|
|
550
|
-
|
|
551
|
-
let status = Command::new("cwebp")
|
|
552
|
-
.args(&["-lossless", &tmp_in.to_string_lossy(), "-o", &tmp_out.to_string_lossy()])
|
|
553
|
-
.stderr(Stdio::null())
|
|
554
|
-
.stdout(Stdio::null())
|
|
555
|
-
.status()?;
|
|
556
|
-
|
|
557
|
-
if status.success() {
|
|
558
|
-
let result = fs::read(&tmp_out)?;
|
|
559
|
-
let _ = fs::remove_file(&tmp_in);
|
|
560
|
-
let _ = fs::remove_file(&tmp_out);
|
|
561
|
-
Ok(result)
|
|
562
|
-
} else {
|
|
563
|
-
let _ = fs::remove_file(&tmp_in);
|
|
564
|
-
let _ = fs::remove_file(&tmp_out);
|
|
565
|
-
Err(anyhow::anyhow!("WebP conversion failed"))
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
fn optimize_to_jxl(png_data: &[u8]) -> Result<Vec<u8>> {
|
|
570
|
-
use std::fs;
|
|
571
|
-
|
|
572
|
-
let tmp_dir = std::env::temp_dir();
|
|
573
|
-
let id = rand::random::<u64>();
|
|
574
|
-
let tmp_in = tmp_dir.join(format!("roxify_{}_in.png", id));
|
|
575
|
-
let tmp_out = tmp_dir.join(format!("roxify_{}_out.jxl", id));
|
|
576
|
-
|
|
577
|
-
fs::write(&tmp_in, png_data)?;
|
|
578
|
-
|
|
579
|
-
let status = Command::new("cjxl")
|
|
580
|
-
.args(&[&tmp_in.to_string_lossy() as &str, &tmp_out.to_string_lossy() as &str, "-d", "0", "-e", "9"])
|
|
581
|
-
.stderr(Stdio::null())
|
|
582
|
-
.stdout(Stdio::null())
|
|
583
|
-
.status()?;
|
|
584
|
-
|
|
585
|
-
if status.success() {
|
|
586
|
-
let result = fs::read(&tmp_out)?;
|
|
587
|
-
let _ = fs::remove_file(&tmp_in);
|
|
588
|
-
let _ = fs::remove_file(&tmp_out);
|
|
589
|
-
Ok(result)
|
|
590
|
-
} else {
|
|
591
|
-
let _ = fs::remove_file(&tmp_in);
|
|
592
|
-
let _ = fs::remove_file(&tmp_out);
|
|
593
|
-
Err(anyhow::anyhow!("JXL conversion failed"))
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
fn reconvert_to_png(data: &[u8], original_format: &str) -> Result<Vec<u8>> {
|
|
598
|
-
use std::fs;
|
|
599
|
-
|
|
600
|
-
let tmp_dir = std::env::temp_dir();
|
|
601
|
-
let id = rand::random::<u64>();
|
|
602
|
-
let tmp_in = match original_format {
|
|
603
|
-
"webp" => tmp_dir.join(format!("roxify_{}_reconvert_in.webp", id)),
|
|
604
|
-
"jxl" => tmp_dir.join(format!("roxify_{}_reconvert_in.jxl", id)),
|
|
605
|
-
_ => return Err(anyhow::anyhow!("Unknown format")),
|
|
606
|
-
};
|
|
607
|
-
let tmp_out = tmp_dir.join(format!("roxify_{}_reconvert_out.png", id));
|
|
608
|
-
|
|
609
|
-
fs::write(&tmp_in, data)?;
|
|
610
|
-
|
|
611
|
-
let status = match original_format {
|
|
612
|
-
"webp" => Command::new("dwebp")
|
|
613
|
-
.args(&[&tmp_in.to_string_lossy() as &str, "-o", &tmp_out.to_string_lossy() as &str])
|
|
614
|
-
.stderr(Stdio::null())
|
|
615
|
-
.stdout(Stdio::null())
|
|
616
|
-
.status()?,
|
|
617
|
-
"jxl" => Command::new("djxl")
|
|
618
|
-
.args(&[&tmp_in.to_string_lossy() as &str, &tmp_out.to_string_lossy() as &str])
|
|
619
|
-
.stderr(Stdio::null())
|
|
620
|
-
.stdout(Stdio::null())
|
|
621
|
-
.status()?,
|
|
622
|
-
_ => return Err(anyhow::anyhow!("Unknown format")),
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
if status.success() {
|
|
626
|
-
let result = fs::read(&tmp_out)?;
|
|
627
|
-
let _ = fs::remove_file(&tmp_in);
|
|
628
|
-
let _ = fs::remove_file(&tmp_out);
|
|
629
|
-
Ok(result)
|
|
630
|
-
} else {
|
|
631
|
-
let _ = fs::remove_file(&tmp_in);
|
|
632
|
-
let _ = fs::remove_file(&tmp_out);
|
|
633
|
-
Err(anyhow::anyhow!("Reconversion to PNG failed"))
|
|
634
|
-
}
|
|
635
|
-
}
|
package/native/hybrid.rs
CHANGED
|
@@ -28,7 +28,7 @@ pub struct HybridCompressor {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
impl HybridCompressor {
|
|
31
|
-
pub fn new(
|
|
31
|
+
pub fn new() -> Self {
|
|
32
32
|
HybridCompressor {
|
|
33
33
|
block_size: BLOCK_SIZE,
|
|
34
34
|
}
|
|
@@ -36,7 +36,6 @@ impl HybridCompressor {
|
|
|
36
36
|
|
|
37
37
|
pub fn compress(&self, data: &[u8]) -> Result<(Vec<u8>, CompressionStats)> {
|
|
38
38
|
let original_size = data.len() as u64;
|
|
39
|
-
|
|
40
39
|
let blocks: Vec<&[u8]> = data.chunks(self.block_size).collect();
|
|
41
40
|
let blocks_count = blocks.len();
|
|
42
41
|
|
|
@@ -136,13 +135,11 @@ impl HybridCompressor {
|
|
|
136
135
|
}
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
fn
|
|
138
|
+
fn compress_block_with_entropy(block: &[u8], entropy: f32) -> Result<Vec<u8>> {
|
|
140
139
|
if block.is_empty() {
|
|
141
140
|
return Ok(vec![BLOCK_FLAG_STORE]);
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
let entropy = analyze_entropy(block);
|
|
145
|
-
|
|
146
143
|
if entropy >= ENTROPY_THRESHOLD_STORE {
|
|
147
144
|
let mut result = Vec::with_capacity(1 + block.len());
|
|
148
145
|
result.push(BLOCK_FLAG_STORE);
|
|
@@ -165,6 +162,19 @@ fn compress_block(block: &[u8]) -> Result<Vec<u8>> {
|
|
|
165
162
|
return Ok(result);
|
|
166
163
|
}
|
|
167
164
|
|
|
165
|
+
try_bwt_or_zstd(block)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn compress_block(block: &[u8]) -> Result<Vec<u8>> {
|
|
169
|
+
if block.is_empty() {
|
|
170
|
+
return Ok(vec![BLOCK_FLAG_STORE]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let entropy = analyze_entropy(block);
|
|
174
|
+
compress_block_with_entropy(block, entropy)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn try_bwt_or_zstd(block: &[u8]) -> Result<Vec<u8>> {
|
|
168
178
|
let bwt = bwt_encode(block)?;
|
|
169
179
|
let mtf_data = mtf_encode(&bwt.transformed);
|
|
170
180
|
let rle_data = rle0_encode(&mtf_data);
|
|
@@ -277,11 +287,11 @@ fn decompress_block_v1(block: &[u8]) -> Result<Vec<u8>> {
|
|
|
277
287
|
}
|
|
278
288
|
|
|
279
289
|
pub fn compress_high_performance(data: &[u8]) -> Result<(Vec<u8>, CompressionStats)> {
|
|
280
|
-
let compressor = HybridCompressor::new(
|
|
290
|
+
let compressor = HybridCompressor::new();
|
|
281
291
|
compressor.compress(data)
|
|
282
292
|
}
|
|
283
293
|
|
|
284
294
|
pub fn decompress_high_performance(data: &[u8]) -> Result<Vec<u8>> {
|
|
285
|
-
let compressor = HybridCompressor::new(
|
|
295
|
+
let compressor = HybridCompressor::new();
|
|
286
296
|
compressor.decompress(data)
|
|
287
297
|
}
|