roxify 1.1.12 → 1.2.1

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',
@@ -752,7 +1454,13 @@ export async function encodeBinaryToPng(input, opts = {}) {
752
1454
  }
753
1455
  }
754
1456
  if (opts.onProgress)
755
- 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);
756
1464
  let bufScr = await sharp(raw, {
757
1465
  raw: { width, height, channels: 3 },
758
1466
  })
@@ -764,9 +1472,53 @@ export async function encodeBinaryToPng(input, opts = {}) {
764
1472
  })
765
1473
  .toBuffer();
766
1474
  if (opts.onProgress)
767
- opts.onProgress({ phase: 'done', loaded: bufScr.length });
768
- progressBar?.stop();
769
- 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
+ }
770
1522
  }
771
1523
  if (mode === 'pixel') {
772
1524
  const nameBuf = opts.name
@@ -838,15 +1590,34 @@ export async function encodeBinaryToPng(input, opts = {}) {
838
1590
  chunksPixel.push({ name: 'IDAT', data: idatData });
839
1591
  chunksPixel.push({ name: 'IEND', data: Buffer.alloc(0) });
840
1592
  if (opts.onProgress)
841
- opts.onProgress({ phase: 'png_gen' });
1593
+ opts.onProgress({ phase: 'png_gen', loaded: 0, total: 2 });
842
1594
  const tmp = Buffer.from(encode(chunksPixel));
843
1595
  const outPng = tmp.slice(0, 8).toString('hex') === PNG_HEADER_HEX
844
1596
  ? tmp
845
1597
  : Buffer.concat([PNG_HEADER, tmp]);
1598
+ if (opts.onProgress)
1599
+ opts.onProgress({ phase: 'png_gen', loaded: 1, total: 2 });
846
1600
  if (opts.onProgress)
847
1601
  opts.onProgress({ phase: 'done', loaded: outPng.length });
848
- progressBar?.stop();
849
- 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
+ }
850
1621
  }
851
1622
  if (mode === 'compact') {
852
1623
  const bytesPerPixel = 4;
@@ -877,15 +1648,34 @@ export async function encodeBinaryToPng(input, opts = {}) {
877
1648
  chunks2.push({ name: CHUNK_TYPE, data: meta });
878
1649
  chunks2.push({ name: 'IEND', data: Buffer.alloc(0) });
879
1650
  if (opts.onProgress)
880
- opts.onProgress({ phase: 'png_gen' });
1651
+ opts.onProgress({ phase: 'png_gen', loaded: 0, total: 2 });
881
1652
  const out = Buffer.from(encode(chunks2));
882
1653
  const outBuf = out.slice(0, 8).toString('hex') === PNG_HEADER_HEX
883
1654
  ? out
884
1655
  : Buffer.concat([PNG_HEADER, out]);
1656
+ if (opts.onProgress)
1657
+ opts.onProgress({ phase: 'png_gen', loaded: 1, total: 2 });
885
1658
  if (opts.onProgress)
886
1659
  opts.onProgress({ phase: 'done', loaded: outBuf.length });
887
- progressBar?.stop();
888
- 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
+ }
889
1679
  }
890
1680
  throw new Error(`Unsupported mode: ${mode}`);
891
1681
  }
@@ -976,7 +1766,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
976
1766
  const ws = createWriteStream(opts.outPath, { highWaterMark: 64 * 1024 });
977
1767
  let headerBuf = Buffer.alloc(0);
978
1768
  let headerSkipped = false;
979
- await tryZstdDecompress(payload, (info) => {
1769
+ await tryDecompress(payload, (info) => {
980
1770
  if (opts.onProgress)
981
1771
  opts.onProgress(info);
982
1772
  }, async (decChunk) => {
@@ -1010,7 +1800,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1010
1800
  if (opts.onProgress)
1011
1801
  opts.onProgress({ phase: 'decompress_start' });
1012
1802
  try {
1013
- payload = await tryZstdDecompress(payload, (info) => {
1803
+ payload = await tryDecompress(payload, (info) => {
1014
1804
  if (opts.onProgress)
1015
1805
  opts.onProgress(info);
1016
1806
  });
@@ -1073,7 +1863,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1073
1863
  const ws = createWriteStream(opts.outPath, { highWaterMark: 64 * 1024 });
1074
1864
  let headerBuf = Buffer.alloc(0);
1075
1865
  let headerSkipped = false;
1076
- await tryZstdDecompress(payload, (info) => {
1866
+ await tryDecompress(payload, (info) => {
1077
1867
  if (opts.onProgress)
1078
1868
  opts.onProgress(info);
1079
1869
  }, async (decChunk) => {
@@ -1235,7 +2025,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1235
2025
  let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
1236
2026
  try {
1237
2027
  if (opts.outPath) {
1238
- await tryZstdDecompress(payload, (info) => {
2028
+ await tryDecompress(payload, (info) => {
1239
2029
  if (opts.onProgress)
1240
2030
  opts.onProgress(info);
1241
2031
  }, undefined, opts.outPath);
@@ -1538,7 +2328,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1538
2328
  return { meta: { name } };
1539
2329
  }
1540
2330
  else {
1541
- payload = await tryZstdDecompress(payload, (info) => {
2331
+ payload = await tryDecompress(payload, (info) => {
1542
2332
  if (opts.onProgress)
1543
2333
  opts.onProgress(info);
1544
2334
  });
@@ -1595,6 +2385,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1595
2385
  }
1596
2386
  throw new DataFormatError('No valid data found in image');
1597
2387
  }
2388
+ export { decodeMinPng, encodeMinPng } from './minpng.js';
1598
2389
  export { packPaths, unpackBuffer } from './pack.js';
1599
2390
  /**
1600
2391
  * List files in a Rox PNG archive without decoding the full payload.