roxify 1.3.2 → 1.4.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/dist/cli.js +6 -4
- package/dist/roxify-cli +0 -0
- package/dist/utils/constants.d.ts +18 -0
- package/dist/utils/constants.js +6 -0
- package/dist/utils/decoder.js +146 -30
- package/dist/utils/encoder.js +182 -126
- package/dist/utils/helpers.d.ts +7 -0
- package/dist/utils/helpers.js +40 -0
- package/dist/utils/rust-cli-wrapper.d.ts +1 -1
- package/dist/utils/rust-cli-wrapper.js +4 -1
- package/dist/utils/types.d.ts +3 -0
- package/libroxify_native.node +0 -0
- package/package.json +9 -2
- package/README.md +0 -391
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, dirname, join, resolve } from 'path';
|
|
|
6
6
|
import { DataFormatError, decodePngToBinary, encodeBinaryToPng, hasPassphraseInPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
|
|
7
7
|
import { packPathsGenerator, unpackBuffer } from './pack.js';
|
|
8
8
|
import { encodeWithRustCLI, isRustBinaryAvailable, } from './utils/rust-cli-wrapper.js';
|
|
9
|
-
const VERSION = '1.
|
|
9
|
+
const VERSION = '1.4.0';
|
|
10
10
|
async function readLargeFile(filePath) {
|
|
11
11
|
const st = statSync(filePath);
|
|
12
12
|
if (st.size <= 2 * 1024 * 1024 * 1024) {
|
|
@@ -211,7 +211,8 @@ async function encodeCommand(args) {
|
|
|
211
211
|
});
|
|
212
212
|
}, 500);
|
|
213
213
|
const encryptType = parsed.encrypt === 'xor' ? 'xor' : 'aes';
|
|
214
|
-
|
|
214
|
+
const fileName = basename(inputPaths[0]);
|
|
215
|
+
await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput, 12, parsed.passphrase, encryptType, fileName);
|
|
215
216
|
clearInterval(progressInterval);
|
|
216
217
|
const encodeTime = Date.now() - startTime;
|
|
217
218
|
encodeBar.update(100, {
|
|
@@ -283,8 +284,9 @@ async function encodeCommand(args) {
|
|
|
283
284
|
Object.assign(options, {
|
|
284
285
|
mode,
|
|
285
286
|
name: parsed.outputName || 'archive',
|
|
286
|
-
skipOptimization:
|
|
287
|
-
compressionLevel:
|
|
287
|
+
skipOptimization: false,
|
|
288
|
+
compressionLevel: 12,
|
|
289
|
+
outputFormat: 'auto',
|
|
288
290
|
});
|
|
289
291
|
if (parsed.verbose)
|
|
290
292
|
options.verbose = true;
|
package/dist/roxify-cli
CHANGED
|
Binary file
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
export declare const CHUNK_TYPE = "rXDT";
|
|
4
4
|
export declare const MAGIC: Buffer;
|
|
5
5
|
export declare const PIXEL_MAGIC: Buffer;
|
|
6
|
+
export declare const PIXEL_MAGIC_BLOCK: Buffer;
|
|
6
7
|
export declare const ENC_NONE = 0;
|
|
7
8
|
export declare const ENC_AES = 1;
|
|
8
9
|
export declare const ENC_XOR = 2;
|
|
@@ -36,3 +37,20 @@ export declare const COMPRESSION_MARKERS: {
|
|
|
36
37
|
b: number;
|
|
37
38
|
}[];
|
|
38
39
|
};
|
|
40
|
+
export declare const FORMAT_MARKERS: {
|
|
41
|
+
png: {
|
|
42
|
+
r: number;
|
|
43
|
+
g: number;
|
|
44
|
+
b: number;
|
|
45
|
+
};
|
|
46
|
+
webp: {
|
|
47
|
+
r: number;
|
|
48
|
+
g: number;
|
|
49
|
+
b: number;
|
|
50
|
+
};
|
|
51
|
+
jxl: {
|
|
52
|
+
r: number;
|
|
53
|
+
g: number;
|
|
54
|
+
b: number;
|
|
55
|
+
};
|
|
56
|
+
};
|
package/dist/utils/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const CHUNK_TYPE = 'rXDT';
|
|
2
2
|
export const MAGIC = Buffer.from('ROX1');
|
|
3
3
|
export const PIXEL_MAGIC = Buffer.from('PXL1');
|
|
4
|
+
export const PIXEL_MAGIC_BLOCK = Buffer.from('BLK2');
|
|
4
5
|
export const ENC_NONE = 0;
|
|
5
6
|
export const ENC_AES = 1;
|
|
6
7
|
export const ENC_XOR = 2;
|
|
@@ -20,3 +21,8 @@ export const COMPRESSION_MARKERS = {
|
|
|
20
21
|
zstd: [{ r: 0, g: 255, b: 0 }],
|
|
21
22
|
lzma: [{ r: 255, g: 255, b: 0 }],
|
|
22
23
|
};
|
|
24
|
+
export const FORMAT_MARKERS = {
|
|
25
|
+
png: { r: 0, g: 255, b: 255 },
|
|
26
|
+
webp: { r: 255, g: 0, b: 255 },
|
|
27
|
+
jxl: { r: 255, g: 255, b: 0 },
|
|
28
|
+
};
|
package/dist/utils/decoder.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
1
2
|
import cliProgress from 'cli-progress';
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
3
6
|
import extract from 'png-chunks-extract';
|
|
4
7
|
import sharp from 'sharp';
|
|
5
8
|
import { unpackBuffer } from '../pack.js';
|
|
6
|
-
import { CHUNK_TYPE, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PNG_HEADER, } from './constants.js';
|
|
9
|
+
import { CHUNK_TYPE, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PIXEL_MAGIC_BLOCK, PNG_HEADER, } from './constants.js';
|
|
7
10
|
import { DataFormatError, IncorrectPassphraseError, PassphraseRequiredError, } from './errors.js';
|
|
8
11
|
import { colorsToBytes, deltaDecode, tryDecryptIfNeeded } from './helpers.js';
|
|
9
12
|
import { cropAndReconstitute } from './reconstitution.js';
|
|
@@ -34,6 +37,52 @@ async function tryDecompress(payload, onProgress) {
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
}
|
|
40
|
+
function detectImageFormat(buf) {
|
|
41
|
+
if (buf.length < 12)
|
|
42
|
+
return 'unknown';
|
|
43
|
+
if (buf[0] === 0x89 &&
|
|
44
|
+
buf[1] === 0x50 &&
|
|
45
|
+
buf[2] === 0x4e &&
|
|
46
|
+
buf[3] === 0x47) {
|
|
47
|
+
return 'png';
|
|
48
|
+
}
|
|
49
|
+
if (buf[0] === 0x52 &&
|
|
50
|
+
buf[1] === 0x49 &&
|
|
51
|
+
buf[2] === 0x46 &&
|
|
52
|
+
buf[3] === 0x46 &&
|
|
53
|
+
buf[8] === 0x57 &&
|
|
54
|
+
buf[9] === 0x45 &&
|
|
55
|
+
buf[10] === 0x42 &&
|
|
56
|
+
buf[11] === 0x50) {
|
|
57
|
+
return 'webp';
|
|
58
|
+
}
|
|
59
|
+
if (buf[0] === 0xff && buf[1] === 0x0a) {
|
|
60
|
+
return 'jxl';
|
|
61
|
+
}
|
|
62
|
+
return 'unknown';
|
|
63
|
+
}
|
|
64
|
+
function convertToPng(buf, format) {
|
|
65
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'rox-decode-'));
|
|
66
|
+
const inputPath = join(tempDir, format === 'webp' ? 'input.webp' : 'input.jxl');
|
|
67
|
+
const outputPath = join(tempDir, 'output.png');
|
|
68
|
+
try {
|
|
69
|
+
writeFileSync(inputPath, buf);
|
|
70
|
+
if (format === 'webp') {
|
|
71
|
+
execFileSync('dwebp', [inputPath, '-o', outputPath]);
|
|
72
|
+
}
|
|
73
|
+
else if (format === 'jxl') {
|
|
74
|
+
execFileSync('djxl', [inputPath, outputPath]);
|
|
75
|
+
}
|
|
76
|
+
const pngBuf = readFileSync(outputPath);
|
|
77
|
+
return pngBuf;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
try {
|
|
81
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
catch (e) { }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
37
86
|
export async function decodePngToBinary(input, opts = {}) {
|
|
38
87
|
let pngBuf;
|
|
39
88
|
if (Buffer.isBuffer(input)) {
|
|
@@ -228,40 +277,93 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
228
277
|
const metadata = await sharp(processedBuf).metadata();
|
|
229
278
|
const currentWidth = metadata.width;
|
|
230
279
|
const currentHeight = metadata.height;
|
|
231
|
-
|
|
232
|
-
let
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const endRow = Math.min(startRow + rowsPerChunk, currentHeight);
|
|
236
|
-
const chunkHeight = endRow - startRow;
|
|
237
|
-
const { data: chunkData, info: chunkInfo } = await sharp(processedBuf)
|
|
280
|
+
let rawRGB = Buffer.alloc(0);
|
|
281
|
+
let isBlockEncoded = false;
|
|
282
|
+
if (currentWidth % 2 === 0 && currentHeight % 2 === 0) {
|
|
283
|
+
const { data: testData } = await sharp(processedBuf)
|
|
238
284
|
.extract({
|
|
239
285
|
left: 0,
|
|
240
|
-
top:
|
|
241
|
-
width: currentWidth,
|
|
242
|
-
height:
|
|
286
|
+
top: 0,
|
|
287
|
+
width: Math.min(4, currentWidth),
|
|
288
|
+
height: Math.min(4, currentHeight),
|
|
243
289
|
})
|
|
244
290
|
.raw()
|
|
245
291
|
.toBuffer({ resolveWithObject: true });
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
292
|
+
let hasBlockPattern = true;
|
|
293
|
+
for (let y = 0; y < Math.min(2, currentHeight / 2); y++) {
|
|
294
|
+
for (let x = 0; x < Math.min(2, currentWidth / 2); x++) {
|
|
295
|
+
const px00 = (y * 2 * Math.min(4, currentWidth) + x * 2) * 3;
|
|
296
|
+
const px01 = (y * 2 * Math.min(4, currentWidth) + (x * 2 + 1)) * 3;
|
|
297
|
+
const px10 = ((y * 2 + 1) * Math.min(4, currentWidth) + x * 2) * 3;
|
|
298
|
+
const px11 = ((y * 2 + 1) * Math.min(4, currentWidth) + (x * 2 + 1)) * 3;
|
|
299
|
+
if (testData[px00] !== testData[px01] ||
|
|
300
|
+
testData[px00] !== testData[px10] ||
|
|
301
|
+
testData[px00] !== testData[px11] ||
|
|
302
|
+
testData[px00 + 1] !== testData[px01 + 1] ||
|
|
303
|
+
testData[px00 + 1] !== testData[px10 + 1] ||
|
|
304
|
+
testData[px00 + 1] !== testData[px11 + 1]) {
|
|
305
|
+
hasBlockPattern = false;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!hasBlockPattern)
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
if (hasBlockPattern) {
|
|
313
|
+
isBlockEncoded = true;
|
|
314
|
+
const blocksWide = currentWidth / 2;
|
|
315
|
+
const blocksHigh = currentHeight / 2;
|
|
316
|
+
rawRGB = Buffer.alloc(blocksWide * blocksHigh * 3);
|
|
317
|
+
let outIdx = 0;
|
|
318
|
+
for (let by = 0; by < blocksHigh; by++) {
|
|
319
|
+
for (let bx = 0; bx < blocksWide; bx++) {
|
|
320
|
+
const { data: blockData } = await sharp(processedBuf)
|
|
321
|
+
.extract({ left: bx * 2, top: by * 2, width: 1, height: 1 })
|
|
322
|
+
.raw()
|
|
323
|
+
.toBuffer({ resolveWithObject: true });
|
|
324
|
+
rawRGB[outIdx++] = blockData[0];
|
|
325
|
+
rawRGB[outIdx++] = blockData[1];
|
|
326
|
+
rawRGB[outIdx++] = blockData[2];
|
|
327
|
+
}
|
|
257
328
|
}
|
|
258
329
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
330
|
+
}
|
|
331
|
+
if (!isBlockEncoded) {
|
|
332
|
+
rawRGB = Buffer.allocUnsafe(currentWidth * currentHeight * 3);
|
|
333
|
+
let writeOffset = 0;
|
|
334
|
+
const rowsPerChunk = 2000;
|
|
335
|
+
for (let startRow = 0; startRow < currentHeight; startRow += rowsPerChunk) {
|
|
336
|
+
const endRow = Math.min(startRow + rowsPerChunk, currentHeight);
|
|
337
|
+
const chunkHeight = endRow - startRow;
|
|
338
|
+
const { data: chunkData, info: chunkInfo } = await sharp(processedBuf)
|
|
339
|
+
.extract({
|
|
340
|
+
left: 0,
|
|
341
|
+
top: startRow,
|
|
342
|
+
width: currentWidth,
|
|
343
|
+
height: chunkHeight,
|
|
344
|
+
})
|
|
345
|
+
.raw()
|
|
346
|
+
.toBuffer({ resolveWithObject: true });
|
|
347
|
+
const channels = chunkInfo.channels;
|
|
348
|
+
const pixelsInChunk = currentWidth * chunkHeight;
|
|
349
|
+
if (channels === 3) {
|
|
350
|
+
chunkData.copy(rawRGB, writeOffset);
|
|
351
|
+
writeOffset += pixelsInChunk * 3;
|
|
352
|
+
}
|
|
353
|
+
else if (channels === 4) {
|
|
354
|
+
for (let i = 0; i < pixelsInChunk; i++) {
|
|
355
|
+
rawRGB[writeOffset++] = chunkData[i * 4];
|
|
356
|
+
rawRGB[writeOffset++] = chunkData[i * 4 + 1];
|
|
357
|
+
rawRGB[writeOffset++] = chunkData[i * 4 + 2];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (opts.onProgress) {
|
|
361
|
+
opts.onProgress({
|
|
362
|
+
phase: 'extract_pixels',
|
|
363
|
+
loaded: endRow,
|
|
364
|
+
total: currentHeight,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
265
367
|
}
|
|
266
368
|
}
|
|
267
369
|
const firstPixels = [];
|
|
@@ -285,6 +387,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
285
387
|
}
|
|
286
388
|
}
|
|
287
389
|
let hasPixelMagic = false;
|
|
390
|
+
let hasBlockMagic = false;
|
|
288
391
|
if (rawRGB.length >= 8 + PIXEL_MAGIC.length) {
|
|
289
392
|
const widthFromDim = rawRGB.readUInt32BE(0);
|
|
290
393
|
const heightFromDim = rawRGB.readUInt32BE(4);
|
|
@@ -293,11 +396,14 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
293
396
|
rawRGB.slice(8, 8 + PIXEL_MAGIC.length).equals(PIXEL_MAGIC)) {
|
|
294
397
|
hasPixelMagic = true;
|
|
295
398
|
}
|
|
399
|
+
else if (rawRGB.slice(8, 8 + PIXEL_MAGIC_BLOCK.length).equals(PIXEL_MAGIC_BLOCK)) {
|
|
400
|
+
hasBlockMagic = true;
|
|
401
|
+
}
|
|
296
402
|
}
|
|
297
403
|
let logicalWidth;
|
|
298
404
|
let logicalHeight;
|
|
299
405
|
let logicalData;
|
|
300
|
-
if (hasMarkerStart || hasPixelMagic) {
|
|
406
|
+
if (hasMarkerStart || hasPixelMagic || hasBlockMagic) {
|
|
301
407
|
logicalWidth = currentWidth;
|
|
302
408
|
logicalHeight = currentHeight;
|
|
303
409
|
logicalData = rawRGB;
|
|
@@ -569,14 +675,24 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
569
675
|
let idx = 0;
|
|
570
676
|
if (pixelBytes.length >= PIXEL_MAGIC.length) {
|
|
571
677
|
const at0 = pixelBytes.slice(0, PIXEL_MAGIC.length).equals(PIXEL_MAGIC);
|
|
678
|
+
const at0Block = pixelBytes
|
|
679
|
+
.slice(0, PIXEL_MAGIC_BLOCK.length)
|
|
680
|
+
.equals(PIXEL_MAGIC_BLOCK);
|
|
572
681
|
if (at0) {
|
|
573
682
|
idx = PIXEL_MAGIC.length;
|
|
574
683
|
}
|
|
684
|
+
else if (at0Block) {
|
|
685
|
+
idx = PIXEL_MAGIC_BLOCK.length;
|
|
686
|
+
}
|
|
575
687
|
else {
|
|
576
688
|
const found = pixelBytes.indexOf(PIXEL_MAGIC);
|
|
689
|
+
const foundBlock = pixelBytes.indexOf(PIXEL_MAGIC_BLOCK);
|
|
577
690
|
if (found !== -1) {
|
|
578
691
|
idx = found + PIXEL_MAGIC.length;
|
|
579
692
|
}
|
|
693
|
+
else if (foundBlock !== -1) {
|
|
694
|
+
idx = foundBlock + PIXEL_MAGIC_BLOCK.length;
|
|
695
|
+
}
|
|
580
696
|
}
|
|
581
697
|
}
|
|
582
698
|
if (idx > 0) {
|
package/dist/utils/encoder.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createCipheriv, pbkdf2Sync, randomBytes } from 'crypto';
|
|
|
3
3
|
import sharp from 'sharp';
|
|
4
4
|
import * as zlib from 'zlib';
|
|
5
5
|
import { unpackBuffer } from '../pack.js';
|
|
6
|
-
import { COMPRESSION_MARKERS, ENC_AES, ENC_NONE, ENC_XOR, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PNG_HEADER, } from './constants.js';
|
|
6
|
+
import { COMPRESSION_MARKERS, ENC_AES, ENC_NONE, ENC_XOR, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PIXEL_MAGIC_BLOCK, PNG_HEADER, } from './constants.js';
|
|
7
7
|
import { crc32 } from './crc.js';
|
|
8
8
|
import { colorsToBytes } from './helpers.js';
|
|
9
9
|
import { optimizePngBuffer } from './optimization.js';
|
|
@@ -220,7 +220,9 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
220
220
|
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
221
221
|
metaPixel = [...metaPixel, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf];
|
|
222
222
|
}
|
|
223
|
-
const
|
|
223
|
+
const useBlockEncoding = opts.useBlockEncoding ?? true;
|
|
224
|
+
const pixelMagic = useBlockEncoding ? PIXEL_MAGIC_BLOCK : PIXEL_MAGIC;
|
|
225
|
+
const dataWithoutMarkers = [pixelMagic, ...metaPixel];
|
|
224
226
|
const dataWithoutMarkersLen = dataWithoutMarkers.reduce((a, b) => a + b.length, 0);
|
|
225
227
|
const padding = (3 - (dataWithoutMarkersLen % 3)) % 3;
|
|
226
228
|
const paddedData = padding > 0
|
|
@@ -233,56 +235,188 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
233
235
|
compressionMarkerBytes,
|
|
234
236
|
...paddedData,
|
|
235
237
|
];
|
|
236
|
-
const bytesPerPixel = 3;
|
|
237
238
|
const dataWithMarkersLen = dataWithMarkers.reduce((a, b) => a + b.length, 0);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (side < MARKER_END.length)
|
|
243
|
-
side = MARKER_END.length;
|
|
244
|
-
let logicalWidth;
|
|
245
|
-
let logicalHeight;
|
|
246
|
-
if (side <= maxWidth) {
|
|
247
|
-
logicalWidth = side;
|
|
248
|
-
logicalHeight = side;
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
logicalWidth = Math.min(maxWidth, totalPixels);
|
|
252
|
-
logicalHeight = Math.ceil(totalPixels / logicalWidth);
|
|
253
|
-
}
|
|
254
|
-
const scale = 1;
|
|
255
|
-
const width = logicalWidth * scale;
|
|
256
|
-
const height = logicalHeight * scale;
|
|
257
|
-
const LARGE_IMAGE_PIXELS = 10000000;
|
|
258
|
-
const useManualPng = width * height > LARGE_IMAGE_PIXELS || !!process.env.ROX_FAST_PNG;
|
|
259
|
-
let raw;
|
|
260
|
-
let stride = 0;
|
|
261
|
-
if (useManualPng) {
|
|
262
|
-
stride = width * 3 + 1;
|
|
263
|
-
raw = Buffer.alloc(height * stride);
|
|
239
|
+
let width;
|
|
240
|
+
let height;
|
|
241
|
+
let bufScr;
|
|
242
|
+
if (useBlockEncoding) {
|
|
264
243
|
const flatData = Buffer.concat(dataWithMarkers);
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
244
|
+
const blocksPerRow = Math.ceil(Math.sqrt(flatData.length));
|
|
245
|
+
const numRows = Math.ceil(flatData.length / blocksPerRow);
|
|
246
|
+
width = blocksPerRow * 2;
|
|
247
|
+
height = numRows * 2;
|
|
248
|
+
const rgbBuffer = Buffer.alloc(width * height * 3);
|
|
249
|
+
for (let i = 0; i < flatData.length; i++) {
|
|
250
|
+
const blockRow = Math.floor(i / blocksPerRow);
|
|
251
|
+
const blockCol = i % blocksPerRow;
|
|
252
|
+
const pixelRow = blockRow * 2;
|
|
253
|
+
const pixelCol = blockCol * 2;
|
|
254
|
+
const byte = flatData[i];
|
|
255
|
+
for (let dy = 0; dy < 2; dy++) {
|
|
256
|
+
for (let dx = 0; dx < 2; dx++) {
|
|
257
|
+
const px = (pixelRow + dy) * width + (pixelCol + dx);
|
|
258
|
+
rgbBuffer[px * 3] = byte;
|
|
259
|
+
rgbBuffer[px * 3 + 1] = byte;
|
|
260
|
+
rgbBuffer[px * 3 + 2] = byte;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
280
263
|
}
|
|
264
|
+
bufScr = await sharp(rgbBuffer, {
|
|
265
|
+
raw: { width, height, channels: 3 },
|
|
266
|
+
})
|
|
267
|
+
.png({
|
|
268
|
+
compressionLevel: 9,
|
|
269
|
+
adaptiveFiltering: true,
|
|
270
|
+
effort: 9,
|
|
271
|
+
})
|
|
272
|
+
.toBuffer();
|
|
281
273
|
}
|
|
282
274
|
else {
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
275
|
+
const bytesPerPixel = 3;
|
|
276
|
+
const dataPixels = Math.ceil(dataWithMarkersLen / 3);
|
|
277
|
+
const totalPixels = dataPixels + MARKER_END.length;
|
|
278
|
+
const maxWidth = 16384;
|
|
279
|
+
let side = Math.ceil(Math.sqrt(totalPixels));
|
|
280
|
+
if (side < MARKER_END.length)
|
|
281
|
+
side = MARKER_END.length;
|
|
282
|
+
let logicalWidth;
|
|
283
|
+
let logicalHeight;
|
|
284
|
+
if (side <= maxWidth) {
|
|
285
|
+
logicalWidth = side;
|
|
286
|
+
logicalHeight = side;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
logicalWidth = Math.min(maxWidth, totalPixels);
|
|
290
|
+
logicalHeight = Math.ceil(totalPixels / logicalWidth);
|
|
291
|
+
}
|
|
292
|
+
const scale = 1;
|
|
293
|
+
width = logicalWidth * scale;
|
|
294
|
+
height = logicalHeight * scale;
|
|
295
|
+
const LARGE_IMAGE_PIXELS = 10000000;
|
|
296
|
+
const useManualPng = (width * height > LARGE_IMAGE_PIXELS || !!process.env.ROX_FAST_PNG) &&
|
|
297
|
+
opts.outputFormat !== 'webp';
|
|
298
|
+
if (process.env.ROX_DEBUG) {
|
|
299
|
+
console.log(`[DEBUG] Width=${width}, Height=${height}, Pixels=${width * height}`);
|
|
300
|
+
console.log(`[DEBUG] outputFormat=${opts.outputFormat}, useManualPng=${useManualPng}`);
|
|
301
|
+
}
|
|
302
|
+
let raw;
|
|
303
|
+
let stride = 0;
|
|
304
|
+
if (useManualPng) {
|
|
305
|
+
stride = width * 3 + 1;
|
|
306
|
+
raw = Buffer.alloc(height * stride);
|
|
307
|
+
const flatData = Buffer.concat(dataWithMarkers);
|
|
308
|
+
const markerEndBytes = Buffer.alloc(MARKER_END.length * 3);
|
|
309
|
+
for (let i = 0; i < MARKER_END.length; i++) {
|
|
310
|
+
markerEndBytes[i * 3] = MARKER_END[i].r;
|
|
311
|
+
markerEndBytes[i * 3 + 1] = MARKER_END[i].g;
|
|
312
|
+
markerEndBytes[i * 3 + 2] = MARKER_END[i].b;
|
|
313
|
+
}
|
|
314
|
+
const totalDataBytes = logicalWidth * logicalHeight * 3;
|
|
315
|
+
const fullData = Buffer.alloc(totalDataBytes);
|
|
316
|
+
const markerStartPos = (logicalHeight - 1) * logicalWidth * 3 +
|
|
317
|
+
(logicalWidth - MARKER_END.length) * 3;
|
|
318
|
+
flatData.copy(fullData, 0, 0, Math.min(flatData.length, markerStartPos));
|
|
319
|
+
markerEndBytes.copy(fullData, markerStartPos);
|
|
320
|
+
for (let row = 0; row < height; row++) {
|
|
321
|
+
raw[row * stride] = 0;
|
|
322
|
+
fullData.copy(raw, row * stride + 1, row * width * 3, (row + 1) * width * 3);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
raw = Buffer.alloc(width * height * 3);
|
|
327
|
+
const flatData = Buffer.concat(dataWithMarkers);
|
|
328
|
+
flatData.copy(raw, 0, 0, Math.min(flatData.length, raw.length));
|
|
329
|
+
}
|
|
330
|
+
if (opts.onProgress)
|
|
331
|
+
opts.onProgress({ phase: 'png_gen', loaded: 0, total: height });
|
|
332
|
+
if (useManualPng) {
|
|
333
|
+
const bytesPerRow = width * 3;
|
|
334
|
+
const scanlinesData = Buffer.alloc(height * (1 + bytesPerRow));
|
|
335
|
+
const progressStep = Math.max(1, Math.floor(height / 20));
|
|
336
|
+
for (let row = 0; row < height; row++) {
|
|
337
|
+
scanlinesData[row * (1 + bytesPerRow)] = 0;
|
|
338
|
+
const srcStart = row * stride + 1;
|
|
339
|
+
const dstStart = row * (1 + bytesPerRow) + 1;
|
|
340
|
+
raw.copy(scanlinesData, dstStart, srcStart, srcStart + bytesPerRow);
|
|
341
|
+
if (opts.onProgress && row % progressStep === 0) {
|
|
342
|
+
opts.onProgress({ phase: 'png_gen', loaded: row, total: height });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (opts.onProgress)
|
|
346
|
+
opts.onProgress({ phase: 'png_compress', loaded: 0, total: 100 });
|
|
347
|
+
const idatData = zlib.deflateSync(scanlinesData, {
|
|
348
|
+
level: 0,
|
|
349
|
+
memLevel: 8,
|
|
350
|
+
strategy: zlib.constants.Z_FILTERED,
|
|
351
|
+
});
|
|
352
|
+
raw = Buffer.alloc(0);
|
|
353
|
+
const ihdrData = Buffer.alloc(13);
|
|
354
|
+
ihdrData.writeUInt32BE(width, 0);
|
|
355
|
+
ihdrData.writeUInt32BE(height, 4);
|
|
356
|
+
ihdrData[8] = 8;
|
|
357
|
+
ihdrData[9] = 2;
|
|
358
|
+
ihdrData[10] = 0;
|
|
359
|
+
ihdrData[11] = 0;
|
|
360
|
+
ihdrData[12] = 0;
|
|
361
|
+
const ihdrType = Buffer.from('IHDR', 'utf8');
|
|
362
|
+
const ihdrCrc = crc32(ihdrData, crc32(ihdrType));
|
|
363
|
+
const ihdrCrcBuf = Buffer.alloc(4);
|
|
364
|
+
ihdrCrcBuf.writeUInt32BE(ihdrCrc, 0);
|
|
365
|
+
const ihdrLen = Buffer.alloc(4);
|
|
366
|
+
ihdrLen.writeUInt32BE(ihdrData.length, 0);
|
|
367
|
+
const idatType = Buffer.from('IDAT', 'utf8');
|
|
368
|
+
const idatCrc = crc32(idatData, crc32(idatType));
|
|
369
|
+
const idatCrcBuf = Buffer.alloc(4);
|
|
370
|
+
idatCrcBuf.writeUInt32BE(idatCrc, 0);
|
|
371
|
+
const idatLen = Buffer.alloc(4);
|
|
372
|
+
idatLen.writeUInt32BE(idatData.length, 0);
|
|
373
|
+
const iendType = Buffer.from('IEND', 'utf8');
|
|
374
|
+
const iendCrc = crc32(Buffer.alloc(0), crc32(iendType));
|
|
375
|
+
const iendCrcBuf = Buffer.alloc(4);
|
|
376
|
+
iendCrcBuf.writeUInt32BE(iendCrc, 0);
|
|
377
|
+
const iendLen = Buffer.alloc(4);
|
|
378
|
+
iendLen.writeUInt32BE(0, 0);
|
|
379
|
+
bufScr = Buffer.concat([
|
|
380
|
+
PNG_HEADER,
|
|
381
|
+
ihdrLen,
|
|
382
|
+
ihdrType,
|
|
383
|
+
ihdrData,
|
|
384
|
+
ihdrCrcBuf,
|
|
385
|
+
idatLen,
|
|
386
|
+
idatType,
|
|
387
|
+
idatData,
|
|
388
|
+
idatCrcBuf,
|
|
389
|
+
iendLen,
|
|
390
|
+
iendType,
|
|
391
|
+
iendCrcBuf,
|
|
392
|
+
]);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
const outputFormat = opts.outputFormat || 'png';
|
|
396
|
+
if (outputFormat === 'webp') {
|
|
397
|
+
bufScr = await sharp(raw, {
|
|
398
|
+
raw: { width, height, channels: 3 },
|
|
399
|
+
})
|
|
400
|
+
.webp({
|
|
401
|
+
lossless: true,
|
|
402
|
+
quality: 100,
|
|
403
|
+
effort: 6,
|
|
404
|
+
})
|
|
405
|
+
.toBuffer();
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
bufScr = await sharp(raw, {
|
|
409
|
+
raw: { width, height, channels: 3 },
|
|
410
|
+
})
|
|
411
|
+
.png({
|
|
412
|
+
compressionLevel: 3,
|
|
413
|
+
palette: false,
|
|
414
|
+
effort: 1,
|
|
415
|
+
adaptiveFiltering: false,
|
|
416
|
+
})
|
|
417
|
+
.toBuffer();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
286
420
|
}
|
|
287
421
|
payload.length = 0;
|
|
288
422
|
dataWithMarkers.length = 0;
|
|
@@ -290,87 +424,9 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
290
424
|
meta.length = 0;
|
|
291
425
|
paddedData.length = 0;
|
|
292
426
|
dataWithoutMarkers.length = 0;
|
|
293
|
-
if (opts.onProgress)
|
|
294
|
-
opts.onProgress({ phase: 'png_gen', loaded: 0, total: height });
|
|
295
|
-
let bufScr;
|
|
296
|
-
if (useManualPng) {
|
|
297
|
-
const bytesPerRow = width * 3;
|
|
298
|
-
const scanlinesData = Buffer.alloc(height * (1 + bytesPerRow));
|
|
299
|
-
const progressStep = Math.max(1, Math.floor(height / 20));
|
|
300
|
-
for (let row = 0; row < height; row++) {
|
|
301
|
-
scanlinesData[row * (1 + bytesPerRow)] = 0;
|
|
302
|
-
const srcStart = row * stride + 1;
|
|
303
|
-
const dstStart = row * (1 + bytesPerRow) + 1;
|
|
304
|
-
raw.copy(scanlinesData, dstStart, srcStart, srcStart + bytesPerRow);
|
|
305
|
-
if (opts.onProgress && row % progressStep === 0) {
|
|
306
|
-
opts.onProgress({ phase: 'png_gen', loaded: row, total: height });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (opts.onProgress)
|
|
310
|
-
opts.onProgress({ phase: 'png_compress', loaded: 0, total: 100 });
|
|
311
|
-
const idatData = zlib.deflateSync(scanlinesData, {
|
|
312
|
-
level: 0,
|
|
313
|
-
memLevel: 8,
|
|
314
|
-
strategy: zlib.constants.Z_FILTERED,
|
|
315
|
-
});
|
|
316
|
-
raw = Buffer.alloc(0);
|
|
317
|
-
const ihdrData = Buffer.alloc(13);
|
|
318
|
-
ihdrData.writeUInt32BE(width, 0);
|
|
319
|
-
ihdrData.writeUInt32BE(height, 4);
|
|
320
|
-
ihdrData[8] = 8;
|
|
321
|
-
ihdrData[9] = 2;
|
|
322
|
-
ihdrData[10] = 0;
|
|
323
|
-
ihdrData[11] = 0;
|
|
324
|
-
ihdrData[12] = 0;
|
|
325
|
-
const ihdrType = Buffer.from('IHDR', 'utf8');
|
|
326
|
-
const ihdrCrc = crc32(ihdrData, crc32(ihdrType));
|
|
327
|
-
const ihdrCrcBuf = Buffer.alloc(4);
|
|
328
|
-
ihdrCrcBuf.writeUInt32BE(ihdrCrc, 0);
|
|
329
|
-
const ihdrLen = Buffer.alloc(4);
|
|
330
|
-
ihdrLen.writeUInt32BE(ihdrData.length, 0);
|
|
331
|
-
const idatType = Buffer.from('IDAT', 'utf8');
|
|
332
|
-
const idatCrc = crc32(idatData, crc32(idatType));
|
|
333
|
-
const idatCrcBuf = Buffer.alloc(4);
|
|
334
|
-
idatCrcBuf.writeUInt32BE(idatCrc, 0);
|
|
335
|
-
const idatLen = Buffer.alloc(4);
|
|
336
|
-
idatLen.writeUInt32BE(idatData.length, 0);
|
|
337
|
-
const iendType = Buffer.from('IEND', 'utf8');
|
|
338
|
-
const iendCrc = crc32(Buffer.alloc(0), crc32(iendType));
|
|
339
|
-
const iendCrcBuf = Buffer.alloc(4);
|
|
340
|
-
iendCrcBuf.writeUInt32BE(iendCrc, 0);
|
|
341
|
-
const iendLen = Buffer.alloc(4);
|
|
342
|
-
iendLen.writeUInt32BE(0, 0);
|
|
343
|
-
bufScr = Buffer.concat([
|
|
344
|
-
PNG_HEADER,
|
|
345
|
-
ihdrLen,
|
|
346
|
-
ihdrType,
|
|
347
|
-
ihdrData,
|
|
348
|
-
ihdrCrcBuf,
|
|
349
|
-
idatLen,
|
|
350
|
-
idatType,
|
|
351
|
-
idatData,
|
|
352
|
-
idatCrcBuf,
|
|
353
|
-
iendLen,
|
|
354
|
-
iendType,
|
|
355
|
-
iendCrcBuf,
|
|
356
|
-
]);
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
bufScr = await sharp(raw, {
|
|
360
|
-
raw: { width, height, channels: 3 },
|
|
361
|
-
})
|
|
362
|
-
.png({
|
|
363
|
-
compressionLevel: 3,
|
|
364
|
-
palette: false,
|
|
365
|
-
effort: 1,
|
|
366
|
-
adaptiveFiltering: false,
|
|
367
|
-
})
|
|
368
|
-
.toBuffer();
|
|
369
|
-
}
|
|
370
|
-
raw = Buffer.alloc(0);
|
|
371
427
|
if (opts.onProgress)
|
|
372
428
|
opts.onProgress({ phase: 'png_compress', loaded: 100, total: 100 });
|
|
373
|
-
if (opts.skipOptimization) {
|
|
429
|
+
if (opts.skipOptimization || opts.outputFormat === 'webp') {
|
|
374
430
|
progressBar?.stop();
|
|
375
431
|
return bufScr;
|
|
376
432
|
}
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -9,3 +9,10 @@ export declare function deltaEncode(data: Buffer): Buffer;
|
|
|
9
9
|
export declare function deltaDecode(data: Buffer): Buffer;
|
|
10
10
|
export declare function applyXor(buf: Buffer, passphrase: string): Buffer;
|
|
11
11
|
export declare function tryDecryptIfNeeded(buf: Buffer, passphrase?: string): Buffer;
|
|
12
|
+
export declare function generatePalette256(): Buffer;
|
|
13
|
+
export declare function encodeDataToBlocks2x2(data: Buffer): {
|
|
14
|
+
buffer: Buffer;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function decodeBlocksToData(indexedBuffer: Buffer, width: number, height: number): Buffer;
|
package/dist/utils/helpers.js
CHANGED
|
@@ -110,3 +110,43 @@ export function tryDecryptIfNeeded(buf, passphrase) {
|
|
|
110
110
|
}
|
|
111
111
|
return buf;
|
|
112
112
|
}
|
|
113
|
+
export function generatePalette256() {
|
|
114
|
+
const palette = Buffer.alloc(256 * 3);
|
|
115
|
+
for (let i = 0; i < 256; i++) {
|
|
116
|
+
palette[i * 3] = i;
|
|
117
|
+
palette[i * 3 + 1] = (i * 127) & 0xff;
|
|
118
|
+
palette[i * 3 + 2] = 255 - i;
|
|
119
|
+
}
|
|
120
|
+
return palette;
|
|
121
|
+
}
|
|
122
|
+
export function encodeDataToBlocks2x2(data) {
|
|
123
|
+
const totalBytes = data.length;
|
|
124
|
+
const totalBlocks = totalBytes;
|
|
125
|
+
const blocksPerRow = Math.ceil(Math.sqrt(totalBlocks));
|
|
126
|
+
const numRows = Math.ceil(totalBlocks / blocksPerRow);
|
|
127
|
+
const pixelWidth = blocksPerRow * 2;
|
|
128
|
+
const pixelHeight = numRows * 2;
|
|
129
|
+
const indexedBuffer = Buffer.alloc(pixelWidth * pixelHeight);
|
|
130
|
+
for (let i = 0; i < totalBytes; i++) {
|
|
131
|
+
const blockX = (i % blocksPerRow) * 2;
|
|
132
|
+
const blockY = Math.floor(i / blocksPerRow) * 2;
|
|
133
|
+
const value = data[i];
|
|
134
|
+
indexedBuffer[blockY * pixelWidth + blockX] = value;
|
|
135
|
+
indexedBuffer[blockY * pixelWidth + blockX + 1] = value;
|
|
136
|
+
indexedBuffer[(blockY + 1) * pixelWidth + blockX] = value;
|
|
137
|
+
indexedBuffer[(blockY + 1) * pixelWidth + blockX + 1] = value;
|
|
138
|
+
}
|
|
139
|
+
return { buffer: indexedBuffer, width: pixelWidth, height: pixelHeight };
|
|
140
|
+
}
|
|
141
|
+
export function decodeBlocksToData(indexedBuffer, width, height) {
|
|
142
|
+
const blocksPerRow = width / 2;
|
|
143
|
+
const numRows = height / 2;
|
|
144
|
+
const totalBlocks = blocksPerRow * numRows;
|
|
145
|
+
const data = Buffer.alloc(totalBlocks);
|
|
146
|
+
for (let i = 0; i < totalBlocks; i++) {
|
|
147
|
+
const blockX = (i % blocksPerRow) * 2;
|
|
148
|
+
const blockY = Math.floor(i / blocksPerRow) * 2;
|
|
149
|
+
data[i] = indexedBuffer[blockY * width + blockX];
|
|
150
|
+
}
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function isRustBinaryAvailable(): boolean;
|
|
2
|
-
export declare function encodeWithRustCLI(inputPath: string, outputPath: string, compressionLevel?: number, passphrase?: string, encryptType?: 'aes' | 'xor'): Promise<void>;
|
|
2
|
+
export declare function encodeWithRustCLI(inputPath: string, outputPath: string, compressionLevel?: number, passphrase?: string, encryptType?: 'aes' | 'xor', name?: string): Promise<void>;
|
|
@@ -56,7 +56,7 @@ function findRustBinary() {
|
|
|
56
56
|
export function isRustBinaryAvailable() {
|
|
57
57
|
return findRustBinary() !== null;
|
|
58
58
|
}
|
|
59
|
-
export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel = 3, passphrase, encryptType = 'aes') {
|
|
59
|
+
export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel = 3, passphrase, encryptType = 'aes', name) {
|
|
60
60
|
const cliPath = findRustBinary();
|
|
61
61
|
if (!cliPath) {
|
|
62
62
|
throw new Error('Rust CLI binary not found. Run: cargo build --release');
|
|
@@ -69,6 +69,9 @@ export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel
|
|
|
69
69
|
'--level',
|
|
70
70
|
String(compressionLevel),
|
|
71
71
|
];
|
|
72
|
+
if (name) {
|
|
73
|
+
args.push('--name', name);
|
|
74
|
+
}
|
|
72
75
|
if (passphrase) {
|
|
73
76
|
args.push('--passphrase', passphrase);
|
|
74
77
|
args.push('--encrypt', encryptType);
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface EncodeOptions {
|
|
|
10
10
|
encrypt?: 'auto' | 'aes' | 'xor' | 'none';
|
|
11
11
|
_skipAuto?: boolean;
|
|
12
12
|
output?: 'auto' | 'png' | 'rox';
|
|
13
|
+
outputFormat?: 'png' | 'webp';
|
|
13
14
|
includeName?: boolean;
|
|
14
15
|
includeFileList?: boolean;
|
|
15
16
|
fileList?: Array<string | {
|
|
@@ -17,6 +18,7 @@ export interface EncodeOptions {
|
|
|
17
18
|
size: number;
|
|
18
19
|
}>;
|
|
19
20
|
skipOptimization?: boolean;
|
|
21
|
+
useBlockEncoding?: boolean;
|
|
20
22
|
onProgress?: (info: {
|
|
21
23
|
phase: string;
|
|
22
24
|
loaded?: number;
|
|
@@ -43,4 +45,5 @@ export interface DecodeOptions {
|
|
|
43
45
|
total?: number;
|
|
44
46
|
}) => void;
|
|
45
47
|
showProgress?: boolean;
|
|
48
|
+
verbose?: boolean;
|
|
46
49
|
}
|
package/libroxify_native.node
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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",
|
|
@@ -22,7 +22,14 @@
|
|
|
22
22
|
"build:cli": "cargo build --release --bin roxify_native && cp target/release/roxify_native dist/roxify-cli",
|
|
23
23
|
"build:all": "npm run build:native && npm run build && npm run build:cli",
|
|
24
24
|
"prepublishOnly": "npm run build:all",
|
|
25
|
-
"test": "node test/
|
|
25
|
+
"test": "npm run build && node test/run-all-tests.js",
|
|
26
|
+
"test:final": "node test/test-final-complete.js",
|
|
27
|
+
"test:benchmark": "node test/benchmark-large-data.js",
|
|
28
|
+
"test:stress": "node test/stress-test-datatypes.js",
|
|
29
|
+
"test:compare": "node test/compare-png-webp.js",
|
|
30
|
+
"test:formats": "node test/benchmark-image-formats.js",
|
|
31
|
+
"test:optimize": "node test/benchmark-auto-optimize.cjs",
|
|
32
|
+
"test:predict": "node test/test-fast-prediction.cjs",
|
|
26
33
|
"cli": "node dist/cli.js"
|
|
27
34
|
},
|
|
28
35
|
"keywords": [
|
package/README.md
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
# RoxCompressor Transform
|
|
2
|
-
|
|
3
|
-
> Encode binary data into PNG images and decode them back. Fast, efficient, with optional encryption and native Rust acceleration.
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/roxify)
|
|
6
|
-
[](LICENSE)
|
|
7
|
-
|
|
8
|
-
## Features
|
|
9
|
-
|
|
10
|
-
- ⚡ **Blazing Fast**: Native Rust acceleration via N-API — **1GB/s** throughput on modern hardware
|
|
11
|
-
- 🚀 **Optimized Compression**: Multi-threaded Zstd compression (level 19) with parallel processing
|
|
12
|
-
- 🔒 **Secure**: AES-256-GCM encryption support with PBKDF2 key derivation
|
|
13
|
-
- 🎨 **Multiple modes**: Compact, chunk, pixel, and screenshot modes
|
|
14
|
-
- 📦 **CLI & API**: Use as command-line tool or JavaScript library
|
|
15
|
-
- 🔄 **Lossless**: Perfect roundtrip encoding/decoding
|
|
16
|
-
- 📖 **Full TSDoc**: Complete TypeScript documentation
|
|
17
|
-
- 🦀 **Rust Powered**: Optional native module for extreme performance (falls back to pure JS)
|
|
18
|
-
|
|
19
|
-
## Real-world benchmarks 🔧
|
|
20
|
-
|
|
21
|
-
**Highlights**
|
|
22
|
-
|
|
23
|
-
- Practical benchmarks on large codebase datasets showing significant compression and high throughput while handling many small files efficiently.
|
|
24
|
-
|
|
25
|
-
**Results**
|
|
26
|
-
|
|
27
|
-
| Dataset | Files | Original | Compressed | Ratio | Time | Throughput | Notes |
|
|
28
|
-
| -------- | ------: | -------: | ---------: | --------: | -----: | ---------: | ------------------------------------------- |
|
|
29
|
-
| 4,000 MB | 731,340 | 3.93 GB | 111.42 MB | **2.8%** | 26.9 s | 149.4 MB/s | gzip: 2.26 GB (57.5%); 7z: 1.87 GB (47.6%) |
|
|
30
|
-
| 1,000 MB | 141,522 | 1.03 GB | 205 MB | **19.4%** | ~6.2 s | ≈170 MB/s | shows benefits for many-small-file datasets |
|
|
31
|
-
|
|
32
|
-
### Methodology
|
|
33
|
-
|
|
34
|
-
- Compression: multithreaded Zstd (level 19) and Brotli (configurable).
|
|
35
|
-
- Setup: parallel I/O and multithreaded compression on modern SSD-backed systems.
|
|
36
|
-
- Measurements: wall-clock time; throughput = original size / time; comparisons against gzip and 7z with typical defaults.
|
|
37
|
-
- Reproducibility: full benchmark details, commands and raw data are available in `docs/BENCHMARK_FINAL_REPORT.md`.
|
|
38
|
-
|
|
39
|
-
These results demonstrate Roxify's strength for packaging large codebases and many-small-file archives where speed and a good compression/throughput trade-off matter.
|
|
40
|
-
|
|
41
|
-
## Documentation
|
|
42
|
-
|
|
43
|
-
- 📘 **[CLI Documentation](./CLI.md)** - Complete command-line usage guide
|
|
44
|
-
- 📗 **[JavaScript SDK](./JAVASCRIPT_SDK.md)** - Programmatic API reference with examples
|
|
45
|
-
- 📙 **[Quick Start](#quick-start)** - Get started in 2 minutes
|
|
46
|
-
|
|
47
|
-
## Installation
|
|
48
|
-
|
|
49
|
-
### As CLI tool (npx)
|
|
50
|
-
|
|
51
|
-
No installation needed! Use directly with npx:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
npx rox encode input.zip output.png
|
|
55
|
-
npx rox decode output.png original.zip
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### As library
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
npm install roxify
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## CLI Usage
|
|
65
|
-
|
|
66
|
-
### Quick Start
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
# Encode a file
|
|
70
|
-
npx rox encode document.pdf document.png
|
|
71
|
-
|
|
72
|
-
# Decode it back
|
|
73
|
-
npx rox decode document.png document.pdf
|
|
74
|
-
|
|
75
|
-
# With encryption
|
|
76
|
-
npx rox encode secret.zip secret.png -p mypassword
|
|
77
|
-
npx rox decode secret.png secret.zip -p mypassword
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### CLI Commands
|
|
81
|
-
|
|
82
|
-
#### `encode` - Encode file to PNG
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
npx rox encode <input> [output] [options]
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**Options:**
|
|
89
|
-
|
|
90
|
-
- `-p, --passphrase <pass>` - Encrypt with passphrase (AES-256-GCM)
|
|
91
|
-
- `-m, --mode <mode>` - Encoding mode: `compact|chunk|pixel|screenshot` (default: `screenshot`)
|
|
92
|
-
- `-q, --quality <0-11>` - Brotli compression quality (default: `1`)
|
|
93
|
-
- `0` = fastest, largest
|
|
94
|
-
- `11` = slowest, smallest
|
|
95
|
-
- `-e, --encrypt <type>` - Encryption: `auto|aes|xor|none` (default: `aes` if passphrase)
|
|
96
|
-
- `--no-compress` - Disable compression
|
|
97
|
-
- `-o, --output <path>` - Output file path
|
|
98
|
-
|
|
99
|
-
**Examples:**
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
# Basic encoding
|
|
103
|
-
npx rox encode data.bin output.png
|
|
104
|
-
|
|
105
|
-
# Fast compression for large files
|
|
106
|
-
npx rox encode large-video.mp4 output.png -q 0
|
|
107
|
-
|
|
108
|
-
# High compression for small files
|
|
109
|
-
npx rox encode config.json output.png -q 11
|
|
110
|
-
|
|
111
|
-
# With encryption
|
|
112
|
-
npx rox encode secret.pdf secure.png -p "my secure password"
|
|
113
|
-
|
|
114
|
-
# Compact mode (smallest PNG)
|
|
115
|
-
npx rox encode data.bin tiny.png -m compact
|
|
116
|
-
|
|
117
|
-
# Screenshot mode (recommended, looks like a real image)
|
|
118
|
-
npx rox encode archive.tar.gz screenshot.png -m screenshot
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
#### `decode` - Decode PNG to file
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
npx rox decode <input> [output] [options]
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Options:**
|
|
128
|
-
|
|
129
|
-
- `-p, --passphrase <pass>` - Decryption passphrase
|
|
130
|
-
- `-o, --output <path>` - Output file path (auto-detected from metadata if not provided)
|
|
131
|
-
|
|
132
|
-
**Examples:**
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
# Basic decoding
|
|
136
|
-
npx rox decode encoded.png output.bin
|
|
137
|
-
|
|
138
|
-
# Auto-detect filename from metadata
|
|
139
|
-
npx rox decode encoded.png
|
|
140
|
-
|
|
141
|
-
# With decryption
|
|
142
|
-
npx rox decode encrypted.png output.pdf -p "my secure password"
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## JavaScript API
|
|
146
|
-
|
|
147
|
-
### Basic Usage
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
import { encodeBinaryToPng, decodePngToBinary } from 'roxify';
|
|
151
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
152
|
-
|
|
153
|
-
// Encode
|
|
154
|
-
const input = readFileSync('input.zip');
|
|
155
|
-
const png = await encodeBinaryToPng(input, {
|
|
156
|
-
mode: 'screenshot',
|
|
157
|
-
name: 'input.zip',
|
|
158
|
-
});
|
|
159
|
-
writeFileSync('output.png', png);
|
|
160
|
-
|
|
161
|
-
// Decode
|
|
162
|
-
const encoded = readFileSync('output.png');
|
|
163
|
-
const result = await decodePngToBinary(encoded);
|
|
164
|
-
writeFileSync(result.meta?.name || 'output.bin', result.buf);
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### With Encryption
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
// Encode with AES-256-GCM
|
|
171
|
-
const png = await encodeBinaryToPng(input, {
|
|
172
|
-
mode: 'screenshot',
|
|
173
|
-
passphrase: 'my-secret-password',
|
|
174
|
-
encrypt: 'aes',
|
|
175
|
-
name: 'secret.zip',
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// Decode with passphrase
|
|
179
|
-
const result = await decodePngToBinary(encoded, {
|
|
180
|
-
passphrase: 'my-secret-password',
|
|
181
|
-
});
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Fast Compression
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
// Optimize for speed (recommended for large files)
|
|
188
|
-
const png = await encodeBinaryToPng(largeBuffer, {
|
|
189
|
-
mode: 'screenshot',
|
|
190
|
-
brQuality: 0, // Fastest
|
|
191
|
-
name: 'large-file.bin',
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Optimize for size (recommended for small files)
|
|
195
|
-
const png = await encodeBinaryToPng(smallBuffer, {
|
|
196
|
-
mode: 'compact',
|
|
197
|
-
brQuality: 11, // Best compression
|
|
198
|
-
name: 'config.json',
|
|
199
|
-
});
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Encoding Modes
|
|
203
|
-
|
|
204
|
-
#### `screenshot` (Recommended)
|
|
205
|
-
|
|
206
|
-
Encodes data as RGB pixel values, optimized for screenshot-like appearance. Best balance of size and compatibility.
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
const png = await encodeBinaryToPng(data, { mode: 'screenshot' });
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
#### `compact` (Smallest)
|
|
213
|
-
|
|
214
|
-
Minimal 1x1 PNG with data in custom chunk. Fastest and smallest.
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
const png = await encodeBinaryToPng(data, { mode: 'compact' });
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
#### `pixel`
|
|
221
|
-
|
|
222
|
-
Encodes data as RGB pixel values without screenshot optimization.
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
const png = await encodeBinaryToPng(data, { mode: 'pixel' });
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
#### `chunk`
|
|
229
|
-
|
|
230
|
-
Standard PNG with data in custom rXDT chunk.
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
const png = await encodeBinaryToPng(data, { mode: 'chunk' });
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
## API Reference
|
|
237
|
-
|
|
238
|
-
### `encodeBinaryToPng(input, options)`
|
|
239
|
-
|
|
240
|
-
Encodes binary data into a PNG image.
|
|
241
|
-
|
|
242
|
-
**Parameters:**
|
|
243
|
-
|
|
244
|
-
- `input: Buffer` - The binary data to encode
|
|
245
|
-
- `options?: EncodeOptions` - Encoding options
|
|
246
|
-
|
|
247
|
-
**Returns:** `Promise<Buffer>` - The encoded PNG
|
|
248
|
-
|
|
249
|
-
**Options:**
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
interface EncodeOptions {
|
|
253
|
-
// Compression algorithm ('br' = Brotli, 'none' = no compression)
|
|
254
|
-
compression?: 'br' | 'none';
|
|
255
|
-
|
|
256
|
-
// Passphrase for encryption
|
|
257
|
-
passphrase?: string;
|
|
258
|
-
|
|
259
|
-
// Original filename to embed
|
|
260
|
-
name?: string;
|
|
261
|
-
|
|
262
|
-
// Encoding mode
|
|
263
|
-
mode?: 'compact' | 'chunk' | 'pixel' | 'screenshot';
|
|
264
|
-
|
|
265
|
-
// Encryption method
|
|
266
|
-
encrypt?: 'auto' | 'aes' | 'xor' | 'none';
|
|
267
|
-
|
|
268
|
-
// Output format
|
|
269
|
-
output?: 'auto' | 'png' | 'rox';
|
|
270
|
-
|
|
271
|
-
// Include filename in metadata (default: true)
|
|
272
|
-
includeName?: boolean;
|
|
273
|
-
|
|
274
|
-
// Brotli quality 0-11 (default: 1)
|
|
275
|
-
brQuality?: number;
|
|
276
|
-
}
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### `decodePngToBinary(pngBuf, options)`
|
|
280
|
-
|
|
281
|
-
Decodes a PNG image back to binary data.
|
|
282
|
-
|
|
283
|
-
**Parameters:**
|
|
284
|
-
|
|
285
|
-
- `pngBuf: Buffer` - The PNG image to decode
|
|
286
|
-
- `options?: DecodeOptions` - Decoding options
|
|
287
|
-
|
|
288
|
-
**Returns:** `Promise<DecodeResult>` - The decoded data and metadata
|
|
289
|
-
|
|
290
|
-
**Options:**
|
|
291
|
-
|
|
292
|
-
```typescript
|
|
293
|
-
interface DecodeOptions {
|
|
294
|
-
// Passphrase for decryption
|
|
295
|
-
passphrase?: string;
|
|
296
|
-
}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
**Result:**
|
|
300
|
-
|
|
301
|
-
```typescript
|
|
302
|
-
interface DecodeResult {
|
|
303
|
-
// Decoded binary data
|
|
304
|
-
buf: Buffer;
|
|
305
|
-
|
|
306
|
-
// Extracted metadata
|
|
307
|
-
meta?: {
|
|
308
|
-
// Original filename
|
|
309
|
-
name?: string;
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
## Performance Tips
|
|
315
|
-
|
|
316
|
-
### For Large Files (>10 MB)
|
|
317
|
-
|
|
318
|
-
```bash
|
|
319
|
-
# Use quality 0 for fastest encoding
|
|
320
|
-
npx rox encode large.bin output.png -q 0
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
const png = await encodeBinaryToPng(largeFile, {
|
|
325
|
-
mode: 'screenshot',
|
|
326
|
-
brQuality: 0, // 10-20x faster than default
|
|
327
|
-
});
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### For Small Files (<1 MB)
|
|
331
|
-
|
|
332
|
-
```bash
|
|
333
|
-
# Use quality 11 for best compression
|
|
334
|
-
npx rox encode small.json output.png -q 11 -m compact
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
```typescript
|
|
338
|
-
const png = await encodeBinaryToPng(smallFile, {
|
|
339
|
-
mode: 'compact',
|
|
340
|
-
brQuality: 11, // Best compression ratio
|
|
341
|
-
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
### Benchmark Results
|
|
345
|
-
|
|
346
|
-
File: 3.8 MB binary
|
|
347
|
-
|
|
348
|
-
- **Quality 0**: ~500-800ms, output ~1.2 MB
|
|
349
|
-
- **Quality 1** (default): ~1-2s, output ~800 KB
|
|
350
|
-
- **Quality 5**: ~8-12s, output ~750 KB
|
|
351
|
-
- **Quality 11**: ~20-30s, output ~720 KB
|
|
352
|
-
|
|
353
|
-
## Error Handling
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
try {
|
|
357
|
-
const result = await decodePngToBinary(encoded, {
|
|
358
|
-
passphrase: 'wrong-password',
|
|
359
|
-
});
|
|
360
|
-
} catch (err) {
|
|
361
|
-
if (err.message.includes('Incorrect passphrase')) {
|
|
362
|
-
console.error('Wrong password!');
|
|
363
|
-
} else if (err.message.includes('Invalid ROX format')) {
|
|
364
|
-
console.error('Not a valid RoxCompressor PNG');
|
|
365
|
-
} else {
|
|
366
|
-
console.error('Decode failed:', err.message);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
## Security
|
|
372
|
-
|
|
373
|
-
- **AES-256-GCM**: Authenticated encryption with 100,000 PBKDF2 iterations
|
|
374
|
-
- **XOR cipher**: Simple obfuscation (not cryptographically secure)
|
|
375
|
-
- **No encryption**: Data is compressed but not encrypted
|
|
376
|
-
|
|
377
|
-
⚠️ **Warning**: Use strong passphrases for sensitive data. The `xor` encryption mode is not secure and should only be used for obfuscation.
|
|
378
|
-
|
|
379
|
-
## License
|
|
380
|
-
|
|
381
|
-
MIT © RoxCompressor
|
|
382
|
-
|
|
383
|
-
## Contributing
|
|
384
|
-
|
|
385
|
-
Contributions welcome! Please open an issue or PR on GitHub.
|
|
386
|
-
|
|
387
|
-
## Links
|
|
388
|
-
|
|
389
|
-
- [GitHub Repository](https://github.com/RoxasYTB/RoxCompressor)
|
|
390
|
-
- [npm Package](https://www.npmjs.com/package/roxify)
|
|
391
|
-
- [Report Issues](https://github.com/RoxasYTB/RoxCompressor/issues)
|