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 CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "roxify_native"
3
- version = "1.13.2"
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.8 s | 1.3x |
74
- | Test B (1.4 GB) | 8.4 s | 2.2 s | 3.8x |
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 progressInterval = setInterval(() => {
323
- barValue = Math.min(barValue + 1, 99);
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(barValue, {
326
- step: 'Encoding',
326
+ encodeBar.update(Math.min(pct, 99), {
327
+ step: step || 'Encoding',
327
328
  elapsed: String(elapsed),
328
329
  });
329
- }, 500);
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
- const progressInterval = setInterval(() => {
603
- barValue = Math.min(barValue + 2, 99);
604
- decodeBar.update(barValue, {
605
- step: 'Decoding',
606
- elapsed: String(Math.floor((Date.now() - startTime) / 1000)),
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
- }, 300);
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();
Binary file
Binary file
Binary file
Binary file
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 declare function encodeWithRustCLI(inputPath: string, outputPath: string, compressionLevel?: number, passphrase?: string, encryptType?: 'aes' | 'xor', name?: string): Promise<void>;
5
- export declare function decodeWithRustCLI(inputPath: string, outputPath: string, passphrase?: string, files?: string[], dict?: string): Promise<void>;
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
- : 'inherit';
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
- const MOD: u32 = 65521;
88
- const NMAX: usize = 5552;
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
- let format = predict_best_format_raw(data);
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
- let format = predict_best_format_raw(data);
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
- encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, None, None, ImageFormat::Png, name, file_list, None)
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
- encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, None, None, ImageFormat::Png, None, None, None)
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
- let format = predict_best_format_raw(data);
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
- encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, passphrase, encrypt_type, ImageFormat::Png, name, file_list, None)
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::no_encryption(&compressed)
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
- format: ImageFormat,
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
- let png = encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, file_list, dict)?;
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::no_encryption(&compressed)
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
- }