roxify 1.0.0
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/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +255 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +1042 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 RoxCompressor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# RoxCompressor Transform
|
|
2
|
+
|
|
3
|
+
> Encode binary data into PNG images and decode them back. Fast, efficient, with optional encryption.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/roxify)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🚀 **Fast**: Optimized Brotli compression (quality 1 by default) — encode 3.8 MB in ~1 second
|
|
11
|
+
- 🔒 **Secure**: AES-256-GCM encryption support with PBKDF2 key derivation
|
|
12
|
+
- 🎨 **Multiple modes**: Compact, chunk, pixel, and screenshot modes
|
|
13
|
+
- 📦 **CLI & API**: Use as command-line tool or JavaScript library
|
|
14
|
+
- 🔄 **Lossless**: Perfect roundtrip encoding/decoding
|
|
15
|
+
- 📊 **Efficient**: Typically 20-30% of original size with compression
|
|
16
|
+
- 📖 **Full TSDoc**: Complete TypeScript documentation
|
|
17
|
+
|
|
18
|
+
## Documentation
|
|
19
|
+
|
|
20
|
+
- 📘 **[CLI Documentation](./CLI.md)** - Complete command-line usage guide
|
|
21
|
+
- 📗 **[JavaScript SDK](./JAVASCRIPT_SDK.md)** - Programmatic API reference with examples
|
|
22
|
+
- 📙 **[Quick Start](#quick-start)** - Get started in 2 minutes
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### As CLI tool (npx)
|
|
27
|
+
|
|
28
|
+
No installation needed! Use directly with npx:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx rox encode input.zip output.png
|
|
32
|
+
npx rox decode output.png original.zip
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### As library
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install roxify
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CLI Usage
|
|
42
|
+
|
|
43
|
+
### Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Encode a file
|
|
47
|
+
npx rox encode document.pdf document.png
|
|
48
|
+
|
|
49
|
+
# Decode it back
|
|
50
|
+
npx rox decode document.png document.pdf
|
|
51
|
+
|
|
52
|
+
# With encryption
|
|
53
|
+
npx rox encode secret.zip secret.png -p mypassword
|
|
54
|
+
npx rox decode secret.png secret.zip -p mypassword
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### CLI Commands
|
|
58
|
+
|
|
59
|
+
#### `encode` - Encode file to PNG
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx rox encode <input> [output] [options]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Options:**
|
|
66
|
+
|
|
67
|
+
- `-p, --passphrase <pass>` - Encrypt with passphrase (AES-256-GCM)
|
|
68
|
+
- `-m, --mode <mode>` - Encoding mode: `compact|chunk|pixel|screenshot` (default: `screenshot`)
|
|
69
|
+
- `-q, --quality <0-11>` - Brotli compression quality (default: `1`)
|
|
70
|
+
- `0` = fastest, largest
|
|
71
|
+
- `11` = slowest, smallest
|
|
72
|
+
- `-e, --encrypt <type>` - Encryption: `auto|aes|xor|none` (default: `aes` if passphrase)
|
|
73
|
+
- `--no-compress` - Disable compression
|
|
74
|
+
- `-o, --output <path>` - Output file path
|
|
75
|
+
|
|
76
|
+
**Examples:**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Basic encoding
|
|
80
|
+
npx rox encode data.bin output.png
|
|
81
|
+
|
|
82
|
+
# Fast compression for large files
|
|
83
|
+
npx rox encode large-video.mp4 output.png -q 0
|
|
84
|
+
|
|
85
|
+
# High compression for small files
|
|
86
|
+
npx rox encode config.json output.png -q 11
|
|
87
|
+
|
|
88
|
+
# With encryption
|
|
89
|
+
npx rox encode secret.pdf secure.png -p "my secure password"
|
|
90
|
+
|
|
91
|
+
# Compact mode (smallest PNG)
|
|
92
|
+
npx rox encode data.bin tiny.png -m compact
|
|
93
|
+
|
|
94
|
+
# Screenshot mode (recommended, looks like a real image)
|
|
95
|
+
npx rox encode archive.tar.gz screenshot.png -m screenshot
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### `decode` - Decode PNG to file
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx rox decode <input> [output] [options]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Options:**
|
|
105
|
+
|
|
106
|
+
- `-p, --passphrase <pass>` - Decryption passphrase
|
|
107
|
+
- `-o, --output <path>` - Output file path (auto-detected from metadata if not provided)
|
|
108
|
+
|
|
109
|
+
**Examples:**
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Basic decoding
|
|
113
|
+
npx rox decode encoded.png output.bin
|
|
114
|
+
|
|
115
|
+
# Auto-detect filename from metadata
|
|
116
|
+
npx rox decode encoded.png
|
|
117
|
+
|
|
118
|
+
# With decryption
|
|
119
|
+
npx rox decode encrypted.png output.pdf -p "my secure password"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## JavaScript API
|
|
123
|
+
|
|
124
|
+
### Basic Usage
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { encodeBinaryToPng, decodePngToBinary } from 'roxify';
|
|
128
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
129
|
+
|
|
130
|
+
// Encode
|
|
131
|
+
const input = readFileSync('input.zip');
|
|
132
|
+
const png = await encodeBinaryToPng(input, {
|
|
133
|
+
mode: 'screenshot',
|
|
134
|
+
name: 'input.zip',
|
|
135
|
+
});
|
|
136
|
+
writeFileSync('output.png', png);
|
|
137
|
+
|
|
138
|
+
// Decode
|
|
139
|
+
const encoded = readFileSync('output.png');
|
|
140
|
+
const result = await decodePngToBinary(encoded);
|
|
141
|
+
writeFileSync(result.meta?.name || 'output.bin', result.buf);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### With Encryption
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Encode with AES-256-GCM
|
|
148
|
+
const png = await encodeBinaryToPng(input, {
|
|
149
|
+
mode: 'screenshot',
|
|
150
|
+
passphrase: 'my-secret-password',
|
|
151
|
+
encrypt: 'aes',
|
|
152
|
+
name: 'secret.zip',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Decode with passphrase
|
|
156
|
+
const result = await decodePngToBinary(encoded, {
|
|
157
|
+
passphrase: 'my-secret-password',
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Fast Compression
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Optimize for speed (recommended for large files)
|
|
165
|
+
const png = await encodeBinaryToPng(largeBuffer, {
|
|
166
|
+
mode: 'screenshot',
|
|
167
|
+
brQuality: 0, // Fastest
|
|
168
|
+
name: 'large-file.bin',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Optimize for size (recommended for small files)
|
|
172
|
+
const png = await encodeBinaryToPng(smallBuffer, {
|
|
173
|
+
mode: 'compact',
|
|
174
|
+
brQuality: 11, // Best compression
|
|
175
|
+
name: 'config.json',
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Encoding Modes
|
|
180
|
+
|
|
181
|
+
#### `screenshot` (Recommended)
|
|
182
|
+
|
|
183
|
+
Encodes data as RGB pixel values, optimized for screenshot-like appearance. Best balance of size and compatibility.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const png = await encodeBinaryToPng(data, { mode: 'screenshot' });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `compact` (Smallest)
|
|
190
|
+
|
|
191
|
+
Minimal 1x1 PNG with data in custom chunk. Fastest and smallest.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const png = await encodeBinaryToPng(data, { mode: 'compact' });
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### `pixel`
|
|
198
|
+
|
|
199
|
+
Encodes data as RGB pixel values without screenshot optimization.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const png = await encodeBinaryToPng(data, { mode: 'pixel' });
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### `chunk`
|
|
206
|
+
|
|
207
|
+
Standard PNG with data in custom rXDT chunk.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const png = await encodeBinaryToPng(data, { mode: 'chunk' });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## API Reference
|
|
214
|
+
|
|
215
|
+
### `encodeBinaryToPng(input, options)`
|
|
216
|
+
|
|
217
|
+
Encodes binary data into a PNG image.
|
|
218
|
+
|
|
219
|
+
**Parameters:**
|
|
220
|
+
|
|
221
|
+
- `input: Buffer` - The binary data to encode
|
|
222
|
+
- `options?: EncodeOptions` - Encoding options
|
|
223
|
+
|
|
224
|
+
**Returns:** `Promise<Buffer>` - The encoded PNG
|
|
225
|
+
|
|
226
|
+
**Options:**
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
interface EncodeOptions {
|
|
230
|
+
// Compression algorithm ('br' = Brotli, 'none' = no compression)
|
|
231
|
+
compression?: 'br' | 'none';
|
|
232
|
+
|
|
233
|
+
// Passphrase for encryption
|
|
234
|
+
passphrase?: string;
|
|
235
|
+
|
|
236
|
+
// Original filename to embed
|
|
237
|
+
name?: string;
|
|
238
|
+
|
|
239
|
+
// Encoding mode
|
|
240
|
+
mode?: 'compact' | 'chunk' | 'pixel' | 'screenshot';
|
|
241
|
+
|
|
242
|
+
// Encryption method
|
|
243
|
+
encrypt?: 'auto' | 'aes' | 'xor' | 'none';
|
|
244
|
+
|
|
245
|
+
// Output format
|
|
246
|
+
output?: 'auto' | 'png' | 'rox';
|
|
247
|
+
|
|
248
|
+
// Include filename in metadata (default: true)
|
|
249
|
+
includeName?: boolean;
|
|
250
|
+
|
|
251
|
+
// Brotli quality 0-11 (default: 1)
|
|
252
|
+
brQuality?: number;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### `decodePngToBinary(pngBuf, options)`
|
|
257
|
+
|
|
258
|
+
Decodes a PNG image back to binary data.
|
|
259
|
+
|
|
260
|
+
**Parameters:**
|
|
261
|
+
|
|
262
|
+
- `pngBuf: Buffer` - The PNG image to decode
|
|
263
|
+
- `options?: DecodeOptions` - Decoding options
|
|
264
|
+
|
|
265
|
+
**Returns:** `Promise<DecodeResult>` - The decoded data and metadata
|
|
266
|
+
|
|
267
|
+
**Options:**
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
interface DecodeOptions {
|
|
271
|
+
// Passphrase for decryption
|
|
272
|
+
passphrase?: string;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Result:**
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
interface DecodeResult {
|
|
280
|
+
// Decoded binary data
|
|
281
|
+
buf: Buffer;
|
|
282
|
+
|
|
283
|
+
// Extracted metadata
|
|
284
|
+
meta?: {
|
|
285
|
+
// Original filename
|
|
286
|
+
name?: string;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Performance Tips
|
|
292
|
+
|
|
293
|
+
### For Large Files (>10 MB)
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# Use quality 0 for fastest encoding
|
|
297
|
+
npx rox encode large.bin output.png -q 0
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
const png = await encodeBinaryToPng(largeFile, {
|
|
302
|
+
mode: 'screenshot',
|
|
303
|
+
brQuality: 0, // 10-20x faster than default
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### For Small Files (<1 MB)
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
# Use quality 11 for best compression
|
|
311
|
+
npx rox encode small.json output.png -q 11 -m compact
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const png = await encodeBinaryToPng(smallFile, {
|
|
316
|
+
mode: 'compact',
|
|
317
|
+
brQuality: 11, // Best compression ratio
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Benchmark Results
|
|
322
|
+
|
|
323
|
+
File: 3.8 MB binary
|
|
324
|
+
|
|
325
|
+
- **Quality 0**: ~500-800ms, output ~1.2 MB
|
|
326
|
+
- **Quality 1** (default): ~1-2s, output ~800 KB
|
|
327
|
+
- **Quality 5**: ~8-12s, output ~750 KB
|
|
328
|
+
- **Quality 11**: ~20-30s, output ~720 KB
|
|
329
|
+
|
|
330
|
+
## Error Handling
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
try {
|
|
334
|
+
const result = await decodePngToBinary(encoded, {
|
|
335
|
+
passphrase: 'wrong-password',
|
|
336
|
+
});
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err.message.includes('Incorrect passphrase')) {
|
|
339
|
+
console.error('Wrong password!');
|
|
340
|
+
} else if (err.message.includes('Invalid ROX format')) {
|
|
341
|
+
console.error('Not a valid RoxCompressor PNG');
|
|
342
|
+
} else {
|
|
343
|
+
console.error('Decode failed:', err.message);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Security
|
|
349
|
+
|
|
350
|
+
- **AES-256-GCM**: Authenticated encryption with 100,000 PBKDF2 iterations
|
|
351
|
+
- **XOR cipher**: Simple obfuscation (not cryptographically secure)
|
|
352
|
+
- **No encryption**: Data is compressed but not encrypted
|
|
353
|
+
|
|
354
|
+
⚠️ **Warning**: Use strong passphrases for sensitive data. The `xor` encryption mode is not secure and should only be used for obfuscation.
|
|
355
|
+
|
|
356
|
+
## License
|
|
357
|
+
|
|
358
|
+
MIT © RoxCompressor
|
|
359
|
+
|
|
360
|
+
## Contributing
|
|
361
|
+
|
|
362
|
+
Contributions welcome! Please open an issue or PR on GitHub.
|
|
363
|
+
|
|
364
|
+
## Links
|
|
365
|
+
|
|
366
|
+
- [GitHub Repository](https://github.com/RoxasYTB/RoxCompressor)
|
|
367
|
+
- [npm Package](https://www.npmjs.com/package/roxify)
|
|
368
|
+
- [Report Issues](https://github.com/RoxasYTB/RoxCompressor/issues)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { basename, resolve } from 'path';
|
|
4
|
+
import { cropAndReconstitute, DataFormatError, decodePngToBinary, encodeBinaryToPng, IncorrectPassphraseError, PassphraseRequiredError, } from './index.js';
|
|
5
|
+
const VERSION = '1.0.4';
|
|
6
|
+
function showHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
ROX CLI — Encode/decode binary in PNG
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
npx rox <command> [options]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
encode <input> [output] Encode file to PNG
|
|
15
|
+
decode <input> [output] Decode PNG to original file
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-p, --passphrase <pass> Use passphrase (AES-256-GCM)
|
|
19
|
+
-m, --mode <mode> Mode: compact|chunk|pixel|screenshot (default: screenshot)
|
|
20
|
+
-q, --quality <0-11> Brotli quality (default: 11)
|
|
21
|
+
-e, --encrypt <type> auto|aes|xor|none
|
|
22
|
+
--no-compress Disable compression
|
|
23
|
+
-o, --output <path> Output file path
|
|
24
|
+
--view-reconst Export the reconstituted PNG for debugging
|
|
25
|
+
-v, --verbose Show detailed errors
|
|
26
|
+
|
|
27
|
+
Run "npx rox help" for this message.
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
function parseArgs(args) {
|
|
31
|
+
const parsed = { _: [] };
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < args.length) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
if (arg.startsWith('--')) {
|
|
36
|
+
const key = arg.slice(2);
|
|
37
|
+
if (key === 'no-compress') {
|
|
38
|
+
parsed.noCompress = true;
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
else if (key === 'verbose') {
|
|
42
|
+
parsed.verbose = true;
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
else if (key === 'view-reconst') {
|
|
46
|
+
parsed.viewReconst = true;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
else if (key === 'debug-dir') {
|
|
50
|
+
parsed.debugDir = args[i + 1];
|
|
51
|
+
i += 2;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const value = args[i + 1];
|
|
55
|
+
parsed[key] = value;
|
|
56
|
+
i += 2;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (arg.startsWith('-')) {
|
|
60
|
+
const flag = arg.slice(1);
|
|
61
|
+
const value = args[i + 1];
|
|
62
|
+
switch (flag) {
|
|
63
|
+
case 'p':
|
|
64
|
+
parsed.passphrase = value;
|
|
65
|
+
i += 2;
|
|
66
|
+
break;
|
|
67
|
+
case 'm':
|
|
68
|
+
parsed.mode = value;
|
|
69
|
+
i += 2;
|
|
70
|
+
break;
|
|
71
|
+
case 'q':
|
|
72
|
+
parsed.quality = parseInt(value, 10);
|
|
73
|
+
i += 2;
|
|
74
|
+
break;
|
|
75
|
+
case 'e':
|
|
76
|
+
parsed.encrypt = value;
|
|
77
|
+
i += 2;
|
|
78
|
+
break;
|
|
79
|
+
case 'o':
|
|
80
|
+
parsed.output = value;
|
|
81
|
+
i += 2;
|
|
82
|
+
break;
|
|
83
|
+
case 'v':
|
|
84
|
+
parsed.verbose = true;
|
|
85
|
+
i += 1;
|
|
86
|
+
break;
|
|
87
|
+
case 'd':
|
|
88
|
+
parsed.debugDir = value;
|
|
89
|
+
i += 2;
|
|
90
|
+
break;
|
|
91
|
+
default:
|
|
92
|
+
console.error(`Unknown option: ${arg}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
parsed._.push(arg);
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
async function encodeCommand(args) {
|
|
104
|
+
const parsed = parseArgs(args);
|
|
105
|
+
const [inputPath, outputPath] = parsed._;
|
|
106
|
+
if (!inputPath) {
|
|
107
|
+
console.error('Error: Input file required');
|
|
108
|
+
console.log('Usage: npx rox encode <input> [output] [options]');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const resolvedInput = resolve(inputPath);
|
|
112
|
+
const resolvedOutput = parsed.output || outputPath || inputPath.replace(/(\.[^.]+)?$/, '.png');
|
|
113
|
+
try {
|
|
114
|
+
console.log(`Reading: ${resolvedInput}`);
|
|
115
|
+
const startRead = Date.now();
|
|
116
|
+
const inputBuffer = readFileSync(resolvedInput);
|
|
117
|
+
const readTime = Date.now() - startRead;
|
|
118
|
+
console.log(`Read ${(inputBuffer.length / 1024 / 1024).toFixed(2)} MB in ${readTime}ms`);
|
|
119
|
+
const options = {
|
|
120
|
+
mode: parsed.mode || 'screenshot',
|
|
121
|
+
name: basename(resolvedInput),
|
|
122
|
+
brQuality: parsed.quality !== undefined ? parsed.quality : 11,
|
|
123
|
+
};
|
|
124
|
+
if (parsed.noCompress) {
|
|
125
|
+
options.compression = 'none';
|
|
126
|
+
}
|
|
127
|
+
if (parsed.passphrase) {
|
|
128
|
+
options.passphrase = parsed.passphrase;
|
|
129
|
+
options.encrypt = parsed.encrypt || 'aes';
|
|
130
|
+
}
|
|
131
|
+
console.log(`Encoding ${basename(resolvedInput)} -> ${resolvedOutput}`);
|
|
132
|
+
const startEncode = Date.now();
|
|
133
|
+
const output = await encodeBinaryToPng(inputBuffer, options);
|
|
134
|
+
const encodeTime = Date.now() - startEncode;
|
|
135
|
+
writeFileSync(resolvedOutput, output);
|
|
136
|
+
const outputSize = (output.length / 1024 / 1024).toFixed(2);
|
|
137
|
+
const inputSize = (inputBuffer.length / 1024 / 1024).toFixed(2);
|
|
138
|
+
const ratio = ((output.length / inputBuffer.length) * 100).toFixed(1);
|
|
139
|
+
console.log(`\nSuccess!`);
|
|
140
|
+
console.log(` Input: ${inputSize} MB`);
|
|
141
|
+
console.log(` Output: ${outputSize} MB (${ratio}% of original)`);
|
|
142
|
+
console.log(` Time: ${encodeTime}ms`);
|
|
143
|
+
console.log(` Saved: ${resolvedOutput}`);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.error('Error: Failed to encode file. Use --verbose for details.');
|
|
147
|
+
if (parsed.verbose)
|
|
148
|
+
console.error('Details:', err.stack || err.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function decodeCommand(args) {
|
|
153
|
+
const parsed = parseArgs(args);
|
|
154
|
+
const [inputPath, outputPath] = parsed._;
|
|
155
|
+
if (!inputPath) {
|
|
156
|
+
console.error('Error: Input PNG file required');
|
|
157
|
+
console.log('Usage: npx rox decode <input> [output] [options]');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const resolvedInput = resolve(inputPath);
|
|
161
|
+
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}`);
|
|
177
|
+
const options = {};
|
|
178
|
+
if (parsed.passphrase) {
|
|
179
|
+
options.passphrase = parsed.passphrase;
|
|
180
|
+
}
|
|
181
|
+
if (parsed.debugDir) {
|
|
182
|
+
options.debugDir = parsed.debugDir;
|
|
183
|
+
}
|
|
184
|
+
console.log(`Decoding...`);
|
|
185
|
+
const startDecode = Date.now();
|
|
186
|
+
if (parsed.verbose)
|
|
187
|
+
options.verbose = true;
|
|
188
|
+
const result = await decodePngToBinary(inputBuffer, options);
|
|
189
|
+
const decodeTime = Date.now() - startDecode;
|
|
190
|
+
const resolvedOutput = parsed.output || outputPath || result.meta?.name || 'decoded.bin';
|
|
191
|
+
writeFileSync(resolvedOutput, result.buf);
|
|
192
|
+
const outputSize = (result.buf.length / 1024 / 1024).toFixed(2);
|
|
193
|
+
console.log(`\nSuccess!`);
|
|
194
|
+
if (result.meta?.name) {
|
|
195
|
+
console.log(` Original name: ${result.meta.name}`);
|
|
196
|
+
}
|
|
197
|
+
console.log(` Output size: ${outputSize} MB`);
|
|
198
|
+
console.log(` Time: ${decodeTime}ms`);
|
|
199
|
+
console.log(` Saved: ${resolvedOutput}`);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err instanceof PassphraseRequiredError ||
|
|
203
|
+
(err.message && err.message.includes('passphrase') && !parsed.passphrase)) {
|
|
204
|
+
console.error('File appears to be encrypted. Provide a passphrase with -p');
|
|
205
|
+
}
|
|
206
|
+
else if (err instanceof IncorrectPassphraseError ||
|
|
207
|
+
(err.message && err.message.includes('Incorrect passphrase'))) {
|
|
208
|
+
console.error('Incorrect passphrase');
|
|
209
|
+
}
|
|
210
|
+
else if (err instanceof DataFormatError ||
|
|
211
|
+
(err.message &&
|
|
212
|
+
(err.message.includes('decompression failed') ||
|
|
213
|
+
err.message.includes('missing ROX1') ||
|
|
214
|
+
err.message.includes('Pixel payload truncated') ||
|
|
215
|
+
err.message.includes('Marker START not found') ||
|
|
216
|
+
err.message.includes('Brotli decompression failed')))) {
|
|
217
|
+
console.error('Data corrupted or unsupported format. Use --verbose for details.');
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.error('Failed to decode file. Use --verbose for details.');
|
|
221
|
+
}
|
|
222
|
+
if (parsed.verbose)
|
|
223
|
+
console.error('Details:', err.stack || err.message);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function main() {
|
|
228
|
+
const args = process.argv.slice(2);
|
|
229
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
|
|
230
|
+
showHelp();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (args[0] === 'version' || args[0] === '--version') {
|
|
234
|
+
console.log(VERSION);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const command = args[0];
|
|
238
|
+
const commandArgs = args.slice(1);
|
|
239
|
+
switch (command) {
|
|
240
|
+
case 'encode':
|
|
241
|
+
await encodeCommand(commandArgs);
|
|
242
|
+
break;
|
|
243
|
+
case 'decode':
|
|
244
|
+
await decodeCommand(commandArgs);
|
|
245
|
+
break;
|
|
246
|
+
default:
|
|
247
|
+
console.error(`Unknown command: ${command}`);
|
|
248
|
+
console.log('Run "npx rox help" for usage information');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
main().catch((err) => {
|
|
253
|
+
console.error('Fatal error:', err);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
});
|