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/README.md +12 -0
- package/dist/cli.js +111 -78
- package/dist/index.d.ts +3 -72
- package/dist/index.js +811 -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',
|
|
@@ -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: '
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
847
|
-
|
|
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
|
-
|
|
886
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|