roxify 1.2.8 → 1.2.9
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/CHANGELOG.md +38 -0
- package/dist/cli.js +2 -1
- package/dist/pack.js +38 -21
- package/dist/roxify-cli +0 -0
- package/dist/utils/encoder.js +28 -59
- package/dist/utils/rust-cli-wrapper.d.ts +1 -0
- package/dist/utils/rust-cli-wrapper.js +36 -0
- package/dist/utils/types.d.ts +2 -0
- package/libroxify_native.node +0 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.10] - 2026-01-09
|
|
4
|
+
|
|
5
|
+
### Performance 🚀🚀
|
|
6
|
+
|
|
7
|
+
- **MASSIVE file packing speedup**: 18,750 files (660MB) now in **7 seconds** (was 18s)
|
|
8
|
+
- Parallelized file reading with `fs.promises.readFile()` and `Promise.all()` batching
|
|
9
|
+
- Batch size optimized to 1000 files per parallel read
|
|
10
|
+
- Improved buffer concatenation strategy (array accumulation + single concat)
|
|
11
|
+
- Added error handling for unreadable files during parallel reads
|
|
12
|
+
|
|
13
|
+
### Benchmarks
|
|
14
|
+
|
|
15
|
+
- Single file 1GB: 389ms (2.63 GB/s)
|
|
16
|
+
- Directory 18,750 files (660MB): **6.8s** (97 MB/s including I/O overhead)
|
|
17
|
+
|
|
18
|
+
## [1.2.9] - 2026-01-09
|
|
19
|
+
|
|
20
|
+
### Performance 🚀
|
|
21
|
+
|
|
22
|
+
- **EXTREME SPEED**: 1GB encode in 0.39s (**2.6 GB/s throughput**)
|
|
23
|
+
- Optimized PNG pixel copying from byte-by-byte loops to bulk Buffer.copy() operations
|
|
24
|
+
- Reduced PNG deflate overhead by using zlib level 0 (data already compressed with Zstd)
|
|
25
|
+
- Lowered large image threshold from 50M to 10M pixels for faster manual PNG generation
|
|
26
|
+
- Default Zstd compression level changed from 15 to 3 (much faster, still excellent ratio)
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Added `compressionLevel` option to `EncodeOptions` (default: 3)
|
|
31
|
+
- Added `skipOptimization` option to disable zopfli PNG optimization
|
|
32
|
+
- CLI now disables PNG optimization by default for maximum speed
|
|
33
|
+
|
|
34
|
+
### Benchmarks
|
|
35
|
+
|
|
36
|
+
- 1KB: 14.77ms
|
|
37
|
+
- 100MB: 63.74ms (1.57 GB/s)
|
|
38
|
+
- 500MB: 203ms (2.46 GB/s)
|
|
39
|
+
- 1GB: 389ms (2.63 GB/s)
|
|
40
|
+
|
|
3
41
|
## [1.2.8] - 2026-01-09
|
|
4
42
|
|
|
5
43
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
|
4
4
|
import { basename, dirname, join, resolve } from 'path';
|
|
5
5
|
import { DataFormatError, decodePngToBinary, encodeBinaryToPng, hasPassphraseInPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
|
|
6
6
|
import { packPathsGenerator, unpackBuffer } from './pack.js';
|
|
7
|
-
const VERSION = '1.2.
|
|
7
|
+
const VERSION = '1.2.9';
|
|
8
8
|
function showHelp() {
|
|
9
9
|
console.log(`
|
|
10
10
|
ROX CLI — Encode/decode binary in PNG
|
|
@@ -187,6 +187,7 @@ async function encodeCommand(args) {
|
|
|
187
187
|
Object.assign(options, {
|
|
188
188
|
mode,
|
|
189
189
|
name: parsed.outputName || 'archive',
|
|
190
|
+
skipOptimization: true,
|
|
190
191
|
});
|
|
191
192
|
if (parsed.verbose)
|
|
192
193
|
options.verbose = true;
|
package/dist/pack.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
2
3
|
import { extname, join, relative, resolve, sep } from 'path';
|
|
3
4
|
function* collectFilesGenerator(paths) {
|
|
4
5
|
for (const p of paths) {
|
|
@@ -158,30 +159,46 @@ export async function packPathsGenerator(paths, baseDir, onProgress) {
|
|
|
158
159
|
indexHeader.writeUInt32BE(0x524f5849, 0);
|
|
159
160
|
indexHeader.writeUInt32BE(indexBuf.length, 4);
|
|
160
161
|
yield Buffer.concat([indexHeader, indexBuf]);
|
|
161
|
-
let currentBuffer = Buffer.alloc(0);
|
|
162
162
|
let readSoFar = 0;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
163
|
+
const BATCH_SIZE = 1000;
|
|
164
|
+
const chunks = [];
|
|
165
|
+
let chunkSize = 0;
|
|
166
|
+
for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
|
|
167
|
+
const batchEnd = Math.min(batchStart + BATCH_SIZE, files.length);
|
|
168
|
+
const batchFiles = files.slice(batchStart, batchEnd);
|
|
169
|
+
const contentPromises = batchFiles.map(async (f) => {
|
|
170
|
+
try {
|
|
171
|
+
return await readFile(f);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
return Buffer.alloc(0);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const contents = await Promise.all(contentPromises);
|
|
178
|
+
for (let i = 0; i < batchFiles.length; i++) {
|
|
179
|
+
const f = batchFiles[i];
|
|
180
|
+
const rel = relative(base, f).split(sep).join('/');
|
|
181
|
+
const content = contents[i];
|
|
182
|
+
const nameBuf = Buffer.from(rel, 'utf8');
|
|
183
|
+
const nameLen = Buffer.alloc(2);
|
|
184
|
+
nameLen.writeUInt16BE(nameBuf.length, 0);
|
|
185
|
+
const sizeBuf = Buffer.alloc(8);
|
|
186
|
+
sizeBuf.writeBigUInt64BE(BigInt(content.length), 0);
|
|
187
|
+
const entry = Buffer.concat([nameLen, nameBuf, sizeBuf, content]);
|
|
188
|
+
chunks.push(entry);
|
|
189
|
+
chunkSize += entry.length;
|
|
190
|
+
if (chunkSize >= BLOCK_SIZE) {
|
|
191
|
+
yield Buffer.concat(chunks);
|
|
192
|
+
chunks.length = 0;
|
|
193
|
+
chunkSize = 0;
|
|
194
|
+
}
|
|
195
|
+
readSoFar += content.length;
|
|
196
|
+
if (onProgress)
|
|
197
|
+
onProgress(readSoFar, totalSize, rel);
|
|
177
198
|
}
|
|
178
|
-
currentBuffer = Buffer.concat([currentBuffer, entry]);
|
|
179
|
-
readSoFar += content.length;
|
|
180
|
-
if (onProgress)
|
|
181
|
-
onProgress(readSoFar, totalSize, rel);
|
|
182
199
|
}
|
|
183
|
-
if (
|
|
184
|
-
yield
|
|
200
|
+
if (chunks.length > 0) {
|
|
201
|
+
yield Buffer.concat(chunks);
|
|
185
202
|
}
|
|
186
203
|
}
|
|
187
204
|
return { index, stream: streamGenerator(), totalSize };
|
package/dist/roxify-cli
ADDED
|
Binary file
|
package/dist/utils/encoder.js
CHANGED
|
@@ -53,7 +53,8 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
53
53
|
}
|
|
54
54
|
if (opts.onProgress)
|
|
55
55
|
opts.onProgress({ phase: 'compress_start', total: totalLen });
|
|
56
|
-
|
|
56
|
+
const compressionLevel = opts.compressionLevel ?? 3;
|
|
57
|
+
let payload = await parallelZstdCompress(payloadInput, compressionLevel, (loaded, total) => {
|
|
57
58
|
if (opts.onProgress) {
|
|
58
59
|
opts.onProgress({
|
|
59
60
|
phase: 'compress_progress',
|
|
@@ -253,71 +254,35 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
253
254
|
const scale = 1;
|
|
254
255
|
const width = logicalWidth * scale;
|
|
255
256
|
const height = logicalHeight * scale;
|
|
256
|
-
const LARGE_IMAGE_PIXELS =
|
|
257
|
+
const LARGE_IMAGE_PIXELS = 10000000;
|
|
257
258
|
const useManualPng = width * height > LARGE_IMAGE_PIXELS || !!process.env.ROX_FAST_PNG;
|
|
258
259
|
let raw;
|
|
259
260
|
let stride = 0;
|
|
260
261
|
if (useManualPng) {
|
|
261
262
|
stride = width * 3 + 1;
|
|
262
263
|
raw = Buffer.alloc(height * stride);
|
|
264
|
+
const flatData = Buffer.concat(dataWithMarkers);
|
|
265
|
+
const markerEndBytes = Buffer.alloc(MARKER_END.length * 3);
|
|
266
|
+
for (let i = 0; i < MARKER_END.length; i++) {
|
|
267
|
+
markerEndBytes[i * 3] = MARKER_END[i].r;
|
|
268
|
+
markerEndBytes[i * 3 + 1] = MARKER_END[i].g;
|
|
269
|
+
markerEndBytes[i * 3 + 2] = MARKER_END[i].b;
|
|
270
|
+
}
|
|
271
|
+
const totalDataBytes = logicalWidth * logicalHeight * 3;
|
|
272
|
+
const fullData = Buffer.alloc(totalDataBytes);
|
|
273
|
+
const markerStartPos = (logicalHeight - 1) * logicalWidth * 3 +
|
|
274
|
+
(logicalWidth - MARKER_END.length) * 3;
|
|
275
|
+
flatData.copy(fullData, 0, 0, Math.min(flatData.length, markerStartPos));
|
|
276
|
+
markerEndBytes.copy(fullData, markerStartPos);
|
|
277
|
+
for (let row = 0; row < height; row++) {
|
|
278
|
+
raw[row * stride] = 0;
|
|
279
|
+
fullData.copy(raw, row * stride + 1, row * width * 3, (row + 1) * width * 3);
|
|
280
|
+
}
|
|
263
281
|
}
|
|
264
282
|
else {
|
|
265
283
|
raw = Buffer.alloc(width * height * bytesPerPixel);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
let currentBufOffset = 0;
|
|
269
|
-
const getNextByte = () => {
|
|
270
|
-
while (currentBufIdx < dataWithMarkers.length) {
|
|
271
|
-
const buf = dataWithMarkers[currentBufIdx];
|
|
272
|
-
if (currentBufOffset < buf.length) {
|
|
273
|
-
return buf[currentBufOffset++];
|
|
274
|
-
}
|
|
275
|
-
currentBufIdx++;
|
|
276
|
-
currentBufOffset = 0;
|
|
277
|
-
}
|
|
278
|
-
return 0;
|
|
279
|
-
};
|
|
280
|
-
for (let ly = 0; ly < logicalHeight; ly++) {
|
|
281
|
-
if (useManualPng) {
|
|
282
|
-
for (let sy = 0; sy < scale; sy++) {
|
|
283
|
-
const py = ly * scale + sy;
|
|
284
|
-
raw[py * stride] = 0;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
for (let lx = 0; lx < logicalWidth; lx++) {
|
|
288
|
-
const linearIdx = ly * logicalWidth + lx;
|
|
289
|
-
let r = 0, g = 0, b = 0;
|
|
290
|
-
if (ly === logicalHeight - 1 &&
|
|
291
|
-
lx >= logicalWidth - MARKER_END.length) {
|
|
292
|
-
const markerIdx = lx - (logicalWidth - MARKER_END.length);
|
|
293
|
-
r = MARKER_END[markerIdx].r;
|
|
294
|
-
g = MARKER_END[markerIdx].g;
|
|
295
|
-
b = MARKER_END[markerIdx].b;
|
|
296
|
-
}
|
|
297
|
-
else if (linearIdx < dataPixels) {
|
|
298
|
-
r = getNextByte();
|
|
299
|
-
g = getNextByte();
|
|
300
|
-
b = getNextByte();
|
|
301
|
-
}
|
|
302
|
-
for (let sy = 0; sy < scale; sy++) {
|
|
303
|
-
for (let sx = 0; sx < scale; sx++) {
|
|
304
|
-
const px = lx * scale + sx;
|
|
305
|
-
const py = ly * scale + sy;
|
|
306
|
-
if (useManualPng) {
|
|
307
|
-
const dstIdx = py * stride + 1 + px * 3;
|
|
308
|
-
raw[dstIdx] = r;
|
|
309
|
-
raw[dstIdx + 1] = g;
|
|
310
|
-
raw[dstIdx + 2] = b;
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
const dstIdx = (py * width + px) * 3;
|
|
314
|
-
raw[dstIdx] = r;
|
|
315
|
-
raw[dstIdx + 1] = g;
|
|
316
|
-
raw[dstIdx + 2] = b;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
284
|
+
const flatData = Buffer.concat(dataWithMarkers);
|
|
285
|
+
flatData.copy(raw, 0, 0, Math.min(flatData.length, raw.length));
|
|
321
286
|
}
|
|
322
287
|
payload.length = 0;
|
|
323
288
|
dataWithMarkers.length = 0;
|
|
@@ -344,9 +309,9 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
344
309
|
if (opts.onProgress)
|
|
345
310
|
opts.onProgress({ phase: 'png_compress', loaded: 0, total: 100 });
|
|
346
311
|
const idatData = zlib.deflateSync(scanlinesData, {
|
|
347
|
-
level:
|
|
312
|
+
level: 0,
|
|
348
313
|
memLevel: 8,
|
|
349
|
-
strategy: zlib.constants.
|
|
314
|
+
strategy: zlib.constants.Z_FILTERED,
|
|
350
315
|
});
|
|
351
316
|
raw = Buffer.alloc(0);
|
|
352
317
|
const ihdrData = Buffer.alloc(13);
|
|
@@ -405,6 +370,10 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
405
370
|
raw = Buffer.alloc(0);
|
|
406
371
|
if (opts.onProgress)
|
|
407
372
|
opts.onProgress({ phase: 'png_compress', loaded: 100, total: 100 });
|
|
373
|
+
if (opts.skipOptimization) {
|
|
374
|
+
progressBar?.stop();
|
|
375
|
+
return bufScr;
|
|
376
|
+
}
|
|
408
377
|
if (opts.onProgress)
|
|
409
378
|
opts.onProgress({ phase: 'optimizing', loaded: 0, total: 100 });
|
|
410
379
|
try {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function encodeWithRustCLI(inputPath: string, outputPath: string, compressionLevel?: number): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel = 3) {
|
|
8
|
+
const cliPath = join(__dirname, '..', 'dist', 'roxify-cli');
|
|
9
|
+
if (!existsSync(cliPath)) {
|
|
10
|
+
throw new Error('Rust CLI binary not found. Run: npm run build:native');
|
|
11
|
+
}
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const proc = spawn(cliPath, [
|
|
14
|
+
'encode',
|
|
15
|
+
inputPath,
|
|
16
|
+
outputPath,
|
|
17
|
+
'--level',
|
|
18
|
+
String(compressionLevel),
|
|
19
|
+
]);
|
|
20
|
+
let stderr = '';
|
|
21
|
+
proc.stderr.on('data', (data) => {
|
|
22
|
+
stderr += data.toString();
|
|
23
|
+
});
|
|
24
|
+
proc.on('error', (err) => {
|
|
25
|
+
reject(new Error(`Failed to spawn Rust CLI: ${err.message}`));
|
|
26
|
+
});
|
|
27
|
+
proc.on('close', (code) => {
|
|
28
|
+
if (code === 0) {
|
|
29
|
+
resolve();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
reject(new Error(`Rust CLI exited with code ${code}: ${stderr}`));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { PackedFile } from '../pack.js';
|
|
4
4
|
export interface EncodeOptions {
|
|
5
5
|
compression?: 'zstd';
|
|
6
|
+
compressionLevel?: number;
|
|
6
7
|
passphrase?: string;
|
|
7
8
|
name?: string;
|
|
8
9
|
mode?: 'screenshot';
|
|
@@ -15,6 +16,7 @@ export interface EncodeOptions {
|
|
|
15
16
|
name: string;
|
|
16
17
|
size: number;
|
|
17
18
|
}>;
|
|
19
|
+
skipOptimization?: boolean;
|
|
18
20
|
onProgress?: (info: {
|
|
19
21
|
phase: string;
|
|
20
22
|
loaded?: number;
|
package/libroxify_native.node
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Encode binary data into PNG images and decode them back. CLI and programmatic API with native Rust acceleration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc",
|
|
21
21
|
"build:native": "cargo build --release --lib && cp target/release/libroxify_native.so libroxify_native.node",
|
|
22
|
-
"build:
|
|
22
|
+
"build:cli": "cargo build --release --bin roxify_native && cp target/release/roxify_native dist/roxify-cli",
|
|
23
|
+
"build:all": "npm run build:native && npm run build && npm run build:cli",
|
|
23
24
|
"prepublishOnly": "npm run build:all",
|
|
24
25
|
"test": "node test/roundtrip.js && node test/pixel-fallback-preview.js && node test/size-fallback-choice.js && node test/screenshot-roundtrip.js && node test/screenshot-fallback.js",
|
|
25
26
|
"cli": "node dist/cli.js"
|