roxify 1.2.3 → 1.2.5
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/cli.js +90 -51
- package/dist/index.d.ts +11 -127
- package/dist/index.js +11 -2547
- package/dist/minpng.js +31 -24
- package/dist/pack.d.ts +11 -10
- package/dist/pack.js +104 -62
- package/dist/utils/constants.d.ts +38 -0
- package/dist/utils/constants.js +22 -0
- package/dist/utils/crc.d.ts +4 -0
- package/dist/utils/crc.js +29 -0
- package/dist/utils/decoder.d.ts +4 -0
- package/dist/utils/decoder.js +626 -0
- package/dist/utils/encoder.d.ts +4 -0
- package/dist/utils/encoder.js +305 -0
- package/dist/utils/errors.d.ts +9 -0
- package/dist/utils/errors.js +18 -0
- package/dist/utils/helpers.d.ts +11 -0
- package/dist/utils/helpers.js +76 -0
- package/dist/utils/inspection.d.ts +14 -0
- package/dist/utils/inspection.js +388 -0
- package/dist/utils/optimization.d.ts +3 -0
- package/dist/utils/optimization.js +636 -0
- package/dist/utils/reconstitution.d.ts +3 -0
- package/dist/utils/reconstitution.js +266 -0
- package/dist/utils/types.d.ts +41 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/zstd.d.ts +17 -0
- package/dist/utils/zstd.js +118 -0
- package/package.json +1 -1
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import encode from 'png-chunks-encode';
|
|
6
|
+
import extract from 'png-chunks-extract';
|
|
7
|
+
import * as zlib from 'zlib';
|
|
8
|
+
import { PNG_HEADER, PNG_HEADER_HEX } from './constants.js';
|
|
9
|
+
export async function optimizePngBuffer(pngBuf, fast = false) {
|
|
10
|
+
const runCommandAsync = (cmd, args, timeout = 120000) => {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
try {
|
|
13
|
+
const child = spawn(cmd, args, { windowsHide: true, stdio: 'ignore' });
|
|
14
|
+
let killed = false;
|
|
15
|
+
const to = setTimeout(() => {
|
|
16
|
+
killed = true;
|
|
17
|
+
try {
|
|
18
|
+
child.kill('SIGTERM');
|
|
19
|
+
}
|
|
20
|
+
catch (e) { }
|
|
21
|
+
}, timeout);
|
|
22
|
+
child.on('close', (code) => {
|
|
23
|
+
clearTimeout(to);
|
|
24
|
+
if (killed)
|
|
25
|
+
resolve({ error: new Error('timeout') });
|
|
26
|
+
else
|
|
27
|
+
resolve({ code: code ?? 0 });
|
|
28
|
+
});
|
|
29
|
+
child.on('error', (err) => {
|
|
30
|
+
clearTimeout(to);
|
|
31
|
+
resolve({ error: err });
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
resolve({ error: err });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
const inPath = join(tmpdir(), `rox_zop_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
|
|
41
|
+
const outPath = inPath + '.out.png';
|
|
42
|
+
writeFileSync(inPath, pngBuf);
|
|
43
|
+
const iterations = fast ? 15 : 40;
|
|
44
|
+
const args = [
|
|
45
|
+
'-y',
|
|
46
|
+
`--iterations=${iterations}`,
|
|
47
|
+
'--filters=01234mepb',
|
|
48
|
+
inPath,
|
|
49
|
+
outPath,
|
|
50
|
+
];
|
|
51
|
+
const res = await runCommandAsync('zopflipng', args, 120000);
|
|
52
|
+
if (!res.error && existsSync(outPath)) {
|
|
53
|
+
const outBuf = readFileSync(outPath);
|
|
54
|
+
try {
|
|
55
|
+
unlinkSync(inPath);
|
|
56
|
+
unlinkSync(outPath);
|
|
57
|
+
}
|
|
58
|
+
catch (e) { }
|
|
59
|
+
return outBuf.length < pngBuf.length ? outBuf : pngBuf;
|
|
60
|
+
}
|
|
61
|
+
if (fast)
|
|
62
|
+
return pngBuf;
|
|
63
|
+
}
|
|
64
|
+
catch (e) { }
|
|
65
|
+
try {
|
|
66
|
+
const chunksRaw = extract(pngBuf);
|
|
67
|
+
const ihdr = chunksRaw.find((c) => c.name === 'IHDR');
|
|
68
|
+
if (!ihdr)
|
|
69
|
+
return pngBuf;
|
|
70
|
+
const ihdrData = Buffer.isBuffer(ihdr.data)
|
|
71
|
+
? ihdr.data
|
|
72
|
+
: Buffer.from(ihdr.data);
|
|
73
|
+
const width = ihdrData.readUInt32BE(0);
|
|
74
|
+
const height = ihdrData.readUInt32BE(4);
|
|
75
|
+
const bitDepth = ihdrData[8];
|
|
76
|
+
const colorType = ihdrData[9];
|
|
77
|
+
if (bitDepth !== 8 || colorType !== 2)
|
|
78
|
+
return pngBuf;
|
|
79
|
+
const idatChunks = chunksRaw.filter((c) => c.name === 'IDAT');
|
|
80
|
+
const idatData = Buffer.concat(idatChunks.map((c) => Buffer.isBuffer(c.data)
|
|
81
|
+
? c.data
|
|
82
|
+
: Buffer.from(c.data)));
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = zlib.inflateSync(idatData);
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
return pngBuf;
|
|
89
|
+
}
|
|
90
|
+
const bytesPerPixel = 3;
|
|
91
|
+
const rowBytes = width * bytesPerPixel;
|
|
92
|
+
const inRowLen = rowBytes + 1;
|
|
93
|
+
if (raw.length !== inRowLen * height)
|
|
94
|
+
return pngBuf;
|
|
95
|
+
function paethPredict(a, b, c) {
|
|
96
|
+
const p = a + b - c;
|
|
97
|
+
const pa = Math.abs(p - a);
|
|
98
|
+
const pb = Math.abs(p - b);
|
|
99
|
+
const pc = Math.abs(p - c);
|
|
100
|
+
if (pa <= pb && pa <= pc)
|
|
101
|
+
return a;
|
|
102
|
+
if (pb <= pc)
|
|
103
|
+
return b;
|
|
104
|
+
return c;
|
|
105
|
+
}
|
|
106
|
+
const outRows = [];
|
|
107
|
+
let prevRow = null;
|
|
108
|
+
for (let y = 0; y < height; y++) {
|
|
109
|
+
const rowStart = y * inRowLen + 1;
|
|
110
|
+
const row = raw.slice(rowStart, rowStart + rowBytes);
|
|
111
|
+
let bestSum = Infinity;
|
|
112
|
+
let bestFiltered = null;
|
|
113
|
+
for (let f = 0; f <= 4; f++) {
|
|
114
|
+
const filtered = Buffer.alloc(rowBytes);
|
|
115
|
+
let sum = 0;
|
|
116
|
+
for (let i = 0; i < rowBytes; i++) {
|
|
117
|
+
const val = row[i];
|
|
118
|
+
let outv = 0;
|
|
119
|
+
const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
|
|
120
|
+
const up = prevRow ? prevRow[i] : 0;
|
|
121
|
+
const upLeft = prevRow && i - bytesPerPixel >= 0 ? prevRow[i - bytesPerPixel] : 0;
|
|
122
|
+
if (f === 0) {
|
|
123
|
+
outv = val;
|
|
124
|
+
}
|
|
125
|
+
else if (f === 1) {
|
|
126
|
+
outv = (val - left + 256) & 0xff;
|
|
127
|
+
}
|
|
128
|
+
else if (f === 2) {
|
|
129
|
+
outv = (val - up + 256) & 0xff;
|
|
130
|
+
}
|
|
131
|
+
else if (f === 3) {
|
|
132
|
+
const avg = Math.floor((left + up) / 2);
|
|
133
|
+
outv = (val - avg + 256) & 0xff;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const p = paethPredict(left, up, upLeft);
|
|
137
|
+
outv = (val - p + 256) & 0xff;
|
|
138
|
+
}
|
|
139
|
+
filtered[i] = outv;
|
|
140
|
+
const signed = outv > 127 ? outv - 256 : outv;
|
|
141
|
+
sum += Math.abs(signed);
|
|
142
|
+
}
|
|
143
|
+
if (sum < bestSum) {
|
|
144
|
+
bestSum = sum;
|
|
145
|
+
bestFiltered = filtered;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const rowBuf = Buffer.alloc(1 + rowBytes);
|
|
149
|
+
let chosenFilter = 0;
|
|
150
|
+
for (let f = 0; f <= 4; f++) {
|
|
151
|
+
const filtered = Buffer.alloc(rowBytes);
|
|
152
|
+
for (let i = 0; i < rowBytes; i++) {
|
|
153
|
+
const val = row[i];
|
|
154
|
+
const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
|
|
155
|
+
const up = prevRow ? prevRow[i] : 0;
|
|
156
|
+
const upLeft = prevRow && i - bytesPerPixel >= 0 ? prevRow[i - bytesPerPixel] : 0;
|
|
157
|
+
if (f === 0)
|
|
158
|
+
filtered[i] = val;
|
|
159
|
+
else if (f === 1)
|
|
160
|
+
filtered[i] = (val - left + 256) & 0xff;
|
|
161
|
+
else if (f === 2)
|
|
162
|
+
filtered[i] = (val - up + 256) & 0xff;
|
|
163
|
+
else if (f === 3)
|
|
164
|
+
filtered[i] = (val - Math.floor((left + up) / 2) + 256) & 0xff;
|
|
165
|
+
else
|
|
166
|
+
filtered[i] = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
|
|
167
|
+
}
|
|
168
|
+
if (filtered.equals(bestFiltered)) {
|
|
169
|
+
chosenFilter = f;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
rowBuf[0] = chosenFilter;
|
|
174
|
+
bestFiltered.copy(rowBuf, 1);
|
|
175
|
+
outRows.push(rowBuf);
|
|
176
|
+
prevRow = row;
|
|
177
|
+
}
|
|
178
|
+
const filteredAll = Buffer.concat(outRows);
|
|
179
|
+
const compressed = zlib.deflateSync(filteredAll, {
|
|
180
|
+
level: 9,
|
|
181
|
+
memLevel: 9,
|
|
182
|
+
strategy: zlib.constants.Z_DEFAULT_STRATEGY,
|
|
183
|
+
});
|
|
184
|
+
const newChunks = [];
|
|
185
|
+
for (const c of chunksRaw) {
|
|
186
|
+
if (c.name === 'IDAT')
|
|
187
|
+
continue;
|
|
188
|
+
newChunks.push({
|
|
189
|
+
name: c.name,
|
|
190
|
+
data: Buffer.isBuffer(c.data)
|
|
191
|
+
? c.data
|
|
192
|
+
: Buffer.from(c.data),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const iendIndex = newChunks.findIndex((c) => c.name === 'IEND');
|
|
196
|
+
const insertIndex = iendIndex >= 0 ? iendIndex : newChunks.length;
|
|
197
|
+
newChunks.splice(insertIndex, 0, { name: 'IDAT', data: compressed });
|
|
198
|
+
function ensurePng(buf) {
|
|
199
|
+
return buf.slice(0, 8).toString('hex') === PNG_HEADER_HEX
|
|
200
|
+
? buf
|
|
201
|
+
: Buffer.concat([PNG_HEADER, buf]);
|
|
202
|
+
}
|
|
203
|
+
const out = ensurePng(Buffer.from(encode(newChunks)));
|
|
204
|
+
let bestBuf = out.length < pngBuf.length ? out : pngBuf;
|
|
205
|
+
const strategies = [
|
|
206
|
+
zlib.constants.Z_DEFAULT_STRATEGY,
|
|
207
|
+
zlib.constants.Z_FILTERED,
|
|
208
|
+
zlib.constants.Z_RLE,
|
|
209
|
+
...(zlib.constants.Z_HUFFMAN_ONLY ? [zlib.constants.Z_HUFFMAN_ONLY] : []),
|
|
210
|
+
...(zlib.constants.Z_FIXED ? [zlib.constants.Z_FIXED] : []),
|
|
211
|
+
];
|
|
212
|
+
for (const strat of strategies) {
|
|
213
|
+
try {
|
|
214
|
+
const comp = zlib.deflateSync(raw, {
|
|
215
|
+
level: 9,
|
|
216
|
+
memLevel: 9,
|
|
217
|
+
strategy: strat,
|
|
218
|
+
});
|
|
219
|
+
const altChunks = newChunks.map((c) => ({
|
|
220
|
+
name: c.name,
|
|
221
|
+
data: c.data,
|
|
222
|
+
}));
|
|
223
|
+
const idx = altChunks.findIndex((c) => c.name === 'IDAT');
|
|
224
|
+
if (idx !== -1)
|
|
225
|
+
altChunks[idx] = { name: 'IDAT', data: comp };
|
|
226
|
+
const candidate = ensurePng(Buffer.from(encode(altChunks)));
|
|
227
|
+
if (candidate.length < bestBuf.length)
|
|
228
|
+
bestBuf = candidate;
|
|
229
|
+
}
|
|
230
|
+
catch (e) { }
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const fflate = await import('fflate');
|
|
234
|
+
const fflateDeflateSync = fflate.deflateSync;
|
|
235
|
+
try {
|
|
236
|
+
const comp = fflateDeflateSync(filteredAll);
|
|
237
|
+
const altChunks = newChunks.map((c) => ({
|
|
238
|
+
name: c.name,
|
|
239
|
+
data: c.data,
|
|
240
|
+
}));
|
|
241
|
+
const idx = altChunks.findIndex((c) => c.name === 'IDAT');
|
|
242
|
+
if (idx !== -1)
|
|
243
|
+
altChunks[idx] = { name: 'IDAT', data: Buffer.from(comp) };
|
|
244
|
+
const candidate = ensurePng(Buffer.from(encode(altChunks)));
|
|
245
|
+
if (candidate.length < bestBuf.length)
|
|
246
|
+
bestBuf = candidate;
|
|
247
|
+
}
|
|
248
|
+
catch (e) { }
|
|
249
|
+
}
|
|
250
|
+
catch (e) { }
|
|
251
|
+
const windowBitsOpts = [15, 12, 9];
|
|
252
|
+
const memLevelOpts = [9, 8];
|
|
253
|
+
for (let f = 0; f <= 4; f++) {
|
|
254
|
+
try {
|
|
255
|
+
const filteredAllGlobalRows = [];
|
|
256
|
+
let prevRowG = null;
|
|
257
|
+
for (let y = 0; y < height; y++) {
|
|
258
|
+
const row = raw.slice(y * inRowLen + 1, y * inRowLen + 1 + rowBytes);
|
|
259
|
+
const filtered = Buffer.alloc(rowBytes);
|
|
260
|
+
for (let i = 0; i < rowBytes; i++) {
|
|
261
|
+
const val = row[i];
|
|
262
|
+
const left = i - bytesPerPixel >= 0 ? row[i - bytesPerPixel] : 0;
|
|
263
|
+
const up = prevRowG ? prevRowG[i] : 0;
|
|
264
|
+
const upLeft = prevRowG && i - bytesPerPixel >= 0
|
|
265
|
+
? prevRowG[i - bytesPerPixel]
|
|
266
|
+
: 0;
|
|
267
|
+
if (f === 0)
|
|
268
|
+
filtered[i] = val;
|
|
269
|
+
else if (f === 1)
|
|
270
|
+
filtered[i] = (val - left + 256) & 0xff;
|
|
271
|
+
else if (f === 2)
|
|
272
|
+
filtered[i] = (val - up + 256) & 0xff;
|
|
273
|
+
else if (f === 3)
|
|
274
|
+
filtered[i] = (val - Math.floor((left + up) / 2) + 256) & 0xff;
|
|
275
|
+
else
|
|
276
|
+
filtered[i] = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
|
|
277
|
+
}
|
|
278
|
+
const rowBuf = Buffer.alloc(1 + rowBytes);
|
|
279
|
+
rowBuf[0] = f;
|
|
280
|
+
filtered.copy(rowBuf, 1);
|
|
281
|
+
filteredAllGlobalRows.push(rowBuf);
|
|
282
|
+
prevRowG = row;
|
|
283
|
+
}
|
|
284
|
+
const filteredAllGlobal = Buffer.concat(filteredAllGlobalRows);
|
|
285
|
+
for (const strat2 of strategies) {
|
|
286
|
+
for (const wb of windowBitsOpts) {
|
|
287
|
+
for (const ml of memLevelOpts) {
|
|
288
|
+
try {
|
|
289
|
+
const comp = zlib.deflateSync(filteredAllGlobal, {
|
|
290
|
+
level: 9,
|
|
291
|
+
memLevel: ml,
|
|
292
|
+
strategy: strat2,
|
|
293
|
+
windowBits: wb,
|
|
294
|
+
});
|
|
295
|
+
const altChunks = newChunks.map((c) => ({
|
|
296
|
+
name: c.name,
|
|
297
|
+
data: c.data,
|
|
298
|
+
}));
|
|
299
|
+
const idx = altChunks.findIndex((c) => c.name === 'IDAT');
|
|
300
|
+
if (idx !== -1)
|
|
301
|
+
altChunks[idx] = { name: 'IDAT', data: comp };
|
|
302
|
+
const candidate = ensurePng(Buffer.from(encode(altChunks)));
|
|
303
|
+
if (candidate.length < bestBuf.length)
|
|
304
|
+
bestBuf = candidate;
|
|
305
|
+
}
|
|
306
|
+
catch (e) { }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (e) { }
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const zopIterations = [1000, 2000];
|
|
315
|
+
zopIterations.push(5000, 10000, 20000);
|
|
316
|
+
for (const iters of zopIterations) {
|
|
317
|
+
try {
|
|
318
|
+
const zIn = join(tmpdir(), `rox_zop_in_${Date.now()}_${Math.random()
|
|
319
|
+
.toString(36)
|
|
320
|
+
.slice(2)}.png`);
|
|
321
|
+
const zOut = zIn + '.out.png';
|
|
322
|
+
writeFileSync(zIn, bestBuf);
|
|
323
|
+
const args2 = [
|
|
324
|
+
'-y',
|
|
325
|
+
`--iterations=${iters}`,
|
|
326
|
+
'--filters=01234mepb',
|
|
327
|
+
zIn,
|
|
328
|
+
zOut,
|
|
329
|
+
];
|
|
330
|
+
try {
|
|
331
|
+
const r2 = await runCommandAsync('zopflipng', args2, 240000);
|
|
332
|
+
if (!r2.error && existsSync(zOut)) {
|
|
333
|
+
const zbuf = readFileSync(zOut);
|
|
334
|
+
try {
|
|
335
|
+
unlinkSync(zIn);
|
|
336
|
+
unlinkSync(zOut);
|
|
337
|
+
}
|
|
338
|
+
catch (e) { }
|
|
339
|
+
if (zbuf.length < bestBuf.length)
|
|
340
|
+
bestBuf = zbuf;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (e) { }
|
|
344
|
+
}
|
|
345
|
+
catch (e) { }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (e) { }
|
|
349
|
+
try {
|
|
350
|
+
const advIn = join(tmpdir(), `rox_adv_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
|
|
351
|
+
writeFileSync(advIn, bestBuf);
|
|
352
|
+
const rAdv = spawnSync('advdef', ['-z4', '-i10', advIn], {
|
|
353
|
+
windowsHide: true,
|
|
354
|
+
stdio: 'ignore',
|
|
355
|
+
timeout: 120000,
|
|
356
|
+
});
|
|
357
|
+
if (!rAdv.error && existsSync(advIn)) {
|
|
358
|
+
const advBuf = readFileSync(advIn);
|
|
359
|
+
try {
|
|
360
|
+
unlinkSync(advIn);
|
|
361
|
+
}
|
|
362
|
+
catch (e) { }
|
|
363
|
+
if (advBuf.length < bestBuf.length)
|
|
364
|
+
bestBuf = advBuf;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (e) { }
|
|
368
|
+
for (const strat of strategies) {
|
|
369
|
+
try {
|
|
370
|
+
const comp = zlib.deflateSync(filteredAll, {
|
|
371
|
+
level: 9,
|
|
372
|
+
memLevel: 9,
|
|
373
|
+
strategy: strat,
|
|
374
|
+
});
|
|
375
|
+
const altChunks = newChunks.map((c) => ({
|
|
376
|
+
name: c.name,
|
|
377
|
+
data: c.data,
|
|
378
|
+
}));
|
|
379
|
+
const idx = altChunks.findIndex((c) => c.name === 'IDAT');
|
|
380
|
+
if (idx !== -1)
|
|
381
|
+
altChunks[idx] = { name: 'IDAT', data: comp };
|
|
382
|
+
const candidate = ensurePng(Buffer.from(encode(altChunks)));
|
|
383
|
+
if (candidate.length < bestBuf.length)
|
|
384
|
+
bestBuf = candidate;
|
|
385
|
+
}
|
|
386
|
+
catch (e) { }
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const pixels = Buffer.alloc(width * height * 3);
|
|
390
|
+
let prev = null;
|
|
391
|
+
for (let y = 0; y < height; y++) {
|
|
392
|
+
const f = raw[y * inRowLen];
|
|
393
|
+
const row = raw.slice(y * inRowLen + 1, y * inRowLen + 1 + rowBytes);
|
|
394
|
+
const recon = Buffer.alloc(rowBytes);
|
|
395
|
+
for (let i = 0; i < rowBytes; i++) {
|
|
396
|
+
const left = i - 3 >= 0 ? recon[i - 3] : 0;
|
|
397
|
+
const up = prev ? prev[i] : 0;
|
|
398
|
+
const upLeft = prev && i - 3 >= 0 ? prev[i - 3] : 0;
|
|
399
|
+
let v = row[i];
|
|
400
|
+
if (f === 0) {
|
|
401
|
+
}
|
|
402
|
+
else if (f === 1)
|
|
403
|
+
v = (v + left) & 0xff;
|
|
404
|
+
else if (f === 2)
|
|
405
|
+
v = (v + up) & 0xff;
|
|
406
|
+
else if (f === 3)
|
|
407
|
+
v = (v + Math.floor((left + up) / 2)) & 0xff;
|
|
408
|
+
else
|
|
409
|
+
v = (v + paethPredict(left, up, upLeft)) & 0xff;
|
|
410
|
+
recon[i] = v;
|
|
411
|
+
}
|
|
412
|
+
recon.copy(pixels, y * rowBytes);
|
|
413
|
+
prev = recon;
|
|
414
|
+
}
|
|
415
|
+
const paletteMap = new Map();
|
|
416
|
+
const palette = [];
|
|
417
|
+
for (let i = 0; i < pixels.length; i += 3) {
|
|
418
|
+
const key = `${pixels[i]},${pixels[i + 1]},${pixels[i + 2]}`;
|
|
419
|
+
if (!paletteMap.has(key)) {
|
|
420
|
+
paletteMap.set(key, paletteMap.size);
|
|
421
|
+
palette.push(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
422
|
+
if (paletteMap.size > 256)
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (paletteMap.size <= 256) {
|
|
427
|
+
const idxRowLen = 1 + width * 1;
|
|
428
|
+
const idxRows = [];
|
|
429
|
+
for (let y = 0; y < height; y++) {
|
|
430
|
+
const rowIdx = Buffer.alloc(width);
|
|
431
|
+
for (let x = 0; x < width; x++) {
|
|
432
|
+
const pos = (y * width + x) * 3;
|
|
433
|
+
const key = `${pixels[pos]},${pixels[pos + 1]},${pixels[pos + 2]}`;
|
|
434
|
+
rowIdx[x] = paletteMap.get(key);
|
|
435
|
+
}
|
|
436
|
+
let bestRowFilter = 0;
|
|
437
|
+
let bestRowSum = Infinity;
|
|
438
|
+
let bestRowFiltered = null;
|
|
439
|
+
for (let f = 0; f <= 4; f++) {
|
|
440
|
+
const filteredRow = Buffer.alloc(width);
|
|
441
|
+
let sum = 0;
|
|
442
|
+
for (let i = 0; i < width; i++) {
|
|
443
|
+
const val = rowIdx[i];
|
|
444
|
+
let outv = 0;
|
|
445
|
+
const left = i - 1 >= 0 ? rowIdx[i - 1] : 0;
|
|
446
|
+
const up = y > 0 ? idxRows[y - 1][i] : 0;
|
|
447
|
+
const upLeft = y > 0 && i - 1 >= 0 ? idxRows[y - 1][i - 1] : 0;
|
|
448
|
+
if (f === 0)
|
|
449
|
+
outv = val;
|
|
450
|
+
else if (f === 1)
|
|
451
|
+
outv = (val - left + 256) & 0xff;
|
|
452
|
+
else if (f === 2)
|
|
453
|
+
outv = (val - up + 256) & 0xff;
|
|
454
|
+
else if (f === 3)
|
|
455
|
+
outv = (val - Math.floor((left + up) / 2) + 256) & 0xff;
|
|
456
|
+
else
|
|
457
|
+
outv = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
|
|
458
|
+
filteredRow[i] = outv;
|
|
459
|
+
const signed = outv > 127 ? outv - 256 : outv;
|
|
460
|
+
sum += Math.abs(signed);
|
|
461
|
+
}
|
|
462
|
+
if (sum < bestRowSum) {
|
|
463
|
+
bestRowSum = sum;
|
|
464
|
+
bestRowFilter = f;
|
|
465
|
+
bestRowFiltered = filteredRow;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const rowBuf = Buffer.alloc(idxRowLen);
|
|
469
|
+
rowBuf[0] = bestRowFilter;
|
|
470
|
+
bestRowFiltered.copy(rowBuf, 1);
|
|
471
|
+
idxRows.push(rowBuf);
|
|
472
|
+
}
|
|
473
|
+
const freqMap = new Map();
|
|
474
|
+
for (let i = 0; i < pixels.length; i += 3) {
|
|
475
|
+
const key = `${pixels[i]},${pixels[i + 1]},${pixels[i + 2]}`;
|
|
476
|
+
freqMap.set(key, (freqMap.get(key) || 0) + 1);
|
|
477
|
+
}
|
|
478
|
+
const paletteVariants = [];
|
|
479
|
+
paletteVariants.push({
|
|
480
|
+
paletteArr: palette.slice(),
|
|
481
|
+
map: new Map(paletteMap),
|
|
482
|
+
});
|
|
483
|
+
const freqSorted = Array.from(freqMap.entries()).sort((a, b) => b[1] - a[1]);
|
|
484
|
+
if (freqSorted.length > 0) {
|
|
485
|
+
const pal2 = [];
|
|
486
|
+
const map2 = new Map();
|
|
487
|
+
let pi = 0;
|
|
488
|
+
for (const [k] of freqSorted) {
|
|
489
|
+
const parts = k.split(',').map((s) => Number(s));
|
|
490
|
+
pal2.push(parts[0], parts[1], parts[2]);
|
|
491
|
+
map2.set(k, pi++);
|
|
492
|
+
if (pi >= 256)
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
if (map2.size <= 256)
|
|
496
|
+
paletteVariants.push({ paletteArr: pal2, map: map2 });
|
|
497
|
+
}
|
|
498
|
+
for (const variant of paletteVariants) {
|
|
499
|
+
const pSize = variant.map.size;
|
|
500
|
+
const bitDepth = pSize <= 2 ? 1 : pSize <= 4 ? 2 : pSize <= 16 ? 4 : 8;
|
|
501
|
+
const idxRowsVar = [];
|
|
502
|
+
for (let y = 0; y < height; y++) {
|
|
503
|
+
const rowIdx = Buffer.alloc(width);
|
|
504
|
+
for (let x = 0; x < width; x++) {
|
|
505
|
+
const pos = (y * width + x) * 3;
|
|
506
|
+
const key = `${pixels[pos]},${pixels[pos + 1]},${pixels[pos + 2]}`;
|
|
507
|
+
rowIdx[x] = variant.map.get(key);
|
|
508
|
+
}
|
|
509
|
+
idxRowsVar.push(rowIdx);
|
|
510
|
+
}
|
|
511
|
+
function packRowIndices(rowIdx, bitDepth) {
|
|
512
|
+
if (bitDepth === 8)
|
|
513
|
+
return rowIdx;
|
|
514
|
+
const bitsPerRow = width * bitDepth;
|
|
515
|
+
const outLen = Math.ceil(bitsPerRow / 8);
|
|
516
|
+
const out = Buffer.alloc(outLen);
|
|
517
|
+
let bitPos = 0;
|
|
518
|
+
for (let i = 0; i < width; i++) {
|
|
519
|
+
const val = rowIdx[i] & ((1 << bitDepth) - 1);
|
|
520
|
+
for (let b = 0; b < bitDepth; b++) {
|
|
521
|
+
const bit = (val >> (bitDepth - 1 - b)) & 1;
|
|
522
|
+
const byteIdx = Math.floor(bitPos / 8);
|
|
523
|
+
const shift = 7 - (bitPos % 8);
|
|
524
|
+
out[byteIdx] |= bit << shift;
|
|
525
|
+
bitPos++;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
const packedRows = [];
|
|
531
|
+
for (let y = 0; y < height; y++) {
|
|
532
|
+
const packed = packRowIndices(idxRowsVar[y], bitDepth);
|
|
533
|
+
let bestRowFilter = 0;
|
|
534
|
+
let bestRowSum = Infinity;
|
|
535
|
+
let bestRowFiltered = null;
|
|
536
|
+
for (let f = 0; f <= 4; f++) {
|
|
537
|
+
const filteredRow = Buffer.alloc(packed.length);
|
|
538
|
+
let sum = 0;
|
|
539
|
+
for (let i = 0; i < packed.length; i++) {
|
|
540
|
+
const val = packed[i];
|
|
541
|
+
const left = i - 1 >= 0 ? packed[i - 1] : 0;
|
|
542
|
+
const up = y > 0 ? packedRows[y - 1][i] : 0;
|
|
543
|
+
const upLeft = y > 0 && i - 1 >= 0 ? packedRows[y - 1][i - 1] : 0;
|
|
544
|
+
let outv = 0;
|
|
545
|
+
if (f === 0)
|
|
546
|
+
outv = val;
|
|
547
|
+
else if (f === 1)
|
|
548
|
+
outv = (val - left + 256) & 0xff;
|
|
549
|
+
else if (f === 2)
|
|
550
|
+
outv = (val - up + 256) & 0xff;
|
|
551
|
+
else if (f === 3)
|
|
552
|
+
outv = (val - Math.floor((left + up) / 2) + 256) & 0xff;
|
|
553
|
+
else
|
|
554
|
+
outv = (val - paethPredict(left, up, upLeft) + 256) & 0xff;
|
|
555
|
+
filteredRow[i] = outv;
|
|
556
|
+
const signed = outv > 127 ? outv - 256 : outv;
|
|
557
|
+
sum += Math.abs(signed);
|
|
558
|
+
}
|
|
559
|
+
if (sum < bestRowSum) {
|
|
560
|
+
bestRowSum = sum;
|
|
561
|
+
bestRowFilter = f;
|
|
562
|
+
bestRowFiltered = filteredRow;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const rowBuf = Buffer.alloc(1 + packed.length);
|
|
566
|
+
rowBuf[0] = bestRowFilter;
|
|
567
|
+
bestRowFiltered.copy(rowBuf, 1);
|
|
568
|
+
packedRows.push(rowBuf);
|
|
569
|
+
}
|
|
570
|
+
const idxFilteredAllVar = Buffer.concat(packedRows);
|
|
571
|
+
const palettesBufVar = Buffer.from(variant.paletteArr);
|
|
572
|
+
const palChunksVar = [];
|
|
573
|
+
const ihdr = Buffer.alloc(13);
|
|
574
|
+
ihdr.writeUInt32BE(width, 0);
|
|
575
|
+
ihdr.writeUInt32BE(height, 4);
|
|
576
|
+
ihdr[8] = bitDepth;
|
|
577
|
+
ihdr[9] = 3;
|
|
578
|
+
ihdr[10] = 0;
|
|
579
|
+
ihdr[11] = 0;
|
|
580
|
+
ihdr[12] = 0;
|
|
581
|
+
palChunksVar.push({ name: 'IHDR', data: ihdr });
|
|
582
|
+
palChunksVar.push({ name: 'PLTE', data: palettesBufVar });
|
|
583
|
+
palChunksVar.push({
|
|
584
|
+
name: 'IDAT',
|
|
585
|
+
data: zlib.deflateSync(idxFilteredAllVar, { level: 9 }),
|
|
586
|
+
});
|
|
587
|
+
palChunksVar.push({ name: 'IEND', data: Buffer.alloc(0) });
|
|
588
|
+
const palOutVar = ensurePng(Buffer.from(encode(palChunksVar)));
|
|
589
|
+
if (palOutVar.length < bestBuf.length)
|
|
590
|
+
bestBuf = palOutVar;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch (e) { }
|
|
595
|
+
const externalAttempts = [
|
|
596
|
+
{ cmd: 'oxipng', args: ['-o', '6', '--strip', 'all'] },
|
|
597
|
+
{ cmd: 'optipng', args: ['-o7'] },
|
|
598
|
+
{ cmd: 'pngcrush', args: ['-brute', '-reduce'] },
|
|
599
|
+
{ cmd: 'pngout', args: [] },
|
|
600
|
+
];
|
|
601
|
+
for (const tool of externalAttempts) {
|
|
602
|
+
try {
|
|
603
|
+
const tIn = join(tmpdir(), `rox_ext_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
|
|
604
|
+
const tOut = tIn + '.out.png';
|
|
605
|
+
writeFileSync(tIn, bestBuf);
|
|
606
|
+
const args = tool.args.concat([tIn, tOut]);
|
|
607
|
+
const r = spawnSync(tool.cmd, args, {
|
|
608
|
+
windowsHide: true,
|
|
609
|
+
stdio: 'ignore',
|
|
610
|
+
timeout: 240000,
|
|
611
|
+
});
|
|
612
|
+
if (!r.error && existsSync(tOut)) {
|
|
613
|
+
const outb = readFileSync(tOut);
|
|
614
|
+
try {
|
|
615
|
+
unlinkSync(tIn);
|
|
616
|
+
unlinkSync(tOut);
|
|
617
|
+
}
|
|
618
|
+
catch (e) { }
|
|
619
|
+
if (outb.length < bestBuf.length)
|
|
620
|
+
bestBuf = outb;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
try {
|
|
624
|
+
unlinkSync(tIn);
|
|
625
|
+
}
|
|
626
|
+
catch (e) { }
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch (e) { }
|
|
630
|
+
}
|
|
631
|
+
return bestBuf;
|
|
632
|
+
}
|
|
633
|
+
catch (e) {
|
|
634
|
+
return pngBuf;
|
|
635
|
+
}
|
|
636
|
+
}
|