roxify 1.9.3 → 1.9.4

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.9.3"
3
+ version = "1.9.4"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
package/dist/cli.js CHANGED
@@ -327,10 +327,10 @@ async function encodeCommand(args) {
327
327
  inputSize = fstatSync(resolvedInputs[0]).size;
328
328
  }
329
329
  const outputSize = fstatSync(resolvedOutput).size;
330
- const ratio = ((outputSize / inputSize) * 100).toFixed(1);
330
+ const saved = (100 - (outputSize / inputSize) * 100).toFixed(1);
331
331
  console.log(`\nSuccess!`);
332
332
  console.log(` Input: ${(inputSize / 1024 / 1024).toFixed(2)} MB`);
333
- console.log(` Output: ${(outputSize / 1024 / 1024).toFixed(2)} MB (${ratio}% of original)`);
333
+ console.log(` Output: ${(outputSize / 1024 / 1024).toFixed(2)} MB (${saved}% saved)`);
334
334
  console.log(` Time: ${encodeTime}ms`);
335
335
  console.log(` Saved: ${resolvedOutput}`);
336
336
  console.log(' ');
@@ -541,10 +541,10 @@ async function encodeCommand(args) {
541
541
  writeFileSync(resolvedOutput, output);
542
542
  const outputSize = (output.length / 1024 / 1024).toFixed(2);
543
543
  const inputSize = (inputSizeVal / 1024 / 1024).toFixed(2);
544
- const ratio = ((output.length / inputSizeVal) * 100).toFixed(1);
544
+ const saved = (100 - (output.length / inputSizeVal) * 100).toFixed(1);
545
545
  console.log(`\nSuccess!`);
546
546
  console.log(` Input: ${inputSize} MB`);
547
- console.log(` Output: ${outputSize} MB (${ratio}% of original)`);
547
+ console.log(` Output: ${outputSize} MB (${saved}% saved)`);
548
548
  console.log(` Time: ${encodeTime}ms`);
549
549
  console.log(` Saved: ${resolvedOutput}`);
550
550
  console.log(' ');
Binary file
@@ -142,8 +142,19 @@ pub fn extract_payload_from_png(png_data: &[u8]) -> Result<Vec<u8>, String> {
142
142
  return Ok(payload);
143
143
  }
144
144
  }
145
- let reconst = crate::reconstitution::crop_and_reconstitute(png_data)?;
146
- extract_payload_direct(&reconst)
145
+ if let Ok(reconst) = crate::reconstitution::crop_and_reconstitute(png_data) {
146
+ if let Ok(payload) = extract_payload_direct(&reconst) {
147
+ if validate_payload_deep(&payload) {
148
+ return Ok(payload);
149
+ }
150
+ }
151
+ }
152
+ let unstretched = crate::reconstitution::unstretch_nn(png_data)?;
153
+ let payload = extract_payload_direct(&unstretched)?;
154
+ if validate_payload_deep(&payload) {
155
+ return Ok(payload);
156
+ }
157
+ Err("No valid payload found after all extraction attempts".to_string())
147
158
  }
148
159
 
149
160
  fn validate_payload_deep(payload: &[u8]) -> bool {
@@ -44,14 +44,13 @@ fn intra_block_transitions_h(
44
44
  get_pixel: &impl Fn(u32, u32) -> [u8; 4],
45
45
  sx: u32, ex: u32, y: u32, candidate: u32,
46
46
  ) -> u32 {
47
- let pw = (ex - sx) as f32;
48
- let lw = candidate as f32;
49
- let ratio = lw / pw;
47
+ let pw = (ex - sx) as f64;
48
+ let lw = candidate as f64;
50
49
  let row: Vec<[u8; 4]> = (sx..ex).map(|x| get_pixel(x, y)).collect();
51
50
  let mut count = 0u32;
52
51
  for i in 1..row.len() {
53
- let lx_prev = ((i as f32 - 0.5) * ratio) as u32;
54
- let lx_curr = ((i as f32 + 0.5) * ratio) as u32;
52
+ let lx_prev = ((i as f64 - 0.5) * lw / pw) as u32;
53
+ let lx_curr = ((i as f64 + 0.5) * lw / pw) as u32;
55
54
  if lx_prev == lx_curr && color_dist(row[i], row[i - 1]) > 0 {
56
55
  count += 1;
57
56
  }
@@ -63,14 +62,13 @@ fn intra_block_transitions_v(
63
62
  get_pixel: &impl Fn(u32, u32) -> [u8; 4],
64
63
  x: u32, sy: u32, ey: u32, candidate: u32,
65
64
  ) -> u32 {
66
- let ph = (ey - sy) as f32;
67
- let lh = candidate as f32;
68
- let ratio = lh / ph;
65
+ let ph = (ey - sy) as f64;
66
+ let lh = candidate as f64;
69
67
  let col: Vec<[u8; 4]> = (sy..ey).map(|y| get_pixel(x, y)).collect();
70
68
  let mut count = 0u32;
71
69
  for i in 1..col.len() {
72
- let ly_prev = ((i as f32 - 0.5) * ratio) as u32;
73
- let ly_curr = ((i as f32 + 0.5) * ratio) as u32;
70
+ let ly_prev = ((i as f64 - 0.5) * lh / ph) as u32;
71
+ let ly_curr = ((i as f64 + 0.5) * lh / ph) as u32;
74
72
  if ly_prev == ly_curr && color_dist(col[i], col[i - 1]) > 0 {
75
73
  count += 1;
76
74
  }
@@ -185,6 +183,7 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
185
183
  }
186
184
 
187
185
 
186
+
188
187
  let mut best_logical_w = 0u32;
189
188
  let mut best_logical_h = 0u32;
190
189
  // Score: (size_err_permille, inverse_area) — lower is better
@@ -206,7 +205,9 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
206
205
 
207
206
  let phys_w = ex - sx;
208
207
  let phys_h = ey - sy;
209
- if phys_w < 3 || phys_h < 3 || phys_w > 1800 || phys_h > 1800 { continue; }
208
+ if phys_w < 3 || phys_h < 3 || phys_w > 1800 || phys_h > 1800 {
209
+ continue;
210
+ }
210
211
 
211
212
 
212
213
  // Estimation depuis les marqueurs (start_marker_w ≈ 3 × scale_x, end_marker_w ≈ 3 × scale_x)
@@ -258,7 +259,9 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
258
259
  // Vérifie cohérence grossière
259
260
  let lw_diff_lo = (lw_cand_lo as f64 - est_lw_f).abs();
260
261
  let lw_diff_hi = (lw_cand_hi as f64 - est_lw_f).abs();
261
- if lw_diff_lo > est_lw_f * 0.25 + 3.0 && lw_diff_hi > est_lw_f * 0.25 + 3.0 { continue; }
262
+ if lw_diff_lo > est_lw_f * 0.25 + 3.0 && lw_diff_hi > est_lw_f * 0.25 + 3.0 {
263
+ continue;
264
+ }
262
265
 
263
266
  let n_scan = 7u32;
264
267
  let lw = if lw_cand_lo == lw_cand_hi {
@@ -318,10 +321,14 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
318
321
  };
319
322
 
320
323
  // Filtre taille min logique
321
- if lw < 3 || lh < 3 { continue; }
324
+ if lw < 3 || lh < 3 {
325
+ continue;
326
+ }
322
327
 
323
328
  let size_err = lw_size_err;
324
- if size_err > 500 { continue; }
329
+ if size_err > 500 {
330
+ continue;
331
+ }
325
332
 
326
333
  // Filtre final : l'intra-block score pour le lw sélectionné doit être faible
327
334
  // (pour une vraie paire avec l'image NN-scalée, = 0; pour une zone de fond aléatoire, >> 0)
@@ -333,9 +340,11 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
333
340
  // Filtre : la zone encodée NN a des blocs monochromes → intra très bas
334
341
  // Zone de fond aléatoire → intra ≈ lw × n_scan × 0.99 >> 0
335
342
  // Seuil : lw/8 × n_scan pour permettre ≈ lw/8 pixels parasites
336
- let intra_threshold = (lw as u32 / 8 + 1) * n_scan;
343
+ let intra_threshold = ((lw as u32 / 4 + 2) * n_scan).max(n_scan * 3);
337
344
 
338
- if intra_final > intra_threshold { continue; }
345
+ if intra_final > intra_threshold {
346
+ continue;
347
+ }
339
348
 
340
349
  // Favori : pas d'ajustement de bord, puis plus grande zone, puis plus petit size_err
341
350
  let area = phys_w as u64 * phys_h as u64;
@@ -406,6 +415,77 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
406
415
  Ok(output)
407
416
  }
408
417
 
418
+ pub fn unstretch_nn(png_data: &[u8]) -> Result<Vec<u8>, String> {
419
+ let img = image::load_from_memory(png_data)
420
+ .map_err(|e| format!("image load error: {}", e))?;
421
+ let rgba = img.to_rgba8();
422
+ let width = rgba.width() as usize;
423
+ let height = rgba.height() as usize;
424
+ if width == 0 || height == 0 {
425
+ return Err("empty image".to_string());
426
+ }
427
+
428
+ let get = |x: usize, y: usize| -> [u8; 4] {
429
+ let p = rgba.get_pixel(x as u32, y as u32);
430
+ [p[0], p[1], p[2], p[3]]
431
+ };
432
+
433
+ let mut unique_rows: Vec<usize> = Vec::new();
434
+ for y in 0..height {
435
+ if unique_rows.is_empty() {
436
+ unique_rows.push(y);
437
+ continue;
438
+ }
439
+ let prev_y = *unique_rows.last().unwrap();
440
+ let mut same = true;
441
+ for x in 0..width {
442
+ if get(x, y) != get(x, prev_y) {
443
+ same = false;
444
+ break;
445
+ }
446
+ }
447
+ if !same {
448
+ unique_rows.push(y);
449
+ }
450
+ }
451
+
452
+ let logical_h = unique_rows.len();
453
+ if logical_h == 0 {
454
+ return Err("no unique rows".to_string());
455
+ }
456
+
457
+ let first_row_y = unique_rows[0];
458
+ let mut col_indices: Vec<usize> = Vec::new();
459
+ for x in 0..width {
460
+ if col_indices.is_empty() {
461
+ col_indices.push(x);
462
+ continue;
463
+ }
464
+ let prev_x = *col_indices.last().unwrap();
465
+ if get(x, first_row_y) != get(prev_x, first_row_y) {
466
+ col_indices.push(x);
467
+ }
468
+ }
469
+
470
+ let logical_w = col_indices.len();
471
+ if logical_w < 2 || logical_h < 2 {
472
+ return Err("unstretched image too small".to_string());
473
+ }
474
+
475
+ let mut out = RgbaImage::new(logical_w as u32, logical_h as u32);
476
+ for (ly, &py) in unique_rows.iter().enumerate() {
477
+ for (lx, &px) in col_indices.iter().enumerate() {
478
+ let p = get(px, py);
479
+ out.put_pixel(lx as u32, ly as u32, Rgba(p));
480
+ }
481
+ }
482
+
483
+ let mut output = Vec::new();
484
+ out.write_to(&mut std::io::Cursor::new(&mut output), image::ImageFormat::Png)
485
+ .map_err(|e| format!("PNG write error: {}", e))?;
486
+ Ok(output)
487
+ }
488
+
409
489
  #[cfg(test)]
410
490
  mod tests {
411
491
  use image::{RgbaImage, Rgba, imageops};
@@ -521,12 +601,13 @@ mod test_transitions {
521
601
  [p[0], p[1], p[2], p[3]]
522
602
  };
523
603
  let intra_50 = super::intra_block_transitions_h(&get_pixel, 0, pw, 0, 50);
524
- if intra_50 > 0 {
604
+ let tolerance = 5u32;
605
+ if intra_50 > tolerance {
525
606
  println!("FAIL: scale_x={scale_x:.1} pw={pw} lw=50: intra={intra_50}");
526
607
  had_failure = true;
527
608
  }
528
609
  }
529
- assert!(!had_failure, "Some scale_x values gave intra > 0 for true lw=50");
610
+ assert!(!had_failure, "Some scale_x values gave intra > tolerance for true lw=50");
530
611
  }
531
612
 
532
613
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.9.3",
3
+ "version": "1.9.4",
4
4
  "type": "module",
5
5
  "description": "Ultra-lightweight PNG steganography with native Rust acceleration. Encode binary data into PNG images with zstd compression.",
6
6
  "main": "dist/index.js",