roxify 1.11.1 → 1.12.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/native/encoder.rs DELETED
@@ -1,692 +0,0 @@
1
- use anyhow::Result;
2
- use std::process::{Command, Stdio};
3
-
4
- const MAGIC: &[u8] = b"ROX1";
5
- const PIXEL_MAGIC: &[u8] = b"PXL1";
6
- const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
7
-
8
- const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
9
- const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
10
- const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
11
-
12
- #[derive(Debug, Clone, Copy)]
13
- pub enum ImageFormat {
14
- Png,
15
- WebP,
16
- JpegXL,
17
- }
18
-
19
- 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)
22
- }
23
-
24
-
25
- 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)
28
- }
29
-
30
- 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
- }
59
- }
60
-
61
- 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)
63
- }
64
-
65
- pub fn encode_to_png_with_encryption_and_name(
66
- data: &[u8],
67
- compression_level: i32,
68
- passphrase: Option<&str>,
69
- encrypt_type: Option<&str>,
70
- name: Option<&str>,
71
- ) -> 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)
74
- }
75
-
76
- pub fn encode_to_png_with_encryption_name_and_filelist(
77
- data: &[u8],
78
- compression_level: i32,
79
- passphrase: Option<&str>,
80
- encrypt_type: Option<&str>,
81
- name: Option<&str>,
82
- file_list: Option<&str>,
83
- ) -> 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)
85
- }
86
-
87
- // ─── WAV container encoding ─────────────────────────────────────────────────
88
-
89
- /// Encode data into WAV container (8-bit PCM).
90
- /// Same compression/encryption pipeline as PNG, but wrapped in a WAV file
91
- /// instead of pixel grid. Overhead: 44 bytes (constant) vs PNG's variable overhead.
92
- pub fn encode_to_wav(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
93
- encode_to_wav_with_encryption_name_and_filelist(data, compression_level, None, None, None, None)
94
- }
95
-
96
- pub fn encode_to_wav_with_name_and_filelist(
97
- data: &[u8],
98
- compression_level: i32,
99
- name: Option<&str>,
100
- file_list: Option<&str>,
101
- ) -> Result<Vec<u8>> {
102
- encode_to_wav_with_encryption_name_and_filelist(data, compression_level, None, None, name, file_list)
103
- }
104
-
105
- pub fn encode_to_wav_with_encryption_name_and_filelist(
106
- data: &[u8],
107
- compression_level: i32,
108
- passphrase: Option<&str>,
109
- encrypt_type: Option<&str>,
110
- name: Option<&str>,
111
- file_list: Option<&str>,
112
- ) -> Result<Vec<u8>> {
113
- // Same compression + encryption pipeline as PNG
114
- let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, None, MAGIC)
115
- .map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
116
-
117
- let encrypted = if let Some(pass) = passphrase {
118
- match encrypt_type.unwrap_or("aes") {
119
- "xor" => crate::crypto::encrypt_xor(&compressed, pass),
120
- "aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
121
- _ => crate::crypto::encrypt_aes(&compressed, pass)?,
122
- }
123
- } else {
124
- crate::crypto::no_encryption(&compressed)
125
- };
126
-
127
- let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
128
-
129
- // Prepend PIXEL_MAGIC so decoder can validate the payload
130
- let wav_payload = [PIXEL_MAGIC, &meta_pixel].concat();
131
-
132
- // Wrap in WAV container (44 bytes overhead, constant)
133
- Ok(crate::audio::bytes_to_wav(&wav_payload))
134
- }
135
-
136
- /// Extract payload from a WAV file and return the raw meta_pixel bytes.
137
- pub fn decode_wav_payload(wav_data: &[u8]) -> Result<Vec<u8>> {
138
- crate::audio::wav_to_bytes(wav_data)
139
- .map_err(|e| anyhow::anyhow!("WAV decode failed: {}", e))
140
- }
141
-
142
- pub fn encode_to_png_with_encryption_name_and_format_and_filelist(
143
- data: &[u8],
144
- compression_level: i32,
145
- passphrase: Option<&str>,
146
- encrypt_type: Option<&str>,
147
- format: ImageFormat,
148
- name: Option<&str>,
149
- file_list: Option<&str>,
150
- dict: Option<&[u8]>,
151
- ) -> 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
- }
169
- }
170
-
171
- fn encode_to_png_with_encryption_name_and_filelist_internal(
172
- data: &[u8],
173
- compression_level: i32,
174
- passphrase: Option<&str>,
175
- encrypt_type: Option<&str>,
176
- name: Option<&str>,
177
- file_list: Option<&str>,
178
- dict: Option<&[u8]>,
179
- ) -> Result<Vec<u8>> {
180
- let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, dict, MAGIC)
181
- .map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
182
-
183
- let encrypted = if let Some(pass) = passphrase {
184
- match encrypt_type.unwrap_or("aes") {
185
- "xor" => crate::crypto::encrypt_xor(&compressed, pass),
186
- "aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
187
- _ => crate::crypto::encrypt_aes(&compressed, pass)?,
188
- }
189
- } else {
190
- crate::crypto::no_encryption(&compressed)
191
- };
192
- drop(compressed);
193
-
194
- let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
195
- drop(encrypted);
196
-
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;
200
-
201
- let marker_start_len = 12;
202
- let marker_end_len = 9;
203
-
204
- let data_with_markers_len = marker_start_len + padded_len;
205
- let data_pixels = (data_with_markers_len + 2) / 3;
206
- let end_marker_pixels = 3;
207
- let total_pixels = data_pixels + end_marker_pixels;
208
-
209
- let side = (total_pixels as f64).sqrt().ceil() as usize;
210
- let side = side.max(end_marker_pixels);
211
- let width = side;
212
- let height = side;
213
-
214
- let total_data_bytes = width * height * 3;
215
- let marker_end_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
216
-
217
- let flat = build_flat_pixel_buffer(&meta_pixel, padding_needed, marker_end_pos, marker_end_len, total_data_bytes);
218
- drop(meta_pixel);
219
-
220
- let row_bytes = width * 3;
221
- let idat_data = create_raw_deflate_from_rows(&flat, row_bytes, height);
222
- drop(flat);
223
-
224
- build_png(width, height, &idat_data, file_list)
225
- }
226
-
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;
241
- }
242
- flat[pos] = MARKER_ZSTD.0; flat[pos + 1] = MARKER_ZSTD.1; flat[pos + 2] = MARKER_ZSTD.2;
243
- pos += 3;
244
-
245
- flat[pos..pos + PIXEL_MAGIC.len()].copy_from_slice(PIXEL_MAGIC);
246
- pos += PIXEL_MAGIC.len();
247
-
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
259
- }
260
-
261
- fn build_meta_pixel(payload: &[u8]) -> Result<Vec<u8>> {
262
- build_meta_pixel_with_name(payload, None)
263
- }
264
-
265
- fn build_meta_pixel_with_name(payload: &[u8], name: Option<&str>) -> Result<Vec<u8>> {
266
- build_meta_pixel_with_name_and_filelist(payload, name, None)
267
- }
268
-
269
- fn build_meta_pixel_with_name_and_filelist(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
270
- let version = 1u8;
271
- let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
272
- let name_len = name_bytes.len().min(255) as u8;
273
- let payload_len_bytes = (payload.len() as u32).to_be_bytes();
274
-
275
- let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 4 + payload.len() + 256);
276
- result.push(version);
277
- result.push(name_len);
278
-
279
- if name_len > 0 {
280
- result.extend_from_slice(&name_bytes[..name_len as usize]);
281
- }
282
-
283
- result.extend_from_slice(&payload_len_bytes);
284
- result.extend_from_slice(payload);
285
-
286
- if let Some(file_list_json) = file_list {
287
- result.extend_from_slice(b"rXFL");
288
- let json_bytes = file_list_json.as_bytes();
289
- let json_len = json_bytes.len() as u32;
290
- result.extend_from_slice(&json_len.to_be_bytes());
291
- result.extend_from_slice(json_bytes);
292
- }
293
-
294
- Ok(result)
295
- }
296
-
297
- fn build_png(width: usize, height: usize, idat_data: &[u8], file_list: Option<&str>) -> Result<Vec<u8>> {
298
- let mut png = Vec::with_capacity(8 + 25 + 12 + idat_data.len() + 12 + 256);
299
-
300
- png.extend_from_slice(PNG_HEADER);
301
-
302
- let mut ihdr_data = [0u8; 13];
303
- ihdr_data[0..4].copy_from_slice(&(width as u32).to_be_bytes());
304
- ihdr_data[4..8].copy_from_slice(&(height as u32).to_be_bytes());
305
- ihdr_data[8] = 8;
306
- ihdr_data[9] = 2;
307
- ihdr_data[10] = 0;
308
- ihdr_data[11] = 0;
309
- ihdr_data[12] = 0;
310
-
311
- write_chunk(&mut png, b"IHDR", &ihdr_data)?;
312
- write_chunk(&mut png, b"IDAT", idat_data)?;
313
-
314
- if let Some(file_list_json) = file_list {
315
- write_chunk(&mut png, b"rXFL", file_list_json.as_bytes())?;
316
- }
317
-
318
- write_chunk(&mut png, b"IEND", &[])?;
319
-
320
- Ok(png)
321
- }
322
-
323
- fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) -> Result<()> {
324
- let len = data.len() as u32;
325
- out.extend_from_slice(&len.to_be_bytes());
326
- out.extend_from_slice(chunk_type);
327
- out.extend_from_slice(data);
328
-
329
- let mut hasher = crc32fast::Hasher::new();
330
- hasher.update(chunk_type);
331
- hasher.update(data);
332
- let crc = hasher.finalize();
333
-
334
- out.extend_from_slice(&crc.to_be_bytes());
335
- Ok(())
336
- }
337
-
338
- fn create_raw_deflate(data: &[u8]) -> Vec<u8> {
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);
343
-
344
- result.push(0x78);
345
- result.push(0x01);
346
-
347
- let mut offset = 0;
348
- while offset < data.len() {
349
- let chunk_size = (data.len() - offset).min(MAX_BLOCK);
350
- let is_last = offset + chunk_size >= data.len();
351
-
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);
360
- result.extend_from_slice(&data[offset..offset + chunk_size]);
361
- offset += chunk_size;
362
- }
363
-
364
- let adler = crate::core::adler32_bytes(data);
365
- result.extend_from_slice(&adler.to_be_bytes());
366
-
367
- result
368
- }
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
-
415
- fn predict_best_format(data: &[u8]) -> ImageFormat {
416
- if data.len() < 2048 {
417
- return ImageFormat::Png;
418
- }
419
-
420
- let sample_size = data.len().min(4096);
421
- let sample = &data[..sample_size];
422
-
423
- let entropy = calculate_shannon_entropy(sample);
424
- let repetition_score = detect_repetition_patterns(sample);
425
-
426
- let unique_bytes = count_unique_bytes(sample);
427
- let unique_ratio = unique_bytes as f64 / 256.0;
428
-
429
- let is_sequential = detect_sequential_pattern(sample);
430
-
431
- if entropy > 7.8 {
432
- ImageFormat::Png
433
- } else if is_sequential || repetition_score > 0.2 {
434
- ImageFormat::JpegXL
435
- } else if unique_ratio < 0.3 && entropy < 6.5 {
436
- ImageFormat::JpegXL
437
- } else if entropy < 5.5 {
438
- ImageFormat::JpegXL
439
- } else {
440
- ImageFormat::Png
441
- }
442
- }
443
-
444
- fn detect_sequential_pattern(data: &[u8]) -> bool {
445
- if data.len() < 256 {
446
- return false;
447
- }
448
-
449
- let check_len = data.len().min(256);
450
- let mut sequential = 0;
451
-
452
- for i in 0..check_len - 1 {
453
- let diff = (data[i + 1] as i16 - data[i] as i16).abs();
454
- if diff <= 1 {
455
- sequential += 1;
456
- }
457
- }
458
-
459
- sequential as f64 / (check_len - 1) as f64 > 0.6
460
- }
461
-
462
- fn count_unique_bytes(data: &[u8]) -> usize {
463
- let mut seen = [false; 256];
464
- for &byte in data {
465
- seen[byte as usize] = true;
466
- }
467
- seen.iter().filter(|&&x| x).count()
468
- }
469
-
470
- fn calculate_shannon_entropy(data: &[u8]) -> f64 {
471
- let mut freq = [0u32; 256];
472
- for &byte in data {
473
- freq[byte as usize] += 1;
474
- }
475
-
476
- let len = data.len() as f64;
477
- let mut entropy = 0.0;
478
-
479
- for &count in &freq {
480
- if count > 0 {
481
- let p = count as f64 / len;
482
- entropy -= p * p.log2();
483
- }
484
- }
485
-
486
- entropy
487
- }
488
-
489
- #[cfg(test)]
490
- mod tests {
491
- use super::*;
492
- use crate::png_utils;
493
-
494
- #[test]
495
- fn test_rxfl_chunk_present_when_file_list_provided() {
496
- let sample_data = b"hello world".to_vec();
497
- let file_list_json = Some("[{\"name\": \"a.txt\", \"size\": 11}]" as &str);
498
- let png = encode_to_png_with_encryption_name_and_filelist_internal(&sample_data, 3, None, None, None, file_list_json, None)
499
- .expect("encode should succeed");
500
-
501
- let chunks = png_utils::extract_png_chunks(&png).expect("extract chunks");
502
- let found = chunks.iter().any(|c| c.name == "rXFL");
503
- assert!(found, "rXFL chunk must be present when file_list is provided");
504
-
505
- let rxfl_chunk = chunks.into_iter().find(|c| c.name == "rXFL").expect("rXFL present");
506
- let s = String::from_utf8_lossy(&rxfl_chunk.data);
507
- assert!(s.contains("a.txt"), "rXFL chunk should contain the file name");
508
- }
509
-
510
- #[test]
511
- fn test_extract_payload_and_partial_unpack() {
512
- use std::fs;
513
- let base = std::env::temp_dir().join(format!("rox_test_{}", rand::random::<u32>()));
514
- let dir = base.join("data");
515
- fs::create_dir_all(dir.join("sub")).unwrap();
516
- fs::write(dir.join("a.txt"), b"hello").unwrap();
517
- fs::write(dir.join("sub").join("b.txt"), b"world").unwrap();
518
-
519
- let pack_result = crate::packer::pack_path_with_metadata(&dir).expect("pack path");
520
- let png = encode_to_png_with_encryption_name_and_filelist_internal(&pack_result.data, 3, None, None, None, pack_result.file_list_json.as_deref(), None)
521
- .expect("encode should succeed");
522
-
523
- let payload = crate::png_utils::extract_payload_from_png(&png).expect("extract payload");
524
- assert!(payload.len() > 1);
525
- assert_eq!(payload[0], 0x00u8);
526
-
527
- let compressed = payload[1..].to_vec();
528
- let mut decompressed = crate::core::zstd_decompress_bytes(&compressed, None).expect("decompress");
529
- if decompressed.starts_with(b"ROX1") {
530
- decompressed = decompressed[4..].to_vec();
531
- }
532
-
533
- let out_dir = base.join("out");
534
- fs::create_dir_all(&out_dir).unwrap();
535
-
536
- let written = crate::packer::unpack_buffer_to_dir(&decompressed, &out_dir, Some(&["sub/b.txt".to_string()])).expect("unpack");
537
- assert_eq!(written.len(), 1);
538
- let got = fs::read_to_string(out_dir.join("sub").join("b.txt")).unwrap();
539
- assert_eq!(got, "world");
540
- }
541
-
542
- #[test]
543
- fn test_encrypt_decrypt_roundtrip() {
544
- let data = b"hello world".to_vec();
545
- let png = encode_to_png_with_encryption_name_and_filelist(&data, 3, Some("password"), Some("aes"), None, None)
546
- .expect("encode should succeed");
547
- let payload = crate::png_utils::extract_payload_from_png(&png).expect("extract");
548
- let decrypted = crate::crypto::try_decrypt(&payload, Some("password")).expect("decrypt");
549
- let mut decompressed = crate::core::zstd_decompress_bytes(&decrypted, None).expect("decompress");
550
- if decompressed.starts_with(b"ROX1") { decompressed = decompressed[4..].to_vec(); }
551
- assert_eq!(decompressed, data);
552
- }
553
- }
554
-
555
- fn detect_repetition_patterns(data: &[u8]) -> f64 {
556
- if data.len() < 4 {
557
- return 0.0;
558
- }
559
-
560
- let mut repetitions = 0;
561
- let mut total_checks = 0;
562
-
563
- for i in 0..data.len().min(1024) {
564
- if i + 3 < data.len() {
565
- let byte = data[i];
566
- if data[i + 1] == byte && data[i + 2] == byte && data[i + 3] == byte {
567
- repetitions += 1;
568
- }
569
- total_checks += 1;
570
- }
571
- }
572
-
573
- if total_checks > 0 {
574
- repetitions as f64 / total_checks as f64
575
- } else {
576
- 0.0
577
- }
578
- }
579
-
580
- fn optimize_format(png_data: &[u8]) -> Result<Vec<u8>> {
581
- let formats = [
582
- ("webp", optimize_to_webp(png_data)),
583
- ("jxl", optimize_to_jxl(png_data)),
584
- ];
585
-
586
- let mut best = png_data.to_vec();
587
- let mut best_size = png_data.len();
588
-
589
- for (_name, result) in formats {
590
- if let Ok(optimized) = result {
591
- if optimized.len() < best_size {
592
- best = optimized;
593
- best_size = best.len();
594
- }
595
- }
596
- }
597
-
598
- Ok(best)
599
- }
600
-
601
- fn optimize_to_webp(png_data: &[u8]) -> Result<Vec<u8>> {
602
- use std::fs;
603
-
604
- let tmp_dir = std::env::temp_dir();
605
- let tmp_in = tmp_dir.join("roxify_temp_in.png");
606
- let tmp_out = tmp_dir.join("roxify_temp_out.webp");
607
-
608
- fs::write(&tmp_in, png_data)?;
609
-
610
- let status = Command::new("cwebp")
611
- .args(&["-lossless", &tmp_in.to_string_lossy(), "-o", &tmp_out.to_string_lossy()])
612
- .stderr(Stdio::null())
613
- .stdout(Stdio::null())
614
- .status()?;
615
-
616
- if status.success() {
617
- let result = fs::read(&tmp_out)?;
618
- let _ = fs::remove_file(&tmp_in);
619
- let _ = fs::remove_file(&tmp_out);
620
- Ok(result)
621
- } else {
622
- let _ = fs::remove_file(&tmp_in);
623
- let _ = fs::remove_file(&tmp_out);
624
- Err(anyhow::anyhow!("WebP conversion failed"))
625
- }
626
- }
627
-
628
- fn optimize_to_jxl(png_data: &[u8]) -> Result<Vec<u8>> {
629
- use std::fs;
630
-
631
- let tmp_dir = std::env::temp_dir();
632
- let tmp_in = tmp_dir.join("roxify_temp_in.png");
633
- let tmp_out = tmp_dir.join("roxify_temp_out.jxl");
634
-
635
- fs::write(&tmp_in, png_data)?;
636
-
637
- let status = Command::new("cjxl")
638
- .args(&[&tmp_in.to_string_lossy() as &str, &tmp_out.to_string_lossy() as &str, "-d", "0", "-e", "9"])
639
- .stderr(Stdio::null())
640
- .stdout(Stdio::null())
641
- .status()?;
642
-
643
- if status.success() {
644
- let result = fs::read(&tmp_out)?;
645
- let _ = fs::remove_file(&tmp_in);
646
- let _ = fs::remove_file(&tmp_out);
647
- Ok(result)
648
- } else {
649
- let _ = fs::remove_file(&tmp_in);
650
- let _ = fs::remove_file(&tmp_out);
651
- Err(anyhow::anyhow!("JXL conversion failed"))
652
- }
653
- }
654
-
655
- fn reconvert_to_png(data: &[u8], original_format: &str) -> Result<Vec<u8>> {
656
- use std::fs;
657
-
658
- let tmp_dir = std::env::temp_dir();
659
- let tmp_in = match original_format {
660
- "webp" => tmp_dir.join("roxify_reconvert_in.webp"),
661
- "jxl" => tmp_dir.join("roxify_reconvert_in.jxl"),
662
- _ => return Err(anyhow::anyhow!("Unknown format")),
663
- };
664
- let tmp_out = tmp_dir.join("roxify_reconvert_out.png");
665
-
666
- fs::write(&tmp_in, data)?;
667
-
668
- let status = match original_format {
669
- "webp" => Command::new("dwebp")
670
- .args(&[&tmp_in.to_string_lossy() as &str, "-o", &tmp_out.to_string_lossy() as &str])
671
- .stderr(Stdio::null())
672
- .stdout(Stdio::null())
673
- .status()?,
674
- "jxl" => Command::new("djxl")
675
- .args(&[&tmp_in.to_string_lossy() as &str, &tmp_out.to_string_lossy() as &str])
676
- .stderr(Stdio::null())
677
- .stdout(Stdio::null())
678
- .status()?,
679
- _ => return Err(anyhow::anyhow!("Unknown format")),
680
- };
681
-
682
- if status.success() {
683
- let result = fs::read(&tmp_out)?;
684
- let _ = fs::remove_file(&tmp_in);
685
- let _ = fs::remove_file(&tmp_out);
686
- Ok(result)
687
- } else {
688
- let _ = fs::remove_file(&tmp_in);
689
- let _ = fs::remove_file(&tmp_out);
690
- Err(anyhow::anyhow!("Reconversion to PNG failed"))
691
- }
692
- }