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 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.3.2';
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
- await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput, 3, parsed.passphrase, encryptType);
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: true,
287
- compressionLevel: 19,
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
+ };
@@ -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
+ };
@@ -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
- const rawRGB = Buffer.allocUnsafe(currentWidth * currentHeight * 3);
232
- let writeOffset = 0;
233
- const rowsPerChunk = 2000;
234
- for (let startRow = 0; startRow < currentHeight; startRow += rowsPerChunk) {
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: startRow,
241
- width: currentWidth,
242
- height: chunkHeight,
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
- const channels = chunkInfo.channels;
247
- const pixelsInChunk = currentWidth * chunkHeight;
248
- if (channels === 3) {
249
- chunkData.copy(rawRGB, writeOffset);
250
- writeOffset += pixelsInChunk * 3;
251
- }
252
- else if (channels === 4) {
253
- for (let i = 0; i < pixelsInChunk; i++) {
254
- rawRGB[writeOffset++] = chunkData[i * 4];
255
- rawRGB[writeOffset++] = chunkData[i * 4 + 1];
256
- rawRGB[writeOffset++] = chunkData[i * 4 + 2];
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
- if (opts.onProgress) {
260
- opts.onProgress({
261
- phase: 'extract_pixels',
262
- loaded: endRow,
263
- total: currentHeight,
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) {
@@ -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 dataWithoutMarkers = [PIXEL_MAGIC, ...metaPixel];
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
- const dataPixels = Math.ceil(dataWithMarkersLen / 3);
239
- const totalPixels = dataPixels + MARKER_END.length;
240
- const maxWidth = 16384;
241
- let side = Math.ceil(Math.sqrt(totalPixels));
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 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);
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
- raw = Buffer.alloc(width * height * bytesPerPixel);
284
- const flatData = Buffer.concat(dataWithMarkers);
285
- flatData.copy(raw, 0, 0, Math.min(flatData.length, raw.length));
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
  }
@@ -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;
@@ -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);
@@ -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
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.3.2",
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/test-*.js",
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
- [![npm version](https://img.shields.io/npm/v/roxify.svg)](https://www.npmjs.com/package/roxify)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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)