roxify 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -124
- package/dist/cli.js +2 -5
- package/dist/index.d.ts +21 -1
- package/dist/index.js +125 -55
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ npm install roxify
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
npx rox encode <inputName>.ext (<outputName>.png)
|
|
31
|
-
|
|
32
31
|
npx rox decode <inputName>.png (<outputName>.ext)
|
|
32
|
+
npx rox list <inputName>.png
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
If no output name is provided:
|
|
@@ -37,154 +37,63 @@ If no output name is provided:
|
|
|
37
37
|
- Encoding: output defaults to `<inputName>.png`.
|
|
38
38
|
- Decoding: if the image contains the original filename it will be restored; otherwise the output will be `decoded.bin`.
|
|
39
39
|
|
|
40
|
-
**Commands:**
|
|
41
|
-
|
|
42
|
-
- `encode <input>... [output]` — Encode file(s)/directory to PNG
|
|
43
|
-
- `decode <input> [output]` — Decode PNG to file(s)
|
|
44
|
-
- `list <input>` — List files in archive without decoding
|
|
45
|
-
|
|
46
40
|
**Options:**
|
|
47
41
|
|
|
48
42
|
- `-p, --passphrase <pass>` — Encrypt with AES-256-GCM
|
|
49
|
-
- `-m, --mode <mode>` — Encoding mode: `screenshot` (default), `pixel`, `compact`, `chunk`
|
|
50
|
-
- `-q, --quality <0-22>` — Roxify compression level (default: 22)
|
|
51
|
-
- `--no-compress` — Disable compression
|
|
52
|
-
- `--files <list>` — Extract only specified files (comma-separated, for archives)
|
|
53
43
|
- `-v, --verbose` — Show detailed errors
|
|
54
44
|
|
|
55
45
|
Run `npx rox help` for full options.
|
|
56
46
|
|
|
57
47
|
## API Usage
|
|
58
48
|
|
|
49
|
+
### Basic Encoding and Decoding
|
|
50
|
+
|
|
59
51
|
```js
|
|
60
|
-
import {
|
|
52
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
53
|
+
import { encodeBinaryToPng } from 'roxify';
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
name: 'message.txt',
|
|
55
|
+
const fileName = 'input.bin';
|
|
56
|
+
const inputBuffer = readFileSync(fileName);
|
|
57
|
+
const pngBuffer = await encodeBinaryToPng(inputBuffer, {
|
|
58
|
+
name: fileName,
|
|
67
59
|
});
|
|
68
|
-
|
|
69
|
-
// Decode
|
|
70
|
-
const { buf, meta } = await decodePngToBinary(png);
|
|
71
|
-
console.log(buf.toString('utf8'));
|
|
72
|
-
console.log(meta?.name);
|
|
73
|
-
|
|
74
|
-
// List files in archive
|
|
75
|
-
const files = listFilesInPng(png);
|
|
76
|
-
console.log(files);
|
|
77
|
-
|
|
78
|
-
// Selective extraction
|
|
79
|
-
const result = await decodePngToBinary(png, { files: ['file1.txt'] });
|
|
80
|
-
if (result.files) {
|
|
81
|
-
// result.files contains only the selected files
|
|
82
|
-
}
|
|
60
|
+
writeFileSync('output.png', pngBuffer);
|
|
83
61
|
```
|
|
84
62
|
|
|
85
|
-
## Example: Progress Logging
|
|
86
|
-
|
|
87
|
-
````js
|
|
88
|
-
import { encodeBinaryToPng, decodePngToBinary } from 'roxify';
|
|
89
|
-
|
|
90
|
-
const data = Buffer.from('Large data to encode...');
|
|
91
|
-
|
|
92
|
-
// Encode with progress logging
|
|
93
|
-
const png = await encodeBinaryToPng(data, {
|
|
94
|
-
onProgress: (info) => {
|
|
95
|
-
console.log(`Encoding phase: ${info.phase}`);
|
|
96
|
-
if (info.loaded && info.total) {
|
|
97
|
-
const percent = Math.round((info.loaded / info.total) * 100);
|
|
98
|
-
console.log(`Progress: ${percent}% (${info.loaded}/${info.total} bytes)`);
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Node.js (detailed chunk progress example)
|
|
105
|
-
|
|
106
63
|
```js
|
|
107
|
-
import { encodeBinaryToPng } from 'roxify';
|
|
108
64
|
import { readFileSync, writeFileSync } from 'fs';
|
|
65
|
+
import { decodePngToBinary } from 'roxify';
|
|
109
66
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (info.phase === 'compress_progress' && info.loaded && info.total) {
|
|
115
|
-
const percent = Math.round((info.loaded / info.total) * 100);
|
|
116
|
-
console.log(`[progress] ${info.phase} ${percent}% (${info.loaded}/${info.total} chunks)`);
|
|
117
|
-
} else {
|
|
118
|
-
console.log(`[progress] ${info.phase} ${info.loaded || ''}/${info.total || ''}`);
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
});
|
|
67
|
+
const pngFromDisk = readFileSync('output.png');
|
|
68
|
+
const { buf, meta } = await decodePngToBinary(pngFromDisk);
|
|
69
|
+
writeFileSync(meta?.name ?? 'decoded.txt', buf);
|
|
70
|
+
```
|
|
122
71
|
|
|
123
|
-
|
|
124
|
-
````
|
|
72
|
+
### With Passphrase
|
|
125
73
|
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
},
|
|
74
|
+
```js
|
|
75
|
+
const pngBuffer = await encodeBinaryToPng(inputBuffer, {
|
|
76
|
+
name: fileName,
|
|
77
|
+
passphrase: 'mysecret',
|
|
131
78
|
});
|
|
132
|
-
|
|
133
|
-
````
|
|
134
|
-
|
|
135
|
-
**API:**
|
|
136
|
-
|
|
137
|
-
- `encodeBinaryToPng(input: Buffer, opts?: EncodeOptions): Promise<Buffer>`
|
|
138
|
-
- `decodePngToBinary(pngBuf: Buffer, opts?: DecodeOptions): Promise<DecodeResult>`
|
|
139
|
-
- `listFilesInPng(pngBuf: Buffer): string[] | null`
|
|
140
|
-
|
|
141
|
-
**EncodeOptions:**
|
|
142
|
-
|
|
143
|
-
- `mode` — `'screenshot'` | `'pixel'` | `'compact'` | `'chunk'` (default: `'screenshot'`)
|
|
144
|
-
- `name` — Original filename (embedded as metadata)
|
|
145
|
-
- `passphrase` — Encryption passphrase (uses AES-256-GCM)
|
|
146
|
-
- `compression` — `'Roxify'` | `'none'` (default: `'Roxify'`)
|
|
147
|
-
- `brQuality` — Roxify compression level 0-22 (default: 22)
|
|
148
|
-
- `showProgress` — Display progress bar (default: `false`)
|
|
149
|
-
- `onProgress` — Callback for progress updates: `(info: { phase: string; loaded?: number; total?: number }) => void`
|
|
150
|
-
- `includeFileList` — Include file list for archives (default: `true` for directories)
|
|
151
|
-
|
|
152
|
-
**DecodeOptions:**
|
|
153
|
-
|
|
154
|
-
- `passphrase` — Decryption passphrase
|
|
155
|
-
- `files` — List of files to extract selectively (for archives)
|
|
156
|
-
- `showProgress` — Display progress bar (default: `false`)
|
|
157
|
-
- `onProgress` — Callback for progress updates: `(info: { phase: string; loaded?: number; total?: number }) => void`
|
|
158
|
-
|
|
159
|
-
**DecodeResult:**
|
|
160
|
-
|
|
161
|
-
- `buf?: Buffer` — Decoded data (if not selective extraction)
|
|
162
|
-
- `files?: PackedFile[]` — Extracted files (if selective extraction)
|
|
163
|
-
- `meta?: { name?: string }` — Metadata
|
|
164
|
-
|
|
165
|
-
## Example: Archive with Selective Extraction
|
|
79
|
+
```
|
|
166
80
|
|
|
167
81
|
```js
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Pack a directory
|
|
171
|
-
const fs = require('fs');
|
|
172
|
-
const dirData = packPaths(['myfolder']); // From pack.js
|
|
173
|
-
const png = await encodeBinaryToPng(dirData.buf, {
|
|
174
|
-
includeFileList: true,
|
|
175
|
-
fileList: dirData.list,
|
|
82
|
+
const { buf, meta } = await decodePngToBinary(pngFromDisk, {
|
|
83
|
+
passphrase: 'mysecret',
|
|
176
84
|
});
|
|
85
|
+
```
|
|
177
86
|
|
|
178
|
-
|
|
179
|
-
const files = listFilesInPng(png);
|
|
180
|
-
console.log('Files:', files);
|
|
87
|
+
### With Progress Logging
|
|
181
88
|
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
89
|
+
```js
|
|
90
|
+
const pngBuffer = await encodeBinaryToPng(inputBuffer, {
|
|
91
|
+
name: fileName,
|
|
92
|
+
onProgress: (info) => {
|
|
93
|
+
console.log(`Phase: ${info.phase}, Loaded: ${info.loaded}/${info.total}`);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
188
97
|
|
|
189
98
|
## Requirements
|
|
190
99
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
|
4
4
|
import { basename, dirname, join, resolve } from 'path';
|
|
5
5
|
import { DataFormatError, decodePngToBinary, encodeBinaryToPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
|
|
6
6
|
import { packPaths, unpackBuffer } from './pack.js';
|
|
7
|
-
const VERSION = '1.1.
|
|
7
|
+
const VERSION = '1.1.9';
|
|
8
8
|
function showHelp() {
|
|
9
9
|
console.log(`
|
|
10
10
|
ROX CLI — Encode/decode binary in PNG
|
|
@@ -190,9 +190,7 @@ async function encodeCommand(args) {
|
|
|
190
190
|
options.fileList = packResult.list;
|
|
191
191
|
}
|
|
192
192
|
else {
|
|
193
|
-
const startRead = Date.now();
|
|
194
193
|
inputBuffer = readFileSync(resolvedInput);
|
|
195
|
-
const readTime = Date.now() - startRead;
|
|
196
194
|
console.log('');
|
|
197
195
|
displayName = basename(resolvedInput);
|
|
198
196
|
}
|
|
@@ -213,7 +211,6 @@ async function encodeCommand(args) {
|
|
|
213
211
|
const encodeBar = new cliProgress.SingleBar({
|
|
214
212
|
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
215
213
|
}, cliProgress.Presets.shades_classic);
|
|
216
|
-
let totalMB = Math.max(1, Math.round(inputBuffer.length / 1024 / 1024));
|
|
217
214
|
encodeBar.start(100, 0, {
|
|
218
215
|
step: 'Starting',
|
|
219
216
|
elapsed: '0',
|
|
@@ -461,7 +458,7 @@ async function listCommand(args) {
|
|
|
461
458
|
const resolvedInput = resolve(inputPath);
|
|
462
459
|
try {
|
|
463
460
|
const inputBuffer = readFileSync(resolvedInput);
|
|
464
|
-
const fileList = listFilesInPng(inputBuffer);
|
|
461
|
+
const fileList = await listFilesInPng(inputBuffer);
|
|
465
462
|
if (fileList) {
|
|
466
463
|
console.log(`Files in ${resolvedInput}:`);
|
|
467
464
|
for (const file of fileList) {
|
package/dist/index.d.ts
CHANGED
|
@@ -157,6 +157,19 @@ export declare function cropAndReconstitute(input: Buffer, debugDir?: string): P
|
|
|
157
157
|
* @param input - Data to encode
|
|
158
158
|
* @param opts - Encoding options
|
|
159
159
|
* @public
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* import { readFileSync, writeFileSync } from 'fs';
|
|
163
|
+
* import { encodeBinaryToPng } from 'roxify';
|
|
164
|
+
*
|
|
165
|
+
* const fileName = 'input.bin'; //Path of your input file here
|
|
166
|
+
* const inputBuffer = readFileSync(fileName);
|
|
167
|
+
* const pngBuffer = await encodeBinaryToPng(inputBuffer, {
|
|
168
|
+
* name: fileName,
|
|
169
|
+
* });
|
|
170
|
+
* writeFileSync('output.png', pngBuffer);
|
|
171
|
+
|
|
172
|
+
* ```
|
|
160
173
|
*/
|
|
161
174
|
export declare function encodeBinaryToPng(input: Buffer, opts?: EncodeOptions): Promise<Buffer>;
|
|
162
175
|
/**
|
|
@@ -166,6 +179,13 @@ export declare function encodeBinaryToPng(input: Buffer, opts?: EncodeOptions):
|
|
|
166
179
|
* @param pngBuf - PNG data
|
|
167
180
|
* @param opts - Options (passphrase for encrypted inputs)
|
|
168
181
|
* @public
|
|
182
|
+
* @example
|
|
183
|
+
* import { readFileSync, writeFileSync } from 'fs';
|
|
184
|
+
* import { decodePngToBinary } from 'roxify';
|
|
185
|
+
*
|
|
186
|
+
* const pngFromDisk = readFileSync('output.png'); //Path of the encoded PNG here
|
|
187
|
+
* const { buf, meta } = await decodePngToBinary(pngFromDisk);
|
|
188
|
+
* writeFileSync(meta?.name ?? 'decoded.txt', buf);
|
|
169
189
|
*/
|
|
170
190
|
export declare function decodePngToBinary(pngBuf: Buffer, opts?: DecodeOptions): Promise<DecodeResult>;
|
|
171
191
|
export { packPaths, unpackBuffer } from './pack.js';
|
|
@@ -175,4 +195,4 @@ export { packPaths, unpackBuffer } from './pack.js';
|
|
|
175
195
|
* @param pngBuf - PNG data
|
|
176
196
|
* @public
|
|
177
197
|
*/
|
|
178
|
-
export declare function listFilesInPng(pngBuf: Buffer): string[] | null
|
|
198
|
+
export declare function listFilesInPng(pngBuf: Buffer): Promise<string[] | null>;
|
package/dist/index.js
CHANGED
|
@@ -90,7 +90,6 @@ async function parallelZstdCompress(payload, level = 22, onProgress) {
|
|
|
90
90
|
if (payload.length <= chunkSize) {
|
|
91
91
|
return Buffer.from(await zstdCompress(payload, level));
|
|
92
92
|
}
|
|
93
|
-
const chunks = [];
|
|
94
93
|
const promises = [];
|
|
95
94
|
const totalChunks = Math.ceil(payload.length / chunkSize);
|
|
96
95
|
let completedChunks = 0;
|
|
@@ -131,6 +130,8 @@ async function parallelZstdDecompress(payload, onProgress, onChunk, outPath) {
|
|
|
131
130
|
}
|
|
132
131
|
const magic = payload.readUInt32BE(0);
|
|
133
132
|
if (magic !== 0x5a535444) {
|
|
133
|
+
if (process.env.ROX_DEBUG)
|
|
134
|
+
console.log('tryZstdDecompress: invalid magic');
|
|
134
135
|
onProgress?.({ phase: 'decompress_start', total: 1 });
|
|
135
136
|
const d = Buffer.from(await zstdDecompress(payload));
|
|
136
137
|
onProgress?.({ phase: 'decompress_progress', loaded: 1, total: 1 });
|
|
@@ -211,9 +212,6 @@ function applyXor(buf, passphrase) {
|
|
|
211
212
|
}
|
|
212
213
|
return out;
|
|
213
214
|
}
|
|
214
|
-
function tryBrotliDecompress(payload) {
|
|
215
|
-
return Buffer.from(zlib.brotliDecompressSync(payload));
|
|
216
|
-
}
|
|
217
215
|
async function tryZstdDecompress(payload, onProgress, onChunk, outPath) {
|
|
218
216
|
return await parallelZstdDecompress(payload, onProgress, onChunk, outPath);
|
|
219
217
|
}
|
|
@@ -253,19 +251,6 @@ function tryDecryptIfNeeded(buf, passphrase) {
|
|
|
253
251
|
}
|
|
254
252
|
return buf;
|
|
255
253
|
}
|
|
256
|
-
function idxFor(x, y, width) {
|
|
257
|
-
return (y * width + x) * 4;
|
|
258
|
-
}
|
|
259
|
-
function eqRGB(a, b) {
|
|
260
|
-
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
261
|
-
}
|
|
262
|
-
async function loadRaw(imgInput) {
|
|
263
|
-
const { data, info } = await sharp(imgInput)
|
|
264
|
-
.ensureAlpha()
|
|
265
|
-
.raw()
|
|
266
|
-
.toBuffer({ resolveWithObject: true });
|
|
267
|
-
return { data, info };
|
|
268
|
-
}
|
|
269
254
|
export async function cropAndReconstitute(input, debugDir) {
|
|
270
255
|
async function loadRaw(imgInput) {
|
|
271
256
|
const { data, info } = await sharp(imgInput)
|
|
@@ -280,7 +265,7 @@ export async function cropAndReconstitute(input, debugDir) {
|
|
|
280
265
|
function eqRGB(a, b) {
|
|
281
266
|
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
282
267
|
}
|
|
283
|
-
const {
|
|
268
|
+
const { info } = await loadRaw(input);
|
|
284
269
|
const doubledBuffer = await sharp(input)
|
|
285
270
|
.resize({
|
|
286
271
|
width: info.width * 2,
|
|
@@ -537,6 +522,19 @@ export async function cropAndReconstitute(input, debugDir) {
|
|
|
537
522
|
* @param input - Data to encode
|
|
538
523
|
* @param opts - Encoding options
|
|
539
524
|
* @public
|
|
525
|
+
* @example
|
|
526
|
+
* ```typescript
|
|
527
|
+
* import { readFileSync, writeFileSync } from 'fs';
|
|
528
|
+
* import { encodeBinaryToPng } from 'roxify';
|
|
529
|
+
*
|
|
530
|
+
* const fileName = 'input.bin'; //Path of your input file here
|
|
531
|
+
* const inputBuffer = readFileSync(fileName);
|
|
532
|
+
* const pngBuffer = await encodeBinaryToPng(inputBuffer, {
|
|
533
|
+
* name: fileName,
|
|
534
|
+
* });
|
|
535
|
+
* writeFileSync('output.png', pngBuffer);
|
|
536
|
+
|
|
537
|
+
* ```
|
|
540
538
|
*/
|
|
541
539
|
export async function encodeBinaryToPng(input, opts = {}) {
|
|
542
540
|
let progressBar = null;
|
|
@@ -572,9 +570,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
572
570
|
}
|
|
573
571
|
}
|
|
574
572
|
let payload = Buffer.concat([MAGIC, input]);
|
|
575
|
-
const brQuality = typeof opts.brQuality === 'number' ? opts.brQuality : 11;
|
|
576
573
|
const mode = opts.mode === undefined ? 'screenshot' : opts.mode;
|
|
577
|
-
const compression = opts.compression || 'zstd';
|
|
578
574
|
if (opts.onProgress)
|
|
579
575
|
opts.onProgress({ phase: 'compress_start', total: payload.length });
|
|
580
576
|
const useDelta = mode !== 'screenshot';
|
|
@@ -622,8 +618,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
622
618
|
payload = Buffer.concat([Buffer.from([ENC_AES]), salt, iv, tag, enc]);
|
|
623
619
|
if (opts.onProgress)
|
|
624
620
|
opts.onProgress({ phase: 'encrypt_done' });
|
|
625
|
-
}
|
|
626
|
-
else if (encChoice === 'xor') {
|
|
627
621
|
const xored = applyXor(payload, opts.passphrase);
|
|
628
622
|
payload = Buffer.concat([Buffer.from([ENC_XOR]), xored]);
|
|
629
623
|
if (opts.onProgress)
|
|
@@ -651,7 +645,13 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
651
645
|
metaParts.push(Buffer.from([0]));
|
|
652
646
|
}
|
|
653
647
|
metaParts.push(payload);
|
|
654
|
-
|
|
648
|
+
let meta = Buffer.concat(metaParts);
|
|
649
|
+
if (opts.includeFileList && opts.fileList) {
|
|
650
|
+
const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
|
|
651
|
+
const lenBuf = Buffer.alloc(4);
|
|
652
|
+
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
653
|
+
meta = Buffer.concat([meta, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf]);
|
|
654
|
+
}
|
|
655
655
|
if (opts.output === 'rox') {
|
|
656
656
|
return Buffer.concat([MAGIC, meta]);
|
|
657
657
|
}
|
|
@@ -663,13 +663,24 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
663
663
|
const payloadLenBuf = Buffer.alloc(4);
|
|
664
664
|
payloadLenBuf.writeUInt32BE(payload.length, 0);
|
|
665
665
|
const version = 1;
|
|
666
|
-
|
|
666
|
+
let metaPixel = Buffer.concat([
|
|
667
667
|
Buffer.from([version]),
|
|
668
668
|
Buffer.from([nameLen]),
|
|
669
669
|
nameBuf,
|
|
670
670
|
payloadLenBuf,
|
|
671
671
|
payload,
|
|
672
672
|
]);
|
|
673
|
+
if (opts.includeFileList && opts.fileList) {
|
|
674
|
+
const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
|
|
675
|
+
const lenBuf = Buffer.alloc(4);
|
|
676
|
+
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
677
|
+
metaPixel = Buffer.concat([
|
|
678
|
+
metaPixel,
|
|
679
|
+
Buffer.from('rXFL', 'utf8'),
|
|
680
|
+
lenBuf,
|
|
681
|
+
jsonBuf,
|
|
682
|
+
]);
|
|
683
|
+
}
|
|
673
684
|
const dataWithoutMarkers = Buffer.concat([PIXEL_MAGIC, metaPixel]);
|
|
674
685
|
const padding = (3 - (dataWithoutMarkers.length % 3)) % 3;
|
|
675
686
|
const paddedData = padding > 0
|
|
@@ -750,15 +761,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
750
761
|
adaptiveFiltering: true,
|
|
751
762
|
})
|
|
752
763
|
.toBuffer();
|
|
753
|
-
if (opts.includeFileList && opts.fileList) {
|
|
754
|
-
const chunks = extract(bufScr);
|
|
755
|
-
const fileListChunk = {
|
|
756
|
-
name: 'rXFL',
|
|
757
|
-
data: Buffer.from(JSON.stringify(opts.fileList), 'utf8'),
|
|
758
|
-
};
|
|
759
|
-
chunks.splice(-1, 0, fileListChunk);
|
|
760
|
-
bufScr = Buffer.from(encode(chunks));
|
|
761
|
-
}
|
|
762
764
|
if (opts.onProgress)
|
|
763
765
|
opts.onProgress({ phase: 'done', loaded: bufScr.length });
|
|
764
766
|
progressBar?.stop();
|
|
@@ -772,13 +774,24 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
772
774
|
const payloadLenBuf = Buffer.alloc(4);
|
|
773
775
|
payloadLenBuf.writeUInt32BE(payload.length, 0);
|
|
774
776
|
const version = 1;
|
|
775
|
-
|
|
777
|
+
let metaPixel = Buffer.concat([
|
|
776
778
|
Buffer.from([version]),
|
|
777
779
|
Buffer.from([nameLen]),
|
|
778
780
|
nameBuf,
|
|
779
781
|
payloadLenBuf,
|
|
780
782
|
payload,
|
|
781
783
|
]);
|
|
784
|
+
if (opts.includeFileList && opts.fileList) {
|
|
785
|
+
const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
|
|
786
|
+
const lenBuf = Buffer.alloc(4);
|
|
787
|
+
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
788
|
+
metaPixel = Buffer.concat([
|
|
789
|
+
metaPixel,
|
|
790
|
+
Buffer.from('rXFL', 'utf8'),
|
|
791
|
+
lenBuf,
|
|
792
|
+
jsonBuf,
|
|
793
|
+
]);
|
|
794
|
+
}
|
|
782
795
|
const full = Buffer.concat([PIXEL_MAGIC, metaPixel]);
|
|
783
796
|
const bytesPerPixel = 3;
|
|
784
797
|
const nPixels = Math.ceil((full.length + 8) / 3);
|
|
@@ -860,12 +873,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
860
873
|
chunks2.push({ name: 'IHDR', data: ihdrData });
|
|
861
874
|
chunks2.push({ name: 'IDAT', data: idatData });
|
|
862
875
|
chunks2.push({ name: CHUNK_TYPE, data: meta });
|
|
863
|
-
if (opts.includeFileList && opts.fileList) {
|
|
864
|
-
chunks2.push({
|
|
865
|
-
name: 'rXFL',
|
|
866
|
-
data: Buffer.from(JSON.stringify(opts.fileList), 'utf8'),
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
876
|
chunks2.push({ name: 'IEND', data: Buffer.alloc(0) });
|
|
870
877
|
if (opts.onProgress)
|
|
871
878
|
opts.onProgress({ phase: 'png_gen' });
|
|
@@ -887,6 +894,13 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
887
894
|
* @param pngBuf - PNG data
|
|
888
895
|
* @param opts - Options (passphrase for encrypted inputs)
|
|
889
896
|
* @public
|
|
897
|
+
* @example
|
|
898
|
+
* import { readFileSync, writeFileSync } from 'fs';
|
|
899
|
+
* import { decodePngToBinary } from 'roxify';
|
|
900
|
+
*
|
|
901
|
+
* const pngFromDisk = readFileSync('output.png'); //Path of the encoded PNG here
|
|
902
|
+
* const { buf, meta } = await decodePngToBinary(pngFromDisk);
|
|
903
|
+
* writeFileSync(meta?.name ?? 'decoded.txt', buf);
|
|
890
904
|
*/
|
|
891
905
|
export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
892
906
|
let progressBar = null;
|
|
@@ -926,9 +940,6 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
926
940
|
if (rawBytesEstimate > MAX_RAW_BYTES) {
|
|
927
941
|
throw new DataFormatError(`Image too large to decode in-process (${Math.round(rawBytesEstimate / 1024 / 1024)} MB). Increase Node heap or use a smaller image/compact mode.`);
|
|
928
942
|
}
|
|
929
|
-
const MAX_DOUBLE_BYTES = 200 * 1024 * 1024;
|
|
930
|
-
const doubledPixels = info.width * 2 * (info.height * 2);
|
|
931
|
-
const doubledBytesEstimate = doubledPixels * 4;
|
|
932
943
|
if (false) {
|
|
933
944
|
const doubledBuffer = await sharp(pngBuf)
|
|
934
945
|
.resize({
|
|
@@ -1479,7 +1490,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
1479
1490
|
await tryZstdDecompress(payload, (info) => {
|
|
1480
1491
|
if (opts.onProgress)
|
|
1481
1492
|
opts.onProgress(info);
|
|
1482
|
-
}, async (decChunk
|
|
1493
|
+
}, async (decChunk) => {
|
|
1483
1494
|
let outChunk = decChunk;
|
|
1484
1495
|
if (version === 3) {
|
|
1485
1496
|
const out = Buffer.alloc(decChunk.length);
|
|
@@ -1518,7 +1529,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
1518
1529
|
await writeInChunks(ws, outChunk, 64 * 1024);
|
|
1519
1530
|
}
|
|
1520
1531
|
});
|
|
1521
|
-
await new Promise((res
|
|
1532
|
+
await new Promise((res) => ws.end(() => res()));
|
|
1522
1533
|
if (opts.onProgress)
|
|
1523
1534
|
opts.onProgress({ phase: 'done' });
|
|
1524
1535
|
progressBar?.stop();
|
|
@@ -1589,7 +1600,60 @@ export { packPaths, unpackBuffer } from './pack.js';
|
|
|
1589
1600
|
* @param pngBuf - PNG data
|
|
1590
1601
|
* @public
|
|
1591
1602
|
*/
|
|
1592
|
-
export function listFilesInPng(pngBuf) {
|
|
1603
|
+
export async function listFilesInPng(pngBuf) {
|
|
1604
|
+
try {
|
|
1605
|
+
try {
|
|
1606
|
+
const { data, info } = await sharp(pngBuf)
|
|
1607
|
+
.ensureAlpha()
|
|
1608
|
+
.raw()
|
|
1609
|
+
.toBuffer({ resolveWithObject: true });
|
|
1610
|
+
const currentWidth = info.width;
|
|
1611
|
+
const currentHeight = info.height;
|
|
1612
|
+
const rawRGB = Buffer.alloc(currentWidth * currentHeight * 3);
|
|
1613
|
+
for (let i = 0; i < currentWidth * currentHeight; i++) {
|
|
1614
|
+
rawRGB[i * 3] = data[i * 4];
|
|
1615
|
+
rawRGB[i * 3 + 1] = data[i * 4 + 1];
|
|
1616
|
+
rawRGB[i * 3 + 2] = data[i * 4 + 2];
|
|
1617
|
+
}
|
|
1618
|
+
const found = rawRGB.indexOf(PIXEL_MAGIC);
|
|
1619
|
+
if (found !== -1) {
|
|
1620
|
+
let idx = found + PIXEL_MAGIC.length;
|
|
1621
|
+
if (idx + 2 <= rawRGB.length) {
|
|
1622
|
+
const version = rawRGB[idx++];
|
|
1623
|
+
const nameLen = rawRGB[idx++];
|
|
1624
|
+
if (process.env.ROX_DEBUG)
|
|
1625
|
+
console.log('listFilesInPng: pixel version', version, 'nameLen', nameLen);
|
|
1626
|
+
if (nameLen > 0 && idx + nameLen <= rawRGB.length) {
|
|
1627
|
+
idx += nameLen;
|
|
1628
|
+
}
|
|
1629
|
+
if (idx + 4 <= rawRGB.length) {
|
|
1630
|
+
const payloadLen = rawRGB.readUInt32BE(idx);
|
|
1631
|
+
idx += 4;
|
|
1632
|
+
const afterPayload = idx + payloadLen;
|
|
1633
|
+
if (afterPayload <= rawRGB.length) {
|
|
1634
|
+
if (afterPayload + 8 <= rawRGB.length) {
|
|
1635
|
+
const marker = rawRGB
|
|
1636
|
+
.slice(afterPayload, afterPayload + 4)
|
|
1637
|
+
.toString('utf8');
|
|
1638
|
+
if (marker === 'rXFL') {
|
|
1639
|
+
const jsonLen = rawRGB.readUInt32BE(afterPayload + 4);
|
|
1640
|
+
const jsonStart = afterPayload + 8;
|
|
1641
|
+
const jsonEnd = jsonStart + jsonLen;
|
|
1642
|
+
if (jsonEnd <= rawRGB.length) {
|
|
1643
|
+
const jsonBuf = rawRGB.slice(jsonStart, jsonEnd);
|
|
1644
|
+
const files = JSON.parse(jsonBuf.toString('utf8'));
|
|
1645
|
+
return files.sort();
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
catch (e) { }
|
|
1655
|
+
}
|
|
1656
|
+
catch (e) { }
|
|
1593
1657
|
try {
|
|
1594
1658
|
const chunks = extract(pngBuf);
|
|
1595
1659
|
const fileListChunk = chunks.find((c) => c.name === 'rXFL');
|
|
@@ -1598,17 +1662,23 @@ export function listFilesInPng(pngBuf) {
|
|
|
1598
1662
|
? fileListChunk.data
|
|
1599
1663
|
: Buffer.from(fileListChunk.data);
|
|
1600
1664
|
const files = JSON.parse(data.toString('utf8'));
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1665
|
+
return files.sort();
|
|
1666
|
+
}
|
|
1667
|
+
const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
|
|
1668
|
+
if (metaChunk) {
|
|
1669
|
+
const dataBuf = Buffer.isBuffer(metaChunk.data)
|
|
1670
|
+
? metaChunk.data
|
|
1671
|
+
: Buffer.from(metaChunk.data);
|
|
1672
|
+
const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
|
|
1673
|
+
if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
|
|
1674
|
+
const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
|
|
1675
|
+
const jsonStart = markerIdx + 8;
|
|
1676
|
+
const jsonEnd = jsonStart + jsonLen;
|
|
1677
|
+
if (jsonEnd <= dataBuf.length) {
|
|
1678
|
+
const files = JSON.parse(dataBuf.slice(jsonStart, jsonEnd).toString('utf8'));
|
|
1679
|
+
return files.sort();
|
|
1608
1680
|
}
|
|
1609
1681
|
}
|
|
1610
|
-
const all = [...dirs, ...files];
|
|
1611
|
-
return all.sort();
|
|
1612
1682
|
}
|
|
1613
1683
|
}
|
|
1614
1684
|
catch (e) { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
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",
|
|
@@ -16,21 +16,24 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"check-publish": "node ../scripts/check-publish.js roxify",
|
|
18
18
|
"cli": "node dist/cli.js",
|
|
19
|
-
"test": "npm run build && node test/pack.test.js && node test/screenshot.test.js"
|
|
19
|
+
"test": "npm run build && node test/pack.test.js && node test/screenshot.test.js && node test/list.test.js"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"steganography",
|
|
23
23
|
"png",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"decompress",
|
|
24
|
+
"zstd",
|
|
25
|
+
"compression",
|
|
26
|
+
"encryption",
|
|
28
27
|
"encode",
|
|
29
28
|
"decode",
|
|
30
29
|
"cli",
|
|
31
30
|
"nodejs",
|
|
32
31
|
"esm",
|
|
33
|
-
"
|
|
32
|
+
"data-embedding",
|
|
33
|
+
"file-archive",
|
|
34
|
+
"lossless",
|
|
35
|
+
"aes-gcm",
|
|
36
|
+
"binary-data"
|
|
34
37
|
],
|
|
35
38
|
"author": "RoxCompressor",
|
|
36
39
|
"license": "UNLICENSED",
|