roxify 1.12.0 → 1.12.2

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.
@@ -1,623 +0,0 @@
1
- use image::{Rgba, RgbaImage, DynamicImage, ImageReader};
2
- use std::cmp::min;
3
- use std::io::Cursor;
4
-
5
- fn load_image_no_limits(data: &[u8]) -> Result<DynamicImage, String> {
6
- let mut reader = ImageReader::new(Cursor::new(data))
7
- .with_guessed_format()
8
- .map_err(|e| format!("format guess error: {}", e))?;
9
- reader.no_limits();
10
- reader.decode().map_err(|e| format!("image decode error: {}", e))
11
- }
12
-
13
- fn color_dist(a: [u8; 4], b: [u8; 4]) -> i32 {
14
- (a[0] as i32 - b[0] as i32).abs() +
15
- (a[1] as i32 - b[1] as i32).abs() +
16
- (a[2] as i32 - b[2] as i32).abs()
17
- }
18
-
19
- fn is_color(p: [u8; 4], target: [u8; 3]) -> bool {
20
- color_dist(p, [target[0], target[1], target[2], 255]) < 50
21
- }
22
-
23
- fn transition_count(pixels: &[[u8; 4]]) -> u32 {
24
- let mut count = 0u32;
25
- for i in 1..pixels.len() {
26
- if color_dist(pixels[i], pixels[i - 1]) > 0 {
27
- count += 1;
28
- }
29
- }
30
- count
31
- }
32
-
33
- fn count_transitions_h(get_pixel: &impl Fn(u32, u32) -> [u8; 4], sx: u32, ex: u32, y: u32) -> u32 {
34
- let row: Vec<[u8; 4]> = (sx..ex).map(|x| get_pixel(x, y)).collect();
35
- transition_count(&row)
36
- }
37
-
38
- fn count_transitions_v(get_pixel: &impl Fn(u32, u32) -> [u8; 4], x: u32, sy: u32, ey: u32) -> u32 {
39
- let col: Vec<[u8; 4]> = (sy..ey).map(|y| get_pixel(x, y)).collect();
40
- transition_count(&col)
41
- }
42
-
43
- fn median(v: &mut Vec<u32>) -> u32 {
44
- if v.is_empty() { return 0; }
45
- v.sort_unstable();
46
- v[v.len() / 2]
47
- }
48
-
49
- // Compte les transitions INTRA-blocs en utilisant le découpage NN de la crate image
50
- // Formule: pixel physique x → bloc logique floor((x + 0.5) × lw / pw)
51
- // Pour le vrai lw, = 0 (chaque bloc NN a une seule couleur)
52
- fn intra_block_transitions_h(
53
- get_pixel: &impl Fn(u32, u32) -> [u8; 4],
54
- sx: u32, ex: u32, y: u32, candidate: u32,
55
- ) -> u32 {
56
- let pw = (ex - sx) as f64;
57
- let lw = candidate as f64;
58
- let row: Vec<[u8; 4]> = (sx..ex).map(|x| get_pixel(x, y)).collect();
59
- let mut count = 0u32;
60
- for i in 1..row.len() {
61
- let lx_prev = ((i as f64 - 0.5) * lw / pw) as u32;
62
- let lx_curr = ((i as f64 + 0.5) * lw / pw) as u32;
63
- if lx_prev == lx_curr && color_dist(row[i], row[i - 1]) > 0 {
64
- count += 1;
65
- }
66
- }
67
- count
68
- }
69
-
70
- fn intra_block_transitions_v(
71
- get_pixel: &impl Fn(u32, u32) -> [u8; 4],
72
- x: u32, sy: u32, ey: u32, candidate: u32,
73
- ) -> u32 {
74
- let ph = (ey - sy) as f64;
75
- let lh = candidate as f64;
76
- let col: Vec<[u8; 4]> = (sy..ey).map(|y| get_pixel(x, y)).collect();
77
- let mut count = 0u32;
78
- for i in 1..col.len() {
79
- let ly_prev = ((i as f64 - 0.5) * lh / ph) as u32;
80
- let ly_curr = ((i as f64 + 0.5) * lh / ph) as u32;
81
- if ly_prev == ly_curr && color_dist(col[i], col[i - 1]) > 0 {
82
- count += 1;
83
- }
84
- }
85
- count
86
- }
87
-
88
- pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
89
- let img = load_image_no_limits(png_data)?;
90
- let rgba = img.to_rgba8();
91
- let width = rgba.width();
92
- let height = rgba.height();
93
-
94
- let get_pixel = |x: u32, y: u32| -> [u8; 4] {
95
- let p = rgba.get_pixel(x, y);
96
- [p[0], p[1], p[2], p[3]]
97
- };
98
-
99
- // (sx, sy, total_marker_w, marker_h)
100
- let mut start_infos: Vec<(u32, u32, u32, u32)> = Vec::new();
101
- for y in 0..height {
102
- let mut x = 0;
103
- while x < width {
104
- if is_color(get_pixel(x, y), [255, 0, 0]) {
105
- let sx = x;
106
- let mut rx = x;
107
- while rx < width && is_color(get_pixel(rx, y), [255, 0, 0]) { rx += 1; }
108
- let w_r = rx - sx;
109
- if w_r > 0 {
110
- let mut gx = rx;
111
- while gx < width && is_color(get_pixel(gx, y), [0, 255, 0]) { gx += 1; }
112
- let w_g = gx - rx;
113
- let ratio_g = w_g as f64 / w_r as f64;
114
- if w_g > 0 && ratio_g > 0.3 && ratio_g < 3.0 {
115
- let mut bx = gx;
116
- while bx < width && is_color(get_pixel(bx, y), [0, 0, 255]) { bx += 1; }
117
- let w_b = bx - gx;
118
- let ratio_b = w_b as f64 / w_r as f64;
119
- if w_b > 0 && ratio_b > 0.3 && ratio_b < 3.0 {
120
- let mut ry = y;
121
- while ry < height && is_color(get_pixel(sx, ry), [255, 0, 0]) { ry += 1; }
122
- let h_r = ry - y;
123
- start_infos.push((sx, y, w_r + w_g + w_b, h_r));
124
- }
125
- }
126
- }
127
- x = rx;
128
- } else { x += 1; }
129
- }
130
- }
131
-
132
- // (ex, ey, total_marker_w, marker_h)
133
- let mut end_infos: Vec<(u32, u32, u32, u32)> = Vec::new();
134
- for y in (0..height).rev() {
135
- let mut x = width as i32 - 1;
136
- while x >= 0 {
137
- if is_color(get_pixel(x as u32, y), [255, 0, 0]) {
138
- let ex = x as u32 + 1;
139
- let mut rx = x;
140
- while rx >= 0 && is_color(get_pixel(rx as u32, y), [255, 0, 0]) { rx -= 1; }
141
- let w_r = (x - rx) as u32;
142
- if w_r > 0 {
143
- let mut gx = rx;
144
- while gx >= 0 && is_color(get_pixel(gx as u32, y), [0, 255, 0]) { gx -= 1; }
145
- let w_g = (rx - gx) as u32;
146
- let ratio_g = w_g as f64 / w_r as f64;
147
- if w_g > 0 && ratio_g > 0.3 && ratio_g < 3.0 {
148
- let mut bx = gx;
149
- while bx >= 0 && is_color(get_pixel(bx as u32, y), [0, 0, 255]) { bx -= 1; }
150
- let w_b = (gx - bx) as u32;
151
- let ratio_b = w_b as f64 / w_r as f64;
152
- if w_b > 0 && ratio_b > 0.3 && ratio_b < 3.0 {
153
- let red_col = x as u32;
154
- let mut ry = y as i32;
155
- while ry >= 0 && is_color(get_pixel(red_col, ry as u32), [255, 0, 0]) { ry -= 1; }
156
- let h_r = (y as i32 - ry) as u32;
157
- end_infos.push((ex, y + 1, w_b + w_g + w_r, h_r));
158
- }
159
- }
160
- }
161
- x = rx;
162
- } else { x -= 1; }
163
- }
164
- }
165
-
166
- // Déduplique les marqueurs proches : garde pour chaque cluster (même ex, ey à ±marker_w)
167
- // le plus grand. On trie par taille décroissante et on supprime les doublons proches.
168
- start_infos.sort_by(|a, b| (b.2 * b.3).cmp(&(a.2 * a.3)));
169
- let mut deduped_starts: Vec<(u32, u32, u32, u32)> = Vec::new();
170
- for s in &start_infos {
171
- let is_dup = deduped_starts.iter().any(|d: &(u32, u32, u32, u32)| {
172
- (s.0 as i64 - d.0 as i64).abs() <= d.2 as i64 &&
173
- (s.1 as i64 - d.1 as i64).abs() <= d.2 as i64
174
- });
175
- if !is_dup { deduped_starts.push(*s); }
176
- if deduped_starts.len() >= 8 { break; }
177
- }
178
-
179
- end_infos.sort_by(|a, b| {
180
- let sa = a.2 * a.3;
181
- let sb = b.2 * b.3;
182
- sb.cmp(&sa).then(b.1.cmp(&a.1))
183
- });
184
- let mut deduped_ends: Vec<(u32, u32, u32, u32)> = Vec::new();
185
- for e in &end_infos {
186
- let is_dup = deduped_ends.iter().any(|d: &(u32, u32, u32, u32)| {
187
- (e.0 as i64 - d.0 as i64).abs() <= d.2 as i64 &&
188
- (e.1 as i64 - d.1 as i64).abs() <= d.2 as i64
189
- });
190
- if !is_dup { deduped_ends.push(*e); }
191
- if deduped_ends.len() >= 8 { break; }
192
- }
193
-
194
-
195
-
196
- let mut best_logical_w = 0u32;
197
- let mut best_logical_h = 0u32;
198
- // Score: (size_err_permille, inverse_area) — lower is better
199
- let mut best_score = (u64::MAX, u64::MAX);
200
- let mut best_sx = 0u32;
201
- let mut best_sy = 0u32;
202
- let mut best_ex = 0u32;
203
- let mut best_ey = 0u32;
204
-
205
- for &(sx, sy, start_marker_w, _) in &deduped_starts {
206
- for &(ex_raw, ey_raw, end_marker_w, _) in &deduped_ends {
207
- if ex_raw <= sx || ey_raw <= sy { continue; }
208
-
209
- // Tester also ex-1 / ey-1 pour compenser pixels parasites de fond au bord du marqueur
210
- for ex_adj in 0u32..=1u32 {
211
- for ey_adj in 0u32..=1u32 {
212
- let ex = if ex_raw > sx + ex_adj { ex_raw - ex_adj } else { continue };
213
- let ey = if ey_raw > sy + ey_adj { ey_raw - ey_adj } else { continue };
214
-
215
- let phys_w = ex - sx;
216
- let phys_h = ey - sy;
217
- if phys_w < 3 || phys_h < 3 || phys_w > 1800 || phys_h > 1800 {
218
- continue;
219
- }
220
-
221
-
222
- // Estimation depuis les marqueurs (start_marker_w ≈ 3 × scale_x, end_marker_w ≈ 3 × scale_x)
223
- // Prend la moyenne pour réduire l'erreur d'arrondi
224
- let est_scale_x = (start_marker_w as f64 + end_marker_w as f64) / 6.0;
225
- let est_lw_f = phys_w as f64 / est_scale_x;
226
- // start_marker_h ≈ scale_y (marqueur occupe 1px logique → scale_y pixels physiques)
227
- let start_h = start_infos.iter().find(|s| s.0 == sx && s.1 == sy).map(|s| s.3).unwrap_or(0);
228
- 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);
229
- let est_lh_f = if start_h > 0 || end_h > 0 {
230
- let scale_y_est = if start_h > 0 && end_h > 0 {
231
- (start_h as f64 + end_h as f64) / 2.0
232
- } else if start_h > 0 { start_h as f64 } else { end_h as f64 };
233
- phys_h as f64 / scale_y_est
234
- } else {
235
- phys_h as f64 / est_scale_x // fallback: assume scale_y ≈ scale_x
236
- };
237
-
238
- // Comptage de transitions: source fiable pour lw et lh
239
- // On scanne toute la zone [sx,ex) mais en ignorant les 2 premières et 2 dernières transitions
240
- // (celles-ci peuvent être parasites si un pixel de fond s'est glissé dans la zone)
241
- // La vraie valeur théorique est lw-1 transitions (N blocs logiques → N-1 frontières)
242
-
243
- let n_lines = 13u32;
244
- let mut h_counts: Vec<u32> = (1..=n_lines).filter_map(|j| {
245
- let y = sy + phys_h * (j * 7 / (n_lines + 1) + 1) / 8;
246
- if y >= ey { return None; }
247
- Some(count_transitions_h(&get_pixel, sx, ex, y))
248
- }).collect();
249
- let mut v_counts: Vec<u32> = (1..=n_lines).filter_map(|j| {
250
- let x = sx + phys_w * (j * 7 / (n_lines + 1) + 1) / 8;
251
- if x >= ex { return None; }
252
- Some(count_transitions_v(&get_pixel, x, sy, ey))
253
- }).collect();
254
- let h_med = median(&mut h_counts) as f64;
255
- let v_med = median(&mut v_counts) as f64;
256
- // Transitions dans zone [sx,ex) = lw-1 (correct) ou lw (1 pixel de fond extra)
257
- // On teste les deux comme candidats
258
- let lw_trans_lo = h_med;
259
- let lw_trans_hi = h_med + 1.0;
260
- let lh_trans_lo = v_med;
261
- let lh_trans_hi = v_med + 1.0;
262
- let lw_cand_lo = lw_trans_lo.round() as u32;
263
- let lw_cand_hi = lw_trans_hi.round() as u32;
264
- let lh_cand_lo = lh_trans_lo.round() as u32;
265
- let lh_cand_hi = lh_trans_hi.round() as u32;
266
-
267
-
268
- // Vérifie cohérence grossière
269
- let lw_diff_lo = (lw_cand_lo as f64 - est_lw_f).abs();
270
- let lw_diff_hi = (lw_cand_hi as f64 - est_lw_f).abs();
271
- if lw_diff_lo > est_lw_f * 0.25 + 3.0 && lw_diff_hi > est_lw_f * 0.25 + 3.0 {
272
- continue;
273
- }
274
-
275
- let n_scan = 7u32;
276
- let lw = if lw_cand_lo == lw_cand_hi {
277
- lw_cand_hi
278
- } else {
279
- let score_lo: u32 = (1..=n_scan).filter_map(|j| {
280
- let y = sy + phys_h * j / (n_scan + 1);
281
- if y >= ey { return None; }
282
- Some(intra_block_transitions_h(&get_pixel, sx, ex, y, lw_cand_lo))
283
- }).sum();
284
- let score_hi: u32 = (1..=n_scan).filter_map(|j| {
285
- let y = sy + phys_h * j / (n_scan + 1);
286
- if y >= ey { return None; }
287
- Some(intra_block_transitions_h(&get_pixel, sx, ex, y, lw_cand_hi))
288
- }).sum();
289
-
290
- if score_lo < score_hi { lw_cand_lo } else { lw_cand_hi }
291
- };
292
-
293
- // Pour lh: tester {lh_cand_lo, lh_cand_hi, lh_cand_hi+1} car v_med peut être
294
- // sous-estimé de 1 (pixels marqueurs dans les colonnes scannées réduisent les transitions)
295
- // Tie-break par proximité à est_lh_f quand les scores intra sont égaux
296
- let lh_candidates: Vec<u32> = {
297
- let mut c = vec![lh_cand_lo, lh_cand_hi, lh_cand_hi + 1];
298
- c.sort_unstable();
299
- c.dedup();
300
- c
301
- };
302
- let lh = {
303
- let mut best_cand = lh_cand_hi;
304
- let mut best_intra = u32::MAX;
305
- let mut best_dist = f64::MAX;
306
- for &cand in &lh_candidates {
307
- if cand < 3 { continue; }
308
- let score: u32 = (1..=n_scan).filter_map(|j| {
309
- let x = sx + phys_w * j / (n_scan + 1);
310
- if x >= ex { return None; }
311
- Some(intra_block_transitions_v(&get_pixel, x, sy, ey, cand))
312
- }).sum();
313
- let dist = (cand as f64 - est_lh_f).abs();
314
- if score < best_intra || (score == best_intra && dist < best_dist) {
315
- best_intra = score;
316
- best_dist = dist;
317
- best_cand = cand;
318
- }
319
- }
320
- best_cand
321
- };
322
-
323
- // Cohérence des marqueurs avec le lw sélectionné
324
- let lw_size_err = {
325
- let scx = phys_w as f64 / lw as f64;
326
- let emw = 3.0 * scx;
327
- let e1 = ((start_marker_w as f64 - emw).abs() / emw * 1000.0) as u64;
328
- let e2 = ((end_marker_w as f64 - emw).abs() / emw * 1000.0) as u64;
329
- e1 + e2
330
- };
331
-
332
- // Filtre taille min logique
333
- if lw < 3 || lh < 3 {
334
- continue;
335
- }
336
-
337
- let size_err = lw_size_err;
338
- if size_err > 500 {
339
- continue;
340
- }
341
-
342
- // Filtre final : l'intra-block score pour le lw sélectionné doit être faible
343
- // (pour une vraie paire avec l'image NN-scalée, = 0; pour une zone de fond aléatoire, >> 0)
344
- let intra_final: u32 = (1..=n_scan).filter_map(|j| {
345
- let y = sy + phys_h * j / (n_scan + 1);
346
- if y >= ey { return None; }
347
- Some(intra_block_transitions_h(&get_pixel, sx, ex, y, lw))
348
- }).sum();
349
- // Filtre : la zone encodée NN a des blocs monochromes → intra très bas
350
- // Zone de fond aléatoire → intra ≈ lw × n_scan × 0.99 >> 0
351
- // Seuil : lw/8 × n_scan pour permettre ≈ lw/8 pixels parasites
352
- let intra_threshold = ((lw as u32 / 2 + 3) * n_scan).max(n_scan * 3);
353
-
354
- if intra_final > intra_threshold {
355
- continue;
356
- }
357
-
358
- // Favori : pas d'ajustement de bord, puis plus grande zone, puis plus petit size_err
359
- let area = phys_w as u64 * phys_h as u64;
360
- let adj_penalty = (ex_adj + ey_adj) as u64 * 1000;
361
- let score = (size_err + adj_penalty, u64::MAX - area);
362
-
363
- if score < best_score {
364
-
365
- best_score = score;
366
- best_logical_w = lw;
367
- best_logical_h = lh;
368
- best_sx = sx;
369
- best_sy = sy;
370
- best_ex = ex;
371
- best_ey = ey;
372
- }
373
- } // end ey_adj
374
- } // end ex_adj
375
- }
376
- }
377
-
378
- if best_score == (u64::MAX, u64::MAX) {
379
- return Err("No valid markers found".to_string());
380
- }
381
-
382
- let sx = best_sx;
383
- let sy = best_sy;
384
- let ex = best_ex;
385
- let ey = best_ey;
386
- let phys_w = ex - sx;
387
- let phys_h = ey - sy;
388
- let scale_x = phys_w as f64 / best_logical_w as f64;
389
- let scale_y = phys_h as f64 / best_logical_h as f64;
390
-
391
- let mut out = RgbaImage::from_pixel(best_logical_w, best_logical_h, Rgba([0, 0, 0, 255]));
392
- for ly in 0..best_logical_h {
393
- for lx in 0..best_logical_w {
394
- let bx0 = (sx as f64 + lx as f64 * scale_x).round() as u32;
395
- let bx1 = (sx as f64 + (lx + 1) as f64 * scale_x).round() as u32;
396
- let by0 = (sy as f64 + ly as f64 * scale_y).round() as u32;
397
- let by1 = (sy as f64 + (ly + 1) as f64 * scale_y).round() as u32;
398
- let bx0 = min(bx0, width - 1);
399
- let bx1 = min(bx1, width).max(bx0 + 1);
400
- let by0 = min(by0, height - 1);
401
- let by1 = min(by1, height).max(by0 + 1);
402
-
403
- let mut rs: Vec<u8> = Vec::new();
404
- let mut gs: Vec<u8> = Vec::new();
405
- let mut bs: Vec<u8> = Vec::new();
406
- for py in by0..by1 {
407
- for px in bx0..bx1 {
408
- let p = get_pixel(px, py);
409
- rs.push(p[0]);
410
- gs.push(p[1]);
411
- bs.push(p[2]);
412
- }
413
- }
414
- rs.sort_unstable();
415
- gs.sort_unstable();
416
- bs.sort_unstable();
417
- let mid = rs.len() / 2;
418
- out.put_pixel(lx, ly, Rgba([rs[mid], gs[mid], bs[mid], 255]));
419
- }
420
- }
421
-
422
- let mut output = Vec::new();
423
- out.write_to(&mut std::io::Cursor::new(&mut output), image::ImageFormat::Png)
424
- .map_err(|e| format!("PNG write error: {}", e))?;
425
- Ok(output)
426
- }
427
-
428
- pub fn unstretch_nn(png_data: &[u8]) -> Result<Vec<u8>, String> {
429
- let img = load_image_no_limits(png_data)?;
430
- let rgba = img.to_rgba8();
431
- let width = rgba.width() as usize;
432
- let height = rgba.height() as usize;
433
- if width == 0 || height == 0 {
434
- return Err("empty image".to_string());
435
- }
436
-
437
- let get = |x: usize, y: usize| -> [u8; 4] {
438
- let p = rgba.get_pixel(x as u32, y as u32);
439
- [p[0], p[1], p[2], p[3]]
440
- };
441
-
442
- let mut unique_rows: Vec<usize> = Vec::new();
443
- for y in 0..height {
444
- if unique_rows.is_empty() {
445
- unique_rows.push(y);
446
- continue;
447
- }
448
- let prev_y = *unique_rows.last().unwrap();
449
- let mut same = true;
450
- for x in 0..width {
451
- if get(x, y) != get(x, prev_y) {
452
- same = false;
453
- break;
454
- }
455
- }
456
- if !same {
457
- unique_rows.push(y);
458
- }
459
- }
460
-
461
- let logical_h = unique_rows.len();
462
- if logical_h == 0 {
463
- return Err("no unique rows".to_string());
464
- }
465
-
466
- let first_row_y = unique_rows[0];
467
- let mut col_indices: Vec<usize> = Vec::new();
468
- for x in 0..width {
469
- if col_indices.is_empty() {
470
- col_indices.push(x);
471
- continue;
472
- }
473
- let prev_x = *col_indices.last().unwrap();
474
- if get(x, first_row_y) != get(prev_x, first_row_y) {
475
- col_indices.push(x);
476
- }
477
- }
478
-
479
- let logical_w = col_indices.len();
480
- if logical_w < 2 || logical_h < 2 {
481
- return Err("unstretched image too small".to_string());
482
- }
483
-
484
- let mut out = RgbaImage::new(logical_w as u32, logical_h as u32);
485
- for (ly, &py) in unique_rows.iter().enumerate() {
486
- for (lx, &px) in col_indices.iter().enumerate() {
487
- let p = get(px, py);
488
- out.put_pixel(lx as u32, ly as u32, Rgba(p));
489
- }
490
- }
491
-
492
- let mut output = Vec::new();
493
- out.write_to(&mut std::io::Cursor::new(&mut output), image::ImageFormat::Png)
494
- .map_err(|e| format!("PNG write error: {}", e))?;
495
- Ok(output)
496
- }
497
-
498
- #[cfg(test)]
499
- mod tests {
500
- use image::{RgbaImage, Rgba, imageops};
501
- use rand::Rng;
502
- use super::*;
503
-
504
- fn generate_mock_encoded_payload(size: u32) -> RgbaImage {
505
- let mut img = RgbaImage::new(size, size);
506
- let mut rng = rand::thread_rng();
507
- for y in 0..size {
508
- for x in 0..size {
509
- img.put_pixel(x, y, Rgba([rng.gen(), rng.gen(), rng.gen(), 255]));
510
- }
511
- }
512
- img.put_pixel(0, 0, Rgba([255, 0, 0, 255]));
513
- img.put_pixel(1, 0, Rgba([0, 255, 0, 255]));
514
- img.put_pixel(2, 0, Rgba([0, 0, 255, 255]));
515
- img.put_pixel(size - 3, size - 1, Rgba([0, 0, 255, 255]));
516
- img.put_pixel(size - 2, size - 1, Rgba([0, 255, 0, 255]));
517
- img.put_pixel(size - 1, size - 1, Rgba([255, 0, 0, 255]));
518
- img
519
- }
520
-
521
- #[test]
522
- fn test_extreme_deformation_and_background() {
523
- let mut rng = rand::thread_rng();
524
- let logical_size = 30;
525
- for i in 0..10 {
526
- let original_payload = generate_mock_encoded_payload(logical_size);
527
- let scale_x = rng.gen_range(1.5..8.0);
528
- let scale_y = rng.gen_range(1.5..8.0);
529
- let scaled_width = (logical_size as f64 * scale_x).round() as u32;
530
- let scaled_height = (logical_size as f64 * scale_y).round() as u32;
531
- let scaled_payload = imageops::resize(&original_payload, scaled_width, scaled_height, imageops::FilterType::Nearest);
532
- let bg_width = 800;
533
- let bg_height = 800;
534
- let mut complex_bg = RgbaImage::new(bg_width, bg_height);
535
- for p in complex_bg.pixels_mut() { *p = Rgba([rng.gen(), rng.gen(), rng.gen(), 255]); }
536
- let offset_x = rng.gen_range(20..(bg_width - scaled_width - 20)) as i64;
537
- let offset_y = rng.gen_range(20..(bg_height - scaled_height - 20)) as i64;
538
- imageops::overlay(&mut complex_bg, &scaled_payload, offset_x, offset_y);
539
- let mut input_png_bytes = Vec::new();
540
- complex_bg.write_to(&mut std::io::Cursor::new(&mut input_png_bytes), image::ImageFormat::Png).unwrap();
541
- let recovered_bytes = crop_and_reconstitute(&input_png_bytes).unwrap();
542
- let recovered_img = image::load_from_memory(&recovered_bytes).unwrap().to_rgba8();
543
- assert_eq!(recovered_img.height(), logical_size, "Failed at iteration {} with scale_x={}, scale_y={}", i, scale_x, scale_y);
544
- assert_eq!(recovered_img.width(), logical_size, "Failed at iteration {} with scale_x={}, scale_y={}", i, scale_x, scale_y);
545
- }
546
- }
547
- }
548
-
549
- #[cfg(test)]
550
- mod test_transitions {
551
- use image::{RgbaImage, Rgba, imageops};
552
- use rand::Rng;
553
-
554
- #[test]
555
- fn test_internal_transitions() {
556
- let logical_size = 50;
557
- let mut img = RgbaImage::new(logical_size, logical_size);
558
- let mut rng = rand::thread_rng();
559
- for y in 0..logical_size {
560
- for x in 0..logical_size {
561
- img.put_pixel(x, y, Rgba([rng.gen(), rng.gen(), rng.gen(), 255]));
562
- }
563
- }
564
-
565
- let scale_x = 5.865853935599068;
566
- let scaled_width = (logical_size as f64 * scale_x).round() as u32;
567
- let scaled_payload = imageops::resize(&img, scaled_width, 50, imageops::FilterType::Nearest);
568
-
569
- let phys_w = scaled_width;
570
- println!("phys_w = {}", phys_w);
571
-
572
- for candidate_w in 45..55 {
573
- let mut internal_transitions = 0;
574
- for i in 0..candidate_w {
575
- let start_x = (i as f64 * phys_w as f64 / candidate_w as f64).round() as u32;
576
- let end_x = ((i + 1) as f64 * phys_w as f64 / candidate_w as f64).round() as u32;
577
-
578
- for y in 0..50 {
579
- let mut last_color = scaled_payload.get_pixel(start_x, y);
580
- for x in start_x+1..end_x {
581
- let color = scaled_payload.get_pixel(x, y);
582
- if color[0] != last_color[0] || color[1] != last_color[1] || color[2] != last_color[2] {
583
- internal_transitions += 1;
584
- last_color = color;
585
- }
586
- }
587
- }
588
- }
589
- println!("W = {}, internal_transitions = {}", candidate_w, internal_transitions);
590
- }
591
- }
592
-
593
- #[test]
594
- fn test_intra_block_formula() {
595
- let logical_size = 50u32;
596
- let mut img = RgbaImage::new(logical_size, logical_size);
597
- let mut rng = rand::thread_rng();
598
- for y in 0..logical_size {
599
- for x in 0..logical_size {
600
- img.put_pixel(x, y, Rgba([rng.gen(), rng.gen(), rng.gen(), 255]));
601
- }
602
- }
603
- let mut had_failure = false;
604
- for scale_x_10 in 11..=159u32 {
605
- let scale_x = scale_x_10 as f64 / 10.0;
606
- let pw = (logical_size as f64 * scale_x).round() as u32;
607
- let scaled = imageops::resize(&img, pw, logical_size, imageops::FilterType::Nearest);
608
- let get_pixel = |x: u32, y: u32| -> [u8; 4] {
609
- let p = scaled.get_pixel(x, y);
610
- [p[0], p[1], p[2], p[3]]
611
- };
612
- let intra_50 = super::intra_block_transitions_h(&get_pixel, 0, pw, 0, 50);
613
- let tolerance = 5u32;
614
- if intra_50 > tolerance {
615
- println!("FAIL: scale_x={scale_x:.1} pw={pw} lw=50: intra={intra_50}");
616
- had_failure = true;
617
- }
618
- }
619
- assert!(!had_failure, "Some scale_x values gave intra > tolerance for true lw=50");
620
- }
621
-
622
-
623
- }