roxify 1.1.11 → 1.1.13

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,7 +1,8 @@
1
1
  import { compress as zstdCompress, decompress as zstdDecompress, } from '@mongodb-js/zstd';
2
+ import { spawn, spawnSync } from 'child_process';
2
3
  import cliProgress from 'cli-progress';
3
4
  import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes, } from 'crypto';
4
- import { createReadStream, createWriteStream, readFileSync, unlinkSync, } from 'fs';
5
+ import { createReadStream, createWriteStream, existsSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
5
6
  import { tmpdir } from 'os';
6
7
  import { join } from 'path';
7
8
  import encode from 'png-chunks-encode';
@@ -55,6 +56,7 @@ async function writeInChunks(ws, buf, chunkSize = CHUNK_SIZE) {
55
56
  }
56
57
  const COMPRESSION_MARKERS = {
57
58
  zstd: [{ r: 0, g: 255, b: 0 }],
59
+ lzma: [{ r: 255, g: 255, b: 0 }],
58
60
  };
59
61
  function colorsToBytes(colors) {
60
62
  const buf = Buffer.alloc(colors.length * 3);
@@ -88,7 +90,12 @@ function deltaDecode(data) {
88
90
  async function parallelZstdCompress(payload, level = 22, onProgress) {
89
91
  const chunkSize = 1024 * 1024 * 1024;
90
92
  if (payload.length <= chunkSize) {
91
- return Buffer.from(await zstdCompress(payload, level));
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);
92
99
  }
93
100
  const promises = [];
94
101
  const totalChunks = Math.ceil(payload.length / chunkSize);
@@ -204,6 +211,665 @@ async function parallelZstdDecompress(payload, onProgress, onChunk, outPath) {
204
211
  }
205
212
  return Buffer.alloc(0);
206
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 args = [
249
+ '-y',
250
+ '--iterations=500',
251
+ '--filters=01234mepb',
252
+ inPath,
253
+ outPath,
254
+ ];
255
+ const runCommandAsync = (cmd, args, timeout = 120000) => {
256
+ return new Promise((resolve) => {
257
+ try {
258
+ const child = spawn(cmd, args, {
259
+ windowsHide: true,
260
+ stdio: 'ignore',
261
+ });
262
+ let killed = false;
263
+ const to = setTimeout(() => {
264
+ killed = true;
265
+ try {
266
+ child.kill('SIGTERM');
267
+ }
268
+ catch (e) { }
269
+ }, timeout);
270
+ child.on('close', (code) => {
271
+ clearTimeout(to);
272
+ if (killed)
273
+ resolve({ error: new Error('timeout') });
274
+ else
275
+ resolve({ code: code ?? 0 });
276
+ });
277
+ child.on('error', (err) => {
278
+ clearTimeout(to);
279
+ resolve({ error: err });
280
+ });
281
+ }
282
+ catch (err) {
283
+ resolve({ error: err });
284
+ }
285
+ });
286
+ };
287
+ const res = await runCommandAsync('zopflipng', args, 120000);
288
+ if (!res.error && existsSync(outPath)) {
289
+ const outBuf = readFileSync(outPath);
290
+ try {
291
+ unlinkSync(inPath);
292
+ unlinkSync(outPath);
293
+ }
294
+ catch (e) { }
295
+ return outBuf.length < pngBuf.length ? outBuf : pngBuf;
296
+ }
297
+ if (fast)
298
+ return pngBuf;
299
+ }
300
+ catch (e) { }
301
+ try {
302
+ const chunksRaw = extract(pngBuf);
303
+ const ihdr = chunksRaw.find((c) => c.name === 'IHDR');
304
+ if (!ihdr)
305
+ return pngBuf;
306
+ const ihdrData = Buffer.isBuffer(ihdr.data)
307
+ ? ihdr.data
308
+ : Buffer.from(ihdr.data);
309
+ const width = ihdrData.readUInt32BE(0);
310
+ const height = ihdrData.readUInt32BE(4);
311
+ const bitDepth = ihdrData[8];
312
+ const colorType = ihdrData[9];
313
+ if (bitDepth !== 8 || colorType !== 2)
314
+ return pngBuf;
315
+ const idatChunks = chunksRaw.filter((c) => c.name === 'IDAT');
316
+ const idatData = Buffer.concat(idatChunks.map((c) => Buffer.isBuffer(c.data)
317
+ ? c.data
318
+ : Buffer.from(c.data)));
319
+ let raw;
320
+ try {
321
+ raw = zlib.inflateSync(idatData);
322
+ }
323
+ catch (e) {
324
+ return pngBuf;
325
+ }
326
+ const bytesPerPixel = 3;
327
+ const rowBytes = width * bytesPerPixel;
328
+ const inRowLen = rowBytes + 1;
329
+ if (raw.length !== inRowLen * height)
330
+ return pngBuf;
331
+ function paethPredict(a, b, c) {
332
+ const p = a + b - c;
333
+ const pa = Math.abs(p - a);
334
+ const pb = Math.abs(p - b);
335
+ const pc = Math.abs(p - c);
336
+ if (pa <= pb && pa <= pc)
337
+ return a;
338
+ if (pb <= pc)
339
+ return b;
340
+ return c;
341
+ }
342
+ const outRows = [];
343
+ let prevRow = null;
344
+ for (let y = 0; y < height; y++) {
345
+ const rowStart = y * inRowLen + 1;
346
+ const row = raw.slice(rowStart, rowStart + rowBytes);
347
+ let bestSum = Infinity;
348
+ let bestFiltered = null;
349
+ for (let f = 0; f <= 4; f++) {
350
+ const filtered = Buffer.alloc(rowBytes);
351
+ let sum = 0;
352
+ for (let i = 0; i < rowBytes; i++) {
353
+ const val = row[i];
354
+ let outv = 0;
355
+ const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
356
+ const up = prevRow ? prevRow[i] : 0;
357
+ const upLeft = prevRow && i - bytesPerPixel >= 0 ? prevRow[i - bytesPerPixel] : 0;
358
+ if (f === 0) {
359
+ outv = val;
360
+ }
361
+ else if (f === 1) {
362
+ outv = (val - left + 256) & 0xff;
363
+ }
364
+ else if (f === 2) {
365
+ outv = (val - up + 256) & 0xff;
366
+ }
367
+ else if (f === 3) {
368
+ const avg = Math.floor((left + up) / 2);
369
+ outv = (val - avg + 256) & 0xff;
370
+ }
371
+ else {
372
+ const p = paethPredict(left, up, upLeft);
373
+ outv = (val - p + 256) & 0xff;
374
+ }
375
+ filtered[i] = outv;
376
+ const signed = outv > 127 ? outv - 256 : outv;
377
+ sum += Math.abs(signed);
378
+ }
379
+ if (sum < bestSum) {
380
+ bestSum = sum;
381
+ bestFiltered = filtered;
382
+ }
383
+ }
384
+ const rowBuf = Buffer.alloc(1 + rowBytes);
385
+ let chosenFilter = 0;
386
+ for (let f = 0; f <= 4; f++) {
387
+ const filtered = Buffer.alloc(rowBytes);
388
+ for (let i = 0; i < rowBytes; i++) {
389
+ const val = row[i];
390
+ const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
391
+ const up = prevRow ? prevRow[i] : 0;
392
+ const upLeft = prevRow && i - bytesPerPixel >= 0 ? prevRow[i - bytesPerPixel] : 0;
393
+ if (f === 0)
394
+ filtered[i] = val;
395
+ else if (f === 1)
396
+ filtered[i] = (val - left + 256) & 0xff;
397
+ else if (f === 2)
398
+ filtered[i] = (val - up + 256) & 0xff;
399
+ else if (f === 3)
400
+ filtered[i] = (val - Math.floor((left + up) / 2) + 256) & 0xff;
401
+ else
402
+ filtered[i] = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
403
+ }
404
+ if (filtered.equals(bestFiltered)) {
405
+ chosenFilter = f;
406
+ break;
407
+ }
408
+ }
409
+ rowBuf[0] = chosenFilter;
410
+ bestFiltered.copy(rowBuf, 1);
411
+ outRows.push(rowBuf);
412
+ prevRow = row;
413
+ }
414
+ const filteredAll = Buffer.concat(outRows);
415
+ const compressed = zlib.deflateSync(filteredAll, {
416
+ level: 9,
417
+ memLevel: 9,
418
+ strategy: zlib.constants.Z_DEFAULT_STRATEGY,
419
+ });
420
+ const newChunks = [];
421
+ for (const c of chunksRaw) {
422
+ if (c.name === 'IDAT')
423
+ continue;
424
+ newChunks.push({
425
+ name: c.name,
426
+ data: Buffer.isBuffer(c.data)
427
+ ? c.data
428
+ : Buffer.from(c.data),
429
+ });
430
+ }
431
+ const iendIndex = newChunks.findIndex((c) => c.name === 'IEND');
432
+ const insertIndex = iendIndex >= 0 ? iendIndex : newChunks.length;
433
+ newChunks.splice(insertIndex, 0, { name: 'IDAT', data: compressed });
434
+ function ensurePng(buf) {
435
+ return buf.slice(0, 8).toString('hex') === PNG_HEADER_HEX
436
+ ? buf
437
+ : Buffer.concat([PNG_HEADER, buf]);
438
+ }
439
+ const out = ensurePng(Buffer.from(encode(newChunks)));
440
+ let bestBuf = out.length < pngBuf.length ? out : pngBuf;
441
+ const strategies = [
442
+ zlib.constants.Z_DEFAULT_STRATEGY,
443
+ zlib.constants.Z_FILTERED,
444
+ zlib.constants.Z_RLE,
445
+ ...(zlib.constants.Z_HUFFMAN_ONLY ? [zlib.constants.Z_HUFFMAN_ONLY] : []),
446
+ ...(zlib.constants.Z_FIXED ? [zlib.constants.Z_FIXED] : []),
447
+ ];
448
+ for (const strat of strategies) {
449
+ try {
450
+ const comp = zlib.deflateSync(raw, {
451
+ level: 9,
452
+ memLevel: 9,
453
+ strategy: strat,
454
+ });
455
+ const altChunks = newChunks.map((c) => ({
456
+ name: c.name,
457
+ data: c.data,
458
+ }));
459
+ const idx = altChunks.findIndex((c) => c.name === 'IDAT');
460
+ if (idx !== -1)
461
+ altChunks[idx] = { name: 'IDAT', data: comp };
462
+ const candidate = ensurePng(Buffer.from(encode(altChunks)));
463
+ if (candidate.length < bestBuf.length)
464
+ bestBuf = candidate;
465
+ }
466
+ catch (e) { }
467
+ }
468
+ try {
469
+ const fflate = await import('fflate');
470
+ const fflateDeflateSync = fflate.deflateSync;
471
+ try {
472
+ const comp = fflateDeflateSync(filteredAll);
473
+ const altChunks = newChunks.map((c) => ({
474
+ name: c.name,
475
+ data: c.data,
476
+ }));
477
+ const idx = altChunks.findIndex((c) => c.name === 'IDAT');
478
+ if (idx !== -1)
479
+ altChunks[idx] = { name: 'IDAT', data: Buffer.from(comp) };
480
+ const candidate = ensurePng(Buffer.from(encode(altChunks)));
481
+ if (candidate.length < bestBuf.length)
482
+ bestBuf = candidate;
483
+ }
484
+ catch (e) { }
485
+ }
486
+ catch (e) { }
487
+ const windowBitsOpts = [15, 12, 9];
488
+ const memLevelOpts = [9, 8];
489
+ for (let f = 0; f <= 4; f++) {
490
+ try {
491
+ const filteredAllGlobalRows = [];
492
+ let prevRowG = null;
493
+ for (let y = 0; y < height; y++) {
494
+ const row = raw.slice(y * inRowLen + 1, y * inRowLen + 1 + rowBytes);
495
+ const filtered = Buffer.alloc(rowBytes);
496
+ for (let i = 0; i < rowBytes; i++) {
497
+ const val = row[i];
498
+ const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
499
+ const up = prevRowG ? prevRowG[i] : 0;
500
+ const upLeft = prevRowG && i - bytesPerPixel >= 0
501
+ ? prevRowG[i - bytesPerPixel]
502
+ : 0;
503
+ if (f === 0)
504
+ filtered[i] = val;
505
+ else if (f === 1)
506
+ filtered[i] = (val - left + 256) & 0xff;
507
+ else if (f === 2)
508
+ filtered[i] = (val - up + 256) & 0xff;
509
+ else if (f === 3)
510
+ filtered[i] = (val - Math.floor((left + up) / 2) + 256) & 0xff;
511
+ else
512
+ filtered[i] = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
513
+ }
514
+ const rowBuf = Buffer.alloc(1 + rowBytes);
515
+ rowBuf[0] = f;
516
+ filtered.copy(rowBuf, 1);
517
+ filteredAllGlobalRows.push(rowBuf);
518
+ prevRowG = row;
519
+ }
520
+ const filteredAllGlobal = Buffer.concat(filteredAllGlobalRows);
521
+ for (const strat2 of strategies) {
522
+ for (const wb of windowBitsOpts) {
523
+ for (const ml of memLevelOpts) {
524
+ try {
525
+ const comp = zlib.deflateSync(filteredAllGlobal, {
526
+ level: 9,
527
+ memLevel: ml,
528
+ strategy: strat2,
529
+ windowBits: wb,
530
+ });
531
+ const altChunks = newChunks.map((c) => ({
532
+ name: c.name,
533
+ data: c.data,
534
+ }));
535
+ const idx = altChunks.findIndex((c) => c.name === 'IDAT');
536
+ if (idx !== -1)
537
+ altChunks[idx] = { name: 'IDAT', data: comp };
538
+ const candidate = ensurePng(Buffer.from(encode(altChunks)));
539
+ if (candidate.length < bestBuf.length)
540
+ bestBuf = candidate;
541
+ }
542
+ catch (e) { }
543
+ }
544
+ }
545
+ }
546
+ }
547
+ catch (e) { }
548
+ }
549
+ try {
550
+ const zopIterations = [1000, 2000];
551
+ zopIterations.push(5000, 10000, 20000);
552
+ for (const iters of zopIterations) {
553
+ try {
554
+ const zIn = join(tmpdir(), `rox_zop_in_${Date.now()}_${Math.random()
555
+ .toString(36)
556
+ .slice(2)}.png`);
557
+ const zOut = zIn + '.out.png';
558
+ writeFileSync(zIn, bestBuf);
559
+ const args2 = [
560
+ '-y',
561
+ `--iterations=${iters}`,
562
+ '--filters=01234mepb',
563
+ zIn,
564
+ zOut,
565
+ ];
566
+ try {
567
+ const r2 = await runCommandAsync('zopflipng', args2, 240000);
568
+ if (!r2.error && existsSync(zOut)) {
569
+ const zbuf = readFileSync(zOut);
570
+ try {
571
+ unlinkSync(zIn);
572
+ unlinkSync(zOut);
573
+ }
574
+ catch (e) { }
575
+ if (zbuf.length < bestBuf.length)
576
+ bestBuf = zbuf;
577
+ }
578
+ }
579
+ catch (e) { }
580
+ }
581
+ catch (e) { }
582
+ }
583
+ }
584
+ catch (e) { }
585
+ try {
586
+ const advIn = join(tmpdir(), `rox_adv_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
587
+ writeFileSync(advIn, bestBuf);
588
+ const rAdv = spawnSync('advdef', ['-z4', '-i10', advIn], {
589
+ windowsHide: true,
590
+ stdio: 'ignore',
591
+ timeout: 120000,
592
+ });
593
+ if (!rAdv.error && existsSync(advIn)) {
594
+ const advBuf = readFileSync(advIn);
595
+ try {
596
+ unlinkSync(advIn);
597
+ }
598
+ catch (e) { }
599
+ if (advBuf.length < bestBuf.length)
600
+ bestBuf = advBuf;
601
+ }
602
+ }
603
+ catch (e) { }
604
+ for (const strat of strategies) {
605
+ try {
606
+ const comp = zlib.deflateSync(filteredAll, {
607
+ level: 9,
608
+ memLevel: 9,
609
+ strategy: strat,
610
+ });
611
+ const altChunks = newChunks.map((c) => ({
612
+ name: c.name,
613
+ data: c.data,
614
+ }));
615
+ const idx = altChunks.findIndex((c) => c.name === 'IDAT');
616
+ if (idx !== -1)
617
+ altChunks[idx] = { name: 'IDAT', data: comp };
618
+ const candidate = ensurePng(Buffer.from(encode(altChunks)));
619
+ if (candidate.length < bestBuf.length)
620
+ bestBuf = candidate;
621
+ }
622
+ catch (e) { }
623
+ }
624
+ try {
625
+ const pixels = Buffer.alloc(width * height * 3);
626
+ let prev = null;
627
+ for (let y = 0; y < height; y++) {
628
+ const f = raw[y * inRowLen];
629
+ const row = raw.slice(y * inRowLen + 1, y * inRowLen + 1 + rowBytes);
630
+ const recon = Buffer.alloc(rowBytes);
631
+ for (let i = 0; i < rowBytes; i++) {
632
+ const left = i - 3 >= 0 ? recon[i - 3] : 0;
633
+ const up = prev ? prev[i] : 0;
634
+ const upLeft = prev && i - 3 >= 0 ? prev[i - 3] : 0;
635
+ let v = row[i];
636
+ if (f === 0) {
637
+ }
638
+ else if (f === 1)
639
+ v = (v + left) & 0xff;
640
+ else if (f === 2)
641
+ v = (v + up) & 0xff;
642
+ else if (f === 3)
643
+ v = (v + Math.floor((left + up) / 2)) & 0xff;
644
+ else
645
+ v = (v + paethPredict(left, up, upLeft)) & 0xff;
646
+ recon[i] = v;
647
+ }
648
+ recon.copy(pixels, y * rowBytes);
649
+ prev = recon;
650
+ }
651
+ const paletteMap = new Map();
652
+ const palette = [];
653
+ for (let i = 0; i < pixels.length; i += 3) {
654
+ const key = `${pixels[i]},${pixels[i + 1]},${pixels[i + 2]}`;
655
+ if (!paletteMap.has(key)) {
656
+ paletteMap.set(key, paletteMap.size);
657
+ palette.push(pixels[i], pixels[i + 1], pixels[i + 2]);
658
+ if (paletteMap.size > 256)
659
+ break;
660
+ }
661
+ }
662
+ if (paletteMap.size <= 256) {
663
+ const idxRowLen = 1 + width * 1;
664
+ const idxRows = [];
665
+ for (let y = 0; y < height; y++) {
666
+ const rowIdx = Buffer.alloc(width);
667
+ for (let x = 0; x < width; x++) {
668
+ const pos = (y * width + x) * 3;
669
+ const key = `${pixels[pos]},${pixels[pos + 1]},${pixels[pos + 2]}`;
670
+ rowIdx[x] = paletteMap.get(key);
671
+ }
672
+ let bestRowFilter = 0;
673
+ let bestRowSum = Infinity;
674
+ let bestRowFiltered = null;
675
+ for (let f = 0; f <= 4; f++) {
676
+ const filteredRow = Buffer.alloc(width);
677
+ let sum = 0;
678
+ for (let i = 0; i < width; i++) {
679
+ const val = rowIdx[i];
680
+ let outv = 0;
681
+ const left = i - 1 >= 0 ? rowIdx[i - 1] : 0;
682
+ const up = y > 0 ? idxRows[y - 1][i] : 0;
683
+ const upLeft = y > 0 && i - 1 >= 0 ? idxRows[y - 1][i - 1] : 0;
684
+ if (f === 0)
685
+ outv = val;
686
+ else if (f === 1)
687
+ outv = (val - left + 256) & 0xff;
688
+ else if (f === 2)
689
+ outv = (val - up + 256) & 0xff;
690
+ else if (f === 3)
691
+ outv = (val - Math.floor((left + up) / 2) + 256) & 0xff;
692
+ else
693
+ outv = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
694
+ filteredRow[i] = outv;
695
+ const signed = outv > 127 ? outv - 256 : outv;
696
+ sum += Math.abs(signed);
697
+ }
698
+ if (sum < bestRowSum) {
699
+ bestRowSum = sum;
700
+ bestRowFilter = f;
701
+ bestRowFiltered = filteredRow;
702
+ }
703
+ }
704
+ const rowBuf = Buffer.alloc(idxRowLen);
705
+ rowBuf[0] = bestRowFilter;
706
+ bestRowFiltered.copy(rowBuf, 1);
707
+ idxRows.push(rowBuf);
708
+ }
709
+ const freqMap = new Map();
710
+ for (let i = 0; i < pixels.length; i += 3) {
711
+ const key = `${pixels[i]},${pixels[i + 1]},${pixels[i + 2]}`;
712
+ freqMap.set(key, (freqMap.get(key) || 0) + 1);
713
+ }
714
+ const paletteVariants = [];
715
+ paletteVariants.push({
716
+ paletteArr: palette.slice(),
717
+ map: new Map(paletteMap),
718
+ });
719
+ const freqSorted = Array.from(freqMap.entries()).sort((a, b) => b[1] - a[1]);
720
+ if (freqSorted.length > 0) {
721
+ const pal2 = [];
722
+ const map2 = new Map();
723
+ let pi = 0;
724
+ for (const [k] of freqSorted) {
725
+ const parts = k.split(',').map((s) => Number(s));
726
+ pal2.push(parts[0], parts[1], parts[2]);
727
+ map2.set(k, pi++);
728
+ if (pi >= 256)
729
+ break;
730
+ }
731
+ if (map2.size <= 256)
732
+ paletteVariants.push({ paletteArr: pal2, map: map2 });
733
+ }
734
+ for (const variant of paletteVariants) {
735
+ const pSize = variant.map.size;
736
+ const bitDepth = pSize <= 2 ? 1 : pSize <= 4 ? 2 : pSize <= 16 ? 4 : 8;
737
+ const idxRowsVar = [];
738
+ for (let y = 0; y < height; y++) {
739
+ const rowIdx = Buffer.alloc(width);
740
+ for (let x = 0; x < width; x++) {
741
+ const pos = (y * width + x) * 3;
742
+ const key = `${pixels[pos]},${pixels[pos + 1]},${pixels[pos + 2]}`;
743
+ rowIdx[x] = variant.map.get(key);
744
+ }
745
+ idxRowsVar.push(rowIdx);
746
+ }
747
+ function packRowIndices(rowIdx, bitDepth) {
748
+ if (bitDepth === 8)
749
+ return rowIdx;
750
+ const bitsPerRow = width * bitDepth;
751
+ const outLen = Math.ceil(bitsPerRow / 8);
752
+ const out = Buffer.alloc(outLen);
753
+ let bitPos = 0;
754
+ for (let i = 0; i < width; i++) {
755
+ const val = rowIdx[i] & ((1 << bitDepth) - 1);
756
+ for (let b = 0; b < bitDepth; b++) {
757
+ const bit = (val >> (bitDepth - 1 - b)) & 1;
758
+ const byteIdx = Math.floor(bitPos / 8);
759
+ const shift = 7 - (bitPos % 8);
760
+ out[byteIdx] |= bit << shift;
761
+ bitPos++;
762
+ }
763
+ }
764
+ return out;
765
+ }
766
+ const packedRows = [];
767
+ for (let y = 0; y < height; y++) {
768
+ const packed = packRowIndices(idxRowsVar[y], bitDepth);
769
+ let bestRowFilter = 0;
770
+ let bestRowSum = Infinity;
771
+ let bestRowFiltered = null;
772
+ for (let f = 0; f <= 4; f++) {
773
+ const filteredRow = Buffer.alloc(packed.length);
774
+ let sum = 0;
775
+ for (let i = 0; i < packed.length; i++) {
776
+ const val = packed[i];
777
+ const left = i - 1 >= 0 ? packed[i - 1] : 0;
778
+ const up = y > 0 ? packedRows[y - 1][i] : 0;
779
+ const upLeft = y > 0 && i - 1 >= 0 ? packedRows[y - 1][i - 1] : 0;
780
+ let outv = 0;
781
+ if (f === 0)
782
+ outv = val;
783
+ else if (f === 1)
784
+ outv = (val - left + 256) & 0xff;
785
+ else if (f === 2)
786
+ outv = (val - up + 256) & 0xff;
787
+ else if (f === 3)
788
+ outv = (val - Math.floor((left + up) / 2) + 256) & 0xff;
789
+ else
790
+ outv = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
791
+ filteredRow[i] = outv;
792
+ const signed = outv > 127 ? outv - 256 : outv;
793
+ sum += Math.abs(signed);
794
+ }
795
+ if (sum < bestRowSum) {
796
+ bestRowSum = sum;
797
+ bestRowFilter = f;
798
+ bestRowFiltered = filteredRow;
799
+ }
800
+ }
801
+ const rowBuf = Buffer.alloc(1 + packed.length);
802
+ rowBuf[0] = bestRowFilter;
803
+ bestRowFiltered.copy(rowBuf, 1);
804
+ packedRows.push(rowBuf);
805
+ }
806
+ const idxFilteredAllVar = Buffer.concat(packedRows);
807
+ const palettesBufVar = Buffer.from(variant.paletteArr);
808
+ const palChunksVar = [];
809
+ const ihdr = Buffer.alloc(13);
810
+ ihdr.writeUInt32BE(width, 0);
811
+ ihdr.writeUInt32BE(height, 4);
812
+ ihdr[8] = bitDepth;
813
+ ihdr[9] = 3;
814
+ ihdr[10] = 0;
815
+ ihdr[11] = 0;
816
+ ihdr[12] = 0;
817
+ palChunksVar.push({ name: 'IHDR', data: ihdr });
818
+ palChunksVar.push({ name: 'PLTE', data: palettesBufVar });
819
+ palChunksVar.push({
820
+ name: 'IDAT',
821
+ data: zlib.deflateSync(idxFilteredAllVar, { level: 9 }),
822
+ });
823
+ palChunksVar.push({ name: 'IEND', data: Buffer.alloc(0) });
824
+ const palOutVar = ensurePng(Buffer.from(encode(palChunksVar)));
825
+ if (palOutVar.length < bestBuf.length)
826
+ bestBuf = palOutVar;
827
+ }
828
+ }
829
+ }
830
+ catch (e) { }
831
+ const externalAttempts = [
832
+ { cmd: 'oxipng', args: ['-o', '6', '--strip', 'all'] },
833
+ { cmd: 'optipng', args: ['-o7'] },
834
+ { cmd: 'pngcrush', args: ['-brute', '-reduce'] },
835
+ { cmd: 'pngout', args: [] },
836
+ ];
837
+ for (const tool of externalAttempts) {
838
+ try {
839
+ const tIn = join(tmpdir(), `rox_ext_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
840
+ const tOut = tIn + '.out.png';
841
+ writeFileSync(tIn, bestBuf);
842
+ const args = tool.args.concat([tIn, tOut]);
843
+ const r = spawnSync(tool.cmd, args, {
844
+ windowsHide: true,
845
+ stdio: 'ignore',
846
+ timeout: 240000,
847
+ });
848
+ if (!r.error && existsSync(tOut)) {
849
+ const outb = readFileSync(tOut);
850
+ try {
851
+ unlinkSync(tIn);
852
+ unlinkSync(tOut);
853
+ }
854
+ catch (e) { }
855
+ if (outb.length < bestBuf.length)
856
+ bestBuf = outb;
857
+ }
858
+ else {
859
+ try {
860
+ unlinkSync(tIn);
861
+ }
862
+ catch (e) { }
863
+ }
864
+ }
865
+ catch (e) { }
866
+ }
867
+ return bestBuf;
868
+ }
869
+ catch (e) {
870
+ return pngBuf;
871
+ }
872
+ }
207
873
  function applyXor(buf, passphrase) {
208
874
  const key = Buffer.from(passphrase, 'utf8');
209
875
  const out = Buffer.alloc(buf.length);
@@ -215,6 +881,42 @@ function applyXor(buf, passphrase) {
215
881
  async function tryZstdDecompress(payload, onProgress, onChunk, outPath) {
216
882
  return await parallelZstdDecompress(payload, onProgress, onChunk, outPath);
217
883
  }
884
+ async function tryDecompress(payload, onProgress, onChunk, outPath) {
885
+ try {
886
+ return await parallelZstdDecompress(payload, onProgress, onChunk, outPath);
887
+ }
888
+ catch (e) {
889
+ try {
890
+ const mod = await import('lzma-purejs');
891
+ const decompressFn = mod && (mod.decompress || (mod.LZMA && mod.LZMA.decompress));
892
+ if (!decompressFn)
893
+ throw new Error('No lzma decompress');
894
+ const dec = await new Promise((resolve, reject) => {
895
+ try {
896
+ decompressFn(Buffer.from(payload), (out) => resolve(out));
897
+ }
898
+ catch (err) {
899
+ reject(err);
900
+ }
901
+ });
902
+ const dBuf = Buffer.isBuffer(dec) ? dec : Buffer.from(dec);
903
+ if (onChunk) {
904
+ await onChunk(dBuf, 1, 1);
905
+ return Buffer.alloc(0);
906
+ }
907
+ if (outPath) {
908
+ const ws = createWriteStream(outPath);
909
+ await writeInChunks(ws, dBuf);
910
+ ws.end();
911
+ return Buffer.alloc(0);
912
+ }
913
+ return dBuf;
914
+ }
915
+ catch (e2) {
916
+ throw e;
917
+ }
918
+ }
919
+ }
218
920
  function tryDecryptIfNeeded(buf, passphrase) {
219
921
  if (!buf || buf.length === 0)
220
922
  return buf;
@@ -575,7 +1277,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
575
1277
  opts.onProgress({ phase: 'compress_start', total: payload.length });
576
1278
  const useDelta = mode !== 'screenshot';
577
1279
  const deltaEncoded = useDelta ? deltaEncode(payload) : payload;
578
- payload = await parallelZstdCompress(deltaEncoded, 1, (loaded, total) => {
1280
+ payload = await parallelZstdCompress(deltaEncoded, 22, (loaded, total) => {
579
1281
  if (opts.onProgress) {
580
1282
  opts.onProgress({
581
1283
  phase: 'compress_progress',
@@ -618,6 +1320,8 @@ export async function encodeBinaryToPng(input, opts = {}) {
618
1320
  payload = Buffer.concat([Buffer.from([ENC_AES]), salt, iv, tag, enc]);
619
1321
  if (opts.onProgress)
620
1322
  opts.onProgress({ phase: 'encrypt_done' });
1323
+ }
1324
+ else if (encChoice === 'xor') {
621
1325
  const xored = applyXor(payload, opts.passphrase);
622
1326
  payload = Buffer.concat([Buffer.from([ENC_XOR]), xored]);
623
1327
  if (opts.onProgress)
@@ -750,7 +1454,13 @@ export async function encodeBinaryToPng(input, opts = {}) {
750
1454
  }
751
1455
  }
752
1456
  if (opts.onProgress)
753
- opts.onProgress({ phase: 'png_gen' });
1457
+ opts.onProgress({ phase: 'png_gen', loaded: 0, total: 100 });
1458
+ let loaded = 0;
1459
+ const progressInterval = setInterval(() => {
1460
+ loaded = Math.min(loaded + 2, 98);
1461
+ if (opts.onProgress)
1462
+ opts.onProgress({ phase: 'png_gen', loaded, total: 100 });
1463
+ }, 50);
754
1464
  let bufScr = await sharp(raw, {
755
1465
  raw: { width, height, channels: 3 },
756
1466
  })
@@ -762,9 +1472,53 @@ export async function encodeBinaryToPng(input, opts = {}) {
762
1472
  })
763
1473
  .toBuffer();
764
1474
  if (opts.onProgress)
765
- opts.onProgress({ phase: 'done', loaded: bufScr.length });
766
- progressBar?.stop();
767
- return bufScr;
1475
+ opts.onProgress({ phase: 'png_gen', loaded: 100, total: 100 });
1476
+ if (opts.onProgress)
1477
+ opts.onProgress({ phase: 'optimizing', loaded: 0, total: 100 });
1478
+ let optInterval = null;
1479
+ const MIN_OPT_MS = 8000;
1480
+ let optStart = Date.now();
1481
+ if (opts.onProgress) {
1482
+ let optLoaded = 0;
1483
+ optStart = Date.now();
1484
+ optInterval = setInterval(() => {
1485
+ optLoaded = Math.min(optLoaded + 2, 99);
1486
+ opts.onProgress?.({
1487
+ phase: 'optimizing',
1488
+ loaded: optLoaded,
1489
+ total: 100,
1490
+ });
1491
+ }, 100);
1492
+ }
1493
+ try {
1494
+ const optimizedPromise = optimizePngBuffer(bufScr, !!opts.onProgress);
1495
+ const optimized = await optimizedPromise;
1496
+ const elapsedOpt = Date.now() - optStart;
1497
+ if (elapsedOpt < MIN_OPT_MS) {
1498
+ await new Promise((r) => setTimeout(r, MIN_OPT_MS - elapsedOpt));
1499
+ }
1500
+ clearInterval(progressInterval);
1501
+ if (optInterval) {
1502
+ clearInterval(optInterval);
1503
+ optInterval = null;
1504
+ }
1505
+ if (opts.onProgress)
1506
+ opts.onProgress({ phase: 'optimizing', loaded: 100, total: 100 });
1507
+ try {
1508
+ const verified = await decodePngToBinary(optimized);
1509
+ if (verified.buf && verified.buf.equals(input)) {
1510
+ progressBar?.stop();
1511
+ return optimized;
1512
+ }
1513
+ }
1514
+ catch (e) { }
1515
+ progressBar?.stop();
1516
+ return bufScr;
1517
+ }
1518
+ catch (e) {
1519
+ progressBar?.stop();
1520
+ return bufScr;
1521
+ }
768
1522
  }
769
1523
  if (mode === 'pixel') {
770
1524
  const nameBuf = opts.name
@@ -836,15 +1590,34 @@ export async function encodeBinaryToPng(input, opts = {}) {
836
1590
  chunksPixel.push({ name: 'IDAT', data: idatData });
837
1591
  chunksPixel.push({ name: 'IEND', data: Buffer.alloc(0) });
838
1592
  if (opts.onProgress)
839
- opts.onProgress({ phase: 'png_gen' });
1593
+ opts.onProgress({ phase: 'png_gen', loaded: 0, total: 2 });
840
1594
  const tmp = Buffer.from(encode(chunksPixel));
841
1595
  const outPng = tmp.slice(0, 8).toString('hex') === PNG_HEADER_HEX
842
1596
  ? tmp
843
1597
  : Buffer.concat([PNG_HEADER, tmp]);
1598
+ if (opts.onProgress)
1599
+ opts.onProgress({ phase: 'png_gen', loaded: 1, total: 2 });
844
1600
  if (opts.onProgress)
845
1601
  opts.onProgress({ phase: 'done', loaded: outPng.length });
846
- progressBar?.stop();
847
- return outPng;
1602
+ if (opts.onProgress)
1603
+ opts.onProgress({ phase: 'done', loaded: outPng.length });
1604
+ try {
1605
+ const optimized = await optimizePngBuffer(outPng);
1606
+ try {
1607
+ const verified = await decodePngToBinary(optimized);
1608
+ if (verified.buf && verified.buf.equals(input)) {
1609
+ progressBar?.stop();
1610
+ return optimized;
1611
+ }
1612
+ }
1613
+ catch (e) { }
1614
+ progressBar?.stop();
1615
+ return outPng;
1616
+ }
1617
+ catch (e) {
1618
+ progressBar?.stop();
1619
+ return outPng;
1620
+ }
848
1621
  }
849
1622
  if (mode === 'compact') {
850
1623
  const bytesPerPixel = 4;
@@ -875,15 +1648,34 @@ export async function encodeBinaryToPng(input, opts = {}) {
875
1648
  chunks2.push({ name: CHUNK_TYPE, data: meta });
876
1649
  chunks2.push({ name: 'IEND', data: Buffer.alloc(0) });
877
1650
  if (opts.onProgress)
878
- opts.onProgress({ phase: 'png_gen' });
1651
+ opts.onProgress({ phase: 'png_gen', loaded: 0, total: 2 });
879
1652
  const out = Buffer.from(encode(chunks2));
880
1653
  const outBuf = out.slice(0, 8).toString('hex') === PNG_HEADER_HEX
881
1654
  ? out
882
1655
  : Buffer.concat([PNG_HEADER, out]);
1656
+ if (opts.onProgress)
1657
+ opts.onProgress({ phase: 'png_gen', loaded: 1, total: 2 });
883
1658
  if (opts.onProgress)
884
1659
  opts.onProgress({ phase: 'done', loaded: outBuf.length });
885
- progressBar?.stop();
886
- return outBuf;
1660
+ if (opts.onProgress)
1661
+ opts.onProgress({ phase: 'done', loaded: outBuf.length });
1662
+ try {
1663
+ const optimized = await optimizePngBuffer(outBuf);
1664
+ try {
1665
+ const verified = await decodePngToBinary(optimized);
1666
+ if (verified.buf && verified.buf.equals(input)) {
1667
+ progressBar?.stop();
1668
+ return optimized;
1669
+ }
1670
+ }
1671
+ catch (e) { }
1672
+ progressBar?.stop();
1673
+ return outBuf;
1674
+ }
1675
+ catch (e) {
1676
+ progressBar?.stop();
1677
+ return outBuf;
1678
+ }
887
1679
  }
888
1680
  throw new Error(`Unsupported mode: ${mode}`);
889
1681
  }
@@ -974,7 +1766,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
974
1766
  const ws = createWriteStream(opts.outPath, { highWaterMark: 64 * 1024 });
975
1767
  let headerBuf = Buffer.alloc(0);
976
1768
  let headerSkipped = false;
977
- await tryZstdDecompress(payload, (info) => {
1769
+ await tryDecompress(payload, (info) => {
978
1770
  if (opts.onProgress)
979
1771
  opts.onProgress(info);
980
1772
  }, async (decChunk) => {
@@ -1008,7 +1800,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1008
1800
  if (opts.onProgress)
1009
1801
  opts.onProgress({ phase: 'decompress_start' });
1010
1802
  try {
1011
- payload = await tryZstdDecompress(payload, (info) => {
1803
+ payload = await tryDecompress(payload, (info) => {
1012
1804
  if (opts.onProgress)
1013
1805
  opts.onProgress(info);
1014
1806
  });
@@ -1071,7 +1863,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1071
1863
  const ws = createWriteStream(opts.outPath, { highWaterMark: 64 * 1024 });
1072
1864
  let headerBuf = Buffer.alloc(0);
1073
1865
  let headerSkipped = false;
1074
- await tryZstdDecompress(payload, (info) => {
1866
+ await tryDecompress(payload, (info) => {
1075
1867
  if (opts.onProgress)
1076
1868
  opts.onProgress(info);
1077
1869
  }, async (decChunk) => {
@@ -1233,7 +2025,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1233
2025
  let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
1234
2026
  try {
1235
2027
  if (opts.outPath) {
1236
- await tryZstdDecompress(payload, (info) => {
2028
+ await tryDecompress(payload, (info) => {
1237
2029
  if (opts.onProgress)
1238
2030
  opts.onProgress(info);
1239
2031
  }, undefined, opts.outPath);
@@ -1536,7 +2328,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1536
2328
  return { meta: { name } };
1537
2329
  }
1538
2330
  else {
1539
- payload = await tryZstdDecompress(payload, (info) => {
2331
+ payload = await tryDecompress(payload, (info) => {
1540
2332
  if (opts.onProgress)
1541
2333
  opts.onProgress(info);
1542
2334
  });
@@ -1593,6 +2385,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1593
2385
  }
1594
2386
  throw new DataFormatError('No valid data found in image');
1595
2387
  }
2388
+ export { decodeMinPng, encodeMinPng } from './minpng.js';
1596
2389
  export { packPaths, unpackBuffer } from './pack.js';
1597
2390
  /**
1598
2391
  * List files in a Rox PNG archive without decoding the full payload.