roxify 1.13.4 → 1.13.6

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,219 +1,13 @@
1
- import { execFileSync } from 'child_process';
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
3
- import { tmpdir } from 'os';
4
- import { join } from 'path';
5
- import { unpackBuffer } from '../pack.js';
6
- import { isWav, wavToBytes } from './audio.js';
7
- import { CHUNK_TYPE, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PIXEL_MAGIC_BLOCK, PNG_HEADER, } from './constants.js';
8
- import { DataFormatError, IncorrectPassphraseError, PassphraseRequiredError, } from './errors.js';
9
- import { colorsToBytes, deltaDecode, tryDecryptIfNeeded } from './helpers.js';
1
+ import { readFileSync } from 'fs';
10
2
  import { native } from './native.js';
11
- import { cropAndReconstitute } from './reconstitution.js';
12
- import { decodeRobustAudio, isRobustAudioWav } from './robust-audio.js';
13
- import { decodeRobustImage, isRobustImage } from './robust-image.js';
14
- import { parallelZstdDecompress } from './zstd.js';
15
- function isColorMatch(r1, g1, b1, r2, g2, b2) {
16
- return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2) < 50;
17
- }
18
- /**
19
- * Un-stretch an image that was nearest-neighbor scaled.
20
- * 1. Crops to non-background bounding box
21
- * 2. Collapses horizontal runs of identical pixels into single pixels
22
- * 3. Removes duplicate consecutive rows
23
- *
24
- * Returns null if the image doesn't appear to be stretched.
25
- */
26
- export function unstretchImage(rawRGB, width, height, tolerance = 0) {
27
- if (width <= 0 || height <= 0 || rawRGB.length < width * height * 3) {
28
- return null;
29
- }
30
- // Step 1: Find bounding box of non-background pixels
31
- // Background = white-ish (all channels >= 240)
32
- let minX = width, maxX = -1, minY = height, maxY = -1;
33
- for (let y = 0; y < height; y++) {
34
- const rowBase = y * width * 3;
35
- for (let x = 0; x < width; x++) {
36
- const idx = rowBase + x * 3;
37
- const r = rawRGB[idx], g = rawRGB[idx + 1], b = rawRGB[idx + 2];
38
- // Skip background: white or near-white
39
- if (r >= 240 && g >= 240 && b >= 240)
40
- continue;
41
- if (x < minX)
42
- minX = x;
43
- if (x > maxX)
44
- maxX = x;
45
- if (y < minY)
46
- minY = y;
47
- if (y > maxY)
48
- maxY = y;
49
- }
50
- }
51
- if (maxX < minX || maxY < minY)
52
- return null; // all background
53
- const cropW = maxX - minX + 1;
54
- const cropH = maxY - minY + 1;
55
- // Don't process tiny images
56
- if (cropW < 2 || cropH < 2)
57
- return null;
58
- // Step 2: Collapse horizontal runs per row + deduplicate rows
59
- const pixelsMatch = (buf, i1, i2) => {
60
- if (tolerance === 0) {
61
- return (buf[i1] === buf[i2] &&
62
- buf[i1 + 1] === buf[i2 + 1] &&
63
- buf[i1 + 2] === buf[i2 + 2]);
64
- }
65
- return (Math.abs(buf[i1] - buf[i2]) +
66
- Math.abs(buf[i1 + 1] - buf[i2 + 1]) +
67
- Math.abs(buf[i1 + 2] - buf[i2 + 2]) <=
68
- tolerance);
69
- };
70
- const logicalRows = [];
71
- let logicalW = -1;
72
- let prevRowKey = '';
73
- for (let y = minY; y <= maxY; y++) {
74
- const rowBase = y * width * 3;
75
- const pixels = [];
76
- let prevIdx = -1;
77
- for (let x = minX; x <= maxX; x++) {
78
- const idx = rowBase + x * 3;
79
- if (prevIdx >= 0 && pixelsMatch(rawRGB, idx, prevIdx)) {
80
- continue; // same as previous pixel, skip (collapse run)
81
- }
82
- pixels.push(rawRGB[idx], rawRGB[idx + 1], rawRGB[idx + 2]);
83
- prevIdx = idx;
84
- }
85
- // Compute row key for dedup
86
- const rowKey = pixels.join(',');
87
- if (rowKey === prevRowKey) {
88
- continue; // duplicate row, skip
89
- }
90
- prevRowKey = rowKey;
91
- const rowW = pixels.length / 3;
92
- if (logicalW === -1) {
93
- logicalW = rowW;
94
- }
95
- else if (rowW !== logicalW) {
96
- // A uniform-color row collapses to 1 pixel — expand to logicalW
97
- if (rowW === 1 && logicalW > 1) {
98
- const r = pixels[0], g = pixels[1], b = pixels[2];
99
- for (let f = 1; f < logicalW; f++) {
100
- pixels.push(r, g, b);
101
- }
102
- }
103
- else if (logicalW === 1 && rowW > 1) {
104
- // First row was uniform, adopt new width and expand it
105
- const prevRow = logicalRows[logicalRows.length - 1];
106
- if (prevRow) {
107
- const pr = prevRow.pixels[0], pg = prevRow.pixels[1], pb = prevRow.pixels[2];
108
- prevRow.pixels = [];
109
- for (let f = 0; f < rowW; f++) {
110
- prevRow.pixels.push(pr, pg, pb);
111
- }
112
- }
113
- logicalW = rowW;
114
- }
115
- else {
116
- // Inconsistent row widths → not a clean stretch
117
- // Try with tolerance if we haven't already
118
- if (tolerance === 0) {
119
- return unstretchImage(rawRGB, width, height, 30);
120
- }
121
- return null;
122
- }
123
- }
124
- logicalRows.push({ pixels, key: rowKey });
125
- }
126
- if (logicalRows.length === 0 || logicalW <= 0)
127
- return null;
128
- const logicalH = logicalRows.length;
129
- // Sanity: must have actually reduced the image
130
- if (logicalW >= cropW && logicalH >= cropH)
131
- return null;
132
- // Build output buffer
133
- const data = Buffer.allocUnsafe(logicalW * logicalH * 3);
134
- let outIdx = 0;
135
- for (const row of logicalRows) {
136
- for (let i = 0; i < row.pixels.length; i++) {
137
- data[outIdx++] = row.pixels[i];
138
- }
139
- }
140
- if (process.env.ROX_DEBUG) {
141
- console.log(`DEBUG: unstretch ${width}x${height} → crop ${cropW}x${cropH} → logical ${logicalW}x${logicalH}`);
142
- }
143
- return { data, width: logicalW, height: logicalH };
144
- }
145
- const RBW1_MAGIC = Buffer.from('RBW1');
146
- async function tryDecompress(payload, onProgress) {
147
- if (payload.length >= 4 && payload.subarray(0, 4).equals(RBW1_MAGIC) && native?.hybridDecompress) {
148
- if (onProgress)
149
- onProgress({ phase: 'decompress_start', total: 1 });
150
- const result = Buffer.from(native.hybridDecompress(payload));
151
- if (onProgress)
152
- onProgress({ phase: 'decompress_done', loaded: 1, total: 1 });
153
- return result;
154
- }
155
- return await parallelZstdDecompress(payload, onProgress);
156
- }
157
- function detectImageFormat(buf) {
158
- if (buf.length < 12)
159
- return 'unknown';
160
- if (buf[0] === 0x89 &&
161
- buf[1] === 0x50 &&
162
- buf[2] === 0x4e &&
163
- buf[3] === 0x47) {
164
- return 'png';
165
- }
166
- if (buf[0] === 0x52 &&
167
- buf[1] === 0x49 &&
168
- buf[2] === 0x46 &&
169
- buf[3] === 0x46 &&
170
- buf[8] === 0x57 &&
171
- buf[9] === 0x45 &&
172
- buf[10] === 0x42 &&
173
- buf[11] === 0x50) {
174
- return 'webp';
175
- }
176
- if (buf[0] === 0xff && buf[1] === 0x0a) {
177
- return 'jxl';
178
- }
179
- return 'unknown';
180
- }
181
- function convertToPng(buf, format) {
182
- const tempDir = mkdtempSync(join(tmpdir(), 'rox-decode-'));
183
- const inputPath = join(tempDir, format === 'webp' ? 'input.webp' : 'input.jxl');
184
- const outputPath = join(tempDir, 'output.png');
185
- try {
186
- writeFileSync(inputPath, buf);
187
- if (format === 'webp') {
188
- execFileSync('dwebp', [inputPath, '-o', outputPath]);
189
- }
190
- else if (format === 'jxl') {
191
- execFileSync('djxl', [inputPath, outputPath]);
192
- }
193
- const pngBuf = readFileSync(outputPath);
194
- return pngBuf;
195
- }
196
- finally {
197
- try {
198
- rmSync(tempDir, { recursive: true, force: true });
199
- }
200
- catch (e) { }
201
- }
202
- }
3
+ import { unpackBuffer } from '../pack.js';
203
4
  /**
204
5
  * Decode a ROX PNG or buffer into the original binary payload or files list.
6
+ * This function uses the Rust native implementation exclusively.
205
7
  *
206
8
  * @param input - Buffer or path to a PNG file.
207
9
  * @param opts - Optional decode options.
208
10
  * @returns A Promise resolving to DecodeResult ({ buf, meta } or { files }).
209
- *
210
- * @example
211
- * ```js
212
- * import { decodePngToBinary } from 'roxify';
213
- * const png = fs.readFileSync('out.png');
214
- * const res = await decodePngToBinary(png);
215
- * console.log(res.meta?.name, res.buf.toString('utf8'));
216
- * ```
217
11
  */
218
12
  export async function decodePngToBinary(input, opts = {}) {
219
13
  let pngBuf;
@@ -221,1046 +15,53 @@ export async function decodePngToBinary(input, opts = {}) {
221
15
  pngBuf = input;
222
16
  }
223
17
  else {
18
+ pngBuf = readFileSync(input);
19
+ }
20
+ // --- Native decoder: let Rust handle extraction/decompression/decryption ---
21
+ const payload = Buffer.from(native.extractPayloadFromPng(pngBuf));
22
+ if (payload.length === 0) {
23
+ throw new Error('No payload found in PNG');
24
+ }
25
+ // Extract name from payload header (version byte, name length, name)
26
+ let name;
27
+ let dataOffset = 0;
28
+ if (payload.length > 2) {
29
+ const version = payload[0];
30
+ const nameLen = payload[1];
31
+ dataOffset = 2;
32
+ if (nameLen > 0 && payload.length >= 2 + nameLen + 8) {
33
+ name = payload.subarray(2, 2 + nameLen).toString('utf8');
34
+ dataOffset = 2 + nameLen;
35
+ }
36
+ // Read payload length (8 bytes, big-endian, after name)
37
+ const payloadLen = Number(payload.readBigUInt64BE(dataOffset));
38
+ dataOffset += 8;
39
+ // The actual compressed/encrypted data starts at dataOffset
40
+ let compressedData = payload.subarray(dataOffset, dataOffset + payloadLen);
41
+ // Try to decompress with zstd if needed
42
+ let decompressed;
224
43
  try {
225
- if (native?.sharpMetadata) {
226
- const inputBuf = readFileSync(input);
227
- const metadata = native.sharpMetadata(inputBuf);
228
- const rawBytesEstimate = metadata.width * metadata.height * 4;
229
- const MAX_RAW_BYTES = 200 * 1024 * 1024;
230
- if (rawBytesEstimate > MAX_RAW_BYTES) {
231
- pngBuf = inputBuf;
232
- }
233
- else {
234
- pngBuf = inputBuf;
235
- }
236
- }
237
- else {
238
- pngBuf = readFileSync(input);
239
- }
240
- }
241
- catch (e) {
242
- try {
243
- pngBuf = readFileSync(input);
244
- }
245
- catch (e2) {
246
- throw e;
247
- }
248
- }
249
- }
250
- let progressBar = null;
251
- if (opts.showProgress) {
252
- progressBar = {
253
- start: () => { },
254
- update: () => { },
255
- stop: () => { },
256
- };
257
- const startTime = Date.now();
258
- if (!opts.onProgress) {
259
- opts.onProgress = (info) => {
260
- let pct = 0;
261
- if (info.phase === 'start') {
262
- pct = 10;
263
- }
264
- else if (info.phase === 'decompress') {
265
- pct = 50;
266
- }
267
- else if (info.phase === 'done') {
268
- pct = 100;
269
- }
270
- };
271
- }
272
- }
273
- if (opts.onProgress)
274
- opts.onProgress({ phase: 'start' });
275
- let processedBuf = pngBuf;
276
- try {
277
- if (native?.sharpMetadata) {
278
- const info = native.sharpMetadata(pngBuf);
279
- if (info.width && info.height) {
280
- const MAX_RAW_BYTES = 1200 * 1024 * 1024;
281
- const rawBytesEstimate = info.width * info.height * 4;
282
- if (rawBytesEstimate > MAX_RAW_BYTES) {
283
- throw new DataFormatError(`Image too large to decode in-process (${Math.round(rawBytesEstimate / 1024 / 1024)} MB). Increase Node heap or use a smaller image/compact mode.`);
284
- }
285
- }
286
- }
287
- processedBuf = pngBuf;
288
- }
289
- catch (e) {
290
- if (e instanceof DataFormatError)
291
- throw e;
292
- }
293
- if (opts.onProgress)
294
- opts.onProgress({ phase: 'processed' });
295
- // ─── Robust audio detection (lossy-resilient WAV) ──────────────────────────
296
- if (isWav(processedBuf) && isRobustAudioWav(processedBuf)) {
297
- try {
298
- const result = decodeRobustAudio(processedBuf);
299
- if (opts.onProgress)
300
- opts.onProgress({ phase: 'done' });
301
- progressBar?.stop();
302
- // Try unpack multi-file archive
303
- try {
304
- const unpack = unpackBuffer(result.data);
305
- if (unpack && unpack.files && unpack.files.length > 0) {
306
- return { files: unpack.files, correctedErrors: result.correctedErrors };
307
- }
308
- }
309
- catch (e) { }
310
- return { buf: result.data, correctedErrors: result.correctedErrors };
311
- }
312
- catch (e) {
313
- // Fall through to legacy WAV decoding
314
- }
315
- }
316
- // ─── WAV container detection ───────────────────────────────────────────────
317
- if (isWav(processedBuf)) {
318
- const pcmData = wavToBytes(processedBuf);
319
- // The WAV payload starts with PIXEL_MAGIC ("PXL1")
320
- if (pcmData.length >= 4 && pcmData.subarray(0, 4).equals(PIXEL_MAGIC)) {
321
- let idx = 4; // skip PIXEL_MAGIC
322
- const version = pcmData[idx++];
323
- const nameLen = pcmData[idx++];
324
- let name;
325
- if (nameLen > 0) {
326
- name = pcmData.subarray(idx, idx + nameLen).toString('utf8');
327
- idx += nameLen;
328
- }
329
- const payloadLen = pcmData.readUInt32BE(idx);
330
- idx += 4;
331
- const rawPayload = pcmData.subarray(idx, idx + payloadLen);
332
- idx += payloadLen;
333
- // Check for rXFL file list after payload
334
- let fileListJson;
335
- if (idx + 8 < pcmData.length &&
336
- pcmData.subarray(idx, idx + 4).toString('utf8') === 'rXFL') {
337
- idx += 4;
338
- const jsonLen = pcmData.readUInt32BE(idx);
339
- idx += 4;
340
- fileListJson = pcmData.subarray(idx, idx + jsonLen).toString('utf8');
341
- }
342
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
343
- if (opts.onProgress)
344
- opts.onProgress({ phase: 'decompress_start' });
345
- try {
346
- payload = await tryDecompress(payload, (info) => {
347
- if (opts.onProgress)
348
- opts.onProgress(info);
349
- });
350
- }
351
- catch (e) {
352
- const errMsg = e instanceof Error ? e.message : String(e);
353
- if (opts.passphrase)
354
- throw new IncorrectPassphraseError('Incorrect passphrase (WAV mode, zstd failed: ' + errMsg + ')');
355
- throw new DataFormatError('WAV mode zstd decompression failed: ' + errMsg);
356
- }
357
- if (!payload.subarray(0, MAGIC.length).equals(MAGIC)) {
358
- throw new DataFormatError('Invalid ROX format in WAV (missing ROX1 magic after decompression)');
359
- }
360
- payload = payload.subarray(MAGIC.length);
361
- if (opts.onProgress)
362
- opts.onProgress({ phase: 'done' });
363
- progressBar?.stop();
364
- // Try unpack multi-file archive
365
- try {
366
- const unpack = unpackBuffer(payload);
367
- if (unpack && unpack.files && unpack.files.length > 0) {
368
- return { files: unpack.files, meta: { name } };
369
- }
370
- }
371
- catch (e) { }
372
- return { buf: payload, meta: { name } };
373
- }
374
- }
375
- // ─── Robust image detection (lossy-resilient PNG) ──────────────────────────
376
- try {
377
- if (isRobustImage(processedBuf)) {
378
- const result = decodeRobustImage(processedBuf);
379
- if (opts.onProgress)
380
- opts.onProgress({ phase: 'done' });
381
- progressBar?.stop();
382
- try {
383
- const unpack = unpackBuffer(result.data);
384
- if (unpack && unpack.files && unpack.files.length > 0) {
385
- return { files: unpack.files, correctedErrors: result.correctedErrors };
386
- }
387
- }
388
- catch (e) { }
389
- return { buf: result.data, correctedErrors: result.correctedErrors };
390
- }
391
- }
392
- catch (e) {
393
- // Fall through to standard decoding
394
- }
395
- // ─── MAGIC header (compact mode) ──────────────────────────────────────────
396
- if (processedBuf.subarray(0, MAGIC.length).equals(MAGIC)) {
397
- const d = processedBuf.subarray(MAGIC.length);
398
- const nameLen = d[0];
399
- let idx = 1;
400
- let name;
401
- if (nameLen > 0) {
402
- name = d.subarray(idx, idx + nameLen).toString('utf8');
403
- idx += nameLen;
404
- }
405
- const rawPayload = d.subarray(idx);
406
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
407
- if (opts.onProgress)
408
- opts.onProgress({ phase: 'decompress_start' });
409
- try {
410
- payload = await tryDecompress(payload, (info) => {
411
- if (opts.onProgress)
412
- opts.onProgress(info);
413
- });
414
- }
415
- catch (e) {
416
- const errMsg = e instanceof Error ? e.message : String(e);
417
- if (opts.passphrase)
418
- throw new IncorrectPassphraseError('Incorrect passphrase (compact mode, zstd failed: ' + errMsg + ')');
419
- throw new DataFormatError('Compact mode zstd decompression failed: ' + errMsg);
420
- }
421
- if (!payload.subarray(0, MAGIC.length).equals(MAGIC)) {
422
- throw new Error('Invalid ROX format (ROX direct: missing ROX1 magic after decompression)');
423
- }
424
- payload = payload.subarray(MAGIC.length);
425
- if (opts.onProgress)
426
- opts.onProgress({ phase: 'done' });
427
- progressBar?.stop();
428
- return { buf: payload, meta: { name } };
429
- }
430
- let chunks = [];
431
- try {
432
- if (native?.extractPngChunks) {
433
- const chunksRaw = native.extractPngChunks(processedBuf);
434
- chunks = chunksRaw.map((c) => ({
435
- name: c.name,
436
- data: Buffer.from(c.data),
437
- }));
438
- }
439
- else {
440
- throw new Error('Native PNG chunk extraction not available');
441
- }
442
- }
443
- catch (e) {
444
- try {
445
- const withHeader = Buffer.concat([PNG_HEADER, pngBuf]);
446
- if (native?.extractPngChunks) {
447
- const chunksRaw = native.extractPngChunks(withHeader);
448
- chunks = chunksRaw.map((c) => ({
449
- name: c.name,
450
- data: Buffer.from(c.data),
451
- }));
452
- }
453
- else {
454
- throw new Error('Native PNG chunk extraction not available');
455
- }
44
+ decompressed = Buffer.from(native.nativeZstdDecompress(compressedData));
456
45
  }
457
- catch (e2) {
458
- chunks = [];
46
+ catch {
47
+ // If decompression fails, use raw data
48
+ decompressed = compressedData;
459
49
  }
460
- }
461
- const target = chunks.find((c) => c.name === CHUNK_TYPE);
462
- if (target) {
463
- const d = target.data;
464
- const nameLen = d[0];
465
- let idx = 1;
466
- let name;
467
- if (nameLen > 0) {
468
- name = d.slice(idx, idx + nameLen).toString('utf8');
469
- idx += nameLen;
50
+ // Check for ROX1 magic
51
+ if (decompressed.length >= 4 && decompressed.subarray(0, 4).toString() === 'ROX1') {
52
+ decompressed = decompressed.subarray(4);
470
53
  }
471
- const rawPayload = d.slice(idx);
472
- if (rawPayload.length === 0)
473
- throw new DataFormatError('Compact mode payload empty');
474
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
475
- if (opts.onProgress)
476
- opts.onProgress({ phase: 'decompress_start' });
54
+ // Try to unpack as multi-file archive
477
55
  try {
478
- payload = await tryDecompress(payload, (info) => {
479
- if (opts.onProgress)
480
- opts.onProgress(info);
481
- });
482
- }
483
- catch (e) {
484
- const errMsg = e instanceof Error ? e.message : String(e);
485
- if (opts.passphrase)
486
- throw new IncorrectPassphraseError('Incorrect passphrase (compact mode, zstd failed: ' + errMsg + ')');
487
- throw new DataFormatError('Compact mode zstd decompression failed: ' + errMsg);
488
- }
489
- if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
490
- throw new DataFormatError('Invalid ROX format (compact mode: missing ROX1 magic after decompression)');
491
- }
492
- payload = payload.slice(MAGIC.length);
493
- if (opts.files) {
494
- const unpacked = unpackBuffer(payload, opts.files);
495
- if (unpacked) {
496
- if (opts.onProgress)
497
- opts.onProgress({ phase: 'done' });
498
- progressBar?.stop();
56
+ const unpacked = unpackBuffer(decompressed);
57
+ if (unpacked && unpacked.files && unpacked.files.length > 0) {
499
58
  return { files: unpacked.files, meta: { name } };
500
59
  }
501
60
  }
502
- if (opts.onProgress)
503
- opts.onProgress({ phase: 'done' });
504
- progressBar?.stop();
505
- return { buf: payload, meta: { name } };
506
- }
507
- try {
508
- const metadata = native.sharpMetadata(processedBuf);
509
- const currentWidth = metadata.width;
510
- const currentHeight = metadata.height;
511
- let rawRGB = Buffer.alloc(0);
512
- let isBlockEncoded = false;
513
- if (currentWidth % 2 === 0 && currentHeight % 2 === 0) {
514
- const rawData = native.sharpToRaw(processedBuf);
515
- const testData = rawData.pixels;
516
- let hasBlockPattern = true;
517
- for (let y = 0; y < Math.min(2, currentHeight / 2); y++) {
518
- for (let x = 0; x < Math.min(2, currentWidth / 2); x++) {
519
- const px00 = (y * 2 * currentWidth + x * 2) * 3;
520
- const px01 = (y * 2 * currentWidth + (x * 2 + 1)) * 3;
521
- const px10 = ((y * 2 + 1) * currentWidth + x * 2) * 3;
522
- const px11 = ((y * 2 + 1) * currentWidth + (x * 2 + 1)) * 3;
523
- if (testData[px00] !== testData[px01] ||
524
- testData[px00] !== testData[px10] ||
525
- testData[px00] !== testData[px11] ||
526
- testData[px00 + 1] !== testData[px01 + 1] ||
527
- testData[px00 + 1] !== testData[px10 + 1] ||
528
- testData[px00 + 1] !== testData[px11 + 1]) {
529
- hasBlockPattern = false;
530
- break;
531
- }
532
- }
533
- if (!hasBlockPattern)
534
- break;
535
- }
536
- if (hasBlockPattern) {
537
- isBlockEncoded = true;
538
- const blocksWide = currentWidth / 2;
539
- const blocksHigh = currentHeight / 2;
540
- rawRGB = Buffer.alloc(blocksWide * blocksHigh * 3);
541
- const fullRaw = native.sharpToRaw(processedBuf);
542
- const fullData = fullRaw.pixels;
543
- let outIdx = 0;
544
- for (let by = 0; by < blocksHigh; by++) {
545
- for (let bx = 0; bx < blocksWide; bx++) {
546
- const pixelOffset = (by * 2 * currentWidth + bx * 2) * 3;
547
- rawRGB[outIdx++] = fullData[pixelOffset];
548
- rawRGB[outIdx++] = fullData[pixelOffset + 1];
549
- rawRGB[outIdx++] = fullData[pixelOffset + 2];
550
- }
551
- }
552
- }
553
- }
554
- if (!isBlockEncoded) {
555
- const rawData = native.sharpToRaw(processedBuf);
556
- rawRGB = Buffer.from(rawData.pixels);
557
- if (opts.onProgress) {
558
- opts.onProgress({
559
- phase: 'extract_pixels',
560
- loaded: currentHeight,
561
- total: currentHeight,
562
- });
563
- }
564
- }
565
- const firstPixels = [];
566
- for (let i = 0; i < Math.min(MARKER_START.length, rawRGB.length / 3); i++) {
567
- firstPixels.push({
568
- r: rawRGB[i * 3],
569
- g: rawRGB[i * 3 + 1],
570
- b: rawRGB[i * 3 + 2],
571
- });
572
- }
573
- let hasMarkerStart = false;
574
- if (firstPixels.length === MARKER_START.length) {
575
- hasMarkerStart = true;
576
- for (let i = 0; i < MARKER_START.length; i++) {
577
- if (!isColorMatch(firstPixels[i].r, firstPixels[i].g, firstPixels[i].b, MARKER_START[i].r, MARKER_START[i].g, MARKER_START[i].b)) {
578
- hasMarkerStart = false;
579
- break;
580
- }
581
- }
582
- }
583
- let hasPixelMagic = false;
584
- let hasBlockMagic = false;
585
- if (rawRGB.length >= 8 + PIXEL_MAGIC.length) {
586
- const widthFromDim = rawRGB.readUInt32BE(0);
587
- const heightFromDim = rawRGB.readUInt32BE(4);
588
- if (widthFromDim === currentWidth &&
589
- heightFromDim === currentHeight &&
590
- rawRGB.slice(8, 8 + PIXEL_MAGIC.length).equals(PIXEL_MAGIC)) {
591
- hasPixelMagic = true;
592
- }
593
- else if (rawRGB.slice(8, 8 + PIXEL_MAGIC_BLOCK.length).equals(PIXEL_MAGIC_BLOCK)) {
594
- hasBlockMagic = true;
595
- }
596
- }
597
- let logicalWidth = 0;
598
- let logicalHeight = 0;
599
- let logicalData = Buffer.alloc(0);
600
- if (hasMarkerStart || hasPixelMagic || hasBlockMagic) {
601
- logicalWidth = currentWidth;
602
- logicalHeight = currentHeight;
603
- logicalData = rawRGB;
604
- }
605
- else {
606
- // Try cropAndReconstitute first (for screenshots with markers)
607
- let reconSuccess = false;
608
- try {
609
- if (process.env.ROX_DEBUG || opts.debugDir) {
610
- console.log('DEBUG: about to call cropAndReconstitute, debugDir=', opts.debugDir);
611
- }
612
- const reconstructed = await cropAndReconstitute(processedBuf, opts.debugDir);
613
- if (process.env.ROX_DEBUG || opts.debugDir) {
614
- console.log('DEBUG: cropAndReconstitute returned, reconstructed len=', reconstructed.length);
615
- }
616
- const rawData = native.sharpToRaw(reconstructed);
617
- if (process.env.ROX_DEBUG || opts.debugDir) {
618
- console.log('DEBUG: rawData from reconstructed:', rawData.width, 'x', rawData.height, 'pixels=', Math.floor(rawData.pixels.length / 3));
619
- }
620
- logicalWidth = rawData.width;
621
- logicalHeight = rawData.height;
622
- logicalData = Buffer.from(rawData.pixels);
623
- reconSuccess = true;
624
- }
625
- catch (reconErr) {
626
- if (process.env.ROX_DEBUG) {
627
- console.log('DEBUG: cropAndReconstitute failed:', reconErr instanceof Error ? reconErr.message : reconErr);
628
- }
629
- }
630
- // Fallback: try un-stretching (nearest-neighbor scaled images)
631
- if (!reconSuccess) {
632
- const pixelW = isBlockEncoded ? currentWidth / 2 : currentWidth;
633
- const pixelH = isBlockEncoded ? currentHeight / 2 : currentHeight;
634
- if (process.env.ROX_DEBUG) {
635
- console.log(`DEBUG: trying unstretch on ${pixelW}x${pixelH} rawRGB`);
636
- }
637
- const unstretched = unstretchImage(rawRGB, pixelW, pixelH);
638
- if (unstretched) {
639
- logicalWidth = unstretched.width;
640
- logicalHeight = unstretched.height;
641
- logicalData = unstretched.data;
642
- }
643
- else {
644
- throw new Error('No valid markers found and image unstretch failed');
645
- }
646
- }
647
- }
648
- if (process.env.ROX_DEBUG) {
649
- console.log('DEBUG: Logical grid reconstructed:', logicalWidth, 'x', logicalHeight, '=', logicalWidth * logicalHeight, 'pixels');
650
- }
651
- if (hasPixelMagic) {
652
- if (logicalData.length < 8 + PIXEL_MAGIC.length) {
653
- throw new DataFormatError('Pixel mode data too short');
654
- }
655
- let idx = 8 + PIXEL_MAGIC.length;
656
- const version = logicalData[idx++];
657
- const nameLen = logicalData[idx++];
658
- let name;
659
- if (nameLen > 0 && nameLen < 256) {
660
- name = logicalData.slice(idx, idx + nameLen).toString('utf8');
661
- idx += nameLen;
662
- }
663
- const payloadLen = logicalData.readUInt32BE(idx);
664
- idx += 4;
665
- const available = logicalData.length - idx;
666
- if (available < payloadLen) {
667
- throw new DataFormatError(`Pixel payload truncated: expected ${payloadLen} bytes but only ${available} available`);
668
- }
669
- const rawPayload = logicalData.slice(idx, idx + payloadLen);
670
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
671
- try {
672
- payload = await tryDecompress(payload, (info) => {
673
- if (opts.onProgress)
674
- opts.onProgress(info);
675
- });
676
- if (version === 3) {
677
- payload = deltaDecode(payload);
678
- }
679
- }
680
- catch (e) { }
681
- if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
682
- throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
683
- }
684
- payload = payload.slice(MAGIC.length);
685
- return { buf: payload, meta: { name } };
686
- }
687
- const totalPixels = (logicalData.length / 3) | 0;
688
- let startIdx = -1;
689
- for (let i = 0; i <= totalPixels - MARKER_START.length; i++) {
690
- let match = true;
691
- for (let mi = 0; mi < MARKER_START.length && match; mi++) {
692
- const offset = (i + mi) * 3;
693
- if (!isColorMatch(logicalData[offset], logicalData[offset + 1], logicalData[offset + 2], MARKER_START[mi].r, MARKER_START[mi].g, MARKER_START[mi].b)) {
694
- match = false;
695
- }
696
- }
697
- if (match) {
698
- startIdx = i;
699
- break;
700
- }
701
- }
702
- if (startIdx === -1) {
703
- if (process.env.ROX_DEBUG) {
704
- console.log('DEBUG: MARKER_START not found in grid of', totalPixels, 'pixels');
705
- console.log('DEBUG: Trying 2D scan for START marker...');
706
- }
707
- let found2D = false;
708
- for (let y = 0; y < logicalHeight && !found2D; y++) {
709
- for (let x = 0; x <= logicalWidth - MARKER_START.length && !found2D; x++) {
710
- let match = true;
711
- for (let mi = 0; mi < MARKER_START.length && match; mi++) {
712
- const idx = (y * logicalWidth + (x + mi)) * 3;
713
- if (idx + 2 >= logicalData.length ||
714
- !isColorMatch(logicalData[idx], logicalData[idx + 1], logicalData[idx + 2], MARKER_START[mi].r, MARKER_START[mi].g, MARKER_START[mi].b)) {
715
- match = false;
716
- }
717
- }
718
- if (match) {
719
- if (process.env.ROX_DEBUG) {
720
- console.log(`DEBUG: Found START marker in 2D at (${x}, ${y})`);
721
- }
722
- let endX = x + MARKER_START.length - 1;
723
- let endY = y;
724
- for (let scanY = y; scanY < logicalHeight; scanY++) {
725
- let rowHasData = false;
726
- for (let scanX = x; scanX < logicalWidth; scanX++) {
727
- const scanIdx = (scanY * logicalWidth + scanX) * 3;
728
- if (scanIdx + 2 < logicalData.length) {
729
- const r = logicalData[scanIdx];
730
- const g = logicalData[scanIdx + 1];
731
- const b = logicalData[scanIdx + 2];
732
- const isBackground = (r === 100 && g === 120 && b === 110) ||
733
- (r === 0 && g === 0 && b === 0) ||
734
- (r >= 50 &&
735
- r <= 220 &&
736
- g >= 50 &&
737
- g <= 220 &&
738
- b >= 50 &&
739
- b <= 220 &&
740
- Math.abs(r - g) < 70 &&
741
- Math.abs(r - b) < 70 &&
742
- Math.abs(g - b) < 70);
743
- if (!isBackground) {
744
- rowHasData = true;
745
- if (scanX > endX) {
746
- endX = scanX;
747
- }
748
- }
749
- }
750
- }
751
- if (rowHasData) {
752
- endY = scanY;
753
- }
754
- else if (scanY > y) {
755
- break;
756
- }
757
- }
758
- const rectWidth = endX - x + 1;
759
- const rectHeight = endY - y + 1;
760
- if (process.env.ROX_DEBUG) {
761
- console.log(`DEBUG: Extracted rectangle: ${rectWidth}x${rectHeight} from (${x},${y})`);
762
- }
763
- const newDataLen = rectWidth * rectHeight * 3;
764
- const newData = Buffer.allocUnsafe(newDataLen);
765
- let writeIdx = 0;
766
- for (let ry = y; ry <= endY; ry++) {
767
- for (let rx = x; rx <= endX; rx++) {
768
- const idx = (ry * logicalWidth + rx) * 3;
769
- newData[writeIdx++] = logicalData[idx];
770
- newData[writeIdx++] = logicalData[idx + 1];
771
- newData[writeIdx++] = logicalData[idx + 2];
772
- }
773
- }
774
- logicalData = newData;
775
- logicalWidth = rectWidth;
776
- logicalHeight = rectHeight;
777
- startIdx = 0;
778
- found2D = true;
779
- }
780
- }
781
- }
782
- if (!found2D) {
783
- if (process.env.ROX_DEBUG) {
784
- const first20 = [];
785
- for (let i = 0; i < Math.min(20, totalPixels); i++) {
786
- const offset = i * 3;
787
- first20.push(`(${logicalData[offset]},${logicalData[offset + 1]},${logicalData[offset + 2]})`);
788
- }
789
- console.log('DEBUG: First 20 pixels:', first20.join(' '));
790
- }
791
- throw new Error('Marker START not found - image format not supported');
792
- }
793
- }
794
- if (process.env.ROX_DEBUG && startIdx === 0) {
795
- console.log(`DEBUG: MARKER_START at index ${startIdx}, grid size: ${totalPixels}`);
796
- }
797
- const dataStartPixel = startIdx + MARKER_START.length + 1;
798
- const curTotalPixels = (logicalData.length / 3) | 0;
799
- if (curTotalPixels < dataStartPixel + MARKER_END.length) {
800
- if (process.env.ROX_DEBUG) {
801
- console.log('DEBUG: grid too small:', curTotalPixels, 'pixels');
802
- }
803
- throw new Error('Marker START or END not found - image format not supported');
804
- }
805
- for (let i = 0; i < MARKER_START.length; i++) {
806
- const offset = (startIdx + i) * 3;
807
- if (!isColorMatch(logicalData[offset], logicalData[offset + 1], logicalData[offset + 2], MARKER_START[i].r, MARKER_START[i].g, MARKER_START[i].b)) {
808
- throw new Error('Marker START not found - image format not supported');
809
- }
810
- }
811
- let compression = 'zstd';
812
- if (curTotalPixels > startIdx + MARKER_START.length) {
813
- const compOffset = (startIdx + MARKER_START.length) * 3;
814
- const compPixel = {
815
- r: logicalData[compOffset],
816
- g: logicalData[compOffset + 1],
817
- b: logicalData[compOffset + 2],
818
- };
819
- if (compPixel.r === 0 && compPixel.g === 255 && compPixel.b === 0) {
820
- compression = 'zstd';
821
- }
822
- else {
823
- compression = 'zstd';
824
- }
825
- }
826
- if (process.env.ROX_DEBUG) {
827
- console.log(`DEBUG: Detected compression: ${compression}`);
828
- }
829
- let endStartPixel = -1;
830
- const lastLineStart = (logicalHeight - 1) * logicalWidth;
831
- const endMarkerStartCol = logicalWidth - MARKER_END.length;
832
- if (lastLineStart + endMarkerStartCol < curTotalPixels) {
833
- let matchEnd = true;
834
- for (let mi = 0; mi < MARKER_END.length && matchEnd; mi++) {
835
- const pixelIdx = lastLineStart + endMarkerStartCol + mi;
836
- if (pixelIdx >= curTotalPixels) {
837
- matchEnd = false;
838
- break;
839
- }
840
- const offset = pixelIdx * 3;
841
- if (!isColorMatch(logicalData[offset], logicalData[offset + 1], logicalData[offset + 2], MARKER_END[mi].r, MARKER_END[mi].g, MARKER_END[mi].b)) {
842
- matchEnd = false;
843
- }
844
- }
845
- if (matchEnd) {
846
- endStartPixel = lastLineStart + endMarkerStartCol - startIdx;
847
- if (process.env.ROX_DEBUG) {
848
- console.log(`DEBUG: Found END marker at last line, col ${endMarkerStartCol}`);
849
- }
850
- }
851
- }
852
- if (endStartPixel === -1) {
853
- const scanLines = Math.min(logicalHeight, 5);
854
- for (let row = logicalHeight - 1; row >= logicalHeight - scanLines && endStartPixel === -1; row--) {
855
- const rowStart = row * logicalWidth;
856
- for (let col = logicalWidth - MARKER_END.length; col >= 0 && endStartPixel === -1; col--) {
857
- let match = true;
858
- for (let mi = 0; mi < MARKER_END.length && match; mi++) {
859
- const pixelIdx = rowStart + col + mi;
860
- if (pixelIdx >= curTotalPixels) {
861
- match = false;
862
- break;
863
- }
864
- const offset = pixelIdx * 3;
865
- if (!isColorMatch(logicalData[offset], logicalData[offset + 1], logicalData[offset + 2], MARKER_END[mi].r, MARKER_END[mi].g, MARKER_END[mi].b)) {
866
- match = false;
867
- }
868
- }
869
- if (match) {
870
- endStartPixel = rowStart + col - startIdx;
871
- if (process.env.ROX_DEBUG) {
872
- console.log(`DEBUG: Found END marker via scan at row=${row}, col=${col}`);
873
- }
874
- }
875
- }
876
- }
877
- }
878
- if (endStartPixel === -1) {
879
- if (process.env.ROX_DEBUG) {
880
- console.log('DEBUG: END marker not found at expected position');
881
- const lastLinePixels = [];
882
- for (let i = Math.max(0, lastLineStart); i < curTotalPixels && i < lastLineStart + 20; i++) {
883
- const offset = i * 3;
884
- lastLinePixels.push(`(${logicalData[offset]},${logicalData[offset + 1]},${logicalData[offset + 2]})`);
885
- }
886
- console.log('DEBUG: Last line pixels:', lastLinePixels.join(' '));
887
- }
888
- endStartPixel = curTotalPixels - startIdx;
889
- }
890
- const dataPixelCount = endStartPixel - (MARKER_START.length + 1);
891
- const srcByteOffset = dataStartPixel * 3;
892
- const byteCount = dataPixelCount * 3;
893
- const pixelBytes = Buffer.allocUnsafe(byteCount);
894
- if (Buffer.isBuffer(logicalData)) {
895
- logicalData.copy(pixelBytes, 0, srcByteOffset, srcByteOffset + byteCount);
896
- }
897
- else {
898
- for (let i = 0; i < byteCount; i++) {
899
- pixelBytes[i] = logicalData[srcByteOffset + i];
900
- }
901
- }
902
- if (process.env.ROX_DEBUG) {
903
- console.log('DEBUG: extracted len', pixelBytes.length);
904
- console.log('DEBUG: extracted head', pixelBytes.slice(0, 32).toString('hex'));
905
- const found = pixelBytes.indexOf(PIXEL_MAGIC);
906
- console.log('DEBUG: PIXEL_MAGIC index:', found);
907
- if (found !== -1) {
908
- console.log('DEBUG: PIXEL_MAGIC head:', pixelBytes.slice(found, found + 64).toString('hex'));
909
- const markerEndBytes = colorsToBytes(MARKER_END);
910
- console.log('DEBUG: MARKER_END index:', pixelBytes.indexOf(markerEndBytes));
911
- }
912
- if (opts.debugDir) {
913
- try {
914
- console.log('DEBUG: writing extracted pixel bytes to', opts.debugDir);
915
- writeFileSync(join(opts.debugDir, 'extracted-pixel-bytes.bin'), pixelBytes);
916
- writeFileSync(join(opts.debugDir, 'extracted-pixel-head.hex'), pixelBytes.slice(0, 512).toString('hex'));
917
- }
918
- catch (e) {
919
- console.log('DEBUG: failed writing extracted bytes', e?.message ?? e);
920
- }
921
- }
922
- }
923
- try {
924
- let idx = 0;
925
- if (pixelBytes.length >= PIXEL_MAGIC.length) {
926
- const at0 = pixelBytes.slice(0, PIXEL_MAGIC.length).equals(PIXEL_MAGIC);
927
- const at0Block = pixelBytes
928
- .slice(0, PIXEL_MAGIC_BLOCK.length)
929
- .equals(PIXEL_MAGIC_BLOCK);
930
- if (at0) {
931
- idx = PIXEL_MAGIC.length;
932
- }
933
- else if (at0Block) {
934
- idx = PIXEL_MAGIC_BLOCK.length;
935
- }
936
- else {
937
- const found = pixelBytes.indexOf(PIXEL_MAGIC);
938
- const foundBlock = pixelBytes.indexOf(PIXEL_MAGIC_BLOCK);
939
- if (found !== -1) {
940
- idx = found + PIXEL_MAGIC.length;
941
- }
942
- else if (foundBlock !== -1) {
943
- idx = foundBlock + PIXEL_MAGIC_BLOCK.length;
944
- }
945
- }
946
- }
947
- if (idx > 0) {
948
- const version = pixelBytes[idx++];
949
- const nameLen = pixelBytes[idx++];
950
- let name;
951
- if (nameLen > 0 && nameLen < 256) {
952
- name = pixelBytes.slice(idx, idx + nameLen).toString('utf8');
953
- idx += nameLen;
954
- }
955
- const payloadLen = pixelBytes.readUInt32BE(idx);
956
- idx += 4;
957
- if (idx + 4 <= pixelBytes.length) {
958
- const marker = pixelBytes.slice(idx, idx + 4).toString('utf8');
959
- if (marker === 'rXFL') {
960
- idx += 4;
961
- if (idx + 4 <= pixelBytes.length) {
962
- const jsonLen = pixelBytes.readUInt32BE(idx);
963
- idx += 4;
964
- idx += jsonLen;
965
- }
966
- }
967
- }
968
- const available = pixelBytes.length - idx;
969
- if (available < payloadLen) {
970
- throw new DataFormatError(`Pixel payload truncated: expected ${payloadLen} bytes but only ${available} available`);
971
- }
972
- const rawPayload = pixelBytes.slice(idx, idx + payloadLen);
973
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
974
- try {
975
- payload = await tryDecompress(payload, (info) => {
976
- if (opts.onProgress)
977
- opts.onProgress(info);
978
- });
979
- if (version === 3) {
980
- payload = deltaDecode(payload);
981
- }
982
- }
983
- catch (e) {
984
- const errMsg = e instanceof Error ? e.message : String(e);
985
- if (opts.passphrase)
986
- throw new IncorrectPassphraseError(`Incorrect passphrase (screenshot mode, zstd failed: ` +
987
- errMsg +
988
- ')');
989
- try {
990
- if (process.env.ROX_DEBUG)
991
- console.log('DEBUG: decompress failed, attempting cropAndReconstitute fallback');
992
- const reconstructed = await cropAndReconstitute(processedBuf, opts.debugDir);
993
- const raw2 = native.sharpToRaw(reconstructed);
994
- let logicalData2 = Buffer.from(raw2.pixels);
995
- let logicalWidth2 = raw2.width;
996
- let logicalHeight2 = raw2.height;
997
- let startIdx2 = -1;
998
- const totalPixels2 = (logicalData2.length / 3) | 0;
999
- for (let i2 = 0; i2 <= totalPixels2 - MARKER_START.length; i2++) {
1000
- let match2 = true;
1001
- for (let mi2 = 0; mi2 < MARKER_START.length && match2; mi2++) {
1002
- const offset2 = (i2 + mi2) * 3;
1003
- if (logicalData2[offset2] !== MARKER_START[mi2].r ||
1004
- logicalData2[offset2 + 1] !== MARKER_START[mi2].g ||
1005
- logicalData2[offset2 + 2] !== MARKER_START[mi2].b) {
1006
- match2 = false;
1007
- }
1008
- }
1009
- if (match2) {
1010
- startIdx2 = i2;
1011
- break;
1012
- }
1013
- }
1014
- if (startIdx2 === -1) {
1015
- let found2D2 = false;
1016
- for (let y = 0; y < logicalHeight2 && !found2D2; y++) {
1017
- for (let x = 0; x <= logicalWidth2 - MARKER_START.length && !found2D2; x++) {
1018
- let match = true;
1019
- for (let mi = 0; mi < MARKER_START.length && match; mi++) {
1020
- const idx = (y * logicalWidth2 + (x + mi)) * 3;
1021
- if (idx + 2 >= logicalData2.length ||
1022
- !isColorMatch(logicalData2[idx], logicalData2[idx + 1], logicalData2[idx + 2], MARKER_START[mi].r, MARKER_START[mi].g, MARKER_START[mi].b)) {
1023
- match = false;
1024
- }
1025
- }
1026
- if (match) {
1027
- let endX = x + MARKER_START.length - 1;
1028
- let endY = y;
1029
- for (let scanY = y; scanY < logicalHeight2; scanY++) {
1030
- let rowHasData = false;
1031
- for (let scanX = x; scanX < logicalWidth2; scanX++) {
1032
- const scanIdx = (scanY * logicalWidth2 + scanX) * 3;
1033
- if (scanIdx + 2 < logicalData2.length) {
1034
- const r = logicalData2[scanIdx];
1035
- const g = logicalData2[scanIdx + 1];
1036
- const b = logicalData2[scanIdx + 2];
1037
- const isBackground = (r === 100 && g === 120 && b === 110) ||
1038
- (r === 0 && g === 0 && b === 0) ||
1039
- (r >= 50 &&
1040
- r <= 220 &&
1041
- g >= 50 &&
1042
- g <= 220 &&
1043
- b >= 50 &&
1044
- b <= 220 &&
1045
- Math.abs(r - g) < 70 &&
1046
- Math.abs(r - b) < 70 &&
1047
- Math.abs(g - b) < 70);
1048
- if (!isBackground) {
1049
- rowHasData = true;
1050
- if (scanX > endX)
1051
- endX = scanX;
1052
- }
1053
- }
1054
- }
1055
- if (rowHasData) {
1056
- endY = scanY;
1057
- }
1058
- else if (scanY > y) {
1059
- break;
1060
- }
1061
- }
1062
- const rectWidth = endX - x + 1;
1063
- const rectHeight = endY - y + 1;
1064
- const newDataLen = rectWidth * rectHeight * 3;
1065
- const newData = Buffer.allocUnsafe(newDataLen);
1066
- let writeIdx = 0;
1067
- for (let ry = y; ry <= endY; ry++) {
1068
- for (let rx = x; rx <= endX; rx++) {
1069
- const idx = (ry * logicalWidth2 + rx) * 3;
1070
- newData[writeIdx++] = logicalData2[idx];
1071
- newData[writeIdx++] = logicalData2[idx + 1];
1072
- newData[writeIdx++] = logicalData2[idx + 2];
1073
- }
1074
- }
1075
- logicalData2 = newData;
1076
- logicalWidth2 = rectWidth;
1077
- logicalHeight2 = rectHeight;
1078
- startIdx2 = 0;
1079
- found2D2 = true;
1080
- }
1081
- }
1082
- }
1083
- if (!found2D2)
1084
- throw new DataFormatError('Screenshot fallback failed: START not found');
1085
- }
1086
- const curTotalPixels2 = (logicalData2.length / 3) | 0;
1087
- const lastLineStart2 = (logicalHeight2 - 1) * logicalWidth2;
1088
- const endMarkerStartCol2 = logicalWidth2 - MARKER_END.length;
1089
- let endStartPixel2 = -1;
1090
- if (lastLineStart2 + endMarkerStartCol2 < curTotalPixels2) {
1091
- let matchEnd2 = true;
1092
- for (let mi = 0; mi < MARKER_END.length && matchEnd2; mi++) {
1093
- const pixelIdx = lastLineStart2 + endMarkerStartCol2 + mi;
1094
- if (pixelIdx >= curTotalPixels2) {
1095
- matchEnd2 = false;
1096
- break;
1097
- }
1098
- const offset = pixelIdx * 3;
1099
- if (!isColorMatch(logicalData2[offset], logicalData2[offset + 1], logicalData2[offset + 2], MARKER_END[mi].r, MARKER_END[mi].g, MARKER_END[mi].b)) {
1100
- matchEnd2 = false;
1101
- }
1102
- }
1103
- if (matchEnd2) {
1104
- endStartPixel2 =
1105
- lastLineStart2 + endMarkerStartCol2 - startIdx2;
1106
- if (process.env.ROX_DEBUG) {
1107
- console.log('DEBUG: Found END marker in fallback at last line');
1108
- }
1109
- }
1110
- }
1111
- if (endStartPixel2 === -1) {
1112
- const scanLines2 = Math.min(logicalHeight2, 5);
1113
- for (let row2 = logicalHeight2 - 1; row2 >= logicalHeight2 - scanLines2 && endStartPixel2 === -1; row2--) {
1114
- const rowStart2 = row2 * logicalWidth2;
1115
- for (let col2 = logicalWidth2 - MARKER_END.length; col2 >= 0 && endStartPixel2 === -1; col2--) {
1116
- let m2 = true;
1117
- for (let mi = 0; mi < MARKER_END.length && m2; mi++) {
1118
- const pixelIdx2 = rowStart2 + col2 + mi;
1119
- if (pixelIdx2 >= curTotalPixels2) {
1120
- m2 = false;
1121
- break;
1122
- }
1123
- const off2 = pixelIdx2 * 3;
1124
- if (!isColorMatch(logicalData2[off2], logicalData2[off2 + 1], logicalData2[off2 + 2], MARKER_END[mi].r, MARKER_END[mi].g, MARKER_END[mi].b)) {
1125
- m2 = false;
1126
- }
1127
- }
1128
- if (m2) {
1129
- endStartPixel2 = rowStart2 + col2 - startIdx2;
1130
- }
1131
- }
1132
- }
1133
- }
1134
- if (endStartPixel2 === -1) {
1135
- if (process.env.ROX_DEBUG) {
1136
- console.log('DEBUG: END marker not found in fallback; using end of grid');
1137
- }
1138
- endStartPixel2 = curTotalPixels2 - startIdx2;
1139
- }
1140
- const dataPixelCount2 = endStartPixel2 - (MARKER_START.length + 1);
1141
- const pixelBytes2 = Buffer.allocUnsafe(dataPixelCount2 * 3);
1142
- for (let i2 = 0; i2 < dataPixelCount2; i2++) {
1143
- const srcOffset = (startIdx2 + MARKER_START.length + 1 + i2) * 3;
1144
- const dstOffset = i2 * 3;
1145
- pixelBytes2[dstOffset] = logicalData2[srcOffset];
1146
- pixelBytes2[dstOffset + 1] = logicalData2[srcOffset + 1];
1147
- pixelBytes2[dstOffset + 2] = logicalData2[srcOffset + 2];
1148
- }
1149
- const foundPX = pixelBytes2.indexOf(PIXEL_MAGIC);
1150
- if (process.env.ROX_DEBUG)
1151
- console.log('DEBUG: PIXEL_MAGIC index in fallback:', foundPX);
1152
- if (pixelBytes2.length >= PIXEL_MAGIC.length) {
1153
- let ii = 0;
1154
- const at0 = pixelBytes2
1155
- .slice(0, PIXEL_MAGIC.length)
1156
- .equals(PIXEL_MAGIC);
1157
- if (at0)
1158
- ii = PIXEL_MAGIC.length;
1159
- else {
1160
- const found = pixelBytes2.indexOf(PIXEL_MAGIC);
1161
- if (found !== -1)
1162
- ii = found + PIXEL_MAGIC.length;
1163
- }
1164
- if (ii > 0) {
1165
- const version2 = pixelBytes2[ii++];
1166
- const nameLen2 = pixelBytes2[ii++];
1167
- const payloadLen2 = pixelBytes2.readUInt32BE(ii + nameLen2);
1168
- const rawPayload2 = pixelBytes2.slice(ii + nameLen2 + 4, ii + nameLen2 + 4 + payloadLen2);
1169
- let payload2 = tryDecryptIfNeeded(rawPayload2, opts.passphrase);
1170
- payload2 = await tryDecompress(payload2, (info) => {
1171
- if (opts.onProgress)
1172
- opts.onProgress(info);
1173
- });
1174
- if (!payload2.slice(0, MAGIC.length).equals(MAGIC)) {
1175
- throw new DataFormatError('Screenshot fallback failed: missing ROX1 magic after decompression');
1176
- }
1177
- payload2 = payload2.slice(MAGIC.length);
1178
- if (opts.files) {
1179
- const unpacked2 = unpackBuffer(payload2, opts.files);
1180
- if (unpacked2) {
1181
- if (opts.onProgress)
1182
- opts.onProgress({ phase: 'done' });
1183
- progressBar?.stop();
1184
- return { files: unpacked2.files, meta: { name } };
1185
- }
1186
- }
1187
- if (opts.onProgress)
1188
- opts.onProgress({ phase: 'done' });
1189
- progressBar?.stop();
1190
- return { buf: payload2, meta: { name } };
1191
- }
1192
- }
1193
- throw new DataFormatError('Screenshot mode zstd decompression failed: ' + errMsg);
1194
- }
1195
- catch (e2) {
1196
- throw new DataFormatError(`Screenshot mode zstd decompression failed: ` + errMsg);
1197
- }
1198
- }
1199
- if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
1200
- throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
1201
- }
1202
- payload = payload.slice(MAGIC.length);
1203
- if (opts.files) {
1204
- const unpacked = unpackBuffer(payload, opts.files);
1205
- if (unpacked) {
1206
- if (opts.onProgress)
1207
- opts.onProgress({ phase: 'done' });
1208
- progressBar?.stop();
1209
- return { files: unpacked.files, meta: { name } };
1210
- }
1211
- }
1212
- if (opts.onProgress)
1213
- opts.onProgress({ phase: 'done' });
1214
- progressBar?.stop();
1215
- return { buf: payload, meta: { name } };
1216
- }
1217
- }
1218
- catch (e) {
1219
- if (e instanceof PassphraseRequiredError ||
1220
- e instanceof IncorrectPassphraseError ||
1221
- e instanceof DataFormatError) {
1222
- throw e;
1223
- }
1224
- const errMsg = e instanceof Error ? e.message : String(e);
1225
- throw new Error('Failed to extract data from screenshot: ' + errMsg);
1226
- }
1227
- }
1228
- catch (e) {
1229
- if (e instanceof PassphraseRequiredError ||
1230
- e instanceof IncorrectPassphraseError ||
1231
- e instanceof DataFormatError) {
1232
- throw e;
1233
- }
1234
- try {
1235
- const rawPayload = Buffer.from(native.extractPayloadFromPng(processedBuf));
1236
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
1237
- payload = await tryDecompress(payload, (info) => {
1238
- if (opts.onProgress)
1239
- opts.onProgress(info);
1240
- });
1241
- if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
1242
- throw new DataFormatError('Missing ROX1 magic after native extraction');
1243
- }
1244
- payload = payload.slice(MAGIC.length);
1245
- const nameFromPng = native.extractNameFromPng
1246
- ? (() => { try {
1247
- return native.extractNameFromPng(processedBuf);
1248
- }
1249
- catch {
1250
- return undefined;
1251
- } })()
1252
- : undefined;
1253
- return { buf: payload, meta: { name: nameFromPng } };
1254
- }
1255
- catch (nativeErr) {
1256
- if (nativeErr instanceof PassphraseRequiredError ||
1257
- nativeErr instanceof IncorrectPassphraseError ||
1258
- nativeErr instanceof DataFormatError) {
1259
- throw nativeErr;
1260
- }
61
+ catch {
62
+ // Fall through to raw buffer return
1261
63
  }
1262
- const errMsg = e instanceof Error ? e.message : String(e);
1263
- throw new Error('Failed to decode PNG: ' + errMsg);
64
+ return { buf: decompressed, meta: { name } };
1264
65
  }
1265
- throw new DataFormatError('No valid data found in image');
66
+ return { buf: payload, meta: { name } };
1266
67
  }