roxify 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,15 @@ Encode binary data into PNG images and decode them back. Supports CLI and progra
6
6
 
7
7
  Roxify is a compact, color-based alternative to QR codes, designed specifically for digital-only use (not for printing). It encodes data using color channels (rather than monochrome patterns) for higher density, and is optimized for decoding from approximate screenshots — including nearest-neighbour resize/stretch and solid or gradient backgrounds. It is not intended for printed media and is not resilient to lossy compression or heavy image filtering.
8
8
 
9
- Roxified PNGs are often more space-efficient than ZIP archives for similar payloads and provide a visual indication of the embedded data size.
9
+ Roxify creates PNGs that are often more space-efficient than ZIP or 7z archives for similar payloads without loss. Roxify provides superior compression ratios, making it ideal for embedding images, GIFs, audio, video, code, and other files without any quality loss — the original file is perfectly recovered upon decoding.
10
+
11
+ Key benefits:
12
+
13
+ - **Superior Compression**: Roxify outperforms traditional ZIP and 7z (LZMA) in speed and ratio, enabling smaller PNG outputs.
14
+ - **Lossless Embedding**: Compress and embed any file type (images, videos, code) with full fidelity restoration.
15
+ - **Code Efficiency**: Hyper-efficient for compressing source code, reducing file sizes dramatically.
16
+ - **Obfuscation & Security**: Obfuscate code or lock files with AES-256-GCM encryption, more compact than password-protected ZIPs.
17
+ - **Visual Data Indicator**: PNG size visually represents embedded data size, providing an intuitive overview.
10
18
 
11
19
  ## Installation
12
20
 
@@ -36,7 +44,7 @@ If no output name is provided:
36
44
 
37
45
  - `-p, --passphrase <pass>` — Encrypt with AES-256-GCM
38
46
  - `-m, --mode <mode>` — Encoding mode: `screenshot` (default), `pixel`, `compact`, `chunk`
39
- - `-q, --quality <0-11>` — Brotli compression quality (default: 11)
47
+ - `-q, --quality <0-22>` — Roxify compression level (default: 22)
40
48
  - `--no-compress` — Disable compression
41
49
  - `-v, --verbose` — Show detailed errors
42
50
 
@@ -68,8 +76,8 @@ console.log(meta?.name);
68
76
  - `mode` — `'screenshot'` | `'pixel'` | `'compact'` | `'chunk'` (default: `'screenshot'`)
69
77
  - `name` — Original filename (embedded as metadata)
70
78
  - `passphrase` — Encryption passphrase (uses AES-256-GCM)
71
- - `compression` — `'br'` | `'none'` (default: `'br'`)
72
- - `brQuality` — Brotli quality 0-11 (default: 4)
79
+ - `compression` — `'Roxify'` | `'none'` (default: `'Roxify'`)
80
+ - `brQuality` — Roxify compression level 0-22 (default: 22)
73
81
 
74
82
  ## Example: Express Endpoint
75
83
 
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, writeFileSync } from 'fs';
3
- import { basename, resolve } from 'path';
3
+ import { basename, dirname, join, resolve } from 'path';
4
+ import sharp from 'sharp';
4
5
  import { cropAndReconstitute, DataFormatError, decodePngToBinary, encodeBinaryToPng, IncorrectPassphraseError, PassphraseRequiredError, } from './index.js';
5
6
  const VERSION = '1.0.4';
6
7
  function showHelp() {
@@ -22,6 +23,7 @@ Options:
22
23
  --no-compress Disable compression
23
24
  -o, --output <path> Output file path
24
25
  --view-reconst Export the reconstituted PNG for debugging
26
+ --debug Export debug images (doubled.png, reconstructed.png)
25
27
  -v, --verbose Show detailed errors
26
28
 
27
29
  Run "npx rox help" for this message.
@@ -46,6 +48,10 @@ function parseArgs(args) {
46
48
  parsed.viewReconst = true;
47
49
  i++;
48
50
  }
51
+ else if (key === 'debug') {
52
+ parsed.debug = true;
53
+ i++;
54
+ }
49
55
  else if (key === 'debug-dir') {
50
56
  parsed.debugDir = args[i + 1];
51
57
  i += 2;
@@ -159,33 +165,33 @@ async function decodeCommand(args) {
159
165
  }
160
166
  const resolvedInput = resolve(inputPath);
161
167
  try {
162
- let inputBuffer = readFileSync(resolvedInput);
163
- let resolvedInputPath = resolvedInput;
164
- try {
165
- const reconst = await cropAndReconstitute(inputBuffer);
166
- inputBuffer = reconst;
167
- resolvedInputPath = resolvedInput.replace(/(\.\w+)?$/, '_reconst.png');
168
- if (parsed.viewReconst) {
169
- writeFileSync(resolvedInputPath, reconst);
170
- console.log(`Reconst PNG: ${resolvedInputPath}`);
171
- }
172
- }
173
- catch (e) {
174
- console.log('Could not generate reconst PNG:', e.message);
175
- }
176
- console.log(`Reading: ${resolvedInputPath.replace('_reconst.png', '.png')}`);
168
+ const inputBuffer = readFileSync(resolvedInput);
169
+ console.log(`Reading: ${resolvedInput}`);
170
+ const info = await sharp(inputBuffer).metadata();
177
171
  const options = {};
178
172
  if (parsed.passphrase) {
179
173
  options.passphrase = parsed.passphrase;
180
174
  }
181
- if (parsed.debugDir) {
182
- options.debugDir = parsed.debugDir;
175
+ if (parsed.debug) {
176
+ options.debugDir = dirname(resolvedInput);
183
177
  }
184
178
  console.log(`Decoding...`);
185
179
  const startDecode = Date.now();
186
180
  if (parsed.verbose)
187
181
  options.verbose = true;
188
- const result = await decodePngToBinary(inputBuffer, options);
182
+ const doubledBuffer = await sharp(inputBuffer)
183
+ .resize({
184
+ width: info.width * 2,
185
+ height: info.height * 2,
186
+ kernel: 'nearest',
187
+ })
188
+ .png()
189
+ .toBuffer();
190
+ if (options.debugDir) {
191
+ writeFileSync(join(options.debugDir, 'doubled.png'), doubledBuffer);
192
+ }
193
+ const reconstructedBuffer = await cropAndReconstitute(doubledBuffer, options.debugDir);
194
+ const result = await decodePngToBinary(reconstructedBuffer, options);
189
195
  const decodeTime = Date.now() - startDecode;
190
196
  const resolvedOutput = parsed.output || outputPath || result.meta?.name || 'decoded.bin';
191
197
  writeFileSync(resolvedOutput, result.buf);
package/dist/index.d.ts CHANGED
@@ -16,11 +16,10 @@ export declare class DataFormatError extends Error {
16
16
  export interface EncodeOptions {
17
17
  /**
18
18
  * Compression algorithm to use.
19
- * - `'br'`: Brotli compression (default for most modes)
20
- * - `'none'`: No compression
21
- * @defaultValue `'br'` for most modes
19
+ * - `'zstd'`: Zstandard compression (maximum compression for smallest files)
20
+ * @defaultValue `'zstd'`
22
21
  */
23
- compression?: 'br' | 'none';
22
+ compression?: 'zstd';
24
23
  /**
25
24
  * Passphrase for encryption. If provided without `encrypt` option, defaults to AES-256-GCM.
26
25
  */
@@ -91,7 +90,21 @@ export interface DecodeResult {
91
90
  name?: string;
92
91
  };
93
92
  }
94
- export declare function cropAndReconstitute(input: Buffer): Promise<Buffer>;
93
+ /**
94
+ * Options for decoding a PNG back to binary data.
95
+ * @public
96
+ */
97
+ export interface DecodeOptions {
98
+ /**
99
+ * Passphrase for encrypted inputs.
100
+ */
101
+ passphrase?: string;
102
+ /**
103
+ * Directory to save debug images (doubled.png, reconstructed.png).
104
+ */
105
+ debugDir?: string;
106
+ }
107
+ export declare function cropAndReconstitute(input: Buffer, debugDir?: string): Promise<Buffer>;
95
108
  /**
96
109
  * Encode a Buffer into a PNG wrapper. Supports optional compression and
97
110
  * encryption. Defaults are chosen for a good balance between speed and size.
@@ -109,6 +122,4 @@ export declare function encodeBinaryToPng(input: Buffer, opts?: EncodeOptions):
109
122
  * @param opts - Options (passphrase for encrypted inputs)
110
123
  * @public
111
124
  */
112
- export declare function decodePngToBinary(pngBuf: Buffer, opts?: {
113
- passphrase?: string;
114
- }): Promise<DecodeResult>;
125
+ export declare function decodePngToBinary(pngBuf: Buffer, opts?: DecodeOptions): Promise<DecodeResult>;
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { compress as zstdCompress, decompress as zstdDecompress, } from '@mongodb-js/zstd';
1
2
  import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes, } from 'crypto';
3
+ import { join } from 'path';
2
4
  import encode from 'png-chunks-encode';
3
5
  import extract from 'png-chunks-extract';
4
6
  import sharp from 'sharp';
@@ -38,6 +40,9 @@ const MARKER_COLORS = [
38
40
  ];
39
41
  const MARKER_START = MARKER_COLORS;
40
42
  const MARKER_END = [...MARKER_COLORS].reverse();
43
+ const COMPRESSION_MARKERS = {
44
+ zstd: [{ r: 0, g: 255, b: 0 }],
45
+ };
41
46
  function colorsToBytes(colors) {
42
47
  const buf = Buffer.alloc(colors.length * 3);
43
48
  for (let i = 0; i < colors.length; i++) {
@@ -58,6 +63,15 @@ function applyXor(buf, passphrase) {
58
63
  function tryBrotliDecompress(payload) {
59
64
  return Buffer.from(zlib.brotliDecompressSync(payload));
60
65
  }
66
+ async function tryZstdDecompress(payload) {
67
+ try {
68
+ const result = await zstdDecompress(payload);
69
+ return Buffer.from(result);
70
+ }
71
+ catch {
72
+ return payload;
73
+ }
74
+ }
61
75
  function tryDecryptIfNeeded(buf, passphrase) {
62
76
  if (!buf || buf.length === 0)
63
77
  return buf;
@@ -107,7 +121,7 @@ async function loadRaw(imgInput) {
107
121
  .toBuffer({ resolveWithObject: true });
108
122
  return { data, info };
109
123
  }
110
- export async function cropAndReconstitute(input) {
124
+ export async function cropAndReconstitute(input, debugDir) {
111
125
  async function loadRaw(imgInput) {
112
126
  const { data, info } = await sharp(imgInput)
113
127
  .ensureAlpha()
@@ -122,11 +136,28 @@ export async function cropAndReconstitute(input) {
122
136
  return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
123
137
  }
124
138
  const { data, info } = await loadRaw(input);
125
- const w = info.width;
126
- const h = info.height;
139
+ const doubledBuffer = await sharp(input)
140
+ .resize({
141
+ width: info.width * 2,
142
+ height: info.height * 2,
143
+ kernel: 'nearest',
144
+ })
145
+ .png()
146
+ .toBuffer();
147
+ if (debugDir) {
148
+ await sharp(doubledBuffer).toFile(join(debugDir, 'doubled.png'));
149
+ }
150
+ const { data: doubledData, info: doubledInfo } = await loadRaw(doubledBuffer);
151
+ const w = doubledInfo.width;
152
+ const h = doubledInfo.height;
127
153
  function at(x, y) {
128
154
  const i = idxFor(x, y, w);
129
- return [data[i], data[i + 1], data[i + 2], data[i + 3]];
155
+ return [
156
+ doubledData[i],
157
+ doubledData[i + 1],
158
+ doubledData[i + 2],
159
+ doubledData[i + 3],
160
+ ];
130
161
  }
131
162
  let startPoint = null;
132
163
  for (let y = 0; y < h && !startPoint; y++) {
@@ -200,7 +231,7 @@ export async function cropAndReconstitute(input) {
200
231
  const cropH = sy2 - sy1 + 1;
201
232
  if (cropW <= 0 || cropH <= 0)
202
233
  throw new Error('Invalid crop dimensions');
203
- const cropped = await sharp(input)
234
+ const cropped = await sharp(doubledBuffer)
204
235
  .extract({ left: sx1, top: sy1, width: cropW, height: cropH })
205
236
  .png()
206
237
  .toBuffer();
@@ -240,25 +271,13 @@ export async function cropAndReconstitute(input) {
240
271
  out[dstI + 3] = cdata[srcI + 3];
241
272
  }
242
273
  }
243
- if (cw >= 3) {
244
- const targetY = ch - 1;
245
- for (let x = cw - 3; x < cw; x++) {
246
- const i = ((targetY * newWidth + x) * 4) | 0;
247
- out[i] = 0;
248
- out[i + 1] = 0;
249
- out[i + 2] = 0;
250
- out[i + 3] = 255;
251
- }
252
- }
253
- else {
254
- const targetY = ch - 1;
255
- for (let x = 0; x < cw; x++) {
256
- const i = ((targetY * newWidth + x) * 4) | 0;
257
- out[i] = 0;
258
- out[i + 1] = 0;
259
- out[i + 2] = 0;
260
- out[i + 3] = 255;
261
- }
274
+ const targetY = ch - 1;
275
+ for (let x = 0; x < cw; x++) {
276
+ const i = ((targetY * newWidth + x) * 4) | 0;
277
+ out[i] = 0;
278
+ out[i + 1] = 0;
279
+ out[i + 2] = 0;
280
+ out[i + 3] = 255;
262
281
  }
263
282
  const lastY = ch;
264
283
  for (let x = 0; x < newWidth; x++) {
@@ -341,11 +360,32 @@ export async function cropAndReconstitute(input) {
341
360
  finalOut[i + 3] = line[x][3] === 0 ? 255 : line[x][3];
342
361
  }
343
362
  }
344
- return sharp(finalOut, {
363
+ if (finalHeight >= 2) {
364
+ const secondLastY = finalHeight - 2;
365
+ for (let x = 0; x < finalWidth; x++) {
366
+ const i = ((secondLastY * finalWidth + x) * 4) | 0;
367
+ const r = finalOut[i];
368
+ const g = finalOut[i + 1];
369
+ const b = finalOut[i + 2];
370
+ if ((r === 255 && g === 0 && b === 0) ||
371
+ (r === 0 && g === 255 && b === 0) ||
372
+ (r === 0 && g === 0 && b === 255)) {
373
+ finalOut[i] = 0;
374
+ finalOut[i + 1] = 0;
375
+ finalOut[i + 2] = 0;
376
+ finalOut[i + 3] = 255;
377
+ }
378
+ }
379
+ }
380
+ const resultBuffer = await sharp(finalOut, {
345
381
  raw: { width: finalWidth, height: finalHeight, channels: 4 },
346
382
  })
347
383
  .png()
348
384
  .toBuffer();
385
+ if (debugDir) {
386
+ await sharp(resultBuffer).toFile(join(debugDir, 'reconstructed.png'));
387
+ }
388
+ return resultBuffer;
349
389
  }
350
390
  /**
351
391
  * Encode a Buffer into a PNG wrapper. Supports optional compression and
@@ -359,14 +399,8 @@ export async function encodeBinaryToPng(input, opts = {}) {
359
399
  let payload = Buffer.concat([MAGIC, input]);
360
400
  const brQuality = typeof opts.brQuality === 'number' ? opts.brQuality : 11;
361
401
  const mode = opts.mode === undefined ? 'screenshot' : opts.mode;
362
- const useBrotli = opts.compression === 'br' ||
363
- (opts.compression === undefined &&
364
- (mode === 'compact' || mode === 'pixel' || mode === 'screenshot'));
365
- if (useBrotli) {
366
- payload = zlib.brotliCompressSync(payload, {
367
- params: { [zlib.constants.BROTLI_PARAM_QUALITY]: brQuality },
368
- });
369
- }
402
+ const compression = opts.compression || 'zstd';
403
+ payload = Buffer.from(await zstdCompress(payload, 22));
370
404
  if (opts.passphrase && !opts.encrypt) {
371
405
  opts.encrypt = 'aes';
372
406
  }
@@ -443,9 +477,14 @@ export async function encodeBinaryToPng(input, opts = {}) {
443
477
  ? Buffer.concat([dataWithoutMarkers, Buffer.alloc(padding)])
444
478
  : dataWithoutMarkers;
445
479
  const markerStartBytes = colorsToBytes(MARKER_START);
446
- const dataWithMarkerStart = Buffer.concat([markerStartBytes, paddedData]);
480
+ const compressionMarkerBytes = colorsToBytes(COMPRESSION_MARKERS.zstd);
481
+ const dataWithMarkers = Buffer.concat([
482
+ markerStartBytes,
483
+ compressionMarkerBytes,
484
+ paddedData,
485
+ ]);
447
486
  const bytesPerPixel = 3;
448
- const dataPixels = Math.ceil(dataWithMarkerStart.length / 3);
487
+ const dataPixels = Math.ceil(dataWithMarkers.length / 3);
449
488
  let logicalWidth = Math.ceil(Math.sqrt(dataPixels));
450
489
  if (logicalWidth < MARKER_END.length) {
451
490
  logicalWidth = MARKER_END.length;
@@ -455,7 +494,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
455
494
  const spaceInLastRow = pixelsInLastRow === 0 ? logicalWidth : logicalWidth - pixelsInLastRow;
456
495
  const needsExtraRow = spaceInLastRow < MARKER_END.length;
457
496
  const logicalHeight = needsExtraRow ? dataRows + 1 : dataRows;
458
- const scale = 2;
497
+ const scale = 1;
459
498
  const width = logicalWidth * scale;
460
499
  const height = logicalHeight * scale;
461
500
  const raw = Buffer.alloc(width * height * bytesPerPixel);
@@ -473,17 +512,14 @@ export async function encodeBinaryToPng(input, opts = {}) {
473
512
  else if (ly < dataRows ||
474
513
  (ly === dataRows && linearIdx < dataPixels)) {
475
514
  const srcIdx = linearIdx * 3;
476
- r =
477
- srcIdx < dataWithMarkerStart.length
478
- ? dataWithMarkerStart[srcIdx]
479
- : 0;
515
+ r = srcIdx < dataWithMarkers.length ? dataWithMarkers[srcIdx] : 0;
480
516
  g =
481
- srcIdx + 1 < dataWithMarkerStart.length
482
- ? dataWithMarkerStart[srcIdx + 1]
517
+ srcIdx + 1 < dataWithMarkers.length
518
+ ? dataWithMarkers[srcIdx + 1]
483
519
  : 0;
484
520
  b =
485
- srcIdx + 2 < dataWithMarkerStart.length
486
- ? dataWithMarkerStart[srcIdx + 2]
521
+ srcIdx + 2 < dataWithMarkers.length
522
+ ? dataWithMarkers[srcIdx + 2]
487
523
  : 0;
488
524
  }
489
525
  for (let sy = 0; sy < scale; sy++) {
@@ -628,13 +664,13 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
628
664
  const rawPayload = d.slice(idx);
629
665
  let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
630
666
  try {
631
- payload = tryBrotliDecompress(payload);
667
+ payload = await tryZstdDecompress(payload);
632
668
  }
633
669
  catch (e) {
634
670
  const errMsg = e instanceof Error ? e.message : String(e);
635
671
  if (opts.passphrase)
636
- throw new Error('Incorrect passphrase (ROX format, brotli failed: ' + errMsg + ')');
637
- throw new Error('ROX format brotli decompression failed: ' + errMsg);
672
+ throw new Error('Incorrect passphrase (ROX format, zstd failed: ' + errMsg + ')');
673
+ throw new Error('ROX format zstd decompression failed: ' + errMsg);
638
674
  }
639
675
  if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
640
676
  throw new Error('Invalid ROX format (ROX direct: missing ROX1 magic after decompression)');
@@ -682,13 +718,13 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
682
718
  throw new DataFormatError('Compact mode payload empty');
683
719
  let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
684
720
  try {
685
- payload = tryBrotliDecompress(payload);
721
+ payload = await tryZstdDecompress(payload);
686
722
  }
687
723
  catch (e) {
688
724
  const errMsg = e instanceof Error ? e.message : String(e);
689
725
  if (opts.passphrase)
690
- throw new IncorrectPassphraseError('Incorrect passphrase (compact mode, brotli failed: ' + errMsg + ')');
691
- throw new DataFormatError('Compact mode brotli decompression failed: ' + errMsg);
726
+ throw new IncorrectPassphraseError('Incorrect passphrase (compact mode, zstd failed: ' + errMsg + ')');
727
+ throw new DataFormatError('Compact mode zstd decompression failed: ' + errMsg);
692
728
  }
693
729
  if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
694
730
  throw new DataFormatError('Invalid ROX format (compact mode: missing ROX1 magic after decompression)');
@@ -748,7 +784,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
748
784
  logicalData = rawRGB;
749
785
  }
750
786
  else {
751
- const reconstructed = await cropAndReconstitute(data);
787
+ const reconstructed = await cropAndReconstitute(data, opts.debugDir);
752
788
  const { data: rdata, info: rinfo } = await sharp(reconstructed)
753
789
  .ensureAlpha()
754
790
  .raw()
@@ -794,14 +830,9 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
794
830
  const rawPayload = logicalData.slice(idx, idx + payloadLen);
795
831
  let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
796
832
  try {
797
- payload = tryBrotliDecompress(payload);
798
- }
799
- catch (e) {
800
- const errMsg = e instanceof Error ? e.message : String(e);
801
- if (opts.passphrase)
802
- throw new IncorrectPassphraseError('Incorrect passphrase (pixel mode, brotli failed: ' + errMsg + ')');
803
- throw new DataFormatError('Pixel mode brotli decompression failed: ' + errMsg);
833
+ payload = await tryZstdDecompress(payload);
804
834
  }
835
+ catch (e) { }
805
836
  if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
806
837
  throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
807
838
  }
@@ -931,6 +962,19 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
931
962
  throw new Error('Marker START not found - image format not supported');
932
963
  }
933
964
  }
965
+ let compression = 'zstd';
966
+ if (gridFromStart.length > MARKER_START.length) {
967
+ const compPixel = gridFromStart[MARKER_START.length];
968
+ if (compPixel.r === 0 && compPixel.g === 255 && compPixel.b === 0) {
969
+ compression = 'zstd';
970
+ }
971
+ else {
972
+ compression = 'zstd';
973
+ }
974
+ }
975
+ if (process.env.ROX_DEBUG) {
976
+ console.log(`DEBUG: Detected compression: ${compression}`);
977
+ }
934
978
  let endStartIdx = -1;
935
979
  const lastLineStart = (logicalHeight - 1) * logicalWidth;
936
980
  const endMarkerStartCol = logicalWidth - MARKER_END.length;
@@ -966,7 +1010,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
966
1010
  }
967
1011
  endStartIdx = gridFromStart.length;
968
1012
  }
969
- const dataGrid = gridFromStart.slice(MARKER_START.length, endStartIdx);
1013
+ const dataGrid = gridFromStart.slice(MARKER_START.length + 1, endStartIdx);
970
1014
  const pixelBytes = Buffer.alloc(dataGrid.length * 3);
971
1015
  for (let i = 0; i < dataGrid.length; i++) {
972
1016
  pixelBytes[i * 3] = dataGrid[i].r;
@@ -1015,15 +1059,15 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
1015
1059
  const rawPayload = pixelBytes.slice(idx, idx + payloadLen);
1016
1060
  let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
1017
1061
  try {
1018
- payload = tryBrotliDecompress(payload);
1062
+ payload = await tryZstdDecompress(payload);
1019
1063
  }
1020
1064
  catch (e) {
1021
1065
  const errMsg = e instanceof Error ? e.message : String(e);
1022
1066
  if (opts.passphrase)
1023
- throw new IncorrectPassphraseError('Incorrect passphrase (pixel mode, brotli failed: ' +
1067
+ throw new IncorrectPassphraseError(`Incorrect passphrase (screenshot mode, zstd failed: ` +
1024
1068
  errMsg +
1025
1069
  ')');
1026
- throw new DataFormatError('Pixel mode brotli decompression failed: ' + errMsg);
1070
+ throw new DataFormatError(`Screenshot mode zstd decompression failed: ` + errMsg);
1027
1071
  }
1028
1072
  if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
1029
1073
  throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.1.1",
4
- "description": "Encode binary data into PNG images and decode them back. Supports CLI and programmatic API (Node.js ESM).",
3
+ "version": "1.1.2",
4
+ "description": "Encode binary data into PNG images with Zstd compression and decode them back. Supports CLI and programmatic API (Node.js ESM).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -40,6 +40,7 @@
40
40
  "node": ">=18"
41
41
  },
42
42
  "dependencies": {
43
+ "@mongodb-js/zstd": "^7.0.0",
43
44
  "png-chunks-encode": "^1.0.0",
44
45
  "png-chunks-extract": "^1.0.0",
45
46
  "sharp": "^0.34.5"