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/README.md
CHANGED
|
@@ -103,3 +103,15 @@ const pngBuffer = await encodeBinaryToPng(inputBuffer, {
|
|
|
103
103
|
## License
|
|
104
104
|
|
|
105
105
|
This package is proprietary (UNLICENSED). The repository remains private; the package is published to npm for distribution. If there is significant community interest, it may be open-sourced in the future.
|
|
106
|
+
|
|
107
|
+
## Minimal PNG container (minpng) 🔧
|
|
108
|
+
|
|
109
|
+
This library includes a compact encoder/decoder that targets the smallest possible PNG container while guaranteeing pixel-perfect recovery from screenshots when no lossy filtering is applied.
|
|
110
|
+
|
|
111
|
+
- Inputs must be raw RGB 8-bit buffers (no alpha).
|
|
112
|
+
- Transformations used: Paeth 2D predictor (left/top), RGB decorrelation (G, R−G, B−G), zigzag traversal.
|
|
113
|
+
- Compression: Zstd at maximum compression level.
|
|
114
|
+
- Output: neutral PNG (no ICC/gamma/alpha), all data mapped to RGB bytes only.
|
|
115
|
+
- The decoder searches for start/end markers and a compact header embedded in pixels to perform a reliable, deterministic roundtrip.
|
|
116
|
+
|
|
117
|
+
Use `encodeMinPng(rgbBuf, width, height)` and `decodeMinPng(pngBuf)` from the public API (`roxify`).
|
package/dist/cli.js
CHANGED
|
@@ -145,29 +145,55 @@ async function encodeCommand(args) {
|
|
|
145
145
|
try {
|
|
146
146
|
let inputBuffer;
|
|
147
147
|
let displayName;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
step: 'Reading files',
|
|
165
|
-
mbValue: (readBytes / 1024 / 1024).toFixed(2),
|
|
148
|
+
const encodeBar = new cliProgress.SingleBar({
|
|
149
|
+
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
150
|
+
}, cliProgress.Presets.shades_classic);
|
|
151
|
+
let barStarted = false;
|
|
152
|
+
const startEncode = Date.now();
|
|
153
|
+
let currentEncodeStep = 'Starting';
|
|
154
|
+
let displayedPct = 0;
|
|
155
|
+
let targetPct = 0;
|
|
156
|
+
const TICK_MS = 100;
|
|
157
|
+
const PCT_STEP = 1;
|
|
158
|
+
const encodeHeartbeat = setInterval(() => {
|
|
159
|
+
const elapsed = Date.now() - startEncode;
|
|
160
|
+
if (!barStarted) {
|
|
161
|
+
encodeBar.start(100, Math.floor(displayedPct), {
|
|
162
|
+
step: currentEncodeStep,
|
|
163
|
+
elapsed: '0',
|
|
166
164
|
});
|
|
167
|
-
|
|
165
|
+
barStarted = true;
|
|
166
|
+
}
|
|
167
|
+
if (displayedPct < targetPct) {
|
|
168
|
+
displayedPct = Math.min(displayedPct + PCT_STEP, targetPct);
|
|
169
|
+
}
|
|
170
|
+
else if (displayedPct < 99) {
|
|
171
|
+
displayedPct = Math.min(displayedPct + PCT_STEP, 99);
|
|
172
|
+
}
|
|
173
|
+
encodeBar.update(Math.floor(displayedPct), {
|
|
174
|
+
step: currentEncodeStep,
|
|
175
|
+
elapsed: String(Math.floor(elapsed / 1000)),
|
|
176
|
+
});
|
|
177
|
+
}, TICK_MS);
|
|
178
|
+
let totalBytes = 0;
|
|
179
|
+
let lastShownFile;
|
|
180
|
+
const onProgress = (readBytes, total, currentFile) => {
|
|
181
|
+
if (totalBytes === 0)
|
|
182
|
+
totalBytes = total;
|
|
183
|
+
const packPct = Math.floor((readBytes / totalBytes) * 25);
|
|
184
|
+
targetPct = Math.max(targetPct, packPct);
|
|
185
|
+
if (currentFile && currentFile !== lastShownFile) {
|
|
186
|
+
lastShownFile = currentFile;
|
|
187
|
+
}
|
|
188
|
+
currentEncodeStep = currentFile
|
|
189
|
+
? `Reading files: ${currentFile}`
|
|
190
|
+
: 'Reading files';
|
|
191
|
+
};
|
|
192
|
+
if (inputPaths.length > 1) {
|
|
193
|
+
currentEncodeStep = 'Reading files';
|
|
168
194
|
const packResult = packPaths(inputPaths, undefined, onProgress);
|
|
169
|
-
bar.stop();
|
|
170
195
|
inputBuffer = packResult.buf;
|
|
196
|
+
console.log('');
|
|
171
197
|
console.log(`Packed ${packResult.list.length} files -> ${(inputBuffer.length /
|
|
172
198
|
1024 /
|
|
173
199
|
1024).toFixed(2)} MB`);
|
|
@@ -180,8 +206,10 @@ async function encodeCommand(args) {
|
|
|
180
206
|
const st = statSync(resolvedInput);
|
|
181
207
|
if (st.isDirectory()) {
|
|
182
208
|
console.log(`Packing directory...`);
|
|
183
|
-
|
|
209
|
+
currentEncodeStep = 'Reading files';
|
|
210
|
+
const packResult = packPaths([resolvedInput], resolvedInput, onProgress);
|
|
184
211
|
inputBuffer = packResult.buf;
|
|
212
|
+
console.log('');
|
|
185
213
|
console.log(`Packed ${packResult.list.length} files -> ${(inputBuffer.length /
|
|
186
214
|
1024 /
|
|
187
215
|
1024).toFixed(2)} MB`);
|
|
@@ -208,72 +236,68 @@ async function encodeCommand(args) {
|
|
|
208
236
|
options.encrypt = parsed.encrypt || 'aes';
|
|
209
237
|
}
|
|
210
238
|
console.log(`Encoding ${displayName} -> ${resolvedOutput}\n`);
|
|
211
|
-
const encodeBar = new cliProgress.SingleBar({
|
|
212
|
-
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
213
|
-
}, cliProgress.Presets.shades_classic);
|
|
214
|
-
encodeBar.start(100, 0, {
|
|
215
|
-
step: 'Starting',
|
|
216
|
-
elapsed: '0',
|
|
217
|
-
});
|
|
218
|
-
const startEncode = Date.now();
|
|
219
|
-
let currentEncodePct = 0;
|
|
220
|
-
let currentEncodeStep = 'Starting';
|
|
221
|
-
const encodeHeartbeat = setInterval(() => {
|
|
222
|
-
encodeBar.update(Math.floor(currentEncodePct), {
|
|
223
|
-
step: currentEncodeStep,
|
|
224
|
-
elapsed: String(Math.floor((Date.now() - startEncode) / 1000)),
|
|
225
|
-
});
|
|
226
|
-
}, 1000);
|
|
227
239
|
options.onProgress = (info) => {
|
|
228
|
-
let pct = 0;
|
|
229
240
|
let stepLabel = 'Processing';
|
|
241
|
+
let pct = 0;
|
|
230
242
|
if (info.phase === 'compress_start') {
|
|
231
|
-
pct =
|
|
243
|
+
pct = 25;
|
|
232
244
|
stepLabel = 'Compressing';
|
|
233
245
|
}
|
|
234
246
|
else if (info.phase === 'compress_progress') {
|
|
235
|
-
pct =
|
|
247
|
+
pct = 25 + Math.floor((info.loaded / info.total) * 50);
|
|
236
248
|
stepLabel = 'Compressing';
|
|
237
249
|
}
|
|
238
250
|
else if (info.phase === 'compress_done') {
|
|
239
|
-
pct =
|
|
251
|
+
pct = 75;
|
|
240
252
|
stepLabel = 'Compressed';
|
|
241
253
|
}
|
|
242
254
|
else if (info.phase === 'encrypt_start') {
|
|
243
|
-
pct =
|
|
255
|
+
pct = 76;
|
|
244
256
|
stepLabel = 'Encrypting';
|
|
245
257
|
}
|
|
246
258
|
else if (info.phase === 'encrypt_done') {
|
|
247
|
-
pct =
|
|
259
|
+
pct = 80;
|
|
248
260
|
stepLabel = 'Encrypted';
|
|
249
261
|
}
|
|
250
262
|
else if (info.phase === 'meta_prep_done') {
|
|
251
|
-
pct =
|
|
263
|
+
pct = 82;
|
|
252
264
|
stepLabel = 'Preparing';
|
|
253
265
|
}
|
|
254
266
|
else if (info.phase === 'png_gen') {
|
|
255
|
-
|
|
267
|
+
if (info.loaded !== undefined && info.total !== undefined) {
|
|
268
|
+
pct = 82 + Math.floor((info.loaded / info.total) * 16);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
pct = 98;
|
|
272
|
+
}
|
|
256
273
|
stepLabel = 'Generating PNG';
|
|
257
274
|
}
|
|
275
|
+
else if (info.phase === 'optimizing') {
|
|
276
|
+
if (info.loaded !== undefined && info.total !== undefined) {
|
|
277
|
+
pct = 82 + Math.floor((info.loaded / info.total) * 18);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
pct = 98;
|
|
281
|
+
}
|
|
282
|
+
stepLabel = 'Optimizing PNG';
|
|
283
|
+
}
|
|
258
284
|
else if (info.phase === 'done') {
|
|
259
285
|
pct = 100;
|
|
260
286
|
stepLabel = 'Done';
|
|
261
287
|
}
|
|
262
|
-
|
|
288
|
+
targetPct = Math.max(targetPct, pct);
|
|
263
289
|
currentEncodeStep = stepLabel;
|
|
264
|
-
encodeBar.update(Math.floor(pct), {
|
|
265
|
-
step: stepLabel,
|
|
266
|
-
elapsed: String(Math.floor((Date.now() - startEncode) / 1000)),
|
|
267
|
-
});
|
|
268
290
|
};
|
|
269
291
|
const output = await encodeBinaryToPng(inputBuffer, options);
|
|
270
292
|
const encodeTime = Date.now() - startEncode;
|
|
271
293
|
clearInterval(encodeHeartbeat);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
294
|
+
if (barStarted) {
|
|
295
|
+
encodeBar.update(100, {
|
|
296
|
+
step: 'done',
|
|
297
|
+
elapsed: String(Math.floor(encodeTime / 1000)),
|
|
298
|
+
});
|
|
299
|
+
encodeBar.stop();
|
|
300
|
+
}
|
|
277
301
|
writeFileSync(resolvedOutput, output);
|
|
278
302
|
const outputSize = (output.length / 1024 / 1024).toFixed(2);
|
|
279
303
|
const inputSize = (inputBuffer.length / 1024 / 1024).toFixed(2);
|
|
@@ -321,52 +345,61 @@ async function decodeCommand(args) {
|
|
|
321
345
|
const decodeBar = new cliProgress.SingleBar({
|
|
322
346
|
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
323
347
|
}, cliProgress.Presets.shades_classic);
|
|
324
|
-
|
|
325
|
-
step: 'Decoding',
|
|
326
|
-
elapsed: '0',
|
|
327
|
-
});
|
|
348
|
+
let barStarted = false;
|
|
328
349
|
const startDecode = Date.now();
|
|
329
|
-
let currentPct =
|
|
350
|
+
let currentPct = 0;
|
|
351
|
+
let targetPct = 0;
|
|
330
352
|
let currentStep = 'Decoding';
|
|
331
353
|
const heartbeat = setInterval(() => {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
354
|
+
if (currentPct < targetPct) {
|
|
355
|
+
currentPct = Math.min(currentPct + 2, targetPct);
|
|
356
|
+
}
|
|
357
|
+
if (!barStarted && targetPct > 0) {
|
|
358
|
+
decodeBar.start(100, Math.floor(currentPct), {
|
|
359
|
+
step: currentStep,
|
|
360
|
+
elapsed: String(Math.floor((Date.now() - startDecode) / 1000)),
|
|
361
|
+
});
|
|
362
|
+
barStarted = true;
|
|
363
|
+
}
|
|
364
|
+
else if (barStarted) {
|
|
365
|
+
decodeBar.update(Math.floor(currentPct), {
|
|
366
|
+
step: currentStep,
|
|
367
|
+
elapsed: String(Math.floor((Date.now() - startDecode) / 1000)),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}, 100);
|
|
337
371
|
options.onProgress = (info) => {
|
|
338
372
|
if (info.phase === 'decompress_start') {
|
|
339
|
-
|
|
373
|
+
targetPct = 50;
|
|
340
374
|
currentStep = 'Decompressing';
|
|
341
375
|
}
|
|
342
376
|
else if (info.phase === 'decompress_progress' &&
|
|
343
377
|
info.loaded &&
|
|
344
378
|
info.total) {
|
|
345
|
-
|
|
379
|
+
targetPct = 50 + Math.floor((info.loaded / info.total) * 40);
|
|
346
380
|
currentStep = `Decompressing (${info.loaded}/${info.total})`;
|
|
347
381
|
}
|
|
348
382
|
else if (info.phase === 'decompress_done') {
|
|
349
|
-
|
|
383
|
+
targetPct = 90;
|
|
350
384
|
currentStep = 'Decompressed';
|
|
351
385
|
}
|
|
352
386
|
else if (info.phase === 'done') {
|
|
353
|
-
|
|
387
|
+
targetPct = 100;
|
|
354
388
|
currentStep = 'Done';
|
|
355
389
|
}
|
|
356
|
-
decodeBar.update(Math.floor(currentPct), {
|
|
357
|
-
step: currentStep,
|
|
358
|
-
elapsed: String(Math.floor((Date.now() - startDecode) / 1000)),
|
|
359
|
-
});
|
|
360
390
|
};
|
|
361
391
|
const inputBuffer = readFileSync(resolvedInput);
|
|
362
392
|
const result = await decodePngToBinary(inputBuffer, options);
|
|
363
393
|
const decodeTime = Date.now() - startDecode;
|
|
364
394
|
clearInterval(heartbeat);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
395
|
+
if (barStarted) {
|
|
396
|
+
currentPct = 100;
|
|
397
|
+
decodeBar.update(100, {
|
|
398
|
+
step: 'done',
|
|
399
|
+
elapsed: String(Math.floor(decodeTime / 1000)),
|
|
400
|
+
});
|
|
401
|
+
decodeBar.stop();
|
|
402
|
+
}
|
|
370
403
|
if (result.files) {
|
|
371
404
|
const baseDir = parsed.output || outputPath || '.';
|
|
372
405
|
for (const file of result.files) {
|
package/dist/index.d.ts
CHANGED
|
@@ -15,80 +15,22 @@ export declare class DataFormatError extends Error {
|
|
|
15
15
|
* @public
|
|
16
16
|
*/
|
|
17
17
|
export interface EncodeOptions {
|
|
18
|
-
/**
|
|
19
|
-
* Compression algorithm to use.
|
|
20
|
-
* - `'zstd'`: Zstandard compression (maximum compression for smallest files)
|
|
21
|
-
* @defaultValue `'zstd'`
|
|
22
|
-
*/
|
|
23
18
|
compression?: 'zstd';
|
|
24
|
-
/**
|
|
25
|
-
* Passphrase for encryption. If provided without `encrypt` option, defaults to AES-256-GCM.
|
|
26
|
-
*/
|
|
27
19
|
passphrase?: string;
|
|
28
|
-
/**
|
|
29
|
-
* Original filename to embed in the encoded data.
|
|
30
|
-
*/
|
|
31
20
|
name?: string;
|
|
32
|
-
/**
|
|
33
|
-
* Encoding mode to use:
|
|
34
|
-
* - `'compact'`: Minimal 1x1 PNG with data in custom chunk (smallest, fastest)
|
|
35
|
-
* - `'pixel'`: Encode data as RGB pixel values
|
|
36
|
-
* - `'screenshot'`: Optimized for screenshot-like appearance (recommended)
|
|
37
|
-
* @defaultValue `'screenshot'`
|
|
38
|
-
*/
|
|
39
21
|
mode?: 'compact' | 'pixel' | 'screenshot';
|
|
40
|
-
/**
|
|
41
|
-
* Encryption method:
|
|
42
|
-
* - `'auto'`: Try all methods and pick smallest result
|
|
43
|
-
* - `'aes'`: AES-256-GCM authenticated encryption (secure)
|
|
44
|
-
* - `'xor'`: Simple XOR cipher (legacy, not recommended)
|
|
45
|
-
* - `'none'`: No encryption
|
|
46
|
-
* @defaultValue `'aes'` when passphrase is provided
|
|
47
|
-
*/
|
|
48
22
|
encrypt?: 'auto' | 'aes' | 'xor' | 'none';
|
|
49
|
-
/**
|
|
50
|
-
* Internal flag to skip auto-detection. Not for public use.
|
|
51
|
-
* @internal
|
|
52
|
-
*/
|
|
53
23
|
_skipAuto?: boolean;
|
|
54
|
-
/**
|
|
55
|
-
* Output format:
|
|
56
|
-
* - `'auto'`: Choose best format automatically
|
|
57
|
-
* - `'png'`: Force PNG output
|
|
58
|
-
* - `'rox'`: Force raw ROX binary format (no PNG wrapper)
|
|
59
|
-
* @defaultValue `'auto'`
|
|
60
|
-
*/
|
|
61
24
|
output?: 'auto' | 'png' | 'rox';
|
|
62
|
-
/**
|
|
63
|
-
* Whether to include the filename in the encoded metadata.
|
|
64
|
-
* @defaultValue `true`
|
|
65
|
-
*/
|
|
66
25
|
includeName?: boolean;
|
|
67
|
-
/**
|
|
68
|
-
* Whether to include the file list in the encoded metadata for archives.
|
|
69
|
-
* @defaultValue `false`
|
|
70
|
-
*/
|
|
71
26
|
includeFileList?: boolean;
|
|
72
|
-
/**
|
|
73
|
-
* List of file paths for archives (used if includeFileList is true).
|
|
74
|
-
*/
|
|
75
27
|
fileList?: string[];
|
|
76
|
-
/**
|
|
77
|
-
* Brotli compression quality (0-11).
|
|
78
|
-
* - Lower values = faster compression, larger output
|
|
79
|
-
* - Higher values = slower compression, smaller output
|
|
80
|
-
* @defaultValue `1` (optimized for speed)
|
|
81
|
-
*/
|
|
82
28
|
brQuality?: number;
|
|
83
29
|
onProgress?: (info: {
|
|
84
30
|
phase: string;
|
|
85
31
|
loaded?: number;
|
|
86
32
|
total?: number;
|
|
87
33
|
}) => void;
|
|
88
|
-
/**
|
|
89
|
-
* Whether to display a progress bar in the console.
|
|
90
|
-
* @defaultValue `false`
|
|
91
|
-
*/
|
|
92
34
|
showProgress?: boolean;
|
|
93
35
|
}
|
|
94
36
|
/**
|
|
@@ -96,27 +38,15 @@ export interface EncodeOptions {
|
|
|
96
38
|
* @public
|
|
97
39
|
*/
|
|
98
40
|
export interface DecodeResult {
|
|
99
|
-
/**
|
|
100
|
-
* The decoded binary data.
|
|
101
|
-
*/
|
|
102
41
|
buf?: Buffer;
|
|
103
|
-
/**
|
|
104
|
-
* Metadata extracted from the encoded image.
|
|
105
|
-
*/
|
|
106
42
|
meta?: {
|
|
107
|
-
/**
|
|
108
|
-
* Original filename, if it was embedded during encoding.
|
|
109
|
-
*/
|
|
110
43
|
name?: string;
|
|
111
44
|
};
|
|
112
|
-
/**
|
|
113
|
-
* Extracted files, if selective extraction was requested.
|
|
114
|
-
*/
|
|
115
45
|
files?: PackedFile[];
|
|
116
46
|
}
|
|
47
|
+
export declare function optimizePngBuffer(pngBuf: Buffer, fast?: boolean): Promise<Buffer>;
|
|
117
48
|
/**
|
|
118
|
-
*
|
|
119
|
-
* @public
|
|
49
|
+
* Path to write decoded output directly to disk (streamed) to avoid high memory usage.
|
|
120
50
|
*/
|
|
121
51
|
export interface DecodeOptions {
|
|
122
52
|
/**
|
|
@@ -188,6 +118,7 @@ export declare function encodeBinaryToPng(input: Buffer, opts?: EncodeOptions):
|
|
|
188
118
|
* writeFileSync(meta?.name ?? 'decoded.txt', buf);
|
|
189
119
|
*/
|
|
190
120
|
export declare function decodePngToBinary(pngBuf: Buffer, opts?: DecodeOptions): Promise<DecodeResult>;
|
|
121
|
+
export { decodeMinPng, encodeMinPng } from './minpng.js';
|
|
191
122
|
export { packPaths, unpackBuffer } from './pack.js';
|
|
192
123
|
/**
|
|
193
124
|
* List files in a Rox PNG archive without decoding the full payload.
|