roxify 1.9.5 → 1.9.7
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/roxify_native.exe +0 -0
- package/dist/utils/decoder.js +28 -0
- package/native/image_utils.rs +12 -7
- package/native/lib.rs +14 -0
- package/native/png_utils.rs +280 -10
- package/native/reconstitution.rs +14 -5
- package/package.json +1 -1
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
package/Cargo.toml
CHANGED
package/dist/roxify_native.exe
CHANGED
|
Binary file
|
package/dist/utils/decoder.js
CHANGED
|
@@ -1170,6 +1170,34 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
1170
1170
|
e instanceof DataFormatError) {
|
|
1171
1171
|
throw e;
|
|
1172
1172
|
}
|
|
1173
|
+
try {
|
|
1174
|
+
const rawPayload = Buffer.from(native.extractPayloadFromPng(processedBuf));
|
|
1175
|
+
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
1176
|
+
payload = await tryDecompress(payload, (info) => {
|
|
1177
|
+
if (opts.onProgress)
|
|
1178
|
+
opts.onProgress(info);
|
|
1179
|
+
});
|
|
1180
|
+
if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
|
|
1181
|
+
throw new DataFormatError('Missing ROX1 magic after native extraction');
|
|
1182
|
+
}
|
|
1183
|
+
payload = payload.slice(MAGIC.length);
|
|
1184
|
+
const nameFromPng = native.extractNameFromPng
|
|
1185
|
+
? (() => { try {
|
|
1186
|
+
return native.extractNameFromPng(processedBuf);
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
return undefined;
|
|
1190
|
+
} })()
|
|
1191
|
+
: undefined;
|
|
1192
|
+
return { buf: payload, meta: { name: nameFromPng } };
|
|
1193
|
+
}
|
|
1194
|
+
catch (nativeErr) {
|
|
1195
|
+
if (nativeErr instanceof PassphraseRequiredError ||
|
|
1196
|
+
nativeErr instanceof IncorrectPassphraseError ||
|
|
1197
|
+
nativeErr instanceof DataFormatError) {
|
|
1198
|
+
throw nativeErr;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1173
1201
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
1174
1202
|
throw new Error('Failed to decode PNG: ' + errMsg);
|
|
1175
1203
|
}
|
package/native/image_utils.rs
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
use image::{ImageFormat, DynamicImage};
|
|
1
|
+
use image::{ImageFormat, DynamicImage, ImageReader};
|
|
2
2
|
use std::io::Cursor;
|
|
3
3
|
|
|
4
|
+
fn load_no_limits(input: &[u8]) -> Result<DynamicImage, String> {
|
|
5
|
+
let mut reader = ImageReader::new(Cursor::new(input))
|
|
6
|
+
.with_guessed_format()
|
|
7
|
+
.map_err(|e| format!("Failed to guess format: {}", e))?;
|
|
8
|
+
reader.no_limits();
|
|
9
|
+
reader.decode().map_err(|e| format!("Failed to load image: {}", e))
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
pub fn sharp_resize(
|
|
5
13
|
input: &[u8],
|
|
6
14
|
width: u32,
|
|
7
15
|
height: u32,
|
|
8
16
|
kernel: &str,
|
|
9
17
|
) -> Result<Vec<u8>, String> {
|
|
10
|
-
let img =
|
|
11
|
-
.map_err(|e| format!("Failed to load image: {}", e))?;
|
|
18
|
+
let img = load_no_limits(input)?;
|
|
12
19
|
|
|
13
20
|
let filter = match kernel {
|
|
14
21
|
"nearest" => image::imageops::FilterType::Nearest,
|
|
@@ -28,8 +35,7 @@ pub fn sharp_resize(
|
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
pub fn sharp_raw_pixels(input: &[u8]) -> Result<(Vec<u8>, u32, u32), String> {
|
|
31
|
-
let img =
|
|
32
|
-
.map_err(|e| format!("Failed to load image: {}", e))?;
|
|
38
|
+
let img = load_no_limits(input)?;
|
|
33
39
|
|
|
34
40
|
let rgb = img.to_rgb8();
|
|
35
41
|
let width = rgb.width();
|
|
@@ -40,8 +46,7 @@ pub fn sharp_raw_pixels(input: &[u8]) -> Result<(Vec<u8>, u32, u32), String> {
|
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
pub fn sharp_metadata(input: &[u8]) -> Result<(u32, u32, String), String> {
|
|
43
|
-
let img =
|
|
44
|
-
.map_err(|e| format!("Failed to load image: {}", e))?;
|
|
49
|
+
let img = load_no_limits(input)?;
|
|
45
50
|
|
|
46
51
|
let width = img.width();
|
|
47
52
|
let height = img.height();
|
package/native/lib.rs
CHANGED
|
@@ -389,6 +389,20 @@ pub fn crop_and_reconstitute(png_buffer: Buffer) -> Result<Vec<u8>> {
|
|
|
389
389
|
.map_err(|e| Error::from_reason(e))
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
#[cfg(not(test))]
|
|
393
|
+
#[napi]
|
|
394
|
+
pub fn unstretch_nn(png_buffer: Buffer) -> Result<Vec<u8>> {
|
|
395
|
+
reconstitution::unstretch_nn(&png_buffer)
|
|
396
|
+
.map_err(|e| Error::from_reason(e))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[cfg(not(test))]
|
|
400
|
+
#[napi]
|
|
401
|
+
pub fn extract_payload_from_png(png_buffer: Buffer) -> Result<Vec<u8>> {
|
|
402
|
+
png_utils::extract_payload_from_png(&png_buffer)
|
|
403
|
+
.map_err(|e| Error::from_reason(e))
|
|
404
|
+
}
|
|
405
|
+
|
|
392
406
|
#[cfg(not(test))]
|
|
393
407
|
#[napi]
|
|
394
408
|
pub fn extract_file_list_from_pixels(png_buffer: Buffer) -> Result<String> {
|
package/native/png_utils.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use bytemuck::{Pod, Zeroable};
|
|
2
|
-
use
|
|
2
|
+
use image::ImageReader;
|
|
3
|
+
use std::io::{Cursor, Read, Seek, SeekFrom};
|
|
3
4
|
|
|
4
5
|
#[repr(C)]
|
|
5
6
|
#[derive(Clone, Copy, Pod, Zeroable)]
|
|
@@ -148,11 +149,25 @@ pub fn extract_payload_from_png(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
|
148
149
|
return Ok(payload);
|
|
149
150
|
}
|
|
150
151
|
}
|
|
152
|
+
if let Ok(unstretched) = crate::reconstitution::unstretch_nn(&reconst) {
|
|
153
|
+
if let Ok(payload) = extract_payload_direct(&unstretched) {
|
|
154
|
+
if validate_payload_deep(&payload) {
|
|
155
|
+
return Ok(payload);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
151
159
|
}
|
|
152
|
-
let unstretched = crate::reconstitution::unstretch_nn(png_data)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
if let Ok(unstretched) = crate::reconstitution::unstretch_nn(png_data) {
|
|
161
|
+
if let Ok(payload) = extract_payload_direct(&unstretched) {
|
|
162
|
+
if validate_payload_deep(&payload) {
|
|
163
|
+
return Ok(payload);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if let Ok(payload) = extract_payload_from_embedded_nn(png_data) {
|
|
168
|
+
if validate_payload_deep(&payload) {
|
|
169
|
+
return Ok(payload);
|
|
170
|
+
}
|
|
156
171
|
}
|
|
157
172
|
Err("No valid payload found after all extraction attempts".to_string())
|
|
158
173
|
}
|
|
@@ -176,10 +191,199 @@ fn find_pixel_header(raw: &[u8]) -> Result<usize, String> {
|
|
|
176
191
|
}
|
|
177
192
|
|
|
178
193
|
fn decode_to_rgb(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
179
|
-
let
|
|
194
|
+
let mut reader = ImageReader::new(Cursor::new(png_data))
|
|
195
|
+
.with_guessed_format()
|
|
196
|
+
.map_err(|e| format!("format guess error: {}", e))?;
|
|
197
|
+
reader.no_limits();
|
|
198
|
+
let img = reader.decode().map_err(|e| format!("image decode error: {}", e))?;
|
|
180
199
|
Ok(img.to_rgb8().into_raw())
|
|
181
200
|
}
|
|
182
201
|
|
|
202
|
+
fn decode_to_rgba_grid(png_data: &[u8]) -> Result<(Vec<[u8; 4]>, u32, u32), String> {
|
|
203
|
+
let mut reader = ImageReader::new(Cursor::new(png_data))
|
|
204
|
+
.with_guessed_format()
|
|
205
|
+
.map_err(|e| format!("format guess error: {}", e))?;
|
|
206
|
+
reader.no_limits();
|
|
207
|
+
let img = reader.decode().map_err(|e| format!("image decode error: {}", e))?;
|
|
208
|
+
let rgba = img.to_rgba8();
|
|
209
|
+
let w = rgba.width();
|
|
210
|
+
let h = rgba.height();
|
|
211
|
+
let pixels: Vec<[u8; 4]> = rgba.pixels().map(|p| [p[0], p[1], p[2], p[3]]).collect();
|
|
212
|
+
Ok((pixels, w, h))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn reconstruct_logical_pixels_from_nn(
|
|
216
|
+
pixels: &[[u8; 4]], width: u32, height: u32
|
|
217
|
+
) -> Result<Vec<u8>, String> {
|
|
218
|
+
let w = width as usize;
|
|
219
|
+
let h = height as usize;
|
|
220
|
+
let get = |x: usize, y: usize| -> [u8; 4] { pixels[y * w + x] };
|
|
221
|
+
|
|
222
|
+
let magic = [b'P', b'X', b'L', b'1'];
|
|
223
|
+
|
|
224
|
+
let mut header_row = None;
|
|
225
|
+
let mut header_col = None;
|
|
226
|
+
'outer: for y in 0..h {
|
|
227
|
+
for x in 0..w.saturating_sub(1) {
|
|
228
|
+
let p0 = get(x, y);
|
|
229
|
+
let p1 = get(x + 1, y);
|
|
230
|
+
let seq = [p0[0], p0[1], p0[2], p1[0], p1[1], p1[2]];
|
|
231
|
+
for start in 0..3 {
|
|
232
|
+
if start + 4 <= 6 && seq[start] == magic[0] && seq[start+1] == magic[1]
|
|
233
|
+
&& seq[start+2] == magic[2] && seq[start+3] == magic[3]
|
|
234
|
+
{
|
|
235
|
+
header_row = Some(y);
|
|
236
|
+
header_col = Some(x);
|
|
237
|
+
break 'outer;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let header_row = header_row.ok_or("PXL1 not found in 2D pixel scan")?;
|
|
243
|
+
let header_col = header_col.ok_or("PXL1 column not found")?;
|
|
244
|
+
|
|
245
|
+
let mut scale_y = 1usize;
|
|
246
|
+
for dy in 1..h - header_row {
|
|
247
|
+
let y2 = header_row + dy;
|
|
248
|
+
let mut same = true;
|
|
249
|
+
for x in header_col..(header_col + 4).min(w) {
|
|
250
|
+
if get(x, y2) != get(x, header_row) { same = false; break; }
|
|
251
|
+
}
|
|
252
|
+
if same { scale_y += 1; } else { break; }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let cur = get(header_col, header_row);
|
|
256
|
+
let mut block_start = header_col;
|
|
257
|
+
while block_start > 0 && get(block_start - 1, header_row) == cur {
|
|
258
|
+
block_start -= 1;
|
|
259
|
+
}
|
|
260
|
+
let mut block_end = header_col + 1;
|
|
261
|
+
while block_end < w && get(block_end, header_row) == cur {
|
|
262
|
+
block_end += 1;
|
|
263
|
+
}
|
|
264
|
+
let scale_x = block_end - block_start;
|
|
265
|
+
if scale_x < 2 {
|
|
266
|
+
return Err("Could not determine NN scale_x".to_string());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let ref_y = header_row;
|
|
270
|
+
let mut embed_left = block_start;
|
|
271
|
+
loop {
|
|
272
|
+
if embed_left < scale_x { break; }
|
|
273
|
+
let candidate = embed_left - scale_x;
|
|
274
|
+
let c0 = get(candidate, ref_y);
|
|
275
|
+
let mut is_block = true;
|
|
276
|
+
for dx in 1..scale_x {
|
|
277
|
+
if candidate + dx >= w || get(candidate + dx, ref_y) != c0 {
|
|
278
|
+
is_block = false;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if !is_block { break; }
|
|
283
|
+
if candidate + scale_x < w && get(candidate + scale_x, ref_y) == c0 {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
embed_left = candidate;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let mut embed_top = header_row;
|
|
290
|
+
loop {
|
|
291
|
+
if embed_top < scale_y { break; }
|
|
292
|
+
let candidate = embed_top - scale_y;
|
|
293
|
+
let mut is_block = true;
|
|
294
|
+
for dy in 0..scale_y {
|
|
295
|
+
if candidate + dy >= h { is_block = false; break; }
|
|
296
|
+
if dy > 0 && get(embed_left, candidate + dy) != get(embed_left, candidate) {
|
|
297
|
+
is_block = false;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if !is_block { break; }
|
|
302
|
+
embed_top = candidate;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let mut logical_cols: Vec<usize> = Vec::new();
|
|
306
|
+
let mut x = embed_left;
|
|
307
|
+
while x < w {
|
|
308
|
+
logical_cols.push(x);
|
|
309
|
+
let c = get(x, ref_y);
|
|
310
|
+
let mut nx = x + 1;
|
|
311
|
+
while nx < w && get(nx, ref_y) == c {
|
|
312
|
+
nx += 1;
|
|
313
|
+
}
|
|
314
|
+
if nx >= w { break; }
|
|
315
|
+
let blk = nx - x;
|
|
316
|
+
if blk < scale_x.saturating_sub(2) || blk > scale_x + 2 {
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
x = nx;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let mut logical_rows: Vec<usize> = Vec::new();
|
|
323
|
+
let mut y = embed_top;
|
|
324
|
+
while y < h {
|
|
325
|
+
logical_rows.push(y);
|
|
326
|
+
let c = get(embed_left, y);
|
|
327
|
+
let mut ny = y + 1;
|
|
328
|
+
while ny < h && get(embed_left, ny) == c {
|
|
329
|
+
ny += 1;
|
|
330
|
+
}
|
|
331
|
+
if ny >= h { break; }
|
|
332
|
+
let blk = ny - y;
|
|
333
|
+
if blk < scale_y.saturating_sub(2) || blk > scale_y + 2 {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
y = ny;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if logical_cols.len() < 3 || logical_rows.len() < 3 {
|
|
340
|
+
return Err("Embedded region too small".to_string());
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let img_w = logical_cols.len();
|
|
344
|
+
let mut logical_rgb = Vec::with_capacity(img_w * logical_rows.len() * 3);
|
|
345
|
+
for &ry in &logical_rows {
|
|
346
|
+
for &cx in &logical_cols {
|
|
347
|
+
let p = get(cx, ry);
|
|
348
|
+
logical_rgb.push(p[0]);
|
|
349
|
+
logical_rgb.push(p[1]);
|
|
350
|
+
logical_rgb.push(p[2]);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
Ok(logical_rgb)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn extract_payload_from_embedded_nn(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
357
|
+
let (pixels, width, height) = decode_to_rgba_grid(png_data)?;
|
|
358
|
+
let logical_rgb = reconstruct_logical_pixels_from_nn(&pixels, width, height)?;
|
|
359
|
+
let pos = {
|
|
360
|
+
let magic = b"PXL1";
|
|
361
|
+
let mut found = None;
|
|
362
|
+
for i in 0..logical_rgb.len().saturating_sub(4) {
|
|
363
|
+
if &logical_rgb[i..i+4] == magic {
|
|
364
|
+
found = Some(i);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
found.ok_or("PXL1 not found in reconstructed pixels")?
|
|
369
|
+
};
|
|
370
|
+
let mut idx = pos + 4;
|
|
371
|
+
if idx + 2 > logical_rgb.len() { return Err("Truncated header in embedded NN".to_string()); }
|
|
372
|
+
let _version = logical_rgb[idx]; idx += 1;
|
|
373
|
+
let name_len = logical_rgb[idx] as usize; idx += 1;
|
|
374
|
+
if idx + name_len > logical_rgb.len() { return Err("Truncated name in embedded NN".to_string()); }
|
|
375
|
+
idx += name_len;
|
|
376
|
+
if idx + 4 > logical_rgb.len() { return Err("Truncated payload length in embedded NN".to_string()); }
|
|
377
|
+
let payload_len = ((logical_rgb[idx] as u32) << 24)
|
|
378
|
+
| ((logical_rgb[idx+1] as u32) << 16)
|
|
379
|
+
| ((logical_rgb[idx+2] as u32) << 8)
|
|
380
|
+
| (logical_rgb[idx+3] as u32);
|
|
381
|
+
idx += 4;
|
|
382
|
+
let end = idx + (payload_len as usize);
|
|
383
|
+
if end > logical_rgb.len() { return Err("Truncated payload in embedded NN".to_string()); }
|
|
384
|
+
Ok(logical_rgb[idx..end].to_vec())
|
|
385
|
+
}
|
|
386
|
+
|
|
183
387
|
pub fn extract_name_from_png(png_data: &[u8]) -> Option<String> {
|
|
184
388
|
if let Some(name) = extract_name_direct(png_data) {
|
|
185
389
|
return Some(name);
|
|
@@ -188,9 +392,21 @@ pub fn extract_name_from_png(png_data: &[u8]) -> Option<String> {
|
|
|
188
392
|
if let Some(name) = extract_name_direct(&reconst) {
|
|
189
393
|
return Some(name);
|
|
190
394
|
}
|
|
395
|
+
if let Ok(unstretched) = crate::reconstitution::unstretch_nn(&reconst) {
|
|
396
|
+
if let Some(name) = extract_name_direct(&unstretched) {
|
|
397
|
+
return Some(name);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if let Ok(unstretched) = crate::reconstitution::unstretch_nn(png_data) {
|
|
402
|
+
if let Some(name) = extract_name_direct(&unstretched) {
|
|
403
|
+
return Some(name);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if let Ok(name) = extract_name_from_embedded_nn(png_data) {
|
|
407
|
+
return Some(name);
|
|
191
408
|
}
|
|
192
|
-
|
|
193
|
-
extract_name_direct(&unstretched)
|
|
409
|
+
None
|
|
194
410
|
}
|
|
195
411
|
|
|
196
412
|
fn extract_name_direct(png_data: &[u8]) -> Option<String> {
|
|
@@ -204,6 +420,18 @@ fn extract_name_direct(png_data: &[u8]) -> Option<String> {
|
|
|
204
420
|
String::from_utf8(raw[idx..idx + name_len].to_vec()).ok()
|
|
205
421
|
}
|
|
206
422
|
|
|
423
|
+
fn extract_name_from_embedded_nn(png_data: &[u8]) -> Result<String, String> {
|
|
424
|
+
let (pixels, width, height) = decode_to_rgba_grid(png_data)?;
|
|
425
|
+
let logical_rgb = reconstruct_logical_pixels_from_nn(&pixels, width, height)?;
|
|
426
|
+
let pos = find_pixel_header(&logical_rgb)?;
|
|
427
|
+
let mut idx = pos + 4;
|
|
428
|
+
if idx + 2 > logical_rgb.len() { return Err("Truncated".to_string()); }
|
|
429
|
+
idx += 1;
|
|
430
|
+
let name_len = logical_rgb[idx] as usize; idx += 1;
|
|
431
|
+
if name_len == 0 || idx + name_len > logical_rgb.len() { return Err("Truncated name".to_string()); }
|
|
432
|
+
String::from_utf8(logical_rgb[idx..idx + name_len].to_vec()).map_err(|e| e.to_string())
|
|
433
|
+
}
|
|
434
|
+
|
|
207
435
|
fn extract_payload_direct(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
208
436
|
let raw = decode_to_rgb(png_data)?;
|
|
209
437
|
let pos = find_pixel_header(&raw)?;
|
|
@@ -233,9 +461,51 @@ pub fn extract_file_list_from_pixels(png_data: &[u8]) -> Result<String, String>
|
|
|
233
461
|
if let Ok(result) = extract_file_list_direct(&reconst) {
|
|
234
462
|
return Ok(result);
|
|
235
463
|
}
|
|
464
|
+
if let Ok(unstretched) = crate::reconstitution::unstretch_nn(&reconst) {
|
|
465
|
+
if let Ok(result) = extract_file_list_direct(&unstretched) {
|
|
466
|
+
return Ok(result);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if let Ok(unstretched) = crate::reconstitution::unstretch_nn(png_data) {
|
|
471
|
+
if let Ok(result) = extract_file_list_direct(&unstretched) {
|
|
472
|
+
return Ok(result);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if let Ok(result) = extract_file_list_from_embedded_nn(png_data) {
|
|
476
|
+
return Ok(result);
|
|
236
477
|
}
|
|
237
|
-
|
|
238
|
-
|
|
478
|
+
Err("No file list found after all extraction attempts".to_string())
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fn extract_file_list_from_embedded_nn(png_data: &[u8]) -> Result<String, String> {
|
|
482
|
+
let (pixels, width, height) = decode_to_rgba_grid(png_data)?;
|
|
483
|
+
let logical_rgb = reconstruct_logical_pixels_from_nn(&pixels, width, height)?;
|
|
484
|
+
let pos = find_pixel_header(&logical_rgb)?;
|
|
485
|
+
let mut idx = pos + 4;
|
|
486
|
+
if idx + 2 > logical_rgb.len() { return Err("Truncated".to_string()); }
|
|
487
|
+
idx += 1;
|
|
488
|
+
let name_len = logical_rgb[idx] as usize; idx += 1;
|
|
489
|
+
if idx + name_len > logical_rgb.len() { return Err("Truncated".to_string()); }
|
|
490
|
+
idx += name_len;
|
|
491
|
+
if idx + 4 > logical_rgb.len() { return Err("Truncated".to_string()); }
|
|
492
|
+
let payload_len = ((logical_rgb[idx] as u32) << 24)
|
|
493
|
+
| ((logical_rgb[idx+1] as u32) << 16)
|
|
494
|
+
| ((logical_rgb[idx+2] as u32) << 8)
|
|
495
|
+
| (logical_rgb[idx+3] as u32);
|
|
496
|
+
idx += 4;
|
|
497
|
+
idx += payload_len as usize;
|
|
498
|
+
if idx + 8 > logical_rgb.len() { return Err("No file list in embedded NN".to_string()); }
|
|
499
|
+
if &logical_rgb[idx..idx + 4] != b"rXFL" { return Err("No rXFL marker in embedded NN".to_string()); }
|
|
500
|
+
idx += 4;
|
|
501
|
+
let json_len = ((logical_rgb[idx] as u32) << 24)
|
|
502
|
+
| ((logical_rgb[idx+1] as u32) << 16)
|
|
503
|
+
| ((logical_rgb[idx+2] as u32) << 8)
|
|
504
|
+
| (logical_rgb[idx+3] as u32);
|
|
505
|
+
idx += 4;
|
|
506
|
+
let json_end = idx + json_len as usize;
|
|
507
|
+
if json_end > logical_rgb.len() { return Err("Truncated file list in embedded NN".to_string()); }
|
|
508
|
+
String::from_utf8(logical_rgb[idx..json_end].to_vec()).map_err(|e| format!("Invalid UTF-8: {}", e))
|
|
239
509
|
}
|
|
240
510
|
|
|
241
511
|
fn extract_file_list_direct(png_data: &[u8]) -> Result<String, String> {
|
package/native/reconstitution.rs
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
use image::{Rgba, RgbaImage};
|
|
1
|
+
use image::{Rgba, RgbaImage, DynamicImage, ImageReader};
|
|
2
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
|
+
}
|
|
3
12
|
|
|
4
13
|
fn color_dist(a: [u8; 4], b: [u8; 4]) -> i32 {
|
|
5
14
|
(a[0] as i32 - b[0] as i32).abs() +
|
|
@@ -77,7 +86,7 @@ fn intra_block_transitions_v(
|
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
80
|
-
let img =
|
|
89
|
+
let img = load_image_no_limits(png_data)?;
|
|
81
90
|
let rgba = img.to_rgba8();
|
|
82
91
|
let width = rgba.width();
|
|
83
92
|
let height = rgba.height();
|
|
@@ -411,13 +420,13 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
|
411
420
|
}
|
|
412
421
|
|
|
413
422
|
let mut output = Vec::new();
|
|
414
|
-
out.write_to(&mut std::io::Cursor::new(&mut output), image::ImageFormat::Png)
|
|
423
|
+
out.write_to(&mut std::io::Cursor::new(&mut output), image::ImageFormat::Png)
|
|
424
|
+
.map_err(|e| format!("PNG write error: {}", e))?;
|
|
415
425
|
Ok(output)
|
|
416
426
|
}
|
|
417
427
|
|
|
418
428
|
pub fn unstretch_nn(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
419
|
-
let img =
|
|
420
|
-
.map_err(|e| format!("image load error: {}", e))?;
|
|
429
|
+
let img = load_image_no_limits(png_data)?;
|
|
421
430
|
let rgba = img.to_rgba8();
|
|
422
431
|
let width = rgba.width() as usize;
|
|
423
432
|
let height = rgba.height() as usize;
|
package/package.json
CHANGED
|
Binary file
|