roxify 1.7.6 → 1.8.0
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 +82 -0
- package/dist/cli.js +1 -1
- package/dist/roxify_native.exe +0 -0
- package/native/archive.rs +176 -0
- package/native/audio.rs +151 -0
- package/native/bwt.rs +100 -0
- package/native/context_mixing.rs +120 -0
- package/native/core.rs +293 -0
- package/native/crypto.rs +119 -0
- package/native/encoder.rs +640 -0
- package/native/gpu.rs +116 -0
- package/native/hybrid.rs +162 -0
- package/native/image_utils.rs +77 -0
- package/native/lib.rs +464 -0
- package/native/main.rs +461 -0
- package/native/packer.rs +444 -0
- package/native/png_utils.rs +192 -0
- package/native/pool.rs +101 -0
- package/native/progress.rs +43 -0
- package/native/rans.rs +149 -0
- package/native/reconstitution.rs +511 -0
- package/package.json +6 -1
- package/scripts/postinstall.cjs +101 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
use image::{Rgba, RgbaImage};
|
|
2
|
+
use std::cmp::min;
|
|
3
|
+
|
|
4
|
+
fn color_dist(a: [u8; 4], b: [u8; 4]) -> i32 {
|
|
5
|
+
(a[0] as i32 - b[0] as i32).abs() +
|
|
6
|
+
(a[1] as i32 - b[1] as i32).abs() +
|
|
7
|
+
(a[2] as i32 - b[2] as i32).abs()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
fn is_color(p: [u8; 4], target: [u8; 3]) -> bool {
|
|
11
|
+
color_dist(p, [target[0], target[1], target[2], 255]) < 50
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fn transition_count(pixels: &[[u8; 4]]) -> u32 {
|
|
15
|
+
let mut count = 0u32;
|
|
16
|
+
for i in 1..pixels.len() {
|
|
17
|
+
if color_dist(pixels[i], pixels[i - 1]) > 0 {
|
|
18
|
+
count += 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
count
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn count_transitions_h(get_pixel: &impl Fn(u32, u32) -> [u8; 4], sx: u32, ex: u32, y: u32) -> u32 {
|
|
25
|
+
let row: Vec<[u8; 4]> = (sx..ex).map(|x| get_pixel(x, y)).collect();
|
|
26
|
+
transition_count(&row)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn count_transitions_v(get_pixel: &impl Fn(u32, u32) -> [u8; 4], x: u32, sy: u32, ey: u32) -> u32 {
|
|
30
|
+
let col: Vec<[u8; 4]> = (sy..ey).map(|y| get_pixel(x, y)).collect();
|
|
31
|
+
transition_count(&col)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn median(v: &mut Vec<u32>) -> u32 {
|
|
35
|
+
if v.is_empty() { return 0; }
|
|
36
|
+
v.sort_unstable();
|
|
37
|
+
v[v.len() / 2]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Compte les transitions INTRA-blocs en utilisant le découpage NN de la crate image
|
|
41
|
+
// Formule: pixel physique x → bloc logique floor((x + 0.5) × lw / pw)
|
|
42
|
+
// Pour le vrai lw, = 0 (chaque bloc NN a une seule couleur)
|
|
43
|
+
fn intra_block_transitions_h(
|
|
44
|
+
get_pixel: &impl Fn(u32, u32) -> [u8; 4],
|
|
45
|
+
sx: u32, ex: u32, y: u32, candidate: u32,
|
|
46
|
+
) -> u32 {
|
|
47
|
+
let pw = (ex - sx) as f32;
|
|
48
|
+
let lw = candidate as f32;
|
|
49
|
+
let ratio = lw / pw;
|
|
50
|
+
let row: Vec<[u8; 4]> = (sx..ex).map(|x| get_pixel(x, y)).collect();
|
|
51
|
+
let mut count = 0u32;
|
|
52
|
+
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;
|
|
55
|
+
if lx_prev == lx_curr && color_dist(row[i], row[i - 1]) > 0 {
|
|
56
|
+
count += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
count
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn intra_block_transitions_v(
|
|
63
|
+
get_pixel: &impl Fn(u32, u32) -> [u8; 4],
|
|
64
|
+
x: u32, sy: u32, ey: u32, candidate: u32,
|
|
65
|
+
) -> u32 {
|
|
66
|
+
let ph = (ey - sy) as f32;
|
|
67
|
+
let lh = candidate as f32;
|
|
68
|
+
let ratio = lh / ph;
|
|
69
|
+
let col: Vec<[u8; 4]> = (sy..ey).map(|y| get_pixel(x, y)).collect();
|
|
70
|
+
let mut count = 0u32;
|
|
71
|
+
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;
|
|
74
|
+
if ly_prev == ly_curr && color_dist(col[i], col[i - 1]) > 0 {
|
|
75
|
+
count += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
count
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
82
|
+
let img = image::load_from_memory(png_data).unwrap();
|
|
83
|
+
let rgba = img.to_rgba8();
|
|
84
|
+
let width = rgba.width();
|
|
85
|
+
let height = rgba.height();
|
|
86
|
+
|
|
87
|
+
let get_pixel = |x: u32, y: u32| -> [u8; 4] {
|
|
88
|
+
let p = rgba.get_pixel(x, y);
|
|
89
|
+
[p[0], p[1], p[2], p[3]]
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// (sx, sy, total_marker_w, marker_h)
|
|
93
|
+
let mut start_infos: Vec<(u32, u32, u32, u32)> = Vec::new();
|
|
94
|
+
for y in 0..height {
|
|
95
|
+
let mut x = 0;
|
|
96
|
+
while x < width {
|
|
97
|
+
if is_color(get_pixel(x, y), [255, 0, 0]) {
|
|
98
|
+
let sx = x;
|
|
99
|
+
let mut rx = x;
|
|
100
|
+
while rx < width && is_color(get_pixel(rx, y), [255, 0, 0]) { rx += 1; }
|
|
101
|
+
let w_r = rx - sx;
|
|
102
|
+
if w_r > 0 {
|
|
103
|
+
let mut gx = rx;
|
|
104
|
+
while gx < width && is_color(get_pixel(gx, y), [0, 255, 0]) { gx += 1; }
|
|
105
|
+
let w_g = gx - rx;
|
|
106
|
+
let ratio_g = w_g as f64 / w_r as f64;
|
|
107
|
+
if w_g > 0 && ratio_g > 0.3 && ratio_g < 3.0 {
|
|
108
|
+
let mut bx = gx;
|
|
109
|
+
while bx < width && is_color(get_pixel(bx, y), [0, 0, 255]) { bx += 1; }
|
|
110
|
+
let w_b = bx - gx;
|
|
111
|
+
let ratio_b = w_b as f64 / w_r as f64;
|
|
112
|
+
if w_b > 0 && ratio_b > 0.3 && ratio_b < 3.0 {
|
|
113
|
+
let mut ry = y;
|
|
114
|
+
while ry < height && is_color(get_pixel(sx, ry), [255, 0, 0]) { ry += 1; }
|
|
115
|
+
let h_r = ry - y;
|
|
116
|
+
start_infos.push((sx, y, w_r + w_g + w_b, h_r));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
x = rx;
|
|
121
|
+
} else { x += 1; }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// (ex, ey, total_marker_w, marker_h)
|
|
126
|
+
let mut end_infos: Vec<(u32, u32, u32, u32)> = Vec::new();
|
|
127
|
+
for y in (0..height).rev() {
|
|
128
|
+
let mut x = width as i32 - 1;
|
|
129
|
+
while x >= 0 {
|
|
130
|
+
if is_color(get_pixel(x as u32, y), [255, 0, 0]) {
|
|
131
|
+
let ex = x as u32 + 1;
|
|
132
|
+
let mut rx = x;
|
|
133
|
+
while rx >= 0 && is_color(get_pixel(rx as u32, y), [255, 0, 0]) { rx -= 1; }
|
|
134
|
+
let w_r = (x - rx) as u32;
|
|
135
|
+
if w_r > 0 {
|
|
136
|
+
let mut gx = rx;
|
|
137
|
+
while gx >= 0 && is_color(get_pixel(gx as u32, y), [0, 255, 0]) { gx -= 1; }
|
|
138
|
+
let w_g = (rx - gx) as u32;
|
|
139
|
+
let ratio_g = w_g as f64 / w_r as f64;
|
|
140
|
+
if w_g > 0 && ratio_g > 0.3 && ratio_g < 3.0 {
|
|
141
|
+
let mut bx = gx;
|
|
142
|
+
while bx >= 0 && is_color(get_pixel(bx as u32, y), [0, 0, 255]) { bx -= 1; }
|
|
143
|
+
let w_b = (gx - bx) as u32;
|
|
144
|
+
let ratio_b = w_b as f64 / w_r as f64;
|
|
145
|
+
if w_b > 0 && ratio_b > 0.3 && ratio_b < 3.0 {
|
|
146
|
+
let red_col = x as u32;
|
|
147
|
+
let mut ry = y as i32;
|
|
148
|
+
while ry >= 0 && is_color(get_pixel(red_col, ry as u32), [255, 0, 0]) { ry -= 1; }
|
|
149
|
+
let h_r = (y as i32 - ry) as u32;
|
|
150
|
+
end_infos.push((ex, y + 1, w_b + w_g + w_r, h_r));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
x = rx;
|
|
155
|
+
} else { x -= 1; }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Déduplique les marqueurs proches : garde pour chaque cluster (même ex, ey à ±marker_w)
|
|
160
|
+
// le plus grand. On trie par taille décroissante et on supprime les doublons proches.
|
|
161
|
+
start_infos.sort_by(|a, b| (b.2 * b.3).cmp(&(a.2 * a.3)));
|
|
162
|
+
let mut deduped_starts: Vec<(u32, u32, u32, u32)> = Vec::new();
|
|
163
|
+
for s in &start_infos {
|
|
164
|
+
let is_dup = deduped_starts.iter().any(|d: &(u32, u32, u32, u32)| {
|
|
165
|
+
(s.0 as i64 - d.0 as i64).abs() <= d.2 as i64 &&
|
|
166
|
+
(s.1 as i64 - d.1 as i64).abs() <= d.2 as i64
|
|
167
|
+
});
|
|
168
|
+
if !is_dup { deduped_starts.push(*s); }
|
|
169
|
+
if deduped_starts.len() >= 8 { break; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
end_infos.sort_by(|a, b| {
|
|
173
|
+
let sa = a.2 * a.3;
|
|
174
|
+
let sb = b.2 * b.3;
|
|
175
|
+
sb.cmp(&sa).then(b.1.cmp(&a.1))
|
|
176
|
+
});
|
|
177
|
+
let mut deduped_ends: Vec<(u32, u32, u32, u32)> = Vec::new();
|
|
178
|
+
for e in &end_infos {
|
|
179
|
+
let is_dup = deduped_ends.iter().any(|d: &(u32, u32, u32, u32)| {
|
|
180
|
+
(e.0 as i64 - d.0 as i64).abs() <= d.2 as i64 &&
|
|
181
|
+
(e.1 as i64 - d.1 as i64).abs() <= d.2 as i64
|
|
182
|
+
});
|
|
183
|
+
if !is_dup { deduped_ends.push(*e); }
|
|
184
|
+
if deduped_ends.len() >= 8 { break; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
let mut best_logical_w = 0u32;
|
|
189
|
+
let mut best_logical_h = 0u32;
|
|
190
|
+
// Score: (size_err_permille, inverse_area) — lower is better
|
|
191
|
+
let mut best_score = (u64::MAX, u64::MAX);
|
|
192
|
+
let mut best_sx = 0u32;
|
|
193
|
+
let mut best_sy = 0u32;
|
|
194
|
+
let mut best_ex = 0u32;
|
|
195
|
+
let mut best_ey = 0u32;
|
|
196
|
+
|
|
197
|
+
for &(sx, sy, start_marker_w, _) in &deduped_starts {
|
|
198
|
+
for &(ex_raw, ey_raw, end_marker_w, _) in &deduped_ends {
|
|
199
|
+
if ex_raw <= sx || ey_raw <= sy { continue; }
|
|
200
|
+
|
|
201
|
+
// Tester also ex-1 / ey-1 pour compenser pixels parasites de fond au bord du marqueur
|
|
202
|
+
for ex_adj in 0u32..=1u32 {
|
|
203
|
+
for ey_adj in 0u32..=1u32 {
|
|
204
|
+
let ex = if ex_raw > sx + ex_adj { ex_raw - ex_adj } else { continue };
|
|
205
|
+
let ey = if ey_raw > sy + ey_adj { ey_raw - ey_adj } else { continue };
|
|
206
|
+
|
|
207
|
+
let phys_w = ex - sx;
|
|
208
|
+
let phys_h = ey - sy;
|
|
209
|
+
if phys_w < 3 || phys_h < 3 || phys_w > 1800 || phys_h > 1800 { continue; }
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
// Estimation depuis les marqueurs (start_marker_w ≈ 3 × scale_x, end_marker_w ≈ 3 × scale_x)
|
|
213
|
+
// Prend la moyenne pour réduire l'erreur d'arrondi
|
|
214
|
+
let est_scale_x = (start_marker_w as f64 + end_marker_w as f64) / 6.0;
|
|
215
|
+
let est_lw_f = phys_w as f64 / est_scale_x;
|
|
216
|
+
// start_marker_h ≈ scale_y (marqueur occupe 1px logique → scale_y pixels physiques)
|
|
217
|
+
let start_h = start_infos.iter().find(|s| s.0 == sx && s.1 == sy).map(|s| s.3).unwrap_or(0);
|
|
218
|
+
let end_h = end_infos.iter().find(|e| e.0 == ex + ex_adj && e.1 == ey + ey_adj).map(|e| e.3).unwrap_or(0);
|
|
219
|
+
let est_lh_f = if start_h > 0 || end_h > 0 {
|
|
220
|
+
let scale_y_est = if start_h > 0 && end_h > 0 {
|
|
221
|
+
(start_h as f64 + end_h as f64) / 2.0
|
|
222
|
+
} else if start_h > 0 { start_h as f64 } else { end_h as f64 };
|
|
223
|
+
phys_h as f64 / scale_y_est
|
|
224
|
+
} else {
|
|
225
|
+
phys_h as f64 / est_scale_x // fallback: assume scale_y ≈ scale_x
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Comptage de transitions: source fiable pour lw et lh
|
|
229
|
+
// On scanne toute la zone [sx,ex) mais en ignorant les 2 premières et 2 dernières transitions
|
|
230
|
+
// (celles-ci peuvent être parasites si un pixel de fond s'est glissé dans la zone)
|
|
231
|
+
// La vraie valeur théorique est lw-1 transitions (N blocs logiques → N-1 frontières)
|
|
232
|
+
|
|
233
|
+
let n_lines = 13u32;
|
|
234
|
+
let mut h_counts: Vec<u32> = (1..=n_lines).filter_map(|j| {
|
|
235
|
+
let y = sy + phys_h * (j * 7 / (n_lines + 1) + 1) / 8;
|
|
236
|
+
if y >= ey { return None; }
|
|
237
|
+
Some(count_transitions_h(&get_pixel, sx, ex, y))
|
|
238
|
+
}).collect();
|
|
239
|
+
let mut v_counts: Vec<u32> = (1..=n_lines).filter_map(|j| {
|
|
240
|
+
let x = sx + phys_w * (j * 7 / (n_lines + 1) + 1) / 8;
|
|
241
|
+
if x >= ex { return None; }
|
|
242
|
+
Some(count_transitions_v(&get_pixel, x, sy, ey))
|
|
243
|
+
}).collect();
|
|
244
|
+
let h_med = median(&mut h_counts) as f64;
|
|
245
|
+
let v_med = median(&mut v_counts) as f64;
|
|
246
|
+
// Transitions dans zone [sx,ex) = lw-1 (correct) ou lw (1 pixel de fond extra)
|
|
247
|
+
// On teste les deux comme candidats
|
|
248
|
+
let lw_trans_lo = h_med;
|
|
249
|
+
let lw_trans_hi = h_med + 1.0;
|
|
250
|
+
let lh_trans_lo = v_med;
|
|
251
|
+
let lh_trans_hi = v_med + 1.0;
|
|
252
|
+
let lw_cand_lo = lw_trans_lo.round() as u32;
|
|
253
|
+
let lw_cand_hi = lw_trans_hi.round() as u32;
|
|
254
|
+
let lh_cand_lo = lh_trans_lo.round() as u32;
|
|
255
|
+
let lh_cand_hi = lh_trans_hi.round() as u32;
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
// Vérifie cohérence grossière
|
|
259
|
+
let lw_diff_lo = (lw_cand_lo as f64 - est_lw_f).abs();
|
|
260
|
+
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
|
+
|
|
263
|
+
let n_scan = 7u32;
|
|
264
|
+
let lw = if lw_cand_lo == lw_cand_hi {
|
|
265
|
+
lw_cand_hi
|
|
266
|
+
} else {
|
|
267
|
+
let score_lo: u32 = (1..=n_scan).filter_map(|j| {
|
|
268
|
+
let y = sy + phys_h * j / (n_scan + 1);
|
|
269
|
+
if y >= ey { return None; }
|
|
270
|
+
Some(intra_block_transitions_h(&get_pixel, sx, ex, y, lw_cand_lo))
|
|
271
|
+
}).sum();
|
|
272
|
+
let score_hi: u32 = (1..=n_scan).filter_map(|j| {
|
|
273
|
+
let y = sy + phys_h * j / (n_scan + 1);
|
|
274
|
+
if y >= ey { return None; }
|
|
275
|
+
Some(intra_block_transitions_h(&get_pixel, sx, ex, y, lw_cand_hi))
|
|
276
|
+
}).sum();
|
|
277
|
+
|
|
278
|
+
if score_lo < score_hi { lw_cand_lo } else { lw_cand_hi }
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Pour lh: tester {lh_cand_lo, lh_cand_hi, lh_cand_hi+1} car v_med peut être
|
|
282
|
+
// sous-estimé de 1 (pixels marqueurs dans les colonnes scannées réduisent les transitions)
|
|
283
|
+
// Tie-break par proximité à est_lh_f quand les scores intra sont égaux
|
|
284
|
+
let lh_candidates: Vec<u32> = {
|
|
285
|
+
let mut c = vec![lh_cand_lo, lh_cand_hi, lh_cand_hi + 1];
|
|
286
|
+
c.sort_unstable();
|
|
287
|
+
c.dedup();
|
|
288
|
+
c
|
|
289
|
+
};
|
|
290
|
+
let lh = {
|
|
291
|
+
let mut best_cand = lh_cand_hi;
|
|
292
|
+
let mut best_intra = u32::MAX;
|
|
293
|
+
let mut best_dist = f64::MAX;
|
|
294
|
+
for &cand in &lh_candidates {
|
|
295
|
+
if cand < 3 { continue; }
|
|
296
|
+
let score: u32 = (1..=n_scan).filter_map(|j| {
|
|
297
|
+
let x = sx + phys_w * j / (n_scan + 1);
|
|
298
|
+
if x >= ex { return None; }
|
|
299
|
+
Some(intra_block_transitions_v(&get_pixel, x, sy, ey, cand))
|
|
300
|
+
}).sum();
|
|
301
|
+
let dist = (cand as f64 - est_lh_f).abs();
|
|
302
|
+
if score < best_intra || (score == best_intra && dist < best_dist) {
|
|
303
|
+
best_intra = score;
|
|
304
|
+
best_dist = dist;
|
|
305
|
+
best_cand = cand;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
best_cand
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Cohérence des marqueurs avec le lw sélectionné
|
|
312
|
+
let lw_size_err = {
|
|
313
|
+
let scx = phys_w as f64 / lw as f64;
|
|
314
|
+
let emw = 3.0 * scx;
|
|
315
|
+
let e1 = ((start_marker_w as f64 - emw).abs() / emw * 1000.0) as u64;
|
|
316
|
+
let e2 = ((end_marker_w as f64 - emw).abs() / emw * 1000.0) as u64;
|
|
317
|
+
e1 + e2
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Filtre taille min logique
|
|
321
|
+
if lw < 3 || lh < 3 { continue; }
|
|
322
|
+
|
|
323
|
+
let size_err = lw_size_err;
|
|
324
|
+
if size_err > 500 { continue; }
|
|
325
|
+
|
|
326
|
+
// Filtre final : l'intra-block score pour le lw sélectionné doit être faible
|
|
327
|
+
// (pour une vraie paire avec l'image NN-scalée, = 0; pour une zone de fond aléatoire, >> 0)
|
|
328
|
+
let intra_final: u32 = (1..=n_scan).filter_map(|j| {
|
|
329
|
+
let y = sy + phys_h * j / (n_scan + 1);
|
|
330
|
+
if y >= ey { return None; }
|
|
331
|
+
Some(intra_block_transitions_h(&get_pixel, sx, ex, y, lw))
|
|
332
|
+
}).sum();
|
|
333
|
+
// Filtre : la zone encodée NN a des blocs monochromes → intra très bas
|
|
334
|
+
// Zone de fond aléatoire → intra ≈ lw × n_scan × 0.99 >> 0
|
|
335
|
+
// Seuil : lw/8 × n_scan pour permettre ≈ lw/8 pixels parasites
|
|
336
|
+
let intra_threshold = (lw as u32 / 8 + 1) * n_scan;
|
|
337
|
+
|
|
338
|
+
if intra_final > intra_threshold { continue; }
|
|
339
|
+
|
|
340
|
+
// Favori : pas d'ajustement de bord, puis plus grande zone, puis plus petit size_err
|
|
341
|
+
let area = phys_w as u64 * phys_h as u64;
|
|
342
|
+
let adj_penalty = (ex_adj + ey_adj) as u64 * 1000;
|
|
343
|
+
let score = (size_err + adj_penalty, u64::MAX - area);
|
|
344
|
+
|
|
345
|
+
if score < best_score {
|
|
346
|
+
|
|
347
|
+
best_score = score;
|
|
348
|
+
best_logical_w = lw;
|
|
349
|
+
best_logical_h = lh;
|
|
350
|
+
best_sx = sx;
|
|
351
|
+
best_sy = sy;
|
|
352
|
+
best_ex = ex;
|
|
353
|
+
best_ey = ey;
|
|
354
|
+
}
|
|
355
|
+
} // end ey_adj
|
|
356
|
+
} // end ex_adj
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if best_score == (u64::MAX, u64::MAX) {
|
|
361
|
+
return Err("No valid markers found".to_string());
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let sx = best_sx;
|
|
365
|
+
let sy = best_sy;
|
|
366
|
+
let ex = best_ex;
|
|
367
|
+
let ey = best_ey;
|
|
368
|
+
let phys_w = ex - sx;
|
|
369
|
+
let phys_h = ey - sy;
|
|
370
|
+
let scale_x = phys_w as f64 / best_logical_w as f64;
|
|
371
|
+
let scale_y = phys_h as f64 / best_logical_h as f64;
|
|
372
|
+
|
|
373
|
+
let mut out = RgbaImage::from_pixel(best_logical_w, best_logical_h, Rgba([0, 0, 0, 255]));
|
|
374
|
+
for ly in 0..best_logical_h {
|
|
375
|
+
for lx in 0..best_logical_w {
|
|
376
|
+
let px = (sx as f64 + (lx as f64 + 0.5) * scale_x) as u32;
|
|
377
|
+
let py = (sy as f64 + (ly as f64 + 0.5) * scale_y) as u32;
|
|
378
|
+
out.put_pixel(lx, ly, Rgba(get_pixel(min(px, width - 1), min(py, height - 1))));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let mut output = Vec::new();
|
|
383
|
+
out.write_to(&mut std::io::Cursor::new(&mut output), image::ImageFormat::Png).unwrap();
|
|
384
|
+
Ok(output)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[cfg(test)]
|
|
388
|
+
mod tests {
|
|
389
|
+
use image::{RgbaImage, Rgba, imageops};
|
|
390
|
+
use rand::Rng;
|
|
391
|
+
use super::*;
|
|
392
|
+
|
|
393
|
+
fn generate_mock_encoded_payload(size: u32) -> RgbaImage {
|
|
394
|
+
let mut img = RgbaImage::new(size, size);
|
|
395
|
+
let mut rng = rand::thread_rng();
|
|
396
|
+
for y in 0..size {
|
|
397
|
+
for x in 0..size {
|
|
398
|
+
img.put_pixel(x, y, Rgba([rng.gen(), rng.gen(), rng.gen(), 255]));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
img.put_pixel(0, 0, Rgba([255, 0, 0, 255]));
|
|
402
|
+
img.put_pixel(1, 0, Rgba([0, 255, 0, 255]));
|
|
403
|
+
img.put_pixel(2, 0, Rgba([0, 0, 255, 255]));
|
|
404
|
+
img.put_pixel(size - 3, size - 1, Rgba([0, 0, 255, 255]));
|
|
405
|
+
img.put_pixel(size - 2, size - 1, Rgba([0, 255, 0, 255]));
|
|
406
|
+
img.put_pixel(size - 1, size - 1, Rgba([255, 0, 0, 255]));
|
|
407
|
+
img
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#[test]
|
|
411
|
+
fn test_extreme_deformation_and_background() {
|
|
412
|
+
let mut rng = rand::thread_rng();
|
|
413
|
+
let logical_size = 30;
|
|
414
|
+
for i in 0..10 {
|
|
415
|
+
let original_payload = generate_mock_encoded_payload(logical_size);
|
|
416
|
+
let scale_x = rng.gen_range(1.5..8.0);
|
|
417
|
+
let scale_y = rng.gen_range(1.5..8.0);
|
|
418
|
+
let scaled_width = (logical_size as f64 * scale_x).round() as u32;
|
|
419
|
+
let scaled_height = (logical_size as f64 * scale_y).round() as u32;
|
|
420
|
+
let scaled_payload = imageops::resize(&original_payload, scaled_width, scaled_height, imageops::FilterType::Nearest);
|
|
421
|
+
let bg_width = 800;
|
|
422
|
+
let bg_height = 800;
|
|
423
|
+
let mut complex_bg = RgbaImage::new(bg_width, bg_height);
|
|
424
|
+
for p in complex_bg.pixels_mut() { *p = Rgba([rng.gen(), rng.gen(), rng.gen(), 255]); }
|
|
425
|
+
let offset_x = rng.gen_range(20..(bg_width - scaled_width - 20)) as i64;
|
|
426
|
+
let offset_y = rng.gen_range(20..(bg_height - scaled_height - 20)) as i64;
|
|
427
|
+
imageops::overlay(&mut complex_bg, &scaled_payload, offset_x, offset_y);
|
|
428
|
+
let mut input_png_bytes = Vec::new();
|
|
429
|
+
complex_bg.write_to(&mut std::io::Cursor::new(&mut input_png_bytes), image::ImageFormat::Png).unwrap();
|
|
430
|
+
let recovered_bytes = crop_and_reconstitute(&input_png_bytes).unwrap();
|
|
431
|
+
let recovered_img = image::load_from_memory(&recovered_bytes).unwrap().to_rgba8();
|
|
432
|
+
assert_eq!(recovered_img.height(), logical_size, "Failed at iteration {} with scale_x={}, scale_y={}", i, scale_x, scale_y);
|
|
433
|
+
assert_eq!(recovered_img.width(), logical_size, "Failed at iteration {} with scale_x={}, scale_y={}", i, scale_x, scale_y);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[cfg(test)]
|
|
439
|
+
mod test_transitions {
|
|
440
|
+
use image::{RgbaImage, Rgba, imageops};
|
|
441
|
+
use rand::Rng;
|
|
442
|
+
|
|
443
|
+
#[test]
|
|
444
|
+
fn test_internal_transitions() {
|
|
445
|
+
let logical_size = 50;
|
|
446
|
+
let mut img = RgbaImage::new(logical_size, logical_size);
|
|
447
|
+
let mut rng = rand::thread_rng();
|
|
448
|
+
for y in 0..logical_size {
|
|
449
|
+
for x in 0..logical_size {
|
|
450
|
+
img.put_pixel(x, y, Rgba([rng.gen(), rng.gen(), rng.gen(), 255]));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let scale_x = 5.865853935599068;
|
|
455
|
+
let scaled_width = (logical_size as f64 * scale_x).round() as u32;
|
|
456
|
+
let scaled_payload = imageops::resize(&img, scaled_width, 50, imageops::FilterType::Nearest);
|
|
457
|
+
|
|
458
|
+
let phys_w = scaled_width;
|
|
459
|
+
println!("phys_w = {}", phys_w);
|
|
460
|
+
|
|
461
|
+
for candidate_w in 45..55 {
|
|
462
|
+
let mut internal_transitions = 0;
|
|
463
|
+
for i in 0..candidate_w {
|
|
464
|
+
let start_x = (i as f64 * phys_w as f64 / candidate_w as f64).round() as u32;
|
|
465
|
+
let end_x = ((i + 1) as f64 * phys_w as f64 / candidate_w as f64).round() as u32;
|
|
466
|
+
|
|
467
|
+
for y in 0..50 {
|
|
468
|
+
let mut last_color = scaled_payload.get_pixel(start_x, y);
|
|
469
|
+
for x in start_x+1..end_x {
|
|
470
|
+
let color = scaled_payload.get_pixel(x, y);
|
|
471
|
+
if color[0] != last_color[0] || color[1] != last_color[1] || color[2] != last_color[2] {
|
|
472
|
+
internal_transitions += 1;
|
|
473
|
+
last_color = color;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
println!("W = {}, internal_transitions = {}", candidate_w, internal_transitions);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#[test]
|
|
483
|
+
fn test_intra_block_formula() {
|
|
484
|
+
let logical_size = 50u32;
|
|
485
|
+
let mut img = RgbaImage::new(logical_size, logical_size);
|
|
486
|
+
let mut rng = rand::thread_rng();
|
|
487
|
+
for y in 0..logical_size {
|
|
488
|
+
for x in 0..logical_size {
|
|
489
|
+
img.put_pixel(x, y, Rgba([rng.gen(), rng.gen(), rng.gen(), 255]));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
let mut had_failure = false;
|
|
493
|
+
for scale_x_10 in 11..=159u32 {
|
|
494
|
+
let scale_x = scale_x_10 as f64 / 10.0;
|
|
495
|
+
let pw = (logical_size as f64 * scale_x).round() as u32;
|
|
496
|
+
let scaled = imageops::resize(&img, pw, logical_size, imageops::FilterType::Nearest);
|
|
497
|
+
let get_pixel = |x: u32, y: u32| -> [u8; 4] {
|
|
498
|
+
let p = scaled.get_pixel(x, y);
|
|
499
|
+
[p[0], p[1], p[2], p[3]]
|
|
500
|
+
};
|
|
501
|
+
let intra_50 = super::intra_block_transitions_h(&get_pixel, 0, pw, 0, 50);
|
|
502
|
+
if intra_50 > 0 {
|
|
503
|
+
println!("FAIL: scale_x={scale_x:.1} pw={pw} lw=50: intra={intra_50}");
|
|
504
|
+
had_failure = true;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
assert!(!had_failure, "Some scale_x values gave intra > 0 for true lw=50");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"libroxify_native-x86_64-apple-darwin.node",
|
|
29
29
|
"libroxify_native-aarch64-apple-darwin.node",
|
|
30
30
|
"dist/roxify_native.exe",
|
|
31
|
+
"dist/roxify_native",
|
|
32
|
+
"scripts/postinstall.cjs",
|
|
33
|
+
"native",
|
|
34
|
+
"Cargo.toml",
|
|
31
35
|
"README.md",
|
|
32
36
|
"LICENSE"
|
|
33
37
|
],
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
"release:flow": "node scripts/release-flow.cjs",
|
|
60
64
|
"release:flow:auto": "AUTO_PUBLISH=1 node scripts/release-flow.cjs",
|
|
61
65
|
"release:full": "node scripts/publish.cjs",
|
|
66
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
62
67
|
"prepublishOnly": "npx -p typescript tsc || echo 'TS build skipped'",
|
|
63
68
|
"test": "npm run build && node ./test/run-all-tests.cjs",
|
|
64
69
|
"test:integration": "node scripts/run-integration-tests.cjs",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const { existsSync, copyFileSync, mkdirSync } = require('fs');
|
|
3
|
+
const { join, dirname } = require('path');
|
|
4
|
+
|
|
5
|
+
const root = join(__dirname, '..');
|
|
6
|
+
const distDir = join(root, 'dist');
|
|
7
|
+
|
|
8
|
+
function hasCargo() {
|
|
9
|
+
try {
|
|
10
|
+
execSync('cargo --version', { stdio: 'ignore', timeout: 5000 });
|
|
11
|
+
return true;
|
|
12
|
+
} catch { return false; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getBinaryName() {
|
|
16
|
+
return process.platform === 'win32' ? 'roxify_native.exe' : 'roxify_native';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findExistingBinary() {
|
|
20
|
+
const name = getBinaryName();
|
|
21
|
+
const candidates = [
|
|
22
|
+
join(distDir, name),
|
|
23
|
+
join(root, 'target', 'release', name),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
candidates.push(join(root, 'target', 'x86_64-pc-windows-gnu', 'release', name));
|
|
28
|
+
candidates.push(join(root, 'target', 'x86_64-pc-windows-msvc', 'release', name));
|
|
29
|
+
} else if (process.platform === 'linux') {
|
|
30
|
+
candidates.push(join(root, 'target', 'x86_64-unknown-linux-gnu', 'release', name));
|
|
31
|
+
} else if (process.platform === 'darwin') {
|
|
32
|
+
candidates.push(join(root, 'target', 'x86_64-apple-darwin', 'release', name));
|
|
33
|
+
candidates.push(join(root, 'target', 'aarch64-apple-darwin', 'release', name));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const c of candidates) {
|
|
37
|
+
if (existsSync(c)) return c;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildBinary() {
|
|
43
|
+
if (!hasCargo()) {
|
|
44
|
+
console.log('roxify: Cargo not found, skipping native binary build (TypeScript fallback will be used)');
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('roxify: Building native CLI binary with Cargo...');
|
|
49
|
+
try {
|
|
50
|
+
execSync('cargo build --release --bin roxify_native', {
|
|
51
|
+
cwd: root,
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
timeout: 600000,
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.log('roxify: Native build failed, TypeScript fallback will be used');
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function run() {
|
|
63
|
+
if (!existsSync(distDir)) {
|
|
64
|
+
mkdirSync(distDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const dest = join(distDir, getBinaryName());
|
|
68
|
+
if (existsSync(dest)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const existing = findExistingBinary();
|
|
73
|
+
if (existing && existing !== dest) {
|
|
74
|
+
copyFileSync(existing, dest);
|
|
75
|
+
if (process.platform !== 'win32') {
|
|
76
|
+
try { require('fs').chmodSync(dest, 0o755); } catch {}
|
|
77
|
+
}
|
|
78
|
+
console.log(`roxify: Copied native binary from ${existing}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (existing) return;
|
|
83
|
+
|
|
84
|
+
if (process.env.ROXIFY_SKIP_BUILD === '1') {
|
|
85
|
+
console.log('roxify: ROXIFY_SKIP_BUILD=1, skipping native build');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (buildBinary()) {
|
|
90
|
+
const built = join(root, 'target', 'release', getBinaryName());
|
|
91
|
+
if (existsSync(built)) {
|
|
92
|
+
copyFileSync(built, dest);
|
|
93
|
+
if (process.platform !== 'win32') {
|
|
94
|
+
try { require('fs').chmodSync(dest, 0o755); } catch {}
|
|
95
|
+
}
|
|
96
|
+
console.log('roxify: Native binary built and copied to dist/');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
run();
|