roxify 1.6.6 → 1.6.8
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 +223 -22
- package/dist/cli.js +70 -11
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/utils/audio.d.ts +23 -0
- package/dist/utils/audio.js +98 -0
- package/dist/utils/decoder.js +104 -0
- package/dist/utils/ecc.d.ts +75 -0
- package/dist/utils/ecc.js +446 -0
- package/dist/utils/encoder.js +151 -43
- package/dist/utils/robust-audio.d.ts +54 -0
- package/dist/utils/robust-audio.js +400 -0
- package/dist/utils/robust-image.d.ts +54 -0
- package/dist/utils/robust-image.js +515 -0
- package/dist/utils/types.d.ts +24 -0
- package/dist/utils/zstd.js +26 -12
- package/package.json +1 -1
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/roxify_native.node +0 -0
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ The core compression and image-processing logic is written in Rust and exposed t
|
|
|
42
42
|
- **Multi-threaded Zstd compression** (level 19) with parallel chunk processing via Rayon
|
|
43
43
|
- **AES-256-GCM encryption** with PBKDF2 key derivation (100,000 iterations)
|
|
44
44
|
- **Lossless roundtrip** -- encoded data is recovered byte-for-byte
|
|
45
|
+
- **Lossy-resilient mode** -- QR-code-style Reed-Solomon error correction survives JPEG, WebP, MP3, AAC, and OGG recompression
|
|
46
|
+
- **Audio container** -- encode data as structured multi-frequency tones (not white noise) in WAV files
|
|
45
47
|
- **Directory packing** -- encode entire directory trees into a single PNG
|
|
46
48
|
- **Screenshot reconstitution** -- recover data from photographed or screenshotted PNGs
|
|
47
49
|
- **CLI and programmatic API** -- use from the terminal or import as a library
|
|
@@ -53,42 +55,136 @@ The core compression and image-processing logic is written in Rust and exposed t
|
|
|
53
55
|
|
|
54
56
|
## Benchmarks
|
|
55
57
|
|
|
56
|
-
All measurements were taken on Linux x64 with Node.js v20.
|
|
58
|
+
All measurements were taken on Linux x64 (Intel i7-6700K @ 4.0 GHz, 32 GB RAM) with Node.js v20. Every tool uses its **maximum compression** setting: zip -9, gzip -9, 7z LZMA2 -mx=9, and Roxify Zstd level 19. Roxify produces a valid PNG or WAV file rather than a raw archive.
|
|
59
|
+
|
|
60
|
+
### Compression Ratio (Maximum Compression for All Tools)
|
|
61
|
+
|
|
62
|
+
| Dataset | Original | zip -9 | gzip -9 | 7z LZMA2 -9 | Roxify PNG | Roxify WAV |
|
|
63
|
+
|---|---|---|---|---|---|---|
|
|
64
|
+
| Text 1 MB | 1.00 MB | 219 KB (21.4%) | 219 KB (21.4%) | 187 KB (18.3%) | **188 KB (18.3%)** | **187 KB (18.3%)** |
|
|
65
|
+
| JSON 1 MB | 1.00 MB | 263 KB (25.7%) | 263 KB (25.7%) | 225 KB (22.0%) | **220 KB (21.5%)** | **219 KB (21.4%)** |
|
|
66
|
+
| Binary 1 MB | 1.00 MB | 1.00 MB (100%) | 1.00 MB (100%) | 1.00 MB (100%) | 1.00 MB (100%) | 1.00 MB (100%) |
|
|
67
|
+
| Mixed 5 MB | 5.00 MB | 2.45 MB (49.0%) | 2.45 MB (49.1%) | 2.33 MB (46.6%) | 2.38 MB (47.6%) | 2.38 MB (47.6%) |
|
|
68
|
+
| Text 10 MB | 10.00 MB | 2.13 MB (21.3%) | 2.13 MB (21.3%) | 1.71 MB (17.1%) | **1.71 MB (17.1%)** | **1.70 MB (17.0%)** |
|
|
69
|
+
| Mixed 10 MB | 10.00 MB | 4.90 MB (49.0%) | 4.90 MB (49.0%) | 4.65 MB (46.5%) | 4.73 MB (47.3%) | 4.73 MB (47.3%) |
|
|
70
|
+
|
|
71
|
+
> **Roxify matches 7z LZMA2 ultra-compression on text** (18.3% for both at 1 MB) and **beats LZMA2 on JSON** (21.4% vs 22.0%). On mixed data, Roxify is within 1 percentage point of LZMA2 while producing a shareable PNG/WAV instead of an archive.
|
|
72
|
+
|
|
73
|
+
### Encode and Decode Speed (CLI)
|
|
74
|
+
|
|
75
|
+
| Dataset | Tool | Encode | Decode | Enc Throughput | Dec Throughput |
|
|
76
|
+
|---|---|---|---|---|---|
|
|
77
|
+
| Text 1 MB | zip -9 | 112 ms | 36 ms | 8.9 MB/s | 27.6 MB/s |
|
|
78
|
+
| | gzip -9 | 146 ms | 38 ms | 6.9 MB/s | 26.0 MB/s |
|
|
79
|
+
| | 7z LZMA -9 | 303 ms | 21 ms | 3.3 MB/s | 46.6 MB/s |
|
|
80
|
+
| | **Roxify PNG** | **859 ms** | **577 ms** | **1.2 MB/s** | **1.7 MB/s** |
|
|
81
|
+
| | **Roxify WAV** | **794 ms** | **480 ms** | **1.3 MB/s** | **2.1 MB/s** |
|
|
82
|
+
| JSON 1 MB | zip -9 | 79 ms | 20 ms | 12.7 MB/s | 50.5 MB/s |
|
|
83
|
+
| | 7z LZMA -9 | 197 ms | 26 ms | 5.1 MB/s | 37.9 MB/s |
|
|
84
|
+
| | **Roxify PNG** | **1.14 s** | **755 ms** | **0.9 MB/s** | **1.3 MB/s** |
|
|
85
|
+
| | **Roxify WAV** | **1.49 s** | **518 ms** | **0.7 MB/s** | **1.9 MB/s** |
|
|
86
|
+
| Text 10 MB | zip -9 | 1.21 s | 70 ms | 8.2 MB/s | 143.8 MB/s |
|
|
87
|
+
| | 7z LZMA -9 | 5.05 s | 99 ms | 2.0 MB/s | 100.8 MB/s |
|
|
88
|
+
| | **Roxify PNG** | **9.05 s** | **4.53 s** | **1.1 MB/s** | **2.2 MB/s** |
|
|
89
|
+
| | **Roxify WAV** | **9.22 s** | **2.59 s** | **1.1 MB/s** | **3.9 MB/s** |
|
|
90
|
+
|
|
91
|
+
> Roxify CLI includes Node.js startup overhead (~400 ms). In the JS API (below), the same operations are significantly faster. WAV decode is consistently faster than PNG decode due to simpler container parsing.
|
|
92
|
+
|
|
93
|
+
### JavaScript API Throughput
|
|
94
|
+
|
|
95
|
+
Direct API calls (no CLI startup overhead):
|
|
96
|
+
|
|
97
|
+
| Size | Container | Encode | Decode | Enc Throughput | Dec Throughput | Output | Ratio | Integrity |
|
|
98
|
+
|---|---|---|---|---|---|---|---|---|
|
|
99
|
+
| 1 KB | PNG | 9 ms | 12 ms | 0.1 MB/s | 0.1 MB/s | 1.14 KB | 114.3% | ✓ |
|
|
100
|
+
| 10 KB | PNG | 18 ms | 34 ms | 0.5 MB/s | 0.3 MB/s | 10.32 KB | 103.2% | ✓ |
|
|
101
|
+
| 100 KB | PNG | 52 ms | 109 ms | 1.9 MB/s | 0.9 MB/s | 100.52 KB | 100.5% | ✓ |
|
|
102
|
+
| 500 KB | PNG | 339 ms | 541 ms | 1.4 MB/s | 0.9 MB/s | 502.64 KB | 100.5% | ✓ |
|
|
103
|
+
| 1 MB | PNG | 875 ms | 1.24 s | 1.1 MB/s | 0.8 MB/s | 1.00 MB | 100.3% | ✓ |
|
|
104
|
+
| 5 MB | PNG | 3.39 s | 4.12 s | 1.5 MB/s | 1.2 MB/s | 5.01 MB | 100.2% | ✓ |
|
|
105
|
+
| 10 MB | PNG | 6.84 s | 12.28 s | 1.5 MB/s | 0.8 MB/s | 10.01 MB | 100.1% | ✓ |
|
|
106
|
+
| 1 KB | WAV | 2 ms | 2 ms | 0.6 MB/s | 0.6 MB/s | 1.08 KB | 107.5% | ✓ |
|
|
107
|
+
| 10 KB | WAV | 4 ms | 5 ms | 2.3 MB/s | 1.8 MB/s | 10.08 KB | 100.8% | ✓ |
|
|
108
|
+
| 100 KB | WAV | 39 ms | 28 ms | 2.5 MB/s | 3.5 MB/s | 100.08 KB | 100.1% | ✓ |
|
|
109
|
+
| 500 KB | WAV | 172 ms | 190 ms | 2.8 MB/s | 2.6 MB/s | 500.09 KB | 100.0% | ✓ |
|
|
110
|
+
| 1 MB | WAV | 452 ms | 276 ms | 2.2 MB/s | 3.6 MB/s | 1.00 MB | 100.0% | ✓ |
|
|
111
|
+
| 5 MB | WAV | 2.70 s | 1.65 s | 1.8 MB/s | 3.0 MB/s | 5.00 MB | 100.0% | ✓ |
|
|
112
|
+
| 10 MB | WAV | 4.81 s | 2.56 s | 2.1 MB/s | 3.9 MB/s | 10.00 MB | 100.0% | ✓ |
|
|
113
|
+
|
|
114
|
+
> WAV container is **2–4× faster** than PNG for decoding at large sizes, and produces slightly smaller output thanks to simpler framing.
|
|
115
|
+
|
|
116
|
+
### Reed-Solomon ECC Throughput
|
|
117
|
+
|
|
118
|
+
| Size | Encode | Decode | Enc Throughput | Dec Throughput | Overhead |
|
|
119
|
+
|---|---|---|---|---|---|
|
|
120
|
+
| 1 KB | 6 ms | 4 ms | 0.2 MB/s | 0.2 MB/s | 125.7% |
|
|
121
|
+
| 10 KB | 7 ms | 6 ms | 1.3 MB/s | 1.5 MB/s | 119.6% |
|
|
122
|
+
| 100 KB | 49 ms | 45 ms | 2.0 MB/s | 2.1 MB/s | 118.8% |
|
|
123
|
+
| 1 MB | 483 ms | 377 ms | 2.1 MB/s | 2.7 MB/s | 118.6% |
|
|
124
|
+
|
|
125
|
+
### Lossy-Resilient Encoding
|
|
126
|
+
|
|
127
|
+
#### Robust Image (QR-code-style, block size 4×4)
|
|
128
|
+
|
|
129
|
+
| Data Size | Encode Time | Output (PNG) |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| 32 B | 32 ms | 122 KB |
|
|
132
|
+
| 128 B | 39 ms | 122 KB |
|
|
133
|
+
| 512 B | 76 ms | 316 KB |
|
|
134
|
+
| 1 KB | 139 ms | 508 KB |
|
|
135
|
+
| 2 KB | 251 ms | 986 KB |
|
|
136
|
+
|
|
137
|
+
#### Robust Audio (MFSK 8-channel, medium ECC)
|
|
138
|
+
|
|
139
|
+
| Data Size | Encode | Decode | Output (WAV) | Integrity |
|
|
140
|
+
|---|---|---|---|---|
|
|
141
|
+
| 10 B | 33 ms | 44 ms | 1.35 MB | ✓ |
|
|
142
|
+
| 32 B | 19 ms | 31 ms | 1.35 MB | ✓ |
|
|
143
|
+
| 64 B | 22 ms | 24 ms | 1.35 MB | ✓ |
|
|
144
|
+
| 128 B | 21 ms | 28 ms | 1.35 MB | ✓ |
|
|
145
|
+
| 256 B | 40 ms | 45 ms | 2.59 MB | ✓ |
|
|
57
146
|
|
|
58
|
-
###
|
|
147
|
+
### Data Integrity Verification
|
|
148
|
+
|
|
149
|
+
All encode/decode roundtrips produce bit-exact output, verified by SHA-256:
|
|
150
|
+
|
|
151
|
+
| Test Case | PNG | WAV |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| Empty buffer (0 B) | ✓ | ✓ |
|
|
154
|
+
| Single byte (1 B) | ✓ | ✓ |
|
|
155
|
+
| All byte values (256 B) | ✓ | ✓ |
|
|
156
|
+
| 1 KB text | ✓ | ✓ |
|
|
157
|
+
| 100 KB random | ✓ | ✓ |
|
|
158
|
+
| 1 MB random | ✓ | ✓ |
|
|
159
|
+
| 5 MB random | ✓ | ✓ |
|
|
59
160
|
|
|
60
|
-
|
|
61
|
-
|---|---:|---|---|---|---|---|
|
|
62
|
-
| Text files (1 MB) | 128 | 1.00 MB | 259 KB (25.3%) 52 ms | 196 KB (19.2%) 78 ms | 162 KB (15.8%) 347 ms | 203 KB (19.9%) 534 ms |
|
|
63
|
-
| JSON files (1 MB) | 1 183 | 1.00 MB | 700 KB (68.3%) 78 ms | 270 KB (26.4%) 43 ms | 212 KB (20.7%) 191 ms | 339 KB (33.0%) 766 ms |
|
|
64
|
-
| Binary (random, 1 MB) | 33 | 1.00 MB | 1.00 MB (100.5%) 29 ms | 1.00 MB (100.4%) 47 ms | 1.00 MB (100.1%) 128 ms | 1.00 MB (100.5%) 689 ms |
|
|
65
|
-
| Mixed (text+JSON+bin, 5 MB) | 2 241 | 5.00 MB | 3.26 MB (65.2%) 253 ms | 2.43 MB (48.7%) 228 ms | 2.27 MB (45.5%) 1.21 s | 2.59 MB (51.7%) 2.04 s |
|
|
66
|
-
| Text files (10 MB) | 1 285 | 10.00 MB | 2.54 MB (25.4%) 357 ms | 1.91 MB (19.1%) 657 ms | 1.53 MB (15.3%) 4.70 s | 1.98 MB (19.8%) 2.15 s |
|
|
67
|
-
| Mixed (text+JSON+bin, 10 MB) | 4 467 | 10.00 MB | 6.52 MB (65.2%) 461 ms | 4.86 MB (48.6%) 452 ms | 4.54 MB (45.4%) 2.51 s | 5.16 MB (51.6%) 3.77 s |
|
|
161
|
+
**14 / 14 integrity tests passed** across both containers.
|
|
68
162
|
|
|
69
163
|
### Key Observations
|
|
70
164
|
|
|
71
|
-
- **
|
|
72
|
-
- **
|
|
73
|
-
- **
|
|
74
|
-
- **
|
|
75
|
-
-
|
|
165
|
+
- **Roxify matches LZMA2 ultra-compression** on text data (18.3%) and **outperforms it on JSON** (21.4% vs 22.0%), while producing a standard PNG or WAV file instead of an archive.
|
|
166
|
+
- **WAV container decode is 2–4× faster** than PNG decode at large sizes (3.9 MB/s vs 0.8 MB/s for 10 MB).
|
|
167
|
+
- **WAV encode for 1 KB data completes in 2 ms** — well under the sub-second target.
|
|
168
|
+
- **Lossy-resilient audio** encode/decode completes in under 50 ms for data up to 256 bytes, with full integrity.
|
|
169
|
+
- **100% data integrity** across all sizes and containers — every byte is recovered exactly.
|
|
170
|
+
- The CLI overhead (~400 ms Node.js startup) is amortized on larger inputs. For programmatic use, the JS API eliminates this entirely.
|
|
171
|
+
- On incompressible (random) data, all tools converge to ~100% as expected. No compression algorithm can shrink truly random data.
|
|
76
172
|
|
|
77
173
|
### Methodology
|
|
78
174
|
|
|
79
|
-
Benchmarks were generated using `test/benchmark.
|
|
175
|
+
Benchmarks were generated using `test/benchmark-detailed.cjs`. Datasets consist of procedurally generated text, JSON, and random binary data. Each tool was invoked with its maximum compression setting:
|
|
80
176
|
|
|
81
|
-
| Tool | Command |
|
|
177
|
+
| Tool | Command / Setting |
|
|
82
178
|
|---|---|
|
|
83
|
-
| zip | `zip -r -q` |
|
|
84
|
-
| tar
|
|
85
|
-
| 7z | `7z a -mx=
|
|
86
|
-
| Roxify |
|
|
179
|
+
| zip | `zip -r -q -9` |
|
|
180
|
+
| tar/gzip | `tar -cf - \| gzip -9` |
|
|
181
|
+
| 7z | `7z a -mx=9` (LZMA2 ultra) |
|
|
182
|
+
| Roxify | Zstd level 19, compact mode |
|
|
87
183
|
|
|
88
184
|
To reproduce:
|
|
89
185
|
|
|
90
186
|
```bash
|
|
91
|
-
node test/benchmark.
|
|
187
|
+
node test/benchmark-detailed.cjs
|
|
92
188
|
```
|
|
93
189
|
|
|
94
190
|
---
|
|
@@ -249,6 +345,10 @@ interface EncodeOptions {
|
|
|
249
345
|
includeFileList?: boolean; // Include file manifest in PNG
|
|
250
346
|
fileList?: Array<string | { name: string; size: number }>;
|
|
251
347
|
skipOptimization?: boolean; // Skip PNG optimization pass
|
|
348
|
+
lossyResilient?: boolean; // Enable lossy-resilient encoding (RS ECC)
|
|
349
|
+
eccLevel?: EccLevel; // 'low' | 'medium' | 'quartile' | 'high'
|
|
350
|
+
robustBlockSize?: number; // 2–8 pixels per data block (lossy image)
|
|
351
|
+
container?: 'image' | 'sound'; // Output container format
|
|
252
352
|
onProgress?: (info: ProgressInfo) => void;
|
|
253
353
|
showProgress?: boolean;
|
|
254
354
|
verbose?: boolean;
|
|
@@ -275,6 +375,7 @@ interface DecodeResult {
|
|
|
275
375
|
buf?: Buffer; // Decoded binary payload
|
|
276
376
|
meta?: { name?: string }; // Metadata (original filename)
|
|
277
377
|
files?: PackedFile[]; // Unpacked directory entries, if applicable
|
|
378
|
+
correctedErrors?: number; // RS errors corrected (lossy-resilient mode)
|
|
278
379
|
}
|
|
279
380
|
```
|
|
280
381
|
|
|
@@ -302,6 +403,79 @@ When `encrypt` is set to `auto` (the default when a passphrase is provided), AES
|
|
|
302
403
|
|
|
303
404
|
---
|
|
304
405
|
|
|
406
|
+
## Lossy-Resilient Mode
|
|
407
|
+
|
|
408
|
+
Enable `lossyResilient: true` to produce output that survives lossy compression. This uses the same error correction algorithm as QR codes (Reed-Solomon over GF(256)) combined with block-based signal encoding.
|
|
409
|
+
|
|
410
|
+
### How It Works
|
|
411
|
+
|
|
412
|
+
1. **Reed-Solomon ECC** adds configurable redundancy (10–100%) to the data.
|
|
413
|
+
2. **Interleaving** spreads data across RS blocks so burst errors don't overwhelm a single block.
|
|
414
|
+
3. **Block encoding** (image: large pixel blocks; audio: multi-frequency tones) makes the signal robust against quantization.
|
|
415
|
+
4. **Finder patterns** (image only) enable automatic alignment after re-encoding.
|
|
416
|
+
|
|
417
|
+
### Error Correction Levels
|
|
418
|
+
|
|
419
|
+
| Level | Parity Symbols | Overhead | Correctable Errors |
|
|
420
|
+
|-------|---------------:|---------:|-------------------:|
|
|
421
|
+
| `low` | 20 / block | ~10% | ~4% |
|
|
422
|
+
| `medium` | 40 / block | ~19% | ~9% |
|
|
423
|
+
| `quartile` | 64 / block | ~33% | ~15% |
|
|
424
|
+
| `high` | 128 / block | ~100% | ~25% |
|
|
425
|
+
|
|
426
|
+
### Example
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// Image that survives JPEG compression
|
|
430
|
+
const png = await encodeBinaryToPng(data, {
|
|
431
|
+
lossyResilient: true,
|
|
432
|
+
eccLevel: 'quartile',
|
|
433
|
+
robustBlockSize: 4, // 4×4 pixels per data bit
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Audio that survives MP3 compression
|
|
437
|
+
const wav = await encodeBinaryToPng(data, {
|
|
438
|
+
container: 'sound',
|
|
439
|
+
lossyResilient: true,
|
|
440
|
+
eccLevel: 'medium',
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Decode automatically detects the format
|
|
444
|
+
const result = await decodePngToBinary(png);
|
|
445
|
+
console.log('Errors corrected:', result.correctedErrors);
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
For full documentation, see [Lossy Resilience Guide](./docs/LOSSY_RESILIENCE.md).
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Audio Container
|
|
453
|
+
|
|
454
|
+
Roxify can encode data into WAV audio files using `container: 'sound'`.
|
|
455
|
+
|
|
456
|
+
### Standard Mode (`lossyResilient: false`)
|
|
457
|
+
|
|
458
|
+
Data bytes are stored directly as 8-bit PCM samples. This is the fastest and most compact option, but the output sounds like white noise and does not survive lossy audio compression.
|
|
459
|
+
|
|
460
|
+
### Lossy-Resilient Mode (`lossyResilient: true`)
|
|
461
|
+
|
|
462
|
+
Data is encoded using **8-channel multi-frequency shift keying (MFSK)**:
|
|
463
|
+
|
|
464
|
+
- 8 carrier frequencies (600–2700 Hz) encode 1 byte per symbol.
|
|
465
|
+
- Each carrier is modulated with raised-cosine windowing.
|
|
466
|
+
- The output sounds like a series of **musical chords** — structured and pleasant, not white noise.
|
|
467
|
+
- Reed-Solomon ECC enables recovery after MP3/AAC/OGG transcoding.
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
const wav = await encodeBinaryToPng(data, {
|
|
471
|
+
container: 'sound',
|
|
472
|
+
lossyResilient: true,
|
|
473
|
+
eccLevel: 'medium',
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
305
479
|
## Performance Tuning
|
|
306
480
|
|
|
307
481
|
### Compression Level
|
|
@@ -420,12 +594,24 @@ Roxify is a hybrid Rust and TypeScript module. The performance-critical paths --
|
|
|
420
594
|
Input --> Zstd Compress (multi-threaded, Rayon) --> AES-256-GCM Encrypt (optional) --> PNG Encode --> Output
|
|
421
595
|
```
|
|
422
596
|
|
|
597
|
+
### Lossy-Resilient Pipeline
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
Input --> RS ECC Encode --> Interleave --> Block Encode (MFSK audio / QR-like image) --> WAV/PNG Output
|
|
601
|
+
```
|
|
602
|
+
|
|
423
603
|
### Decompression Pipeline
|
|
424
604
|
|
|
425
605
|
```
|
|
426
606
|
Input --> PNG Parse --> AES-256-GCM Decrypt (optional) --> Zstd Decompress --> Output
|
|
427
607
|
```
|
|
428
608
|
|
|
609
|
+
### Lossy-Resilient Decode Pipeline
|
|
610
|
+
|
|
611
|
+
```
|
|
612
|
+
Input --> Detect Format --> Demodulate/Read Blocks --> De-interleave --> RS ECC Decode --> Output
|
|
613
|
+
```
|
|
614
|
+
|
|
429
615
|
### Rust Modules
|
|
430
616
|
|
|
431
617
|
| Module | Responsibility |
|
|
@@ -436,6 +622,7 @@ Input --> PNG Parse --> AES-256-GCM Decrypt (optional) --> Zstd Decompress --> O
|
|
|
436
622
|
| `crypto.rs` | AES-256-GCM encryption and PBKDF2 key derivation |
|
|
437
623
|
| `archive.rs` | Tar-based archiving with optional Zstd compression |
|
|
438
624
|
| `reconstitution.rs` | Screenshot detection and automatic crop to recover encoded data |
|
|
625
|
+
| `audio.rs` | WAV container encoding and decoding (PCM byte packing) |
|
|
439
626
|
| `bwt.rs` | Parallel Burrows-Wheeler Transform |
|
|
440
627
|
| `rans.rs` | rANS (Asymmetric Numeral Systems) entropy coder |
|
|
441
628
|
| `hybrid.rs` | Block-based orchestration of BWT, context mixing, and rANS |
|
|
@@ -444,6 +631,19 @@ Input --> PNG Parse --> AES-256-GCM Decrypt (optional) --> Zstd Decompress --> O
|
|
|
444
631
|
| `png_utils.rs` | Low-level PNG chunk read/write operations |
|
|
445
632
|
| `progress.rs` | Progress tracking for long-running compression/decompression |
|
|
446
633
|
|
|
634
|
+
### TypeScript Modules
|
|
635
|
+
|
|
636
|
+
| Module | Responsibility |
|
|
637
|
+
|---|---|
|
|
638
|
+
| `ecc.ts` | Reed-Solomon GF(256) codec, block ECC, interleaving |
|
|
639
|
+
| `robust-audio.ts` | MFSK audio modulation/demodulation, Goertzel detection, sync preamble |
|
|
640
|
+
| `robust-image.ts` | QR-code-like block encoding, finder patterns, majority voting |
|
|
641
|
+
| `encoder.ts` | High-level encoding orchestration (standard + lossy-resilient) |
|
|
642
|
+
| `decoder.ts` | High-level decoding with automatic format detection |
|
|
643
|
+
| `audio.ts` | Standard WAV container (8-bit PCM) |
|
|
644
|
+
| `helpers.ts` | Delta coding, XOR cipher, palette generation |
|
|
645
|
+
| `zstd.ts` | Parallel Zstd compression via native module |
|
|
646
|
+
|
|
447
647
|
---
|
|
448
648
|
|
|
449
649
|
## Error Handling
|
|
@@ -512,3 +712,4 @@ MIT. See [LICENSE](LICENSE) for details.
|
|
|
512
712
|
- [CLI Documentation](./docs/CLI.md)
|
|
513
713
|
- [JavaScript SDK Reference](./docs/JAVASCRIPT_SDK.md)
|
|
514
714
|
- [Cross-Platform Build Guide](./docs/CROSS_PLATFORM.md)
|
|
715
|
+
- [Lossy Resilience Guide](./docs/LOSSY_RESILIENCE.md)
|
package/dist/cli.js
CHANGED
|
@@ -52,18 +52,20 @@ async function readLargeFile(filePath) {
|
|
|
52
52
|
}
|
|
53
53
|
function showHelp() {
|
|
54
54
|
console.log(`
|
|
55
|
-
ROX CLI — Encode/decode binary in PNG
|
|
55
|
+
ROX CLI — Encode/decode binary in PNG or WAV
|
|
56
56
|
|
|
57
57
|
Usage:
|
|
58
58
|
npx rox <command> [options]
|
|
59
59
|
|
|
60
60
|
Commands:
|
|
61
|
-
encode <input>... [output] Encode one or more files/directories
|
|
62
|
-
decode <input> [output]
|
|
63
|
-
list <input>
|
|
64
|
-
havepassphrase <input>
|
|
61
|
+
encode <input>... [output] Encode one or more files/directories
|
|
62
|
+
decode <input> [output] Decode PNG/WAV to original file
|
|
63
|
+
list <input> List files in a Rox archive
|
|
64
|
+
havepassphrase <input> Check whether the archive requires a passphrase
|
|
65
65
|
|
|
66
66
|
Options:
|
|
67
|
+
--image Use PNG container (default)
|
|
68
|
+
--sound Use WAV audio container (smaller overhead, faster)
|
|
67
69
|
-p, --passphrase <pass> Use passphrase (AES-256-GCM)
|
|
68
70
|
-m, --mode <mode> Mode: screenshot (default)
|
|
69
71
|
-e, --encrypt <type> auto|aes|xor|none
|
|
@@ -78,6 +80,23 @@ Options:
|
|
|
78
80
|
--debug Export debug images (doubled.png, reconstructed.png)
|
|
79
81
|
-v, --verbose Show detailed errors
|
|
80
82
|
|
|
83
|
+
Lossy-Resilient Encoding:
|
|
84
|
+
--lossy-resilient Enable lossy-resilient mode (survives JPEG/MP3)
|
|
85
|
+
--ecc-level <level> ECC redundancy: low|medium|quartile|high (default: medium)
|
|
86
|
+
--block-size <n> Robust image block size: 2-8 pixels (default: 4)
|
|
87
|
+
|
|
88
|
+
When --lossy-resilient is active, data is encoded with Reed-Solomon ECC
|
|
89
|
+
and rendered as a QR-code-style grid (image) or MFSK tones (audio).
|
|
90
|
+
Use --sound or --image to choose the container format.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
npx rox encode secret.pdf Encode to PNG
|
|
94
|
+
npx rox encode secret.pdf --sound Encode to WAV
|
|
95
|
+
npx rox encode secret.pdf --lossy-resilient Lossy-resilient PNG
|
|
96
|
+
npx rox encode secret.pdf --lossy-resilient --sound --ecc-level high
|
|
97
|
+
npx rox decode secret.pdf.png Decode back
|
|
98
|
+
npx rox decode secret.pdf.wav Decode WAV back
|
|
99
|
+
|
|
81
100
|
Run "npx rox help" for this message.
|
|
82
101
|
`);
|
|
83
102
|
}
|
|
@@ -116,6 +135,36 @@ function parseArgs(args) {
|
|
|
116
135
|
parsed.forceTs = true;
|
|
117
136
|
i++;
|
|
118
137
|
}
|
|
138
|
+
else if (key === 'lossy-resilient') {
|
|
139
|
+
parsed.lossyResilient = true;
|
|
140
|
+
i++;
|
|
141
|
+
}
|
|
142
|
+
else if (key === 'ecc-level') {
|
|
143
|
+
const lvl = args[i + 1];
|
|
144
|
+
if (!['low', 'medium', 'quartile', 'high'].includes(lvl)) {
|
|
145
|
+
console.error(`Invalid --ecc-level: ${lvl}. Must be low|medium|quartile|high`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
parsed.eccLevel = lvl;
|
|
149
|
+
i += 2;
|
|
150
|
+
}
|
|
151
|
+
else if (key === 'block-size') {
|
|
152
|
+
const bs = parseInt(args[i + 1], 10);
|
|
153
|
+
if (isNaN(bs) || bs < 2 || bs > 8) {
|
|
154
|
+
console.error(`Invalid --block-size: ${args[i + 1]}. Must be 2-8`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
parsed.blockSize = bs;
|
|
158
|
+
i += 2;
|
|
159
|
+
}
|
|
160
|
+
else if (key === 'sound') {
|
|
161
|
+
parsed.container = 'sound';
|
|
162
|
+
i++;
|
|
163
|
+
}
|
|
164
|
+
else if (key === 'image') {
|
|
165
|
+
parsed.container = 'image';
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
119
168
|
else if (key === 'debug-dir') {
|
|
120
169
|
parsed.debugDir = args[i + 1];
|
|
121
170
|
i += 2;
|
|
@@ -201,12 +250,14 @@ async function encodeCommand(args) {
|
|
|
201
250
|
safeCwd = '/';
|
|
202
251
|
}
|
|
203
252
|
const resolvedInputs = inputPaths.map((p) => resolve(safeCwd, p));
|
|
253
|
+
const containerMode = parsed.container || 'image'; // default: image (PNG)
|
|
254
|
+
const containerExt = containerMode === 'sound' ? '.wav' : '.png';
|
|
204
255
|
let outputName = inputPaths.length === 1 ? basename(firstInput) : 'archive';
|
|
205
256
|
if (inputPaths.length === 1 && !statSync(resolvedInputs[0]).isDirectory()) {
|
|
206
|
-
outputName = outputName.replace(/(\.[^.]+)?$/,
|
|
257
|
+
outputName = outputName.replace(/(\.[^.]+)?$/, containerExt);
|
|
207
258
|
}
|
|
208
259
|
else {
|
|
209
|
-
outputName +=
|
|
260
|
+
outputName += containerExt;
|
|
210
261
|
}
|
|
211
262
|
let resolvedOutput;
|
|
212
263
|
try {
|
|
@@ -241,7 +292,7 @@ async function encodeCommand(args) {
|
|
|
241
292
|
catch (e) {
|
|
242
293
|
anyInputDir = false;
|
|
243
294
|
}
|
|
244
|
-
if (isRustBinaryAvailable() && !parsed.forceTs) {
|
|
295
|
+
if (isRustBinaryAvailable() && !parsed.forceTs && containerMode !== 'sound') {
|
|
245
296
|
try {
|
|
246
297
|
console.log(`Encoding to ${resolvedOutput} (Using native Rust encoder)\n`);
|
|
247
298
|
const startTime = Date.now();
|
|
@@ -258,7 +309,7 @@ async function encodeCommand(args) {
|
|
|
258
309
|
}, 500);
|
|
259
310
|
const encryptType = parsed.encrypt === 'xor' ? 'xor' : 'aes';
|
|
260
311
|
const fileName = basename(inputPaths[0]);
|
|
261
|
-
await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput,
|
|
312
|
+
await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput, 19, parsed.passphrase, encryptType, fileName);
|
|
262
313
|
clearInterval(progressInterval);
|
|
263
314
|
const encodeTime = Date.now() - startTime;
|
|
264
315
|
encodeBar.update(100, {
|
|
@@ -336,8 +387,9 @@ async function encodeCommand(args) {
|
|
|
336
387
|
mode,
|
|
337
388
|
name: parsed.outputName || 'archive',
|
|
338
389
|
skipOptimization: false,
|
|
339
|
-
compressionLevel:
|
|
390
|
+
compressionLevel: 19,
|
|
340
391
|
outputFormat: 'auto',
|
|
392
|
+
container: containerMode,
|
|
341
393
|
});
|
|
342
394
|
if (parsed.verbose)
|
|
343
395
|
options.verbose = true;
|
|
@@ -347,7 +399,14 @@ async function encodeCommand(args) {
|
|
|
347
399
|
options.passphrase = parsed.passphrase;
|
|
348
400
|
options.encrypt = parsed.encrypt || 'aes';
|
|
349
401
|
}
|
|
350
|
-
|
|
402
|
+
if (parsed.lossyResilient) {
|
|
403
|
+
options.lossyResilient = true;
|
|
404
|
+
if (parsed.eccLevel)
|
|
405
|
+
options.eccLevel = parsed.eccLevel;
|
|
406
|
+
if (parsed.blockSize)
|
|
407
|
+
options.robustBlockSize = parsed.blockSize;
|
|
408
|
+
}
|
|
409
|
+
console.log(`Encoding to ${resolvedOutput} (Mode: ${mode}, Container: ${containerMode === 'sound' ? 'WAV' : 'PNG'})\n`);
|
|
351
410
|
let inputData;
|
|
352
411
|
let inputSizeVal = 0;
|
|
353
412
|
let displayName;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export * from './utils/audio.js';
|
|
1
2
|
export * from './utils/constants.js';
|
|
2
3
|
export * from './utils/crc.js';
|
|
3
4
|
export * from './utils/decoder.js';
|
|
5
|
+
export * from './utils/ecc.js';
|
|
4
6
|
export * from './utils/encoder.js';
|
|
5
7
|
export * from './utils/errors.js';
|
|
6
8
|
export * from './utils/helpers.js';
|
|
@@ -8,7 +10,9 @@ export * from './utils/inspection.js';
|
|
|
8
10
|
export { native } from './utils/native.js';
|
|
9
11
|
export * from './utils/optimization.js';
|
|
10
12
|
export * from './utils/reconstitution.js';
|
|
11
|
-
export
|
|
13
|
+
export * from './utils/robust-audio.js';
|
|
14
|
+
export * from './utils/robust-image.js';
|
|
15
|
+
export { encodeWithRustCLI, isRustBinaryAvailable } from './utils/rust-cli-wrapper.js';
|
|
12
16
|
export * from './utils/types.js';
|
|
13
17
|
export * from './utils/zstd.js';
|
|
14
18
|
export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export * from './utils/audio.js';
|
|
1
2
|
export * from './utils/constants.js';
|
|
2
3
|
export * from './utils/crc.js';
|
|
3
4
|
export * from './utils/decoder.js';
|
|
5
|
+
export * from './utils/ecc.js';
|
|
4
6
|
export * from './utils/encoder.js';
|
|
5
7
|
export * from './utils/errors.js';
|
|
6
8
|
export * from './utils/helpers.js';
|
|
@@ -8,7 +10,9 @@ export * from './utils/inspection.js';
|
|
|
8
10
|
export { native } from './utils/native.js';
|
|
9
11
|
export * from './utils/optimization.js';
|
|
10
12
|
export * from './utils/reconstitution.js';
|
|
11
|
-
export
|
|
13
|
+
export * from './utils/robust-audio.js';
|
|
14
|
+
export * from './utils/robust-image.js';
|
|
15
|
+
export { encodeWithRustCLI, isRustBinaryAvailable } from './utils/rust-cli-wrapper.js';
|
|
12
16
|
export * from './utils/types.js';
|
|
13
17
|
export * from './utils/zstd.js';
|
|
14
18
|
export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAV container for binary data.
|
|
3
|
+
*
|
|
4
|
+
* Encodes raw bytes as 8-bit unsigned PCM mono samples (44100 Hz).
|
|
5
|
+
* Header is exactly 44 bytes. Total container overhead: 44 bytes (constant).
|
|
6
|
+
*
|
|
7
|
+
* Compared to PNG (stored deflate): PNG overhead grows with data size
|
|
8
|
+
* (zlib framing, filter bytes, chunk CRCs). WAV overhead is constant.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Pack raw bytes into a WAV file (8-bit PCM, mono, 44100 Hz).
|
|
12
|
+
* The bytes are stored directly as unsigned PCM samples.
|
|
13
|
+
*/
|
|
14
|
+
export declare function bytesToWav(data: Buffer): Buffer;
|
|
15
|
+
/**
|
|
16
|
+
* Extract raw bytes from a WAV file.
|
|
17
|
+
* Returns the PCM data (the original bytes).
|
|
18
|
+
*/
|
|
19
|
+
export declare function wavToBytes(wav: Buffer): Buffer;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a buffer starts with a RIFF/WAVE header.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isWav(buf: Buffer): boolean;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAV container for binary data.
|
|
3
|
+
*
|
|
4
|
+
* Encodes raw bytes as 8-bit unsigned PCM mono samples (44100 Hz).
|
|
5
|
+
* Header is exactly 44 bytes. Total container overhead: 44 bytes (constant).
|
|
6
|
+
*
|
|
7
|
+
* Compared to PNG (stored deflate): PNG overhead grows with data size
|
|
8
|
+
* (zlib framing, filter bytes, chunk CRCs). WAV overhead is constant.
|
|
9
|
+
*/
|
|
10
|
+
const WAV_HEADER_SIZE = 44;
|
|
11
|
+
const SAMPLE_RATE = 44100;
|
|
12
|
+
const BITS_PER_SAMPLE = 8;
|
|
13
|
+
const NUM_CHANNELS = 1;
|
|
14
|
+
/**
|
|
15
|
+
* Pack raw bytes into a WAV file (8-bit PCM, mono, 44100 Hz).
|
|
16
|
+
* The bytes are stored directly as unsigned PCM samples.
|
|
17
|
+
*/
|
|
18
|
+
export function bytesToWav(data) {
|
|
19
|
+
const dataSize = data.length;
|
|
20
|
+
const fileSize = WAV_HEADER_SIZE - 8 + dataSize;
|
|
21
|
+
const byteRate = SAMPLE_RATE * NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
|
|
22
|
+
const blockAlign = NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
|
|
23
|
+
const wav = Buffer.alloc(WAV_HEADER_SIZE + dataSize);
|
|
24
|
+
let offset = 0;
|
|
25
|
+
// RIFF header
|
|
26
|
+
wav.write('RIFF', offset, 'ascii');
|
|
27
|
+
offset += 4;
|
|
28
|
+
wav.writeUInt32LE(fileSize, offset);
|
|
29
|
+
offset += 4;
|
|
30
|
+
wav.write('WAVE', offset, 'ascii');
|
|
31
|
+
offset += 4;
|
|
32
|
+
// fmt sub-chunk
|
|
33
|
+
wav.write('fmt ', offset, 'ascii');
|
|
34
|
+
offset += 4;
|
|
35
|
+
wav.writeUInt32LE(16, offset);
|
|
36
|
+
offset += 4; // sub-chunk size (PCM = 16)
|
|
37
|
+
wav.writeUInt16LE(1, offset);
|
|
38
|
+
offset += 2; // audio format (1 = PCM)
|
|
39
|
+
wav.writeUInt16LE(NUM_CHANNELS, offset);
|
|
40
|
+
offset += 2;
|
|
41
|
+
wav.writeUInt32LE(SAMPLE_RATE, offset);
|
|
42
|
+
offset += 4;
|
|
43
|
+
wav.writeUInt32LE(byteRate, offset);
|
|
44
|
+
offset += 4;
|
|
45
|
+
wav.writeUInt16LE(blockAlign, offset);
|
|
46
|
+
offset += 2;
|
|
47
|
+
wav.writeUInt16LE(BITS_PER_SAMPLE, offset);
|
|
48
|
+
offset += 2;
|
|
49
|
+
// data sub-chunk
|
|
50
|
+
wav.write('data', offset, 'ascii');
|
|
51
|
+
offset += 4;
|
|
52
|
+
wav.writeUInt32LE(dataSize, offset);
|
|
53
|
+
offset += 4;
|
|
54
|
+
data.copy(wav, offset);
|
|
55
|
+
return wav;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract raw bytes from a WAV file.
|
|
59
|
+
* Returns the PCM data (the original bytes).
|
|
60
|
+
*/
|
|
61
|
+
export function wavToBytes(wav) {
|
|
62
|
+
if (wav.length < WAV_HEADER_SIZE) {
|
|
63
|
+
throw new Error('WAV data too short');
|
|
64
|
+
}
|
|
65
|
+
if (wav.toString('ascii', 0, 4) !== 'RIFF') {
|
|
66
|
+
throw new Error('Not a RIFF file');
|
|
67
|
+
}
|
|
68
|
+
if (wav.toString('ascii', 8, 12) !== 'WAVE') {
|
|
69
|
+
throw new Error('Not a WAVE file');
|
|
70
|
+
}
|
|
71
|
+
// Find the "data" sub-chunk
|
|
72
|
+
let offset = 12;
|
|
73
|
+
while (offset + 8 <= wav.length) {
|
|
74
|
+
const chunkId = wav.toString('ascii', offset, offset + 4);
|
|
75
|
+
const chunkSize = wav.readUInt32LE(offset + 4);
|
|
76
|
+
if (chunkId === 'data') {
|
|
77
|
+
const dataStart = offset + 8;
|
|
78
|
+
const dataEnd = dataStart + chunkSize;
|
|
79
|
+
if (dataEnd > wav.length) {
|
|
80
|
+
return wav.subarray(dataStart);
|
|
81
|
+
}
|
|
82
|
+
return wav.subarray(dataStart, dataEnd);
|
|
83
|
+
}
|
|
84
|
+
offset += 8 + chunkSize;
|
|
85
|
+
if (chunkSize % 2 !== 0)
|
|
86
|
+
offset += 1; // RIFF word alignment
|
|
87
|
+
}
|
|
88
|
+
throw new Error('data chunk not found in WAV');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if a buffer starts with a RIFF/WAVE header.
|
|
92
|
+
*/
|
|
93
|
+
export function isWav(buf) {
|
|
94
|
+
return (buf.length >= 12 &&
|
|
95
|
+
buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && // RIFF
|
|
96
|
+
buf[8] === 0x57 && buf[9] === 0x41 && buf[10] === 0x56 && buf[11] === 0x45 // WAVE
|
|
97
|
+
);
|
|
98
|
+
}
|