roxify 1.9.2 → 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 +1 -1
- package/dist/cli.js +4 -4
- package/dist/roxify_native.exe +0 -0
- package/native/png_utils.rs +23 -2
- package/native/reconstitution.rs +124 -21
- package/package.json +1 -1
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/scripts/postinstall.cjs +2 -2
package/Cargo.toml
CHANGED
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
|
|
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 (${
|
|
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
|
|
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 (${
|
|
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(' ');
|
package/dist/roxify_native.exe
CHANGED
|
Binary file
|
package/native/png_utils.rs
CHANGED
|
@@ -138,10 +138,31 @@ pub fn get_png_metadata(png_data: &[u8]) -> Result<(u32, u32, u8, u8), String> {
|
|
|
138
138
|
|
|
139
139
|
pub fn extract_payload_from_png(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
140
140
|
if let Ok(payload) = extract_payload_direct(png_data) {
|
|
141
|
+
if validate_payload_deep(&payload) {
|
|
142
|
+
return Ok(payload);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
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) {
|
|
141
155
|
return Ok(payload);
|
|
142
156
|
}
|
|
143
|
-
|
|
144
|
-
|
|
157
|
+
Err("No valid payload found after all extraction attempts".to_string())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fn validate_payload_deep(payload: &[u8]) -> bool {
|
|
161
|
+
if payload.len() < 5 { return false; }
|
|
162
|
+
if payload[0] == 0x01 || payload[0] == 0x02 { return true; }
|
|
163
|
+
let compressed = if payload[0] == 0x00 { &payload[1..] } else { payload };
|
|
164
|
+
if compressed.starts_with(b"ROX1") { return true; }
|
|
165
|
+
crate::core::zstd_decompress_bytes(compressed, None).is_ok()
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
fn find_pixel_header(raw: &[u8]) -> Result<usize, String> {
|
package/native/reconstitution.rs
CHANGED
|
@@ -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
|
|
48
|
-
let lw = candidate as
|
|
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
|
|
54
|
-
let lx_curr = ((i as
|
|
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
|
|
67
|
-
let lh = candidate as
|
|
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
|
|
73
|
-
let ly_curr = ((i as
|
|
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 {
|
|
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 {
|
|
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 {
|
|
324
|
+
if lw < 3 || lh < 3 {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
322
327
|
|
|
323
328
|
let size_err = lw_size_err;
|
|
324
|
-
if size_err > 500 {
|
|
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 /
|
|
343
|
+
let intra_threshold = ((lw as u32 / 4 + 2) * n_scan).max(n_scan * 3);
|
|
337
344
|
|
|
338
|
-
if intra_final > intra_threshold {
|
|
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;
|
|
@@ -373,9 +382,31 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
|
373
382
|
let mut out = RgbaImage::from_pixel(best_logical_w, best_logical_h, Rgba([0, 0, 0, 255]));
|
|
374
383
|
for ly in 0..best_logical_h {
|
|
375
384
|
for lx in 0..best_logical_w {
|
|
376
|
-
let
|
|
377
|
-
let
|
|
378
|
-
|
|
385
|
+
let bx0 = (sx as f64 + lx as f64 * scale_x).round() as u32;
|
|
386
|
+
let bx1 = (sx as f64 + (lx + 1) as f64 * scale_x).round() as u32;
|
|
387
|
+
let by0 = (sy as f64 + ly as f64 * scale_y).round() as u32;
|
|
388
|
+
let by1 = (sy as f64 + (ly + 1) as f64 * scale_y).round() as u32;
|
|
389
|
+
let bx0 = min(bx0, width - 1);
|
|
390
|
+
let bx1 = min(bx1, width).max(bx0 + 1);
|
|
391
|
+
let by0 = min(by0, height - 1);
|
|
392
|
+
let by1 = min(by1, height).max(by0 + 1);
|
|
393
|
+
|
|
394
|
+
let mut rs: Vec<u8> = Vec::new();
|
|
395
|
+
let mut gs: Vec<u8> = Vec::new();
|
|
396
|
+
let mut bs: Vec<u8> = Vec::new();
|
|
397
|
+
for py in by0..by1 {
|
|
398
|
+
for px in bx0..bx1 {
|
|
399
|
+
let p = get_pixel(px, py);
|
|
400
|
+
rs.push(p[0]);
|
|
401
|
+
gs.push(p[1]);
|
|
402
|
+
bs.push(p[2]);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
rs.sort_unstable();
|
|
406
|
+
gs.sort_unstable();
|
|
407
|
+
bs.sort_unstable();
|
|
408
|
+
let mid = rs.len() / 2;
|
|
409
|
+
out.put_pixel(lx, ly, Rgba([rs[mid], gs[mid], bs[mid], 255]));
|
|
379
410
|
}
|
|
380
411
|
}
|
|
381
412
|
|
|
@@ -384,6 +415,77 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
|
384
415
|
Ok(output)
|
|
385
416
|
}
|
|
386
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
|
+
|
|
387
489
|
#[cfg(test)]
|
|
388
490
|
mod tests {
|
|
389
491
|
use image::{RgbaImage, Rgba, imageops};
|
|
@@ -499,12 +601,13 @@ mod test_transitions {
|
|
|
499
601
|
[p[0], p[1], p[2], p[3]]
|
|
500
602
|
};
|
|
501
603
|
let intra_50 = super::intra_block_transitions_h(&get_pixel, 0, pw, 0, 50);
|
|
502
|
-
|
|
604
|
+
let tolerance = 5u32;
|
|
605
|
+
if intra_50 > tolerance {
|
|
503
606
|
println!("FAIL: scale_x={scale_x:.1} pw={pw} lw=50: intra={intra_50}");
|
|
504
607
|
had_failure = true;
|
|
505
608
|
}
|
|
506
609
|
}
|
|
507
|
-
assert!(!had_failure, "Some scale_x values gave intra >
|
|
610
|
+
assert!(!had_failure, "Some scale_x values gave intra > tolerance for true lw=50");
|
|
508
611
|
}
|
|
509
612
|
|
|
510
613
|
|
package/package.json
CHANGED
|
Binary file
|
package/scripts/postinstall.cjs
CHANGED
|
@@ -106,7 +106,7 @@ function ensureCliBinary() {
|
|
|
106
106
|
if (existing && existing !== dest) {
|
|
107
107
|
copyFileSync(existing, dest);
|
|
108
108
|
if (platform() !== 'win32') {
|
|
109
|
-
try { require('fs').chmodSync(dest, 0o755); } catch {}
|
|
109
|
+
try { require('fs').chmodSync(dest, 0o755); } catch { }
|
|
110
110
|
}
|
|
111
111
|
console.log(`roxify: Copied CLI binary from ${existing}`);
|
|
112
112
|
return;
|
|
@@ -119,7 +119,7 @@ function ensureCliBinary() {
|
|
|
119
119
|
if (existsSync(built)) {
|
|
120
120
|
copyFileSync(built, dest);
|
|
121
121
|
if (platform() !== 'win32') {
|
|
122
|
-
try { require('fs').chmodSync(dest, 0o755); } catch {}
|
|
122
|
+
try { require('fs').chmodSync(dest, 0o755); } catch { }
|
|
123
123
|
}
|
|
124
124
|
console.log('roxify: CLI binary built and copied to dist/');
|
|
125
125
|
}
|