roxify 1.13.2 → 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 -1
- package/README.md +4 -2
- package/dist/cli.js +14 -19
- 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/core.rs +3 -48
- package/native/crypto.rs +5 -0
- package/native/encoder.rs +10 -228
- package/native/lib.rs +54 -39
- package/native/main.rs +9 -3
- package/native/streaming_decode.rs +91 -7
- package/native/streaming_encode.rs +54 -59
- 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/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "roxify_native"
|
|
3
|
-
version = "1.13.
|
|
3
|
+
version = "1.13.3"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
publish = false
|
|
6
6
|
|
|
@@ -51,6 +51,7 @@ pbkdf2 = "0.12"
|
|
|
51
51
|
rand = "0.8"
|
|
52
52
|
sha2 = "0.10"
|
|
53
53
|
mimalloc = "0.1"
|
|
54
|
+
simd-adler32 = "0.3"
|
|
54
55
|
|
|
55
56
|
memmap2 = "0.9"
|
|
56
57
|
bytemuck = { version = "1.14", features = ["derive"] }
|
package/README.md
CHANGED
|
@@ -70,8 +70,8 @@ All measurements taken on Linux x64 (Intel i7-6700K @ 4.0 GHz, 32 GB RAM). Roxif
|
|
|
70
70
|
|
|
71
71
|
| Dataset | unzip | Roxify decode | Speedup |
|
|
72
72
|
| --- | --- | --- | --- |
|
|
73
|
-
| Test A (177 MB) | 2.4 s | 1.
|
|
74
|
-
| Test B (1.4 GB) | 8.4 s | 2.
|
|
73
|
+
| Test A (177 MB) | 2.4 s | 1.3 s | 1.8x |
|
|
74
|
+
| Test B (1.4 GB) | 8.4 s | 2.9 s | 2.9x |
|
|
75
75
|
|
|
76
76
|
Roxify produces a valid PNG image instead of a ZIP archive. On these real-world datasets it compresses 20-37% smaller than ZIP -9 while encoding 15x faster, thanks to multi-threaded Zstd with long-distance matching.
|
|
77
77
|
|
|
@@ -537,6 +537,8 @@ Input --> Detect Format --> Demodulate/Read Blocks --> De-interleave --> RS ECC
|
|
|
537
537
|
| `image_utils.rs` | Image resizing, pixel format conversion, metadata extraction |
|
|
538
538
|
| `png_utils.rs` | Low-level PNG chunk read/write operations |
|
|
539
539
|
| `progress.rs` | Progress tracking for long-running compression/decompression |
|
|
540
|
+
| `streaming_encode.rs` | Streaming directory-to-PNG encoder with real-time progress |
|
|
541
|
+
| `streaming_decode.rs` | Streaming PNG-to-directory decoder with real-time progress |
|
|
540
542
|
|
|
541
543
|
### TypeScript Modules
|
|
542
544
|
|
package/dist/cli.js
CHANGED
|
@@ -317,20 +317,17 @@ async function encodeCommand(args) {
|
|
|
317
317
|
console.log(`Encoding to ${resolvedOutput} (Using native Rust encoder)\n`);
|
|
318
318
|
const startTime = Date.now();
|
|
319
319
|
const encodeBar = new cliProgress.SingleBar({ format: ' {bar} {percentage}% | {step} | {elapsed}s' }, cliProgress.Presets.shades_classic);
|
|
320
|
-
let barValue = 0;
|
|
321
320
|
encodeBar.start(100, 0, { step: 'Encoding', elapsed: '0' });
|
|
322
|
-
const
|
|
323
|
-
|
|
321
|
+
const encryptType = parsed.encrypt === 'xor' ? 'xor' : 'aes';
|
|
322
|
+
const fileName = basename(inputPaths[0]);
|
|
323
|
+
await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput, 19, parsed.passphrase, encryptType, fileName, (current, total, step) => {
|
|
324
|
+
const pct = total > 0 ? Math.floor((current / total) * 100) : 0;
|
|
324
325
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
325
|
-
encodeBar.update(
|
|
326
|
-
step: 'Encoding',
|
|
326
|
+
encodeBar.update(Math.min(pct, 99), {
|
|
327
|
+
step: step || 'Encoding',
|
|
327
328
|
elapsed: String(elapsed),
|
|
328
329
|
});
|
|
329
|
-
}
|
|
330
|
-
const encryptType = parsed.encrypt === 'xor' ? 'xor' : 'aes';
|
|
331
|
-
const fileName = basename(inputPaths[0]);
|
|
332
|
-
await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput, 19, parsed.passphrase, encryptType, fileName);
|
|
333
|
-
clearInterval(progressInterval);
|
|
330
|
+
});
|
|
334
331
|
const encodeTime = Date.now() - startTime;
|
|
335
332
|
encodeBar.update(100, {
|
|
336
333
|
step: 'done',
|
|
@@ -597,17 +594,15 @@ async function decodeCommand(args) {
|
|
|
597
594
|
console.log('Decoding... (Using native Rust decoder)\n');
|
|
598
595
|
const startTime = Date.now();
|
|
599
596
|
const decodeBar = new cliProgress.SingleBar({ format: ' {bar} {percentage}% | {step} | {elapsed}s' }, cliProgress.Presets.shades_classic);
|
|
600
|
-
let barValue = 0;
|
|
601
597
|
decodeBar.start(100, 0, { step: 'Decoding', elapsed: '0' });
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
598
|
+
await decodeWithRustCLI(resolvedInput, resolvedOutput, parsed.passphrase, parsed.files, parsed.dict, (current, total, step) => {
|
|
599
|
+
const pct = total > 0 ? Math.floor((current / total) * 100) : 0;
|
|
600
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
601
|
+
decodeBar.update(Math.min(pct, 99), {
|
|
602
|
+
step: step || 'Decoding',
|
|
603
|
+
elapsed: String(elapsed),
|
|
607
604
|
});
|
|
608
|
-
}
|
|
609
|
-
await decodeWithRustCLI(resolvedInput, resolvedOutput, parsed.passphrase, parsed.files, parsed.dict);
|
|
610
|
-
clearInterval(progressInterval);
|
|
605
|
+
});
|
|
611
606
|
const decodeTime = Date.now() - startTime;
|
|
612
607
|
decodeBar.update(100, { step: 'done', elapsed: String(Math.floor(decodeTime / 1000)) });
|
|
613
608
|
decodeBar.stop();
|
package/dist/rox-macos-universal
CHANGED
|
Binary file
|
package/dist/roxify_native
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/roxify_native.exe
CHANGED
|
Binary file
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
declare function findRustBinary(): string | null;
|
|
2
2
|
export { findRustBinary };
|
|
3
3
|
export declare function isRustBinaryAvailable(): boolean;
|
|
4
|
-
export
|
|
5
|
-
export declare function
|
|
4
|
+
export type ProgressCallback = (current: number, total: number, step: string) => void;
|
|
5
|
+
export declare function encodeWithRustCLI(inputPath: string, outputPath: string, compressionLevel?: number, passphrase?: string, encryptType?: 'aes' | 'xor', name?: string, onProgress?: ProgressCallback): Promise<void>;
|
|
6
|
+
export declare function decodeWithRustCLI(inputPath: string, outputPath: string, passphrase?: string, files?: string[], dict?: string, onProgress?: ProgressCallback): Promise<void>;
|
|
6
7
|
export declare function listWithRustCLI(inputPath: string): Promise<string>;
|
|
7
8
|
export declare function havepassphraseWithRustCLI(inputPath: string): Promise<string>;
|
|
@@ -162,9 +162,12 @@ function spawnRustCLI(args, options) {
|
|
|
162
162
|
let stdout = '';
|
|
163
163
|
const runSpawn = (exePath) => {
|
|
164
164
|
let proc;
|
|
165
|
+
const hasProgress = !!options?.onProgress;
|
|
165
166
|
const stdio = options?.collectStdout
|
|
166
|
-
? ['pipe', 'pipe', 'inherit']
|
|
167
|
-
:
|
|
167
|
+
? ['pipe', 'pipe', hasProgress ? 'pipe' : 'inherit']
|
|
168
|
+
: hasProgress
|
|
169
|
+
? ['pipe', 'inherit', 'pipe']
|
|
170
|
+
: 'inherit';
|
|
168
171
|
try {
|
|
169
172
|
proc = spawn(exePath, args, { stdio });
|
|
170
173
|
}
|
|
@@ -184,6 +187,23 @@ function spawnRustCLI(args, options) {
|
|
|
184
187
|
if (options?.collectStdout && proc.stdout) {
|
|
185
188
|
proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
186
189
|
}
|
|
190
|
+
if (hasProgress && proc.stderr) {
|
|
191
|
+
let stderrBuf = '';
|
|
192
|
+
proc.stderr.on('data', (chunk) => {
|
|
193
|
+
stderrBuf += chunk.toString();
|
|
194
|
+
const lines = stderrBuf.split('\n');
|
|
195
|
+
stderrBuf = lines.pop() || '';
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
const match = line.match(/^PROGRESS:(\d+):(\d+):(.+)$/);
|
|
198
|
+
if (match) {
|
|
199
|
+
options.onProgress(Number(match[1]), Number(match[2]), match[3]);
|
|
200
|
+
}
|
|
201
|
+
else if (line.trim()) {
|
|
202
|
+
process.stderr.write(line + '\n');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
187
207
|
proc.on('error', (err) => {
|
|
188
208
|
if (!triedExtract) {
|
|
189
209
|
triedExtract = true;
|
|
@@ -213,7 +233,7 @@ function spawnRustCLI(args, options) {
|
|
|
213
233
|
runSpawn(cliPath);
|
|
214
234
|
});
|
|
215
235
|
}
|
|
216
|
-
export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel = 3, passphrase, encryptType = 'aes', name) {
|
|
236
|
+
export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel = 3, passphrase, encryptType = 'aes', name, onProgress) {
|
|
217
237
|
const cliPath = findRustBinary();
|
|
218
238
|
if (!cliPath)
|
|
219
239
|
throw new Error('Rust CLI binary not found');
|
|
@@ -231,9 +251,9 @@ export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel
|
|
|
231
251
|
args.push('--encrypt', encryptType);
|
|
232
252
|
}
|
|
233
253
|
args.push(inputPath, outputPath);
|
|
234
|
-
await spawnRustCLI(args);
|
|
254
|
+
await spawnRustCLI(args, { onProgress });
|
|
235
255
|
}
|
|
236
|
-
export async function decodeWithRustCLI(inputPath, outputPath, passphrase, files, dict) {
|
|
256
|
+
export async function decodeWithRustCLI(inputPath, outputPath, passphrase, files, dict, onProgress) {
|
|
237
257
|
const args = ['decompress', inputPath, outputPath];
|
|
238
258
|
if (passphrase)
|
|
239
259
|
args.push('--passphrase', passphrase);
|
|
@@ -241,7 +261,7 @@ export async function decodeWithRustCLI(inputPath, outputPath, passphrase, files
|
|
|
241
261
|
args.push('--files', JSON.stringify(files));
|
|
242
262
|
if (dict)
|
|
243
263
|
args.push('--dict', dict);
|
|
244
|
-
await spawnRustCLI(args);
|
|
264
|
+
await spawnRustCLI(args, { onProgress });
|
|
245
265
|
}
|
|
246
266
|
export async function listWithRustCLI(inputPath) {
|
|
247
267
|
return spawnRustCLI(['list', inputPath], { collectStdout: true });
|
package/native/core.rs
CHANGED
|
@@ -84,54 +84,9 @@ pub fn crc32_bytes(buf: &[u8]) -> u32 {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
pub fn adler32_bytes(buf: &[u8]) -> u32 {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if buf.len() > 4 * 1024 * 1024 {
|
|
91
|
-
return adler32_parallel(buf);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let mut a: u32 = 1;
|
|
95
|
-
let mut b: u32 = 0;
|
|
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;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
(b << 16) | a
|
|
107
|
-
}
|
|
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)
|
|
87
|
+
let mut hasher = simd_adler32::Adler32::new();
|
|
88
|
+
hasher.write(buf);
|
|
89
|
+
hasher.finish()
|
|
135
90
|
}
|
|
136
91
|
|
|
137
92
|
pub fn delta_encode_bytes(buf: &[u8]) -> Vec<u8> {
|
package/native/crypto.rs
CHANGED
|
@@ -75,6 +75,11 @@ pub fn no_encryption(data: &[u8]) -> Vec<u8> {
|
|
|
75
75
|
result
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
pub fn no_encryption_in_place(mut data: Vec<u8>) -> Vec<u8> {
|
|
79
|
+
data.insert(0, ENC_NONE);
|
|
80
|
+
data
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
pub fn decrypt_xor(data: &[u8], passphrase: &str) -> Result<Vec<u8>> {
|
|
79
84
|
if data.is_empty() { return Err(anyhow!("Empty xor payload")); }
|
|
80
85
|
if passphrase.is_empty() { return Err(anyhow!("Passphrase required")); }
|
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
|
-
}
|