roxify 1.2.4 → 1.2.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.
package/dist/index.js CHANGED
@@ -1,2641 +1,13 @@
1
- import { compress as zstdCompress, decompress as zstdDecompress, } from '@mongodb-js/zstd';
2
- import { spawn, spawnSync } from 'child_process';
3
- import cliProgress from 'cli-progress';
4
- import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes, } from 'crypto';
5
- import { createReadStream, createWriteStream, existsSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
6
- import { tmpdir } from 'os';
7
- import { join } from 'path';
8
- import encode from 'png-chunks-encode';
9
- import extract from 'png-chunks-extract';
10
- import sharp from 'sharp';
11
- import * as zlib from 'zlib';
12
- import { unpackBuffer } from './pack.js';
13
- const CHUNK_TYPE = 'rXDT';
14
- const MAGIC = Buffer.from('ROX1');
15
- const PIXEL_MAGIC = Buffer.from('PXL1');
16
- const ENC_NONE = 0;
17
- const ENC_AES = 1;
18
- const ENC_XOR = 2;
19
- export class PassphraseRequiredError extends Error {
20
- constructor(message = 'Passphrase required') {
21
- super(message);
22
- this.name = 'PassphraseRequiredError';
23
- }
24
- }
25
- export class IncorrectPassphraseError extends Error {
26
- constructor(message = 'Incorrect passphrase') {
27
- super(message);
28
- this.name = 'IncorrectPassphraseError';
29
- }
30
- }
31
- export class DataFormatError extends Error {
32
- constructor(message = 'Data format error') {
33
- super(message);
34
- this.name = 'DataFormatError';
35
- }
36
- }
37
- const PNG_HEADER = Buffer.from([
38
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
39
- ]);
40
- const PNG_HEADER_HEX = PNG_HEADER.toString('hex');
41
- const MARKER_COLORS = [
42
- { r: 255, g: 0, b: 0 },
43
- { r: 0, g: 255, b: 0 },
44
- { r: 0, g: 0, b: 255 },
45
- ];
46
- const MARKER_START = MARKER_COLORS;
47
- const MARKER_END = [...MARKER_COLORS].reverse();
48
- const CHUNK_SIZE = 8 * 1024;
49
- async function writeInChunks(ws, buf, chunkSize = CHUNK_SIZE) {
50
- for (let i = 0; i < buf.length; i += chunkSize) {
51
- const part = buf.slice(i, i + chunkSize);
52
- if (!ws.write(part))
53
- await new Promise((resolve) => ws.once('drain', resolve));
54
- await new Promise((resolve) => setTimeout(resolve, 50));
55
- }
56
- }
57
- const COMPRESSION_MARKERS = {
58
- zstd: [{ r: 0, g: 255, b: 0 }],
59
- lzma: [{ r: 255, g: 255, b: 0 }],
60
- };
61
- function colorsToBytes(colors) {
62
- const buf = Buffer.alloc(colors.length * 3);
63
- for (let i = 0; i < colors.length; i++) {
64
- buf[i * 3] = colors[i].r;
65
- buf[i * 3 + 1] = colors[i].g;
66
- buf[i * 3 + 2] = colors[i].b;
67
- }
68
- return buf;
69
- }
70
- function deltaEncode(data) {
71
- if (data.length === 0)
72
- return data;
73
- const out = Buffer.alloc(data.length);
74
- out[0] = data[0];
75
- for (let i = 1; i < data.length; i++) {
76
- out[i] = (data[i] - data[i - 1] + 256) & 0xff;
77
- }
78
- return out;
79
- }
80
- function deltaDecode(data) {
81
- if (data.length === 0)
82
- return data;
83
- const out = Buffer.alloc(data.length);
84
- out[0] = data[0];
85
- for (let i = 1; i < data.length; i++) {
86
- out[i] = (out[i - 1] + data[i]) & 0xff;
87
- }
88
- return out;
89
- }
90
- async function parallelZstdCompress(payload, level = 11, onProgress) {
91
- const chunkSize = 1024 * 1024 * 1024;
92
- if (payload.length <= chunkSize) {
93
- if (onProgress)
94
- onProgress(0, 1);
95
- const result = await zstdCompress(payload, level);
96
- if (onProgress)
97
- onProgress(1, 1);
98
- return Buffer.from(result);
99
- }
100
- const promises = [];
101
- const totalChunks = Math.ceil(payload.length / chunkSize);
102
- let completedChunks = 0;
103
- for (let i = 0; i < payload.length; i += chunkSize) {
104
- const chunk = payload.slice(i, Math.min(i + chunkSize, payload.length));
105
- promises.push(zstdCompress(chunk, level).then((compressed) => {
106
- completedChunks++;
107
- if (onProgress) {
108
- onProgress(completedChunks, totalChunks);
109
- }
110
- return Buffer.from(compressed);
111
- }));
112
- }
113
- const compressedChunks = await Promise.all(promises);
114
- const chunkSizes = Buffer.alloc(compressedChunks.length * 4);
115
- for (let i = 0; i < compressedChunks.length; i++) {
116
- chunkSizes.writeUInt32BE(compressedChunks[i].length, i * 4);
117
- }
118
- const header = Buffer.alloc(8);
119
- header.writeUInt32BE(0x5a535444, 0);
120
- header.writeUInt32BE(compressedChunks.length, 4);
121
- return Buffer.concat([header, chunkSizes, ...compressedChunks]);
122
- }
123
- async function parallelZstdDecompress(payload, onProgress, onChunk, outPath) {
124
- if (payload.length < 8) {
125
- onProgress?.({ phase: 'decompress_start', total: 1 });
126
- const d = Buffer.from(await zstdDecompress(payload));
127
- if (onChunk)
128
- await onChunk(d, 0, 1);
129
- onProgress?.({ phase: 'decompress_progress', loaded: 1, total: 1 });
130
- onProgress?.({ phase: 'decompress_done', loaded: 1, total: 1 });
131
- if (outPath) {
132
- const ws = createWriteStream(outPath);
133
- await writeInChunks(ws, d);
134
- ws.end();
135
- }
136
- return onChunk ? Buffer.alloc(0) : d;
137
- }
138
- const magic = payload.readUInt32BE(0);
139
- if (magic !== 0x5a535444) {
140
- if (process.env.ROX_DEBUG)
141
- console.log('tryZstdDecompress: invalid magic');
142
- onProgress?.({ phase: 'decompress_start', total: 1 });
143
- const d = Buffer.from(await zstdDecompress(payload));
144
- onProgress?.({ phase: 'decompress_progress', loaded: 1, total: 1 });
145
- onProgress?.({ phase: 'decompress_done', loaded: 1, total: 1 });
146
- if (outPath) {
147
- const ws = createWriteStream(outPath);
148
- await writeInChunks(ws, d);
149
- ws.end();
150
- }
151
- return d;
152
- }
153
- const numChunks = payload.readUInt32BE(4);
154
- const chunkSizes = [];
155
- let offset = 8;
156
- for (let i = 0; i < numChunks; i++) {
157
- chunkSizes.push(payload.readUInt32BE(offset));
158
- offset += 4;
159
- }
160
- onProgress?.({ phase: 'decompress_start', total: numChunks });
161
- const tempFiles = [];
162
- for (let i = 0; i < numChunks; i++) {
163
- const size = chunkSizes[i];
164
- const chunk = payload.slice(offset, offset + size);
165
- offset += size;
166
- const tempFile = join(tmpdir(), `rox_chunk_${Date.now()}_${i}.tmp`);
167
- const wsChunk = createWriteStream(tempFile);
168
- const dec = Buffer.from(await zstdDecompress(chunk));
169
- if (onChunk) {
170
- await onChunk(dec, i + 1, numChunks);
171
- unlinkSync(tempFile);
172
- }
173
- else {
174
- await writeInChunks(wsChunk, dec);
175
- await new Promise((res) => wsChunk.end(() => res()));
176
- tempFiles.push(tempFile);
177
- }
178
- onProgress?.({
179
- phase: 'decompress_progress',
180
- loaded: i + 1,
181
- total: numChunks,
182
- });
183
- }
184
- onProgress?.({
185
- phase: 'decompress_done',
186
- loaded: numChunks,
187
- total: numChunks,
188
- });
189
- if (onChunk) {
190
- return Buffer.alloc(0);
191
- }
192
- if (outPath || tempFiles.length > 0) {
193
- const finalPath = outPath || join(tmpdir(), `rox_final_${Date.now()}.tmp`);
194
- const ws = createWriteStream(finalPath);
195
- for (const tempFile of tempFiles) {
196
- const rs = createReadStream(tempFile);
197
- await new Promise((resolve, reject) => {
198
- rs.on('data', (chunk) => ws.write(chunk));
199
- rs.on('end', resolve);
200
- rs.on('error', reject);
201
- });
202
- unlinkSync(tempFile);
203
- }
204
- await new Promise((res) => ws.end(() => res()));
205
- if (!outPath) {
206
- const finalBuf = readFileSync(finalPath);
207
- unlinkSync(finalPath);
208
- return finalBuf;
209
- }
210
- return Buffer.alloc(0);
211
- }
212
- return Buffer.alloc(0);
213
- }
214
- export async function optimizePngBuffer(pngBuf, fast = false) {
215
- const runCommandAsync = (cmd, args, timeout = 120000) => {
216
- return new Promise((resolve) => {
217
- try {
218
- const child = spawn(cmd, args, { windowsHide: true, stdio: 'ignore' });
219
- let killed = false;
220
- const to = setTimeout(() => {
221
- killed = true;
222
- try {
223
- child.kill('SIGTERM');
224
- }
225
- catch (e) { }
226
- }, timeout);
227
- child.on('close', (code) => {
228
- clearTimeout(to);
229
- if (killed)
230
- resolve({ error: new Error('timeout') });
231
- else
232
- resolve({ code: code ?? 0 });
233
- });
234
- child.on('error', (err) => {
235
- clearTimeout(to);
236
- resolve({ error: err });
237
- });
238
- }
239
- catch (err) {
240
- resolve({ error: err });
241
- }
242
- });
243
- };
244
- try {
245
- const inPath = join(tmpdir(), `rox_zop_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
246
- const outPath = inPath + '.out.png';
247
- writeFileSync(inPath, pngBuf);
248
- const iterations = fast ? 15 : 40;
249
- const args = [
250
- '-y',
251
- `--iterations=${iterations}`,
252
- '--filters=01234mepb',
253
- inPath,
254
- outPath,
255
- ];
256
- const runCommandAsync = (cmd, args, timeout = 120000) => {
257
- return new Promise((resolve) => {
258
- try {
259
- const child = spawn(cmd, args, {
260
- windowsHide: true,
261
- stdio: 'ignore',
262
- });
263
- let killed = false;
264
- const to = setTimeout(() => {
265
- killed = true;
266
- try {
267
- child.kill('SIGTERM');
268
- }
269
- catch (e) { }
270
- }, timeout);
271
- child.on('close', (code) => {
272
- clearTimeout(to);
273
- if (killed)
274
- resolve({ error: new Error('timeout') });
275
- else
276
- resolve({ code: code ?? 0 });
277
- });
278
- child.on('error', (err) => {
279
- clearTimeout(to);
280
- resolve({ error: err });
281
- });
282
- }
283
- catch (err) {
284
- resolve({ error: err });
285
- }
286
- });
287
- };
288
- const res = await runCommandAsync('zopflipng', args, 120000);
289
- if (!res.error && existsSync(outPath)) {
290
- const outBuf = readFileSync(outPath);
291
- try {
292
- unlinkSync(inPath);
293
- unlinkSync(outPath);
294
- }
295
- catch (e) { }
296
- return outBuf.length < pngBuf.length ? outBuf : pngBuf;
297
- }
298
- if (fast)
299
- return pngBuf;
300
- }
301
- catch (e) { }
302
- try {
303
- const chunksRaw = extract(pngBuf);
304
- const ihdr = chunksRaw.find((c) => c.name === 'IHDR');
305
- if (!ihdr)
306
- return pngBuf;
307
- const ihdrData = Buffer.isBuffer(ihdr.data)
308
- ? ihdr.data
309
- : Buffer.from(ihdr.data);
310
- const width = ihdrData.readUInt32BE(0);
311
- const height = ihdrData.readUInt32BE(4);
312
- const bitDepth = ihdrData[8];
313
- const colorType = ihdrData[9];
314
- if (bitDepth !== 8 || colorType !== 2)
315
- return pngBuf;
316
- const idatChunks = chunksRaw.filter((c) => c.name === 'IDAT');
317
- const idatData = Buffer.concat(idatChunks.map((c) => Buffer.isBuffer(c.data)
318
- ? c.data
319
- : Buffer.from(c.data)));
320
- let raw;
321
- try {
322
- raw = zlib.inflateSync(idatData);
323
- }
324
- catch (e) {
325
- return pngBuf;
326
- }
327
- const bytesPerPixel = 3;
328
- const rowBytes = width * bytesPerPixel;
329
- const inRowLen = rowBytes + 1;
330
- if (raw.length !== inRowLen * height)
331
- return pngBuf;
332
- function paethPredict(a, b, c) {
333
- const p = a + b - c;
334
- const pa = Math.abs(p - a);
335
- const pb = Math.abs(p - b);
336
- const pc = Math.abs(p - c);
337
- if (pa <= pb && pa <= pc)
338
- return a;
339
- if (pb <= pc)
340
- return b;
341
- return c;
342
- }
343
- const outRows = [];
344
- let prevRow = null;
345
- for (let y = 0; y < height; y++) {
346
- const rowStart = y * inRowLen + 1;
347
- const row = raw.slice(rowStart, rowStart + rowBytes);
348
- let bestSum = Infinity;
349
- let bestFiltered = null;
350
- for (let f = 0; f <= 4; f++) {
351
- const filtered = Buffer.alloc(rowBytes);
352
- let sum = 0;
353
- for (let i = 0; i < rowBytes; i++) {
354
- const val = row[i];
355
- let outv = 0;
356
- const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
357
- const up = prevRow ? prevRow[i] : 0;
358
- const upLeft = prevRow && i - bytesPerPixel >= 0 ? prevRow[i - bytesPerPixel] : 0;
359
- if (f === 0) {
360
- outv = val;
361
- }
362
- else if (f === 1) {
363
- outv = (val - left + 256) & 0xff;
364
- }
365
- else if (f === 2) {
366
- outv = (val - up + 256) & 0xff;
367
- }
368
- else if (f === 3) {
369
- const avg = Math.floor((left + up) / 2);
370
- outv = (val - avg + 256) & 0xff;
371
- }
372
- else {
373
- const p = paethPredict(left, up, upLeft);
374
- outv = (val - p + 256) & 0xff;
375
- }
376
- filtered[i] = outv;
377
- const signed = outv > 127 ? outv - 256 : outv;
378
- sum += Math.abs(signed);
379
- }
380
- if (sum < bestSum) {
381
- bestSum = sum;
382
- bestFiltered = filtered;
383
- }
384
- }
385
- const rowBuf = Buffer.alloc(1 + rowBytes);
386
- let chosenFilter = 0;
387
- for (let f = 0; f <= 4; f++) {
388
- const filtered = Buffer.alloc(rowBytes);
389
- for (let i = 0; i < rowBytes; i++) {
390
- const val = row[i];
391
- const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
392
- const up = prevRow ? prevRow[i] : 0;
393
- const upLeft = prevRow && i - bytesPerPixel >= 0 ? prevRow[i - bytesPerPixel] : 0;
394
- if (f === 0)
395
- filtered[i] = val;
396
- else if (f === 1)
397
- filtered[i] = (val - left + 256) & 0xff;
398
- else if (f === 2)
399
- filtered[i] = (val - up + 256) & 0xff;
400
- else if (f === 3)
401
- filtered[i] = (val - Math.floor((left + up) / 2) + 256) & 0xff;
402
- else
403
- filtered[i] = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
404
- }
405
- if (filtered.equals(bestFiltered)) {
406
- chosenFilter = f;
407
- break;
408
- }
409
- }
410
- rowBuf[0] = chosenFilter;
411
- bestFiltered.copy(rowBuf, 1);
412
- outRows.push(rowBuf);
413
- prevRow = row;
414
- }
415
- const filteredAll = Buffer.concat(outRows);
416
- const compressed = zlib.deflateSync(filteredAll, {
417
- level: 9,
418
- memLevel: 9,
419
- strategy: zlib.constants.Z_DEFAULT_STRATEGY,
420
- });
421
- const newChunks = [];
422
- for (const c of chunksRaw) {
423
- if (c.name === 'IDAT')
424
- continue;
425
- newChunks.push({
426
- name: c.name,
427
- data: Buffer.isBuffer(c.data)
428
- ? c.data
429
- : Buffer.from(c.data),
430
- });
431
- }
432
- const iendIndex = newChunks.findIndex((c) => c.name === 'IEND');
433
- const insertIndex = iendIndex >= 0 ? iendIndex : newChunks.length;
434
- newChunks.splice(insertIndex, 0, { name: 'IDAT', data: compressed });
435
- function ensurePng(buf) {
436
- return buf.slice(0, 8).toString('hex') === PNG_HEADER_HEX
437
- ? buf
438
- : Buffer.concat([PNG_HEADER, buf]);
439
- }
440
- const out = ensurePng(Buffer.from(encode(newChunks)));
441
- let bestBuf = out.length < pngBuf.length ? out : pngBuf;
442
- const strategies = [
443
- zlib.constants.Z_DEFAULT_STRATEGY,
444
- zlib.constants.Z_FILTERED,
445
- zlib.constants.Z_RLE,
446
- ...(zlib.constants.Z_HUFFMAN_ONLY ? [zlib.constants.Z_HUFFMAN_ONLY] : []),
447
- ...(zlib.constants.Z_FIXED ? [zlib.constants.Z_FIXED] : []),
448
- ];
449
- for (const strat of strategies) {
450
- try {
451
- const comp = zlib.deflateSync(raw, {
452
- level: 9,
453
- memLevel: 9,
454
- strategy: strat,
455
- });
456
- const altChunks = newChunks.map((c) => ({
457
- name: c.name,
458
- data: c.data,
459
- }));
460
- const idx = altChunks.findIndex((c) => c.name === 'IDAT');
461
- if (idx !== -1)
462
- altChunks[idx] = { name: 'IDAT', data: comp };
463
- const candidate = ensurePng(Buffer.from(encode(altChunks)));
464
- if (candidate.length < bestBuf.length)
465
- bestBuf = candidate;
466
- }
467
- catch (e) { }
468
- }
469
- try {
470
- const fflate = await import('fflate');
471
- const fflateDeflateSync = fflate.deflateSync;
472
- try {
473
- const comp = fflateDeflateSync(filteredAll);
474
- const altChunks = newChunks.map((c) => ({
475
- name: c.name,
476
- data: c.data,
477
- }));
478
- const idx = altChunks.findIndex((c) => c.name === 'IDAT');
479
- if (idx !== -1)
480
- altChunks[idx] = { name: 'IDAT', data: Buffer.from(comp) };
481
- const candidate = ensurePng(Buffer.from(encode(altChunks)));
482
- if (candidate.length < bestBuf.length)
483
- bestBuf = candidate;
484
- }
485
- catch (e) { }
486
- }
487
- catch (e) { }
488
- const windowBitsOpts = [15, 12, 9];
489
- const memLevelOpts = [9, 8];
490
- for (let f = 0; f <= 4; f++) {
491
- try {
492
- const filteredAllGlobalRows = [];
493
- let prevRowG = null;
494
- for (let y = 0; y < height; y++) {
495
- const row = raw.slice(y * inRowLen + 1, y * inRowLen + 1 + rowBytes);
496
- const filtered = Buffer.alloc(rowBytes);
497
- for (let i = 0; i < rowBytes; i++) {
498
- const val = row[i];
499
- const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
500
- const up = prevRowG ? prevRowG[i] : 0;
501
- const upLeft = prevRowG && i - bytesPerPixel >= 0
502
- ? prevRowG[i - bytesPerPixel]
503
- : 0;
504
- if (f === 0)
505
- filtered[i] = val;
506
- else if (f === 1)
507
- filtered[i] = (val - left + 256) & 0xff;
508
- else if (f === 2)
509
- filtered[i] = (val - up + 256) & 0xff;
510
- else if (f === 3)
511
- filtered[i] = (val - Math.floor((left + up) / 2) + 256) & 0xff;
512
- else
513
- filtered[i] = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
514
- }
515
- const rowBuf = Buffer.alloc(1 + rowBytes);
516
- rowBuf[0] = f;
517
- filtered.copy(rowBuf, 1);
518
- filteredAllGlobalRows.push(rowBuf);
519
- prevRowG = row;
520
- }
521
- const filteredAllGlobal = Buffer.concat(filteredAllGlobalRows);
522
- for (const strat2 of strategies) {
523
- for (const wb of windowBitsOpts) {
524
- for (const ml of memLevelOpts) {
525
- try {
526
- const comp = zlib.deflateSync(filteredAllGlobal, {
527
- level: 9,
528
- memLevel: ml,
529
- strategy: strat2,
530
- windowBits: wb,
531
- });
532
- const altChunks = newChunks.map((c) => ({
533
- name: c.name,
534
- data: c.data,
535
- }));
536
- const idx = altChunks.findIndex((c) => c.name === 'IDAT');
537
- if (idx !== -1)
538
- altChunks[idx] = { name: 'IDAT', data: comp };
539
- const candidate = ensurePng(Buffer.from(encode(altChunks)));
540
- if (candidate.length < bestBuf.length)
541
- bestBuf = candidate;
542
- }
543
- catch (e) { }
544
- }
545
- }
546
- }
547
- }
548
- catch (e) { }
549
- }
550
- try {
551
- const zopIterations = [1000, 2000];
552
- zopIterations.push(5000, 10000, 20000);
553
- for (const iters of zopIterations) {
554
- try {
555
- const zIn = join(tmpdir(), `rox_zop_in_${Date.now()}_${Math.random()
556
- .toString(36)
557
- .slice(2)}.png`);
558
- const zOut = zIn + '.out.png';
559
- writeFileSync(zIn, bestBuf);
560
- const args2 = [
561
- '-y',
562
- `--iterations=${iters}`,
563
- '--filters=01234mepb',
564
- zIn,
565
- zOut,
566
- ];
567
- try {
568
- const r2 = await runCommandAsync('zopflipng', args2, 240000);
569
- if (!r2.error && existsSync(zOut)) {
570
- const zbuf = readFileSync(zOut);
571
- try {
572
- unlinkSync(zIn);
573
- unlinkSync(zOut);
574
- }
575
- catch (e) { }
576
- if (zbuf.length < bestBuf.length)
577
- bestBuf = zbuf;
578
- }
579
- }
580
- catch (e) { }
581
- }
582
- catch (e) { }
583
- }
584
- }
585
- catch (e) { }
586
- try {
587
- const advIn = join(tmpdir(), `rox_adv_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
588
- writeFileSync(advIn, bestBuf);
589
- const rAdv = spawnSync('advdef', ['-z4', '-i10', advIn], {
590
- windowsHide: true,
591
- stdio: 'ignore',
592
- timeout: 120000,
593
- });
594
- if (!rAdv.error && existsSync(advIn)) {
595
- const advBuf = readFileSync(advIn);
596
- try {
597
- unlinkSync(advIn);
598
- }
599
- catch (e) { }
600
- if (advBuf.length < bestBuf.length)
601
- bestBuf = advBuf;
602
- }
603
- }
604
- catch (e) { }
605
- for (const strat of strategies) {
606
- try {
607
- const comp = zlib.deflateSync(filteredAll, {
608
- level: 9,
609
- memLevel: 9,
610
- strategy: strat,
611
- });
612
- const altChunks = newChunks.map((c) => ({
613
- name: c.name,
614
- data: c.data,
615
- }));
616
- const idx = altChunks.findIndex((c) => c.name === 'IDAT');
617
- if (idx !== -1)
618
- altChunks[idx] = { name: 'IDAT', data: comp };
619
- const candidate = ensurePng(Buffer.from(encode(altChunks)));
620
- if (candidate.length < bestBuf.length)
621
- bestBuf = candidate;
622
- }
623
- catch (e) { }
624
- }
625
- try {
626
- const pixels = Buffer.alloc(width * height * 3);
627
- let prev = null;
628
- for (let y = 0; y < height; y++) {
629
- const f = raw[y * inRowLen];
630
- const row = raw.slice(y * inRowLen + 1, y * inRowLen + 1 + rowBytes);
631
- const recon = Buffer.alloc(rowBytes);
632
- for (let i = 0; i < rowBytes; i++) {
633
- const left = i - 3 >= 0 ? recon[i - 3] : 0;
634
- const up = prev ? prev[i] : 0;
635
- const upLeft = prev && i - 3 >= 0 ? prev[i - 3] : 0;
636
- let v = row[i];
637
- if (f === 0) {
638
- }
639
- else if (f === 1)
640
- v = (v + left) & 0xff;
641
- else if (f === 2)
642
- v = (v + up) & 0xff;
643
- else if (f === 3)
644
- v = (v + Math.floor((left + up) / 2)) & 0xff;
645
- else
646
- v = (v + paethPredict(left, up, upLeft)) & 0xff;
647
- recon[i] = v;
648
- }
649
- recon.copy(pixels, y * rowBytes);
650
- prev = recon;
651
- }
652
- const paletteMap = new Map();
653
- const palette = [];
654
- for (let i = 0; i < pixels.length; i += 3) {
655
- const key = `${pixels[i]},${pixels[i + 1]},${pixels[i + 2]}`;
656
- if (!paletteMap.has(key)) {
657
- paletteMap.set(key, paletteMap.size);
658
- palette.push(pixels[i], pixels[i + 1], pixels[i + 2]);
659
- if (paletteMap.size > 256)
660
- break;
661
- }
662
- }
663
- if (paletteMap.size <= 256) {
664
- const idxRowLen = 1 + width * 1;
665
- const idxRows = [];
666
- for (let y = 0; y < height; y++) {
667
- const rowIdx = Buffer.alloc(width);
668
- for (let x = 0; x < width; x++) {
669
- const pos = (y * width + x) * 3;
670
- const key = `${pixels[pos]},${pixels[pos + 1]},${pixels[pos + 2]}`;
671
- rowIdx[x] = paletteMap.get(key);
672
- }
673
- let bestRowFilter = 0;
674
- let bestRowSum = Infinity;
675
- let bestRowFiltered = null;
676
- for (let f = 0; f <= 4; f++) {
677
- const filteredRow = Buffer.alloc(width);
678
- let sum = 0;
679
- for (let i = 0; i < width; i++) {
680
- const val = rowIdx[i];
681
- let outv = 0;
682
- const left = i - 1 >= 0 ? rowIdx[i - 1] : 0;
683
- const up = y > 0 ? idxRows[y - 1][i] : 0;
684
- const upLeft = y > 0 && i - 1 >= 0 ? idxRows[y - 1][i - 1] : 0;
685
- if (f === 0)
686
- outv = val;
687
- else if (f === 1)
688
- outv = (val - left + 256) & 0xff;
689
- else if (f === 2)
690
- outv = (val - up + 256) & 0xff;
691
- else if (f === 3)
692
- outv = (val - Math.floor((left + up) / 2) + 256) & 0xff;
693
- else
694
- outv = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
695
- filteredRow[i] = outv;
696
- const signed = outv > 127 ? outv - 256 : outv;
697
- sum += Math.abs(signed);
698
- }
699
- if (sum < bestRowSum) {
700
- bestRowSum = sum;
701
- bestRowFilter = f;
702
- bestRowFiltered = filteredRow;
703
- }
704
- }
705
- const rowBuf = Buffer.alloc(idxRowLen);
706
- rowBuf[0] = bestRowFilter;
707
- bestRowFiltered.copy(rowBuf, 1);
708
- idxRows.push(rowBuf);
709
- }
710
- const freqMap = new Map();
711
- for (let i = 0; i < pixels.length; i += 3) {
712
- const key = `${pixels[i]},${pixels[i + 1]},${pixels[i + 2]}`;
713
- freqMap.set(key, (freqMap.get(key) || 0) + 1);
714
- }
715
- const paletteVariants = [];
716
- paletteVariants.push({
717
- paletteArr: palette.slice(),
718
- map: new Map(paletteMap),
719
- });
720
- const freqSorted = Array.from(freqMap.entries()).sort((a, b) => b[1] - a[1]);
721
- if (freqSorted.length > 0) {
722
- const pal2 = [];
723
- const map2 = new Map();
724
- let pi = 0;
725
- for (const [k] of freqSorted) {
726
- const parts = k.split(',').map((s) => Number(s));
727
- pal2.push(parts[0], parts[1], parts[2]);
728
- map2.set(k, pi++);
729
- if (pi >= 256)
730
- break;
731
- }
732
- if (map2.size <= 256)
733
- paletteVariants.push({ paletteArr: pal2, map: map2 });
734
- }
735
- for (const variant of paletteVariants) {
736
- const pSize = variant.map.size;
737
- const bitDepth = pSize <= 2 ? 1 : pSize <= 4 ? 2 : pSize <= 16 ? 4 : 8;
738
- const idxRowsVar = [];
739
- for (let y = 0; y < height; y++) {
740
- const rowIdx = Buffer.alloc(width);
741
- for (let x = 0; x < width; x++) {
742
- const pos = (y * width + x) * 3;
743
- const key = `${pixels[pos]},${pixels[pos + 1]},${pixels[pos + 2]}`;
744
- rowIdx[x] = variant.map.get(key);
745
- }
746
- idxRowsVar.push(rowIdx);
747
- }
748
- function packRowIndices(rowIdx, bitDepth) {
749
- if (bitDepth === 8)
750
- return rowIdx;
751
- const bitsPerRow = width * bitDepth;
752
- const outLen = Math.ceil(bitsPerRow / 8);
753
- const out = Buffer.alloc(outLen);
754
- let bitPos = 0;
755
- for (let i = 0; i < width; i++) {
756
- const val = rowIdx[i] & ((1 << bitDepth) - 1);
757
- for (let b = 0; b < bitDepth; b++) {
758
- const bit = (val >> (bitDepth - 1 - b)) & 1;
759
- const byteIdx = Math.floor(bitPos / 8);
760
- const shift = 7 - (bitPos % 8);
761
- out[byteIdx] |= bit << shift;
762
- bitPos++;
763
- }
764
- }
765
- return out;
766
- }
767
- const packedRows = [];
768
- for (let y = 0; y < height; y++) {
769
- const packed = packRowIndices(idxRowsVar[y], bitDepth);
770
- let bestRowFilter = 0;
771
- let bestRowSum = Infinity;
772
- let bestRowFiltered = null;
773
- for (let f = 0; f <= 4; f++) {
774
- const filteredRow = Buffer.alloc(packed.length);
775
- let sum = 0;
776
- for (let i = 0; i < packed.length; i++) {
777
- const val = packed[i];
778
- const left = i - 1 >= 0 ? packed[i - 1] : 0;
779
- const up = y > 0 ? packedRows[y - 1][i] : 0;
780
- const upLeft = y > 0 && i - 1 >= 0 ? packedRows[y - 1][i - 1] : 0;
781
- let outv = 0;
782
- if (f === 0)
783
- outv = val;
784
- else if (f === 1)
785
- outv = (val - left + 256) & 0xff;
786
- else if (f === 2)
787
- outv = (val - up + 256) & 0xff;
788
- else if (f === 3)
789
- outv = (val - Math.floor((left + up) / 2) + 256) & 0xff;
790
- else
791
- outv = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
792
- filteredRow[i] = outv;
793
- const signed = outv > 127 ? outv - 256 : outv;
794
- sum += Math.abs(signed);
795
- }
796
- if (sum < bestRowSum) {
797
- bestRowSum = sum;
798
- bestRowFilter = f;
799
- bestRowFiltered = filteredRow;
800
- }
801
- }
802
- const rowBuf = Buffer.alloc(1 + packed.length);
803
- rowBuf[0] = bestRowFilter;
804
- bestRowFiltered.copy(rowBuf, 1);
805
- packedRows.push(rowBuf);
806
- }
807
- const idxFilteredAllVar = Buffer.concat(packedRows);
808
- const palettesBufVar = Buffer.from(variant.paletteArr);
809
- const palChunksVar = [];
810
- const ihdr = Buffer.alloc(13);
811
- ihdr.writeUInt32BE(width, 0);
812
- ihdr.writeUInt32BE(height, 4);
813
- ihdr[8] = bitDepth;
814
- ihdr[9] = 3;
815
- ihdr[10] = 0;
816
- ihdr[11] = 0;
817
- ihdr[12] = 0;
818
- palChunksVar.push({ name: 'IHDR', data: ihdr });
819
- palChunksVar.push({ name: 'PLTE', data: palettesBufVar });
820
- palChunksVar.push({
821
- name: 'IDAT',
822
- data: zlib.deflateSync(idxFilteredAllVar, { level: 9 }),
823
- });
824
- palChunksVar.push({ name: 'IEND', data: Buffer.alloc(0) });
825
- const palOutVar = ensurePng(Buffer.from(encode(palChunksVar)));
826
- if (palOutVar.length < bestBuf.length)
827
- bestBuf = palOutVar;
828
- }
829
- }
830
- }
831
- catch (e) { }
832
- const externalAttempts = [
833
- { cmd: 'oxipng', args: ['-o', '6', '--strip', 'all'] },
834
- { cmd: 'optipng', args: ['-o7'] },
835
- { cmd: 'pngcrush', args: ['-brute', '-reduce'] },
836
- { cmd: 'pngout', args: [] },
837
- ];
838
- for (const tool of externalAttempts) {
839
- try {
840
- const tIn = join(tmpdir(), `rox_ext_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
841
- const tOut = tIn + '.out.png';
842
- writeFileSync(tIn, bestBuf);
843
- const args = tool.args.concat([tIn, tOut]);
844
- const r = spawnSync(tool.cmd, args, {
845
- windowsHide: true,
846
- stdio: 'ignore',
847
- timeout: 240000,
848
- });
849
- if (!r.error && existsSync(tOut)) {
850
- const outb = readFileSync(tOut);
851
- try {
852
- unlinkSync(tIn);
853
- unlinkSync(tOut);
854
- }
855
- catch (e) { }
856
- if (outb.length < bestBuf.length)
857
- bestBuf = outb;
858
- }
859
- else {
860
- try {
861
- unlinkSync(tIn);
862
- }
863
- catch (e) { }
864
- }
865
- }
866
- catch (e) { }
867
- }
868
- return bestBuf;
869
- }
870
- catch (e) {
871
- return pngBuf;
872
- }
873
- }
874
- function applyXor(buf, passphrase) {
875
- const key = Buffer.from(passphrase, 'utf8');
876
- const out = Buffer.alloc(buf.length);
877
- for (let i = 0; i < buf.length; i++) {
878
- out[i] = buf[i] ^ key[i % key.length];
879
- }
880
- return out;
881
- }
882
- async function tryZstdDecompress(payload, onProgress, onChunk, outPath) {
883
- return await parallelZstdDecompress(payload, onProgress, onChunk, outPath);
884
- }
885
- async function tryDecompress(payload, onProgress, onChunk, outPath) {
886
- try {
887
- return await parallelZstdDecompress(payload, onProgress, onChunk, outPath);
888
- }
889
- catch (e) {
890
- try {
891
- const mod = await import('lzma-purejs');
892
- const decompressFn = mod && (mod.decompress || (mod.LZMA && mod.LZMA.decompress));
893
- if (!decompressFn)
894
- throw new Error('No lzma decompress');
895
- const dec = await new Promise((resolve, reject) => {
896
- try {
897
- decompressFn(Buffer.from(payload), (out) => resolve(out));
898
- }
899
- catch (err) {
900
- reject(err);
901
- }
902
- });
903
- const dBuf = Buffer.isBuffer(dec) ? dec : Buffer.from(dec);
904
- if (onChunk) {
905
- await onChunk(dBuf, 1, 1);
906
- return Buffer.alloc(0);
907
- }
908
- if (outPath) {
909
- const ws = createWriteStream(outPath);
910
- await writeInChunks(ws, dBuf);
911
- ws.end();
912
- return Buffer.alloc(0);
913
- }
914
- return dBuf;
915
- }
916
- catch (e2) {
917
- throw e;
918
- }
919
- }
920
- }
921
- function tryDecryptIfNeeded(buf, passphrase) {
922
- if (!buf || buf.length === 0)
923
- return buf;
924
- const flag = buf[0];
925
- if (flag === ENC_AES) {
926
- const MIN_AES_LEN = 1 + 16 + 12 + 16 + 1;
927
- if (buf.length < MIN_AES_LEN)
928
- throw new IncorrectPassphraseError();
929
- if (!passphrase)
930
- throw new PassphraseRequiredError();
931
- const salt = buf.slice(1, 17);
932
- const iv = buf.slice(17, 29);
933
- const tag = buf.slice(29, 45);
934
- const enc = buf.slice(45);
935
- const PBKDF2_ITERS = 1000000;
936
- const key = pbkdf2Sync(passphrase, salt, PBKDF2_ITERS, 32, 'sha256');
937
- const dec = createDecipheriv('aes-256-gcm', key, iv);
938
- dec.setAuthTag(tag);
939
- try {
940
- const decrypted = Buffer.concat([dec.update(enc), dec.final()]);
941
- return decrypted;
942
- }
943
- catch (e) {
944
- throw new IncorrectPassphraseError();
945
- }
946
- }
947
- if (flag === ENC_XOR) {
948
- if (!passphrase)
949
- throw new PassphraseRequiredError();
950
- return applyXor(buf.slice(1), passphrase);
951
- }
952
- if (flag === ENC_NONE) {
953
- return buf.slice(1);
954
- }
955
- return buf;
956
- }
957
- export async function cropAndReconstitute(input, debugDir) {
958
- async function loadRaw(imgInput) {
959
- const { data, info } = await sharp(imgInput)
960
- .ensureAlpha()
961
- .raw()
962
- .toBuffer({ resolveWithObject: true });
963
- return { data, info };
964
- }
965
- function idxFor(x, y, width) {
966
- return (y * width + x) * 4;
967
- }
968
- function eqRGB(a, b) {
969
- return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
970
- }
971
- const { info } = await loadRaw(input);
972
- const doubledBuffer = await sharp(input)
973
- .resize({
974
- width: info.width * 2,
975
- height: info.height * 2,
976
- kernel: 'nearest',
977
- })
978
- .png()
979
- .toBuffer();
980
- if (debugDir) {
981
- await sharp(doubledBuffer).toFile(join(debugDir, 'doubled.png'));
982
- }
983
- const { data: doubledData, info: doubledInfo } = await loadRaw(doubledBuffer);
984
- const w = doubledInfo.width, h = doubledInfo.height;
985
- const at = (x, y) => {
986
- const i = idxFor(x, y, w);
987
- return [
988
- doubledData[i],
989
- doubledData[i + 1],
990
- doubledData[i + 2],
991
- doubledData[i + 3],
992
- ];
993
- };
994
- const findPattern = (startX, startY, dirX, dirY, pattern) => {
995
- for (let y = startY; y >= 0 && y < h; y += dirY) {
996
- for (let x = startX; x >= 0 && x < w; x += dirX) {
997
- const p = at(x, y);
998
- if (p[0] !== 255 || p[1] !== 0 || p[2] !== 0)
999
- continue;
1000
- let nx = x + dirX;
1001
- while (nx >= 0 && nx < w && eqRGB(at(nx, y), p))
1002
- nx += dirX;
1003
- if (nx < 0 || nx >= w)
1004
- continue;
1005
- const a = at(nx, y);
1006
- let nx2 = nx + dirX;
1007
- while (nx2 >= 0 && nx2 < w && eqRGB(at(nx2, y), a))
1008
- nx2 += dirX;
1009
- if (nx2 < 0 || nx2 >= w)
1010
- continue;
1011
- const b = at(nx2, y);
1012
- if (a[0] === pattern[0][0] &&
1013
- a[1] === pattern[0][1] &&
1014
- a[2] === pattern[0][2] &&
1015
- b[0] === pattern[1][0] &&
1016
- b[1] === pattern[1][1] &&
1017
- b[2] === pattern[1][2]) {
1018
- return { x, y };
1019
- }
1020
- }
1021
- }
1022
- return null;
1023
- };
1024
- const startPoint = findPattern(0, 0, 1, 1, [
1025
- [0, 255, 0],
1026
- [0, 0, 255],
1027
- ]);
1028
- const endPoint = findPattern(w - 1, h - 1, -1, -1, [
1029
- [0, 255, 0],
1030
- [0, 0, 255],
1031
- ]);
1032
- if (!startPoint || !endPoint)
1033
- throw new Error('Patterns not found');
1034
- const sx1 = Math.min(startPoint.x, endPoint.x), sy1 = Math.min(startPoint.y, endPoint.y);
1035
- const sx2 = Math.max(startPoint.x, endPoint.x), sy2 = Math.max(startPoint.y, endPoint.y);
1036
- const cropW = sx2 - sx1 + 1, cropH = sy2 - sy1 + 1;
1037
- if (cropW <= 0 || cropH <= 0)
1038
- throw new Error('Invalid crop dimensions');
1039
- const cropped = await sharp(doubledBuffer)
1040
- .extract({ left: sx1, top: sy1, width: cropW, height: cropH })
1041
- .png()
1042
- .toBuffer();
1043
- const { data: cdata, info: cinfo } = await loadRaw(cropped);
1044
- const cw = cinfo.width, ch = cinfo.height;
1045
- const newWidth = cw, newHeight = ch + 1;
1046
- const out = Buffer.alloc(newWidth * newHeight * 4, 0);
1047
- for (let i = 0; i < out.length; i += 4)
1048
- out[i + 3] = 255;
1049
- for (let y = 0; y < ch; y++) {
1050
- for (let x = 0; x < cw; x++) {
1051
- const srcI = (y * cw + x) * 4;
1052
- const dstI = (y * newWidth + x) * 4;
1053
- out[dstI] = cdata[srcI];
1054
- out[dstI + 1] = cdata[srcI + 1];
1055
- out[dstI + 2] = cdata[srcI + 2];
1056
- out[dstI + 3] = cdata[srcI + 3];
1057
- }
1058
- }
1059
- for (let x = 0; x < newWidth; x++) {
1060
- const i = ((ch - 1) * newWidth + x) * 4;
1061
- out[i] = out[i + 1] = out[i + 2] = 0;
1062
- out[i + 3] = 255;
1063
- const j = (ch * newWidth + x) * 4;
1064
- out[j] = out[j + 1] = out[j + 2] = 0;
1065
- out[j + 3] = 255;
1066
- }
1067
- if (newWidth >= 3) {
1068
- const bgrStart = newWidth - 3;
1069
- const bgr = [
1070
- [0, 0, 255],
1071
- [0, 255, 0],
1072
- [255, 0, 0],
1073
- ];
1074
- for (let k = 0; k < 3; k++) {
1075
- const i = (ch * newWidth + bgrStart + k) * 4;
1076
- out[i] = bgr[k][0];
1077
- out[i + 1] = bgr[k][1];
1078
- out[i + 2] = bgr[k][2];
1079
- out[i + 3] = 255;
1080
- }
1081
- }
1082
- const getPixel = (x, y) => {
1083
- const i = (y * newWidth + x) * 4;
1084
- return [out[i], out[i + 1], out[i + 2], out[i + 3]];
1085
- };
1086
- const compressedLines = [];
1087
- for (let y = 0; y < newHeight; y++) {
1088
- const line = [];
1089
- for (let x = 0; x < newWidth; x++)
1090
- line.push(getPixel(x, y));
1091
- const isAllBlack = line.every((p) => p[0] === 0 && p[1] === 0 && p[2] === 0 && p[3] === 255);
1092
- if (!isAllBlack &&
1093
- (compressedLines.length === 0 ||
1094
- !line.every((p, i) => p.every((v, j) => v === compressedLines[compressedLines.length - 1][i][j])))) {
1095
- compressedLines.push(line);
1096
- }
1097
- }
1098
- if (compressedLines.length === 0) {
1099
- return sharp({
1100
- create: {
1101
- width: 1,
1102
- height: 1,
1103
- channels: 4,
1104
- background: { r: 0, g: 0, b: 0, alpha: 1 },
1105
- },
1106
- })
1107
- .png()
1108
- .toBuffer();
1109
- }
1110
- let finalWidth = newWidth, finalHeight = compressedLines.length;
1111
- let finalOut = Buffer.alloc(finalWidth * finalHeight * 4, 0);
1112
- for (let i = 0; i < finalOut.length; i += 4)
1113
- finalOut[i + 3] = 255;
1114
- for (let y = 0; y < finalHeight; y++) {
1115
- for (let x = 0; x < finalWidth; x++) {
1116
- const i = (y * finalWidth + x) * 4;
1117
- finalOut[i] = compressedLines[y][x][0];
1118
- finalOut[i + 1] = compressedLines[y][x][1];
1119
- finalOut[i + 2] = compressedLines[y][x][2];
1120
- finalOut[i + 3] = compressedLines[y][x][3] || 255;
1121
- }
1122
- }
1123
- if (finalHeight >= 1 && finalWidth >= 3) {
1124
- const lastY = finalHeight - 1;
1125
- for (let k = 0; k < 3; k++) {
1126
- const i = (lastY * finalWidth + finalWidth - 3 + k) * 4;
1127
- finalOut[i] = finalOut[i + 1] = finalOut[i + 2] = 0;
1128
- finalOut[i + 3] = 255;
1129
- }
1130
- }
1131
- if (finalWidth >= 2) {
1132
- const kept = [];
1133
- for (let x = 0; x < finalWidth; x++) {
1134
- if (kept.length === 0) {
1135
- kept.push(x);
1136
- continue;
1137
- }
1138
- const prevX = kept[kept.length - 1];
1139
- let same = true;
1140
- for (let y = 0; y < finalHeight; y++) {
1141
- const ia = (y * finalWidth + prevX) * 4, ib = (y * finalWidth + x) * 4;
1142
- if (finalOut[ia] !== finalOut[ib] ||
1143
- finalOut[ia + 1] !== finalOut[ib + 1] ||
1144
- finalOut[ia + 2] !== finalOut[ib + 2] ||
1145
- finalOut[ia + 3] !== finalOut[ib + 3]) {
1146
- same = false;
1147
- break;
1148
- }
1149
- }
1150
- if (!same)
1151
- kept.push(x);
1152
- }
1153
- if (kept.length !== finalWidth) {
1154
- const newFinalWidth = kept.length;
1155
- const newOut = Buffer.alloc(newFinalWidth * finalHeight * 4, 0);
1156
- for (let i = 0; i < newOut.length; i += 4)
1157
- newOut[i + 3] = 255;
1158
- for (let nx = 0; nx < kept.length; nx++) {
1159
- const sx = kept[nx];
1160
- for (let y = 0; y < finalHeight; y++) {
1161
- const srcI = (y * finalWidth + sx) * 4, dstI = (y * newFinalWidth + nx) * 4;
1162
- newOut[dstI] = finalOut[srcI];
1163
- newOut[dstI + 1] = finalOut[srcI + 1];
1164
- newOut[dstI + 2] = finalOut[srcI + 2];
1165
- newOut[dstI + 3] = finalOut[srcI + 3];
1166
- }
1167
- }
1168
- finalOut = newOut;
1169
- finalWidth = newFinalWidth;
1170
- }
1171
- }
1172
- if (finalHeight >= 2 && finalWidth >= 3) {
1173
- const secondLastY = finalHeight - 2;
1174
- const bgrSeq = [
1175
- [0, 0, 255],
1176
- [0, 255, 0],
1177
- [255, 0, 0],
1178
- ];
1179
- let hasBGR = true;
1180
- for (let k = 0; k < 3; k++) {
1181
- const i = (secondLastY * finalWidth + finalWidth - 3 + k) * 4;
1182
- if (finalOut[i] !== bgrSeq[k][0] ||
1183
- finalOut[i + 1] !== bgrSeq[k][1] ||
1184
- finalOut[i + 2] !== bgrSeq[k][2]) {
1185
- hasBGR = false;
1186
- break;
1187
- }
1188
- }
1189
- if (hasBGR) {
1190
- for (let k = 0; k < 3; k++) {
1191
- const i = (secondLastY * finalWidth + finalWidth - 3 + k) * 4;
1192
- finalOut[i] = finalOut[i + 1] = finalOut[i + 2] = 0;
1193
- finalOut[i + 3] = 255;
1194
- }
1195
- }
1196
- }
1197
- if (finalHeight >= 1 && finalWidth >= 1) {
1198
- const lastYFinal = finalHeight - 1;
1199
- const bgrSeq = [
1200
- [0, 0, 255],
1201
- [0, 255, 0],
1202
- [255, 0, 0],
1203
- ];
1204
- for (let k = 0; k < 3; k++) {
1205
- const sx = finalWidth - 3 + k;
1206
- if (sx >= 0) {
1207
- const i = (lastYFinal * finalWidth + sx) * 4;
1208
- finalOut[i] = bgrSeq[k][0];
1209
- finalOut[i + 1] = bgrSeq[k][1];
1210
- finalOut[i + 2] = bgrSeq[k][2];
1211
- finalOut[i + 3] = 255;
1212
- }
1213
- }
1214
- }
1215
- return sharp(finalOut, {
1216
- raw: { width: finalWidth, height: finalHeight, channels: 4 },
1217
- })
1218
- .png()
1219
- .toBuffer();
1220
- }
1221
- /**
1222
- * Encode a Buffer into a PNG wrapper. Supports optional compression and
1223
- * encryption. Defaults are chosen for a good balance between speed and size.
1224
- *
1225
- * @param input - Data to encode
1226
- * @param opts - Encoding options
1227
- * @public
1228
- * @example
1229
- * ```typescript
1230
- * import { readFileSync, writeFileSync } from 'fs';
1231
- * import { encodeBinaryToPng } from 'roxify';
1232
- *
1233
- * const fileName = 'input.bin'; //Path of your input file here
1234
- * const inputBuffer = readFileSync(fileName);
1235
- * const pngBuffer = await encodeBinaryToPng(inputBuffer, {
1236
- * name: fileName,
1237
- * });
1238
- * writeFileSync('output.png', pngBuffer);
1239
-
1240
- * ```
1241
- */
1242
- export async function encodeBinaryToPng(input, opts = {}) {
1243
- let progressBar = null;
1244
- if (opts.showProgress) {
1245
- progressBar = new cliProgress.SingleBar({
1246
- format: ' {bar} {percentage}% | {step} | {elapsed}s',
1247
- }, cliProgress.Presets.shades_classic);
1248
- progressBar.start(100, 0, { step: 'Starting', elapsed: '0' });
1249
- const startTime = Date.now();
1250
- if (!opts.onProgress) {
1251
- opts.onProgress = (info) => {
1252
- let pct = 0;
1253
- if (info.phase === 'compress_progress' && info.loaded && info.total) {
1254
- pct = (info.loaded / info.total) * 50;
1255
- }
1256
- else if (info.phase === 'compress_done') {
1257
- pct = 50;
1258
- }
1259
- else if (info.phase === 'encrypt_done') {
1260
- pct = 80;
1261
- }
1262
- else if (info.phase === 'png_gen') {
1263
- pct = 90;
1264
- }
1265
- else if (info.phase === 'done') {
1266
- pct = 100;
1267
- }
1268
- progressBar.update(Math.floor(pct), {
1269
- step: info.phase.replace('_', ' '),
1270
- elapsed: String(Math.floor((Date.now() - startTime) / 1000)),
1271
- });
1272
- };
1273
- }
1274
- }
1275
- let payload = Buffer.concat([MAGIC, input]);
1276
- const mode = opts.mode === undefined ? 'screenshot' : opts.mode;
1277
- if (opts.onProgress)
1278
- opts.onProgress({ phase: 'compress_start', total: payload.length });
1279
- const useDelta = mode !== 'screenshot';
1280
- const deltaEncoded = useDelta ? deltaEncode(payload) : payload;
1281
- payload = await parallelZstdCompress(deltaEncoded, 11, (loaded, total) => {
1282
- if (opts.onProgress) {
1283
- opts.onProgress({
1284
- phase: 'compress_progress',
1285
- loaded,
1286
- total,
1287
- });
1288
- }
1289
- });
1290
- if (opts.onProgress)
1291
- opts.onProgress({ phase: 'compress_done', loaded: payload.length });
1292
- if (opts.passphrase && !opts.encrypt) {
1293
- opts.encrypt = 'aes';
1294
- }
1295
- if (opts.encrypt === 'auto' && !opts._skipAuto) {
1296
- const candidates = ['none', 'xor', 'aes'];
1297
- const candidateBufs = [];
1298
- for (const c of candidates) {
1299
- const testBuf = await encodeBinaryToPng(input, {
1300
- ...opts,
1301
- encrypt: c,
1302
- _skipAuto: true,
1303
- });
1304
- candidateBufs.push({ enc: c, buf: testBuf });
1305
- }
1306
- candidateBufs.sort((a, b) => a.buf.length - b.buf.length);
1307
- return candidateBufs[0].buf;
1308
- }
1309
- if (opts.passphrase && opts.encrypt && opts.encrypt !== 'auto') {
1310
- const encChoice = opts.encrypt;
1311
- if (opts.onProgress)
1312
- opts.onProgress({ phase: 'encrypt_start' });
1313
- if (encChoice === 'aes') {
1314
- const salt = randomBytes(16);
1315
- const iv = randomBytes(12);
1316
- const PBKDF2_ITERS = 1000000;
1317
- const key = pbkdf2Sync(opts.passphrase, salt, PBKDF2_ITERS, 32, 'sha256');
1318
- const cipher = createCipheriv('aes-256-gcm', key, iv);
1319
- const enc = Buffer.concat([cipher.update(payload), cipher.final()]);
1320
- const tag = cipher.getAuthTag();
1321
- payload = Buffer.concat([Buffer.from([ENC_AES]), salt, iv, tag, enc]);
1322
- if (opts.onProgress)
1323
- opts.onProgress({ phase: 'encrypt_done' });
1324
- }
1325
- else if (encChoice === 'xor') {
1326
- const xored = applyXor(payload, opts.passphrase);
1327
- payload = Buffer.concat([Buffer.from([ENC_XOR]), xored]);
1328
- if (opts.onProgress)
1329
- opts.onProgress({ phase: 'encrypt_done' });
1330
- }
1331
- else if (encChoice === 'none') {
1332
- payload = Buffer.concat([Buffer.from([ENC_NONE]), payload]);
1333
- if (opts.onProgress)
1334
- opts.onProgress({ phase: 'encrypt_done' });
1335
- }
1336
- }
1337
- else {
1338
- payload = Buffer.concat([Buffer.from([ENC_NONE]), payload]);
1339
- }
1340
- if (opts.onProgress)
1341
- opts.onProgress({ phase: 'meta_prep_done', loaded: payload.length });
1342
- const metaParts = [];
1343
- const includeName = opts.includeName === undefined ? true : !!opts.includeName;
1344
- if (includeName && opts.name) {
1345
- const nameBuf = Buffer.from(opts.name, 'utf8');
1346
- metaParts.push(Buffer.from([nameBuf.length]));
1347
- metaParts.push(nameBuf);
1348
- }
1349
- else {
1350
- metaParts.push(Buffer.from([0]));
1351
- }
1352
- metaParts.push(payload);
1353
- let meta = Buffer.concat(metaParts);
1354
- if (opts.includeFileList && opts.fileList) {
1355
- const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
1356
- const lenBuf = Buffer.alloc(4);
1357
- lenBuf.writeUInt32BE(jsonBuf.length, 0);
1358
- meta = Buffer.concat([meta, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf]);
1359
- }
1360
- if (opts.output === 'rox') {
1361
- return Buffer.concat([MAGIC, meta]);
1362
- }
1363
- if (mode === 'screenshot') {
1364
- const nameBuf = opts.name
1365
- ? Buffer.from(opts.name, 'utf8')
1366
- : Buffer.alloc(0);
1367
- const nameLen = nameBuf.length;
1368
- const payloadLenBuf = Buffer.alloc(4);
1369
- payloadLenBuf.writeUInt32BE(payload.length, 0);
1370
- const version = 1;
1371
- let metaPixel = Buffer.concat([
1372
- Buffer.from([version]),
1373
- Buffer.from([nameLen]),
1374
- nameBuf,
1375
- payloadLenBuf,
1376
- payload,
1377
- ]);
1378
- if (opts.includeFileList && opts.fileList) {
1379
- const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
1380
- const lenBuf = Buffer.alloc(4);
1381
- lenBuf.writeUInt32BE(jsonBuf.length, 0);
1382
- metaPixel = Buffer.concat([
1383
- metaPixel,
1384
- Buffer.from('rXFL', 'utf8'),
1385
- lenBuf,
1386
- jsonBuf,
1387
- ]);
1388
- }
1389
- const dataWithoutMarkers = Buffer.concat([PIXEL_MAGIC, metaPixel]);
1390
- const padding = (3 - (dataWithoutMarkers.length % 3)) % 3;
1391
- const paddedData = padding > 0
1392
- ? Buffer.concat([dataWithoutMarkers, Buffer.alloc(padding)])
1393
- : dataWithoutMarkers;
1394
- const markerStartBytes = colorsToBytes(MARKER_START);
1395
- const compressionMarkerBytes = colorsToBytes(COMPRESSION_MARKERS.zstd);
1396
- const dataWithMarkers = Buffer.concat([
1397
- markerStartBytes,
1398
- compressionMarkerBytes,
1399
- paddedData,
1400
- ]);
1401
- const bytesPerPixel = 3;
1402
- const dataPixels = Math.ceil(dataWithMarkers.length / 3);
1403
- const totalPixels = dataPixels + MARKER_END.length;
1404
- const maxWidth = 16384;
1405
- let side = Math.ceil(Math.sqrt(totalPixels));
1406
- if (side < MARKER_END.length)
1407
- side = MARKER_END.length;
1408
- let logicalWidth;
1409
- let logicalHeight;
1410
- if (side <= maxWidth) {
1411
- logicalWidth = side;
1412
- logicalHeight = side;
1413
- }
1414
- else {
1415
- logicalWidth = Math.min(maxWidth, totalPixels);
1416
- logicalHeight = Math.ceil(totalPixels / logicalWidth);
1417
- }
1418
- const scale = 1;
1419
- const width = logicalWidth * scale;
1420
- const height = logicalHeight * scale;
1421
- const raw = Buffer.alloc(width * height * bytesPerPixel);
1422
- for (let ly = 0; ly < logicalHeight; ly++) {
1423
- for (let lx = 0; lx < logicalWidth; lx++) {
1424
- const linearIdx = ly * logicalWidth + lx;
1425
- let r = 0, g = 0, b = 0;
1426
- if (ly === logicalHeight - 1 &&
1427
- lx >= logicalWidth - MARKER_END.length) {
1428
- const markerIdx = lx - (logicalWidth - MARKER_END.length);
1429
- r = MARKER_END[markerIdx].r;
1430
- g = MARKER_END[markerIdx].g;
1431
- b = MARKER_END[markerIdx].b;
1432
- }
1433
- else if (linearIdx < dataPixels) {
1434
- const srcIdx = linearIdx * 3;
1435
- r = srcIdx < dataWithMarkers.length ? dataWithMarkers[srcIdx] : 0;
1436
- g =
1437
- srcIdx + 1 < dataWithMarkers.length
1438
- ? dataWithMarkers[srcIdx + 1]
1439
- : 0;
1440
- b =
1441
- srcIdx + 2 < dataWithMarkers.length
1442
- ? dataWithMarkers[srcIdx + 2]
1443
- : 0;
1444
- }
1445
- for (let sy = 0; sy < scale; sy++) {
1446
- for (let sx = 0; sx < scale; sx++) {
1447
- const px = lx * scale + sx;
1448
- const py = ly * scale + sy;
1449
- const dstIdx = (py * width + px) * 3;
1450
- raw[dstIdx] = r;
1451
- raw[dstIdx + 1] = g;
1452
- raw[dstIdx + 2] = b;
1453
- }
1454
- }
1455
- }
1456
- }
1457
- if (opts.onProgress)
1458
- opts.onProgress({ phase: 'png_gen', loaded: 0, total: 100 });
1459
- let loaded = 0;
1460
- const progressInterval = setInterval(() => {
1461
- loaded = Math.min(loaded + 2, 98);
1462
- if (opts.onProgress)
1463
- opts.onProgress({ phase: 'png_gen', loaded, total: 100 });
1464
- }, 50);
1465
- let bufScr = await sharp(raw, {
1466
- raw: { width, height, channels: 3 },
1467
- })
1468
- .png({
1469
- compressionLevel: 6,
1470
- palette: false,
1471
- effort: 1,
1472
- adaptiveFiltering: false,
1473
- })
1474
- .toBuffer();
1475
- if (opts.onProgress)
1476
- opts.onProgress({ phase: 'png_gen', loaded: 100, total: 100 });
1477
- if (opts.onProgress)
1478
- opts.onProgress({ phase: 'optimizing', loaded: 0, total: 100 });
1479
- let optInterval = null;
1480
- if (opts.onProgress) {
1481
- let optLoaded = 0;
1482
- optInterval = setInterval(() => {
1483
- optLoaded = Math.min(optLoaded + 5, 95);
1484
- opts.onProgress?.({
1485
- phase: 'optimizing',
1486
- loaded: optLoaded,
1487
- total: 100,
1488
- });
1489
- }, 100);
1490
- }
1491
- try {
1492
- const optimized = await optimizePngBuffer(bufScr, true);
1493
- clearInterval(progressInterval);
1494
- if (optInterval) {
1495
- clearInterval(optInterval);
1496
- optInterval = null;
1497
- }
1498
- if (opts.onProgress)
1499
- opts.onProgress({ phase: 'optimizing', loaded: 100, total: 100 });
1500
- progressBar?.stop();
1501
- return optimized;
1502
- }
1503
- catch (e) {
1504
- clearInterval(progressInterval);
1505
- if (optInterval)
1506
- clearInterval(optInterval);
1507
- progressBar?.stop();
1508
- return bufScr;
1509
- }
1510
- }
1511
- if (mode === 'pixel') {
1512
- const nameBuf = opts.name
1513
- ? Buffer.from(opts.name, 'utf8')
1514
- : Buffer.alloc(0);
1515
- const nameLen = nameBuf.length;
1516
- const payloadLenBuf = Buffer.alloc(4);
1517
- payloadLenBuf.writeUInt32BE(payload.length, 0);
1518
- const version = 1;
1519
- let metaPixel = Buffer.concat([
1520
- Buffer.from([version]),
1521
- Buffer.from([nameLen]),
1522
- nameBuf,
1523
- payloadLenBuf,
1524
- payload,
1525
- ]);
1526
- if (opts.includeFileList && opts.fileList) {
1527
- const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
1528
- const lenBuf = Buffer.alloc(4);
1529
- lenBuf.writeUInt32BE(jsonBuf.length, 0);
1530
- metaPixel = Buffer.concat([
1531
- metaPixel,
1532
- Buffer.from('rXFL', 'utf8'),
1533
- lenBuf,
1534
- jsonBuf,
1535
- ]);
1536
- }
1537
- const full = Buffer.concat([PIXEL_MAGIC, metaPixel]);
1538
- const bytesPerPixel = 3;
1539
- const nPixels = Math.ceil((full.length + 8) / 3);
1540
- const desiredSide = Math.ceil(Math.sqrt(nPixels));
1541
- const sideClamped = Math.max(1, Math.min(desiredSide, 65535));
1542
- const width = sideClamped;
1543
- const height = sideClamped === desiredSide ? sideClamped : Math.ceil(nPixels / width);
1544
- const dimHeader = Buffer.alloc(8);
1545
- dimHeader.writeUInt32BE(width, 0);
1546
- dimHeader.writeUInt32BE(height, 4);
1547
- const fullWithDim = Buffer.concat([dimHeader, full]);
1548
- const rowLen = 1 + width * bytesPerPixel;
1549
- const raw = Buffer.alloc(rowLen * height);
1550
- for (let y = 0; y < height; y++) {
1551
- const rowOffset = y * rowLen;
1552
- raw[rowOffset] = 0;
1553
- for (let x = 0; x < width; x++) {
1554
- const srcIdx = (y * width + x) * 3;
1555
- const dstIdx = rowOffset + 1 + x * bytesPerPixel;
1556
- raw[dstIdx] = srcIdx < fullWithDim.length ? fullWithDim[srcIdx] : 0;
1557
- raw[dstIdx + 1] =
1558
- srcIdx + 1 < fullWithDim.length ? fullWithDim[srcIdx + 1] : 0;
1559
- raw[dstIdx + 2] =
1560
- srcIdx + 2 < fullWithDim.length ? fullWithDim[srcIdx + 2] : 0;
1561
- }
1562
- }
1563
- const idatData = zlib.deflateSync(raw, {
1564
- level: 6,
1565
- memLevel: 8,
1566
- strategy: zlib.constants.Z_RLE,
1567
- });
1568
- const ihdrData = Buffer.alloc(13);
1569
- ihdrData.writeUInt32BE(width, 0);
1570
- ihdrData.writeUInt32BE(height, 4);
1571
- ihdrData[8] = 8;
1572
- ihdrData[9] = 2;
1573
- ihdrData[10] = 0;
1574
- ihdrData[11] = 0;
1575
- ihdrData[12] = 0;
1576
- const chunksPixel = [];
1577
- chunksPixel.push({ name: 'IHDR', data: ihdrData });
1578
- chunksPixel.push({ name: 'IDAT', data: idatData });
1579
- chunksPixel.push({ name: 'IEND', data: Buffer.alloc(0) });
1580
- if (opts.onProgress)
1581
- opts.onProgress({ phase: 'png_gen', loaded: 0, total: 2 });
1582
- const tmp = Buffer.from(encode(chunksPixel));
1583
- const outPng = tmp.slice(0, 8).toString('hex') === PNG_HEADER_HEX
1584
- ? tmp
1585
- : Buffer.concat([PNG_HEADER, tmp]);
1586
- if (opts.onProgress)
1587
- opts.onProgress({ phase: 'png_gen', loaded: 1, total: 2 });
1588
- if (opts.onProgress)
1589
- opts.onProgress({ phase: 'done', loaded: outPng.length });
1590
- if (opts.onProgress)
1591
- opts.onProgress({ phase: 'done', loaded: outPng.length });
1592
- try {
1593
- const optimized = await optimizePngBuffer(outPng);
1594
- try {
1595
- const verified = await decodePngToBinary(optimized);
1596
- if (verified.buf && verified.buf.equals(input)) {
1597
- progressBar?.stop();
1598
- return optimized;
1599
- }
1600
- }
1601
- catch (e) { }
1602
- progressBar?.stop();
1603
- return outPng;
1604
- }
1605
- catch (e) {
1606
- progressBar?.stop();
1607
- return outPng;
1608
- }
1609
- }
1610
- if (mode === 'compact') {
1611
- const bytesPerPixel = 4;
1612
- const side = 1;
1613
- const width = side;
1614
- const height = side;
1615
- const rowLen = 1 + width * bytesPerPixel;
1616
- const raw = Buffer.alloc(rowLen * height);
1617
- for (let y = 0; y < height; y++) {
1618
- raw[y * rowLen] = 0;
1619
- }
1620
- const idatData = zlib.deflateSync(raw, {
1621
- level: 9,
1622
- memLevel: 9,
1623
- strategy: zlib.constants.Z_DEFAULT_STRATEGY,
1624
- });
1625
- const ihdrData = Buffer.alloc(13);
1626
- ihdrData.writeUInt32BE(width, 0);
1627
- ihdrData.writeUInt32BE(height, 4);
1628
- ihdrData[8] = 8;
1629
- ihdrData[9] = 6;
1630
- ihdrData[10] = 0;
1631
- ihdrData[11] = 0;
1632
- ihdrData[12] = 0;
1633
- const chunks2 = [];
1634
- chunks2.push({ name: 'IHDR', data: ihdrData });
1635
- chunks2.push({ name: 'IDAT', data: idatData });
1636
- chunks2.push({ name: CHUNK_TYPE, data: meta });
1637
- chunks2.push({ name: 'IEND', data: Buffer.alloc(0) });
1638
- if (opts.onProgress)
1639
- opts.onProgress({ phase: 'png_gen', loaded: 0, total: 2 });
1640
- const out = Buffer.from(encode(chunks2));
1641
- const outBuf = out.slice(0, 8).toString('hex') === PNG_HEADER_HEX
1642
- ? out
1643
- : Buffer.concat([PNG_HEADER, out]);
1644
- if (opts.onProgress)
1645
- opts.onProgress({ phase: 'png_gen', loaded: 1, total: 2 });
1646
- if (opts.onProgress)
1647
- opts.onProgress({ phase: 'done', loaded: outBuf.length });
1648
- if (opts.onProgress)
1649
- opts.onProgress({ phase: 'done', loaded: outBuf.length });
1650
- try {
1651
- const optimized = await optimizePngBuffer(outBuf);
1652
- try {
1653
- const verified = await decodePngToBinary(optimized);
1654
- if (verified.buf && verified.buf.equals(input)) {
1655
- progressBar?.stop();
1656
- return optimized;
1657
- }
1658
- }
1659
- catch (e) { }
1660
- progressBar?.stop();
1661
- return outBuf;
1662
- }
1663
- catch (e) {
1664
- progressBar?.stop();
1665
- return outBuf;
1666
- }
1667
- }
1668
- throw new Error(`Unsupported mode: ${mode}`);
1669
- }
1670
- /**
1671
- * Decode a PNG produced by this library back to the original Buffer.
1672
- * Supports the ROX binary format, rXDT chunk, and pixel encodings.
1673
- *
1674
- * @param pngBuf - PNG data
1675
- * @param opts - Options (passphrase for encrypted inputs)
1676
- * @public
1677
- * @example
1678
- * import { readFileSync, writeFileSync } from 'fs';
1679
- * import { decodePngToBinary } from 'roxify';
1680
- *
1681
- * const pngFromDisk = readFileSync('output.png'); //Path of the encoded PNG here
1682
- * const { buf, meta } = await decodePngToBinary(pngFromDisk);
1683
- * writeFileSync(meta?.name ?? 'decoded.txt', buf);
1684
- */
1685
- export async function decodePngToBinary(pngBuf, opts = {}) {
1686
- let progressBar = null;
1687
- if (opts.showProgress) {
1688
- progressBar = new cliProgress.SingleBar({
1689
- format: ' {bar} {percentage}% | {step} | {elapsed}s',
1690
- }, cliProgress.Presets.shades_classic);
1691
- progressBar.start(100, 0, { step: 'Starting', elapsed: '0' });
1692
- const startTime = Date.now();
1693
- if (!opts.onProgress) {
1694
- opts.onProgress = (info) => {
1695
- let pct = 0;
1696
- if (info.phase === 'start') {
1697
- pct = 10;
1698
- }
1699
- else if (info.phase === 'decompress') {
1700
- pct = 50;
1701
- }
1702
- else if (info.phase === 'done') {
1703
- pct = 100;
1704
- }
1705
- progressBar.update(Math.floor(pct), {
1706
- step: info.phase.replace('_', ' '),
1707
- elapsed: String(Math.floor((Date.now() - startTime) / 1000)),
1708
- });
1709
- };
1710
- }
1711
- }
1712
- if (opts.onProgress)
1713
- opts.onProgress({ phase: 'start' });
1714
- let processedBuf = pngBuf;
1715
- try {
1716
- const info = await sharp(pngBuf).metadata();
1717
- if (info.width && info.height) {
1718
- const MAX_RAW_BYTES = 150 * 1024 * 1024;
1719
- const rawBytesEstimate = info.width * info.height * 4;
1720
- if (rawBytesEstimate > MAX_RAW_BYTES) {
1721
- 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.`);
1722
- }
1723
- if (false) {
1724
- const doubledBuffer = await sharp(pngBuf)
1725
- .resize({
1726
- width: info.width * 2,
1727
- height: info.height * 2,
1728
- kernel: 'nearest',
1729
- })
1730
- .png()
1731
- .toBuffer();
1732
- processedBuf = await cropAndReconstitute(doubledBuffer, opts.debugDir);
1733
- }
1734
- else {
1735
- processedBuf = pngBuf;
1736
- }
1737
- }
1738
- }
1739
- catch (e) { }
1740
- if (opts.onProgress)
1741
- opts.onProgress({ phase: 'processed' });
1742
- if (processedBuf.slice(0, MAGIC.length).equals(MAGIC)) {
1743
- const d = processedBuf.slice(MAGIC.length);
1744
- const nameLen = d[0];
1745
- let idx = 1;
1746
- let name;
1747
- if (nameLen > 0) {
1748
- name = d.slice(idx, idx + nameLen).toString('utf8');
1749
- idx += nameLen;
1750
- }
1751
- const rawPayload = d.slice(idx);
1752
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
1753
- if (opts.outPath) {
1754
- const ws = createWriteStream(opts.outPath, { highWaterMark: 64 * 1024 });
1755
- let headerBuf = Buffer.alloc(0);
1756
- let headerSkipped = false;
1757
- await tryDecompress(payload, (info) => {
1758
- if (opts.onProgress)
1759
- opts.onProgress(info);
1760
- }, async (decChunk) => {
1761
- if (!headerSkipped) {
1762
- if (decChunk.length < MAGIC.length) {
1763
- headerBuf = Buffer.concat([headerBuf, decChunk]);
1764
- return;
1765
- }
1766
- const mag = decChunk.slice(0, MAGIC.length);
1767
- if (!mag.equals(MAGIC)) {
1768
- ws.close();
1769
- throw new Error('Invalid ROX format (ROX direct: missing ROX1 magic after decompression)');
1770
- }
1771
- const toWriteBuf = decChunk.slice(MAGIC.length);
1772
- if (toWriteBuf.length > 0) {
1773
- await writeInChunks(ws, toWriteBuf, 16 * 1024);
1774
- }
1775
- headerBuf = Buffer.alloc(0);
1776
- headerSkipped = true;
1777
- }
1778
- else {
1779
- await writeInChunks(ws, decChunk, 64 * 1024);
1780
- }
1781
- });
1782
- await new Promise((res) => ws.end(() => res()));
1783
- if (opts.onProgress)
1784
- opts.onProgress({ phase: 'done' });
1785
- progressBar?.stop();
1786
- return { meta: { name } };
1787
- }
1788
- if (opts.onProgress)
1789
- opts.onProgress({ phase: 'decompress_start' });
1790
- try {
1791
- payload = await tryDecompress(payload, (info) => {
1792
- if (opts.onProgress)
1793
- opts.onProgress(info);
1794
- });
1795
- }
1796
- catch (e) {
1797
- const errMsg = e instanceof Error ? e.message : String(e);
1798
- if (opts.passphrase)
1799
- throw new IncorrectPassphraseError('Incorrect passphrase (compact mode, zstd failed: ' + errMsg + ')');
1800
- throw new DataFormatError('Compact mode zstd decompression failed: ' + errMsg);
1801
- }
1802
- if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
1803
- throw new Error('Invalid ROX format (ROX direct: missing ROX1 magic after decompression)');
1804
- }
1805
- payload = payload.slice(MAGIC.length);
1806
- if (opts.onProgress)
1807
- opts.onProgress({ phase: 'done' });
1808
- progressBar?.stop();
1809
- return { buf: payload, meta: { name } };
1810
- }
1811
- let chunks = [];
1812
- try {
1813
- const chunksRaw = extract(processedBuf);
1814
- chunks = chunksRaw.map((c) => ({
1815
- name: c.name,
1816
- data: Buffer.isBuffer(c.data)
1817
- ? c.data
1818
- : Buffer.from(c.data),
1819
- }));
1820
- }
1821
- catch (e) {
1822
- try {
1823
- const withHeader = Buffer.concat([PNG_HEADER, pngBuf]);
1824
- const chunksRaw = extract(withHeader);
1825
- chunks = chunksRaw.map((c) => ({
1826
- name: c.name,
1827
- data: Buffer.isBuffer(c.data)
1828
- ? c.data
1829
- : Buffer.from(c.data),
1830
- }));
1831
- }
1832
- catch (e2) {
1833
- chunks = [];
1834
- }
1835
- }
1836
- const target = chunks.find((c) => c.name === CHUNK_TYPE);
1837
- if (target) {
1838
- const d = target.data;
1839
- const nameLen = d[0];
1840
- let idx = 1;
1841
- let name;
1842
- if (nameLen > 0) {
1843
- name = d.slice(idx, idx + nameLen).toString('utf8');
1844
- idx += nameLen;
1845
- }
1846
- const rawPayload = d.slice(idx);
1847
- if (rawPayload.length === 0)
1848
- throw new DataFormatError('Compact mode payload empty');
1849
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
1850
- if (opts.outPath) {
1851
- const ws = createWriteStream(opts.outPath, { highWaterMark: 64 * 1024 });
1852
- let headerBuf = Buffer.alloc(0);
1853
- let headerSkipped = false;
1854
- await tryDecompress(payload, (info) => {
1855
- if (opts.onProgress)
1856
- opts.onProgress(info);
1857
- }, async (decChunk) => {
1858
- if (!headerSkipped) {
1859
- if (decChunk.length < MAGIC.length) {
1860
- headerBuf = Buffer.concat([headerBuf, decChunk]);
1861
- return;
1862
- }
1863
- const mag = decChunk.slice(0, MAGIC.length);
1864
- if (!mag.equals(MAGIC)) {
1865
- ws.close();
1866
- throw new DataFormatError('Invalid ROX format (compact mode: missing ROX1 magic after decompression)');
1867
- }
1868
- const toWriteBuf = decChunk.slice(MAGIC.length);
1869
- if (toWriteBuf.length > 0) {
1870
- await writeInChunks(ws, toWriteBuf);
1871
- }
1872
- headerBuf = Buffer.alloc(0);
1873
- headerSkipped = true;
1874
- }
1875
- else {
1876
- await writeInChunks(ws, decChunk, 64 * 1024);
1877
- }
1878
- });
1879
- await new Promise((res) => ws.end(() => res()));
1880
- if (opts.onProgress)
1881
- opts.onProgress({ phase: 'done' });
1882
- progressBar?.stop();
1883
- return { meta: { name } };
1884
- }
1885
- if (opts.onProgress)
1886
- opts.onProgress({ phase: 'decompress_start' });
1887
- try {
1888
- payload = await tryZstdDecompress(payload, (info) => {
1889
- if (opts.onProgress)
1890
- opts.onProgress(info);
1891
- });
1892
- }
1893
- catch (e) {
1894
- const errMsg = e instanceof Error ? e.message : String(e);
1895
- if (opts.passphrase)
1896
- throw new IncorrectPassphraseError('Incorrect passphrase (compact mode, zstd failed: ' + errMsg + ')');
1897
- throw new DataFormatError('Compact mode zstd decompression failed: ' + errMsg);
1898
- }
1899
- if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
1900
- throw new DataFormatError('Invalid ROX format (compact mode: missing ROX1 magic after decompression)');
1901
- }
1902
- payload = payload.slice(MAGIC.length);
1903
- if (opts.files) {
1904
- const unpacked = unpackBuffer(payload, opts.files);
1905
- if (unpacked) {
1906
- if (opts.onProgress)
1907
- opts.onProgress({ phase: 'done' });
1908
- progressBar?.stop();
1909
- return { files: unpacked.files, meta: { name } };
1910
- }
1911
- }
1912
- if (opts.onProgress)
1913
- opts.onProgress({ phase: 'done' });
1914
- progressBar?.stop();
1915
- return { buf: payload, meta: { name } };
1916
- }
1917
- try {
1918
- const { data, info } = await sharp(processedBuf)
1919
- .ensureAlpha()
1920
- .raw()
1921
- .toBuffer({ resolveWithObject: true });
1922
- const currentWidth = info.width;
1923
- const currentHeight = info.height;
1924
- const rawRGB = Buffer.alloc(currentWidth * currentHeight * 3);
1925
- for (let i = 0; i < currentWidth * currentHeight; i++) {
1926
- rawRGB[i * 3] = data[i * 4];
1927
- rawRGB[i * 3 + 1] = data[i * 4 + 1];
1928
- rawRGB[i * 3 + 2] = data[i * 4 + 2];
1929
- }
1930
- const firstPixels = [];
1931
- for (let i = 0; i < Math.min(MARKER_START.length, rawRGB.length / 3); i++) {
1932
- firstPixels.push({
1933
- r: rawRGB[i * 3],
1934
- g: rawRGB[i * 3 + 1],
1935
- b: rawRGB[i * 3 + 2],
1936
- });
1937
- }
1938
- let hasMarkerStart = false;
1939
- if (firstPixels.length === MARKER_START.length) {
1940
- hasMarkerStart = true;
1941
- for (let i = 0; i < MARKER_START.length; i++) {
1942
- if (firstPixels[i].r !== MARKER_START[i].r ||
1943
- firstPixels[i].g !== MARKER_START[i].g ||
1944
- firstPixels[i].b !== MARKER_START[i].b) {
1945
- hasMarkerStart = false;
1946
- break;
1947
- }
1948
- }
1949
- }
1950
- let hasPixelMagic = false;
1951
- if (rawRGB.length >= 8 + PIXEL_MAGIC.length) {
1952
- const widthFromDim = rawRGB.readUInt32BE(0);
1953
- const heightFromDim = rawRGB.readUInt32BE(4);
1954
- if (widthFromDim === currentWidth &&
1955
- heightFromDim === currentHeight &&
1956
- rawRGB.slice(8, 8 + PIXEL_MAGIC.length).equals(PIXEL_MAGIC)) {
1957
- hasPixelMagic = true;
1958
- }
1959
- }
1960
- let logicalWidth;
1961
- let logicalHeight;
1962
- let logicalData;
1963
- if (hasMarkerStart || hasPixelMagic) {
1964
- logicalWidth = currentWidth;
1965
- logicalHeight = currentHeight;
1966
- logicalData = rawRGB;
1967
- }
1968
- else {
1969
- const reconstructed = await cropAndReconstitute(processedBuf, opts.debugDir);
1970
- const { data: rdata, info: rinfo } = await sharp(reconstructed)
1971
- .ensureAlpha()
1972
- .raw()
1973
- .toBuffer({ resolveWithObject: true });
1974
- logicalWidth = rinfo.width;
1975
- logicalHeight = rinfo.height;
1976
- logicalData = Buffer.alloc(rinfo.width * rinfo.height * 3);
1977
- for (let i = 0; i < rinfo.width * rinfo.height; i++) {
1978
- logicalData[i * 3] = rdata[i * 4];
1979
- logicalData[i * 3 + 1] = rdata[i * 4 + 1];
1980
- logicalData[i * 3 + 2] = rdata[i * 4 + 2];
1981
- }
1982
- }
1983
- if (process.env.ROX_DEBUG) {
1984
- console.log('DEBUG: Logical grid reconstructed:', logicalWidth, 'x', logicalHeight, '=', logicalWidth * logicalHeight, 'pixels');
1985
- }
1986
- const finalGrid = [];
1987
- for (let i = 0; i < logicalData.length; i += 3) {
1988
- finalGrid.push({
1989
- r: logicalData[i],
1990
- g: logicalData[i + 1],
1991
- b: logicalData[i + 2],
1992
- });
1993
- }
1994
- if (hasPixelMagic) {
1995
- if (logicalData.length < 8 + PIXEL_MAGIC.length) {
1996
- throw new DataFormatError('Pixel mode data too short');
1997
- }
1998
- let idx = 8 + PIXEL_MAGIC.length;
1999
- const version = logicalData[idx++];
2000
- const nameLen = logicalData[idx++];
2001
- let name;
2002
- if (nameLen > 0 && nameLen < 256) {
2003
- name = logicalData.slice(idx, idx + nameLen).toString('utf8');
2004
- idx += nameLen;
2005
- }
2006
- const payloadLen = logicalData.readUInt32BE(idx);
2007
- idx += 4;
2008
- const available = logicalData.length - idx;
2009
- if (available < payloadLen) {
2010
- throw new DataFormatError(`Pixel payload truncated: expected ${payloadLen} bytes but only ${available} available`);
2011
- }
2012
- const rawPayload = logicalData.slice(idx, idx + payloadLen);
2013
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
2014
- try {
2015
- if (opts.outPath) {
2016
- await tryDecompress(payload, (info) => {
2017
- if (opts.onProgress)
2018
- opts.onProgress(info);
2019
- }, undefined, opts.outPath);
2020
- }
2021
- else {
2022
- payload = await tryZstdDecompress(payload, (info) => {
2023
- if (opts.onProgress)
2024
- opts.onProgress(info);
2025
- });
2026
- }
2027
- if (version === 3) {
2028
- if (opts.outPath) {
2029
- throw new Error('outPath not supported with delta encoding yet');
2030
- }
2031
- else {
2032
- payload = deltaDecode(payload);
2033
- }
2034
- }
2035
- }
2036
- catch (e) { }
2037
- if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
2038
- throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
2039
- }
2040
- payload = payload.slice(MAGIC.length);
2041
- return { buf: payload, meta: { name } };
2042
- }
2043
- let startIdx = -1;
2044
- for (let i = 0; i <= finalGrid.length - MARKER_START.length; i++) {
2045
- let match = true;
2046
- for (let mi = 0; mi < MARKER_START.length && match; mi++) {
2047
- const p = finalGrid[i + mi];
2048
- if (!p ||
2049
- p.r !== MARKER_START[mi].r ||
2050
- p.g !== MARKER_START[mi].g ||
2051
- p.b !== MARKER_START[mi].b) {
2052
- match = false;
2053
- }
2054
- }
2055
- if (match) {
2056
- startIdx = i;
2057
- break;
2058
- }
2059
- }
2060
- if (startIdx === -1) {
2061
- if (process.env.ROX_DEBUG) {
2062
- console.log('DEBUG: MARKER_START not found in grid of', finalGrid.length, 'pixels');
2063
- console.log('DEBUG: Trying 2D scan for START marker...');
2064
- }
2065
- let found2D = false;
2066
- for (let y = 0; y < logicalHeight && !found2D; y++) {
2067
- for (let x = 0; x <= logicalWidth - MARKER_START.length && !found2D; x++) {
2068
- let match = true;
2069
- for (let mi = 0; mi < MARKER_START.length && match; mi++) {
2070
- const idx = (y * logicalWidth + (x + mi)) * 3;
2071
- if (idx + 2 >= logicalData.length ||
2072
- logicalData[idx] !== MARKER_START[mi].r ||
2073
- logicalData[idx + 1] !== MARKER_START[mi].g ||
2074
- logicalData[idx + 2] !== MARKER_START[mi].b) {
2075
- match = false;
2076
- }
2077
- }
2078
- if (match) {
2079
- if (process.env.ROX_DEBUG) {
2080
- console.log(`DEBUG: Found START marker in 2D at (${x}, ${y})`);
2081
- }
2082
- let endX = x + MARKER_START.length - 1;
2083
- let endY = y;
2084
- for (let scanY = y; scanY < logicalHeight; scanY++) {
2085
- let rowHasData = false;
2086
- for (let scanX = x; scanX < logicalWidth; scanX++) {
2087
- const scanIdx = (scanY * logicalWidth + scanX) * 3;
2088
- if (scanIdx + 2 < logicalData.length) {
2089
- const r = logicalData[scanIdx];
2090
- const g = logicalData[scanIdx + 1];
2091
- const b = logicalData[scanIdx + 2];
2092
- const isBackground = (r === 100 && g === 120 && b === 110) ||
2093
- (r === 0 && g === 0 && b === 0) ||
2094
- (r >= 50 &&
2095
- r <= 220 &&
2096
- g >= 50 &&
2097
- g <= 220 &&
2098
- b >= 50 &&
2099
- b <= 220 &&
2100
- Math.abs(r - g) < 70 &&
2101
- Math.abs(r - b) < 70 &&
2102
- Math.abs(g - b) < 70);
2103
- if (!isBackground) {
2104
- rowHasData = true;
2105
- if (scanX > endX) {
2106
- endX = scanX;
2107
- }
2108
- }
2109
- }
2110
- }
2111
- if (rowHasData) {
2112
- endY = scanY;
2113
- }
2114
- else if (scanY > y) {
2115
- break;
2116
- }
2117
- }
2118
- const rectWidth = endX - x + 1;
2119
- const rectHeight = endY - y + 1;
2120
- if (process.env.ROX_DEBUG) {
2121
- console.log(`DEBUG: Extracted rectangle: ${rectWidth}x${rectHeight} from (${x},${y})`);
2122
- }
2123
- finalGrid.length = 0;
2124
- for (let ry = y; ry <= endY; ry++) {
2125
- for (let rx = x; rx <= endX; rx++) {
2126
- const idx = (ry * logicalWidth + rx) * 3;
2127
- finalGrid.push({
2128
- r: logicalData[idx],
2129
- g: logicalData[idx + 1],
2130
- b: logicalData[idx + 2],
2131
- });
2132
- }
2133
- }
2134
- startIdx = 0;
2135
- found2D = true;
2136
- }
2137
- }
2138
- }
2139
- if (!found2D) {
2140
- if (process.env.ROX_DEBUG) {
2141
- console.log('DEBUG: First 20 pixels:', finalGrid
2142
- .slice(0, 20)
2143
- .map((p) => `(${p.r},${p.g},${p.b})`)
2144
- .join(' '));
2145
- }
2146
- throw new Error('Marker START not found - image format not supported');
2147
- }
2148
- }
2149
- if (process.env.ROX_DEBUG && startIdx === 0) {
2150
- console.log(`DEBUG: MARKER_START at index ${startIdx}, grid size: ${finalGrid.length}`);
2151
- }
2152
- const gridFromStart = finalGrid.slice(startIdx);
2153
- if (gridFromStart.length < MARKER_START.length + MARKER_END.length) {
2154
- if (process.env.ROX_DEBUG) {
2155
- console.log('DEBUG: gridFromStart too small:', gridFromStart.length, 'pixels');
2156
- }
2157
- throw new Error('Marker START or END not found - image format not supported');
2158
- }
2159
- for (let i = 0; i < MARKER_START.length; i++) {
2160
- if (gridFromStart[i].r !== MARKER_START[i].r ||
2161
- gridFromStart[i].g !== MARKER_START[i].g ||
2162
- gridFromStart[i].b !== MARKER_START[i].b) {
2163
- throw new Error('Marker START not found - image format not supported');
2164
- }
2165
- }
2166
- let compression = 'zstd';
2167
- if (gridFromStart.length > MARKER_START.length) {
2168
- const compPixel = gridFromStart[MARKER_START.length];
2169
- if (compPixel.r === 0 && compPixel.g === 255 && compPixel.b === 0) {
2170
- compression = 'zstd';
2171
- }
2172
- else {
2173
- compression = 'zstd';
2174
- }
2175
- }
2176
- if (process.env.ROX_DEBUG) {
2177
- console.log(`DEBUG: Detected compression: ${compression}`);
2178
- }
2179
- let endStartIdx = -1;
2180
- const lastLineStart = (logicalHeight - 1) * logicalWidth;
2181
- const endMarkerStartCol = logicalWidth - MARKER_END.length;
2182
- if (lastLineStart + endMarkerStartCol < finalGrid.length) {
2183
- let matchEnd = true;
2184
- for (let mi = 0; mi < MARKER_END.length && matchEnd; mi++) {
2185
- const idx = lastLineStart + endMarkerStartCol + mi;
2186
- if (idx >= finalGrid.length) {
2187
- matchEnd = false;
2188
- break;
2189
- }
2190
- const p = finalGrid[idx];
2191
- if (p.r !== MARKER_END[mi].r ||
2192
- p.g !== MARKER_END[mi].g ||
2193
- p.b !== MARKER_END[mi].b) {
2194
- matchEnd = false;
2195
- }
2196
- }
2197
- if (matchEnd) {
2198
- endStartIdx = lastLineStart + endMarkerStartCol - startIdx;
2199
- if (process.env.ROX_DEBUG) {
2200
- console.log(`DEBUG: Found END marker at last line, col ${endMarkerStartCol}`);
2201
- }
2202
- }
2203
- }
2204
- if (endStartIdx === -1) {
2205
- if (process.env.ROX_DEBUG) {
2206
- console.log('DEBUG: END marker not found at expected position');
2207
- console.log('DEBUG: Last line pixels:', finalGrid
2208
- .slice(Math.max(0, lastLineStart), finalGrid.length)
2209
- .map((p) => `(${p.r},${p.g},${p.b})`)
2210
- .join(' '));
2211
- }
2212
- endStartIdx = gridFromStart.length;
2213
- }
2214
- const dataGrid = gridFromStart.slice(MARKER_START.length + 1, endStartIdx);
2215
- const pixelBytes = Buffer.alloc(dataGrid.length * 3);
2216
- for (let i = 0; i < dataGrid.length; i++) {
2217
- pixelBytes[i * 3] = dataGrid[i].r;
2218
- pixelBytes[i * 3 + 1] = dataGrid[i].g;
2219
- pixelBytes[i * 3 + 2] = dataGrid[i].b;
2220
- }
2221
- if (process.env.ROX_DEBUG) {
2222
- console.log('DEBUG: extracted len', pixelBytes.length);
2223
- console.log('DEBUG: extracted head', pixelBytes.slice(0, 32).toString('hex'));
2224
- const found = pixelBytes.indexOf(PIXEL_MAGIC);
2225
- console.log('DEBUG: PIXEL_MAGIC index:', found);
2226
- if (found !== -1) {
2227
- console.log('DEBUG: PIXEL_MAGIC head:', pixelBytes.slice(found, found + 64).toString('hex'));
2228
- const markerEndBytes = colorsToBytes(MARKER_END);
2229
- console.log('DEBUG: MARKER_END index:', pixelBytes.indexOf(markerEndBytes));
2230
- }
2231
- }
2232
- try {
2233
- let idx = 0;
2234
- if (pixelBytes.length >= PIXEL_MAGIC.length) {
2235
- const at0 = pixelBytes.slice(0, PIXEL_MAGIC.length).equals(PIXEL_MAGIC);
2236
- if (at0) {
2237
- idx = PIXEL_MAGIC.length;
2238
- }
2239
- else {
2240
- const found = pixelBytes.indexOf(PIXEL_MAGIC);
2241
- if (found !== -1) {
2242
- idx = found + PIXEL_MAGIC.length;
2243
- }
2244
- }
2245
- }
2246
- if (idx > 0) {
2247
- const version = pixelBytes[idx++];
2248
- const nameLen = pixelBytes[idx++];
2249
- let name;
2250
- if (nameLen > 0 && nameLen < 256) {
2251
- name = pixelBytes.slice(idx, idx + nameLen).toString('utf8');
2252
- idx += nameLen;
2253
- }
2254
- const payloadLen = pixelBytes.readUInt32BE(idx);
2255
- idx += 4;
2256
- const available = pixelBytes.length - idx;
2257
- if (available < payloadLen) {
2258
- throw new DataFormatError(`Pixel payload truncated: expected ${payloadLen} bytes but only ${available} available`);
2259
- }
2260
- const rawPayload = pixelBytes.slice(idx, idx + payloadLen);
2261
- let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
2262
- try {
2263
- if (opts.outPath) {
2264
- const ws = createWriteStream(opts.outPath, {
2265
- highWaterMark: 64 * 1024,
2266
- });
2267
- let headerBuf = Buffer.alloc(0);
2268
- let headerSkipped = false;
2269
- let lastOutByte = null;
2270
- await tryZstdDecompress(payload, (info) => {
2271
- if (opts.onProgress)
2272
- opts.onProgress(info);
2273
- }, async (decChunk) => {
2274
- let outChunk = decChunk;
2275
- if (version === 3) {
2276
- const out = Buffer.alloc(decChunk.length);
2277
- for (let i = 0; i < decChunk.length; i++) {
2278
- if (i === 0) {
2279
- out[0] =
2280
- typeof lastOutByte === 'number'
2281
- ? (lastOutByte + decChunk[0]) & 0xff
2282
- : decChunk[0];
2283
- }
2284
- else {
2285
- out[i] = (out[i - 1] + decChunk[i]) & 0xff;
2286
- }
2287
- }
2288
- lastOutByte = out[out.length - 1];
2289
- outChunk = out;
2290
- }
2291
- if (!headerSkipped) {
2292
- if (outChunk.length < MAGIC.length) {
2293
- headerBuf = Buffer.concat([headerBuf, outChunk]);
2294
- return;
2295
- }
2296
- const mag = outChunk.slice(0, MAGIC.length);
2297
- if (!mag.equals(MAGIC)) {
2298
- ws.close();
2299
- throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
2300
- }
2301
- const toWriteBuf = outChunk.slice(MAGIC.length);
2302
- if (toWriteBuf.length > 0) {
2303
- await writeInChunks(ws, toWriteBuf, 64 * 1024);
2304
- }
2305
- headerBuf = Buffer.alloc(0);
2306
- headerSkipped = true;
2307
- }
2308
- else {
2309
- await writeInChunks(ws, outChunk, 64 * 1024);
2310
- }
2311
- });
2312
- await new Promise((res) => ws.end(() => res()));
2313
- if (opts.onProgress)
2314
- opts.onProgress({ phase: 'done' });
2315
- progressBar?.stop();
2316
- return { meta: { name } };
2317
- }
2318
- else {
2319
- payload = await tryDecompress(payload, (info) => {
2320
- if (opts.onProgress)
2321
- opts.onProgress(info);
2322
- });
2323
- if (version === 3) {
2324
- payload = deltaDecode(payload);
2325
- }
2326
- }
2327
- }
2328
- catch (e) {
2329
- const errMsg = e instanceof Error ? e.message : String(e);
2330
- if (opts.passphrase)
2331
- throw new IncorrectPassphraseError(`Incorrect passphrase (screenshot mode, zstd failed: ` +
2332
- errMsg +
2333
- ')');
2334
- throw new DataFormatError(`Screenshot mode zstd decompression failed: ` + errMsg);
2335
- }
2336
- if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
2337
- throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
2338
- }
2339
- payload = payload.slice(MAGIC.length);
2340
- if (opts.files) {
2341
- const unpacked = unpackBuffer(payload, opts.files);
2342
- if (unpacked) {
2343
- if (opts.onProgress)
2344
- opts.onProgress({ phase: 'done' });
2345
- progressBar?.stop();
2346
- return { files: unpacked.files, meta: { name } };
2347
- }
2348
- }
2349
- if (opts.onProgress)
2350
- opts.onProgress({ phase: 'done' });
2351
- progressBar?.stop();
2352
- return { buf: payload, meta: { name } };
2353
- }
2354
- }
2355
- catch (e) {
2356
- if (e instanceof PassphraseRequiredError ||
2357
- e instanceof IncorrectPassphraseError ||
2358
- e instanceof DataFormatError) {
2359
- throw e;
2360
- }
2361
- const errMsg = e instanceof Error ? e.message : String(e);
2362
- throw new Error('Failed to extract data from screenshot: ' + errMsg);
2363
- }
2364
- }
2365
- catch (e) {
2366
- if (e instanceof PassphraseRequiredError ||
2367
- e instanceof IncorrectPassphraseError ||
2368
- e instanceof DataFormatError) {
2369
- throw e;
2370
- }
2371
- const errMsg = e instanceof Error ? e.message : String(e);
2372
- throw new Error('Failed to decode PNG: ' + errMsg);
2373
- }
2374
- throw new DataFormatError('No valid data found in image');
2375
- }
1
+ export * from './utils/constants.js';
2
+ export * from './utils/crc.js';
3
+ export * from './utils/decoder.js';
4
+ export * from './utils/encoder.js';
5
+ export * from './utils/errors.js';
6
+ export * from './utils/helpers.js';
7
+ export * from './utils/inspection.js';
8
+ export * from './utils/optimization.js';
9
+ export * from './utils/reconstitution.js';
10
+ export * from './utils/types.js';
11
+ export * from './utils/zstd.js';
2376
12
  export { decodeMinPng, encodeMinPng } from './minpng.js';
2377
13
  export { packPaths, unpackBuffer } from './pack.js';
2378
- /**
2379
- * List files in a Rox PNG archive without decoding the full payload.
2380
- * Returns the file list if available, otherwise null.
2381
- * @param pngBuf - PNG data
2382
- * @public
2383
- */
2384
- export async function listFilesInPng(pngBuf) {
2385
- try {
2386
- try {
2387
- const { data, info } = await sharp(pngBuf)
2388
- .ensureAlpha()
2389
- .raw()
2390
- .toBuffer({ resolveWithObject: true });
2391
- const currentWidth = info.width;
2392
- const currentHeight = info.height;
2393
- const rawRGB = Buffer.alloc(currentWidth * currentHeight * 3);
2394
- for (let i = 0; i < currentWidth * currentHeight; i++) {
2395
- rawRGB[i * 3] = data[i * 4];
2396
- rawRGB[i * 3 + 1] = data[i * 4 + 1];
2397
- rawRGB[i * 3 + 2] = data[i * 4 + 2];
2398
- }
2399
- const found = rawRGB.indexOf(PIXEL_MAGIC);
2400
- if (found !== -1) {
2401
- let idx = found + PIXEL_MAGIC.length;
2402
- if (idx + 2 <= rawRGB.length) {
2403
- const version = rawRGB[idx++];
2404
- const nameLen = rawRGB[idx++];
2405
- if (process.env.ROX_DEBUG)
2406
- console.log('listFilesInPng: pixel version', version, 'nameLen', nameLen);
2407
- if (nameLen > 0 && idx + nameLen <= rawRGB.length) {
2408
- idx += nameLen;
2409
- }
2410
- if (idx + 4 <= rawRGB.length) {
2411
- const payloadLen = rawRGB.readUInt32BE(idx);
2412
- idx += 4;
2413
- const afterPayload = idx + payloadLen;
2414
- if (afterPayload <= rawRGB.length) {
2415
- if (afterPayload + 8 <= rawRGB.length) {
2416
- const marker = rawRGB
2417
- .slice(afterPayload, afterPayload + 4)
2418
- .toString('utf8');
2419
- if (marker === 'rXFL') {
2420
- const jsonLen = rawRGB.readUInt32BE(afterPayload + 4);
2421
- const jsonStart = afterPayload + 8;
2422
- const jsonEnd = jsonStart + jsonLen;
2423
- if (jsonEnd <= rawRGB.length) {
2424
- const jsonBuf = rawRGB.slice(jsonStart, jsonEnd);
2425
- const files = JSON.parse(jsonBuf.toString('utf8'));
2426
- return files.sort();
2427
- }
2428
- }
2429
- }
2430
- }
2431
- }
2432
- }
2433
- }
2434
- }
2435
- catch (e) { }
2436
- }
2437
- catch (e) { }
2438
- try {
2439
- const reconstructed = await cropAndReconstitute(pngBuf);
2440
- try {
2441
- const { data, info } = await sharp(reconstructed)
2442
- .ensureAlpha()
2443
- .raw()
2444
- .toBuffer({ resolveWithObject: true });
2445
- const currentWidth = info.width;
2446
- const currentHeight = info.height;
2447
- const rawRGB = Buffer.alloc(currentWidth * currentHeight * 3);
2448
- for (let i = 0; i < currentWidth * currentHeight; i++) {
2449
- rawRGB[i * 3] = data[i * 4];
2450
- rawRGB[i * 3 + 1] = data[i * 4 + 1];
2451
- rawRGB[i * 3 + 2] = data[i * 4 + 2];
2452
- }
2453
- const found = rawRGB.indexOf(PIXEL_MAGIC);
2454
- if (found !== -1) {
2455
- let idx = found + PIXEL_MAGIC.length;
2456
- if (idx + 2 <= rawRGB.length) {
2457
- const version = rawRGB[idx++];
2458
- const nameLen = rawRGB[idx++];
2459
- if (process.env.ROX_DEBUG)
2460
- console.log('listFilesInPng (reconstructed): pixel version', version, 'nameLen', nameLen);
2461
- if (nameLen > 0 && idx + nameLen <= rawRGB.length) {
2462
- idx += nameLen;
2463
- }
2464
- if (idx + 4 <= rawRGB.length) {
2465
- const payloadLen = rawRGB.readUInt32BE(idx);
2466
- idx += 4;
2467
- const afterPayload = idx + payloadLen;
2468
- if (afterPayload <= rawRGB.length) {
2469
- if (afterPayload + 8 <= rawRGB.length) {
2470
- const marker = rawRGB
2471
- .slice(afterPayload, afterPayload + 4)
2472
- .toString('utf8');
2473
- if (marker === 'rXFL') {
2474
- const jsonLen = rawRGB.readUInt32BE(afterPayload + 4);
2475
- const jsonStart = afterPayload + 8;
2476
- const jsonEnd = jsonStart + jsonLen;
2477
- if (jsonEnd <= rawRGB.length) {
2478
- const jsonBuf = rawRGB.slice(jsonStart, jsonEnd);
2479
- const files = JSON.parse(jsonBuf.toString('utf8'));
2480
- return files.sort();
2481
- }
2482
- }
2483
- }
2484
- }
2485
- }
2486
- }
2487
- }
2488
- }
2489
- catch (e) { }
2490
- try {
2491
- const chunks = extract(reconstructed);
2492
- const fileListChunk = chunks.find((c) => c.name === 'rXFL');
2493
- if (fileListChunk) {
2494
- const data = Buffer.isBuffer(fileListChunk.data)
2495
- ? fileListChunk.data
2496
- : Buffer.from(fileListChunk.data);
2497
- const files = JSON.parse(data.toString('utf8'));
2498
- return files.sort();
2499
- }
2500
- const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
2501
- if (metaChunk) {
2502
- const dataBuf = Buffer.isBuffer(metaChunk.data)
2503
- ? metaChunk.data
2504
- : Buffer.from(metaChunk.data);
2505
- const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
2506
- if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
2507
- const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
2508
- const jsonStart = markerIdx + 8;
2509
- const jsonEnd = jsonStart + jsonLen;
2510
- if (jsonEnd <= dataBuf.length) {
2511
- const files = JSON.parse(dataBuf.slice(jsonStart, jsonEnd).toString('utf8'));
2512
- return files.sort();
2513
- }
2514
- }
2515
- }
2516
- }
2517
- catch (e) { }
2518
- }
2519
- catch (e) { }
2520
- try {
2521
- const chunks = extract(pngBuf);
2522
- const fileListChunk = chunks.find((c) => c.name === 'rXFL');
2523
- if (fileListChunk) {
2524
- const data = Buffer.isBuffer(fileListChunk.data)
2525
- ? fileListChunk.data
2526
- : Buffer.from(fileListChunk.data);
2527
- const files = JSON.parse(data.toString('utf8'));
2528
- return files.sort();
2529
- }
2530
- const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
2531
- if (metaChunk) {
2532
- const dataBuf = Buffer.isBuffer(metaChunk.data)
2533
- ? metaChunk.data
2534
- : Buffer.from(metaChunk.data);
2535
- const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
2536
- if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
2537
- const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
2538
- const jsonStart = markerIdx + 8;
2539
- const jsonEnd = jsonStart + jsonLen;
2540
- if (jsonEnd <= dataBuf.length) {
2541
- const files = JSON.parse(dataBuf.slice(jsonStart, jsonEnd).toString('utf8'));
2542
- return files.sort();
2543
- }
2544
- }
2545
- }
2546
- }
2547
- catch (e) { }
2548
- return null;
2549
- }
2550
- /**
2551
- * Detect if a PNG/ROX buffer contains an encrypted payload (requires passphrase)
2552
- * Returns true if encryption flag indicates AES or XOR.
2553
- */
2554
- export async function hasPassphraseInPng(pngBuf) {
2555
- try {
2556
- if (pngBuf.slice(0, MAGIC.length).equals(MAGIC)) {
2557
- let offset = MAGIC.length;
2558
- if (offset >= pngBuf.length)
2559
- return false;
2560
- const nameLen = pngBuf.readUInt8(offset);
2561
- offset += 1 + nameLen;
2562
- if (offset >= pngBuf.length)
2563
- return false;
2564
- const flag = pngBuf[offset];
2565
- return flag === ENC_AES || flag === ENC_XOR;
2566
- }
2567
- try {
2568
- const chunksRaw = extract(pngBuf);
2569
- const target = chunksRaw.find((c) => c.name === CHUNK_TYPE);
2570
- if (target) {
2571
- const data = Buffer.isBuffer(target.data)
2572
- ? target.data
2573
- : Buffer.from(target.data);
2574
- if (data.length >= 1) {
2575
- const nameLen = data.readUInt8(0);
2576
- const payloadStart = 1 + nameLen;
2577
- if (payloadStart < data.length) {
2578
- const flag = data[payloadStart];
2579
- return flag === ENC_AES || flag === ENC_XOR;
2580
- }
2581
- }
2582
- }
2583
- }
2584
- catch (e) { }
2585
- try {
2586
- const sharpLib = await import('sharp');
2587
- const { data } = await sharpLib
2588
- .default(pngBuf)
2589
- .raw()
2590
- .toBuffer({ resolveWithObject: true });
2591
- const rawRGB = Buffer.from(data);
2592
- const markerLen = MARKER_COLORS.length * 3;
2593
- for (let i = 0; i <= rawRGB.length - markerLen; i += 3) {
2594
- let ok = true;
2595
- for (let m = 0; m < MARKER_COLORS.length; m++) {
2596
- const j = i + m * 3;
2597
- if (rawRGB[j] !== MARKER_COLORS[m].r ||
2598
- rawRGB[j + 1] !== MARKER_COLORS[m].g ||
2599
- rawRGB[j + 2] !== MARKER_COLORS[m].b) {
2600
- ok = false;
2601
- break;
2602
- }
2603
- }
2604
- if (!ok)
2605
- continue;
2606
- const headerStart = i + markerLen;
2607
- if (headerStart + PIXEL_MAGIC.length >= rawRGB.length)
2608
- continue;
2609
- if (!rawRGB
2610
- .slice(headerStart, headerStart + PIXEL_MAGIC.length)
2611
- .equals(PIXEL_MAGIC))
2612
- continue;
2613
- const metaStart = headerStart + PIXEL_MAGIC.length;
2614
- if (metaStart + 2 >= rawRGB.length)
2615
- continue;
2616
- const nameLen = rawRGB[metaStart + 1];
2617
- const payloadLenOff = metaStart + 2 + nameLen;
2618
- const payloadStart = payloadLenOff + 4;
2619
- if (payloadStart >= rawRGB.length)
2620
- continue;
2621
- const flag = rawRGB[payloadStart];
2622
- return flag === ENC_AES || flag === ENC_XOR;
2623
- }
2624
- }
2625
- catch (e) { }
2626
- try {
2627
- await decodePngToBinary(pngBuf, { showProgress: false });
2628
- return false;
2629
- }
2630
- catch (e) {
2631
- if (e instanceof PassphraseRequiredError)
2632
- return true;
2633
- if (e.message && e.message.toLowerCase().includes('passphrase'))
2634
- return true;
2635
- return false;
2636
- }
2637
- }
2638
- catch (e) {
2639
- return false;
2640
- }
2641
- }