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/README.md +12 -0
- package/dist/cli.js +111 -78
- package/dist/index.d.ts +3 -72
- package/dist/index.js +809 -18
- package/dist/minpng.d.ts +8 -0
- package/dist/minpng.js +231 -0
- package/dist/pack.d.ts +1 -1
- package/dist/pack.js +1 -1
- package/package.json +5 -2
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
|
-
|
|
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,
|
|
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: '
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
849
|
-
|
|
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
|
-
|
|
888
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|