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/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
- }
package/native/hybrid.rs CHANGED
@@ -28,7 +28,7 @@ pub struct HybridCompressor {
28
28
  }
29
29
 
30
30
  impl HybridCompressor {
31
- pub fn new(_enable_gpu: bool, _pool_size: usize) -> Self {
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 compress_block(block: &[u8]) -> Result<Vec<u8>> {
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(false, 0);
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(false, 0);
295
+ let compressor = HybridCompressor::new();
286
296
  compressor.decompress(data)
287
297
  }