roxify 1.9.8 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +14 -2
- package/LICENSE +63 -21
- package/README.md +178 -163
- package/dist/cli.js +8 -1
- package/dist/utils/constants.d.ts +5 -0
- package/dist/utils/constants.js +1 -0
- package/dist/utils/decoder.js +12 -3
- package/dist/utils/encoder.js +24 -11
- package/dist/utils/rust-cli-wrapper.js +6 -3
- package/dist/utils/types.d.ts +1 -1
- package/libroxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/native/bench_hybrid.rs +145 -0
- package/native/bwt.rs +25 -69
- package/native/hybrid.rs +92 -70
- package/native/lib.rs +6 -3
- package/native/mtf.rs +106 -0
- package/native/rans_byte.rs +190 -0
- package/native/test_small_bwt.rs +31 -0
- package/native/test_stages.rs +70 -0
- package/package.json +5 -2
- package/dist/roxify_native.exe +0 -0
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Encode binary data into PNG images and decode them back, losslessly. Roxify combines native Rust acceleration, multi-threaded Zstd compression, and AES-256-GCM encryption into a single, portable Node.js module.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/roxify)
|
|
6
|
-
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -39,6 +39,7 @@ The core compression and image-processing logic is written in Rust and exposed t
|
|
|
39
39
|
## Features
|
|
40
40
|
|
|
41
41
|
- **Native Rust acceleration** via N-API with automatic fallback to pure JavaScript
|
|
42
|
+
- **BWT-ANS compression** -- Burrows-Wheeler Transform + Move-to-Front + RLE + rANS entropy coding via libsais O(n) SA-IS (18.1 MB/s encode, 31.2 MB/s decode)
|
|
42
43
|
- **Multi-threaded Zstd compression** (level 19) with parallel chunk processing via Rayon
|
|
43
44
|
- **AES-256-GCM encryption** with PBKDF2 key derivation (100,000 iterations)
|
|
44
45
|
- **Lossless roundtrip** -- encoded data is recovered byte-for-byte
|
|
@@ -58,36 +59,50 @@ The core compression and image-processing logic is written in Rust and exposed t
|
|
|
58
59
|
|
|
59
60
|
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.
|
|
60
61
|
|
|
62
|
+
### BWT-ANS Native Compression (Rust, via N-API)
|
|
63
|
+
|
|
64
|
+
Direct API calls through the native module (BWT → MTF → RLE0 → rANS, 1 MB blocks):
|
|
65
|
+
|
|
66
|
+
| Dataset | Original | Compressed | Ratio | Encode | Decode | Enc Throughput | Dec Throughput |
|
|
67
|
+
| ------- | -------- | ---------- | ----- | ------ | ------ | -------------- | -------------- |
|
|
68
|
+
| Repetitive text 45 KB | 45 KB | 207 B | 0.5% | < 1 ms | < 1 ms | — | — |
|
|
69
|
+
| Rust source 6 KB | 6 KB | 2.0 KB | 33.7% | < 1 ms | < 1 ms | — | — |
|
|
70
|
+
| node_modules tar 175 MB | 175.5 MB | 38.4 MB | 21.9% | 9.68 s | 5.62 s | 18.1 MB/s | 31.2 MB/s |
|
|
71
|
+
| Random 100 KB | 100 KB | 101 KB | 101.5% | < 1 ms | < 1 ms | — | — |
|
|
72
|
+
| Zeros 100 KB | 100 KB | 51 B | 0.1% | < 1 ms | < 1 ms | — | — |
|
|
73
|
+
|
|
74
|
+
> BWT-ANS achieves **21.9% on real-world node_modules** (175 MB), with **18 MB/s encode** and **31 MB/s decode** throughput. 100% lossless roundtrip verified on all datasets.
|
|
75
|
+
|
|
61
76
|
### Compression Ratio (Maximum Compression for All Tools)
|
|
62
77
|
|
|
63
|
-
| Dataset
|
|
64
|
-
|
|
65
|
-
| Text 1 MB
|
|
66
|
-
| JSON 1 MB
|
|
67
|
-
| Binary 1 MB | 1.00 MB
|
|
68
|
-
| Mixed 5 MB
|
|
69
|
-
| Text 10 MB
|
|
70
|
-
| 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%)
|
|
78
|
+
| Dataset | Original | zip -9 | gzip -9 | 7z LZMA2 -9 | Roxify PNG | Roxify WAV |
|
|
79
|
+
| ----------- | -------- | --------------- | --------------- | --------------- | ------------------- | ------------------- |
|
|
80
|
+
| 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%)** |
|
|
81
|
+
| 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%)** |
|
|
82
|
+
| 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%) |
|
|
83
|
+
| 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%) |
|
|
84
|
+
| 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%)** |
|
|
85
|
+
| 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%) |
|
|
71
86
|
|
|
72
87
|
> **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.
|
|
73
88
|
|
|
74
89
|
### Encode and Decode Speed (CLI)
|
|
75
90
|
|
|
76
|
-
| Dataset
|
|
77
|
-
|
|
78
|
-
| Text 1 MB
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
| JSON 1 MB
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
| Text 10 MB | zip -9
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
+
| Dataset | Tool | Encode | Decode | Enc Throughput | Dec Throughput |
|
|
92
|
+
| ---------- | -------------- | ---------- | ---------- | -------------- | -------------- |
|
|
93
|
+
| Text 1 MB | zip -9 | 112 ms | 36 ms | 8.9 MB/s | 27.6 MB/s |
|
|
94
|
+
| | gzip -9 | 146 ms | 38 ms | 6.9 MB/s | 26.0 MB/s |
|
|
95
|
+
| | 7z LZMA -9 | 303 ms | 21 ms | 3.3 MB/s | 46.6 MB/s |
|
|
96
|
+
| | **Roxify PNG** | **859 ms** | **577 ms** | **1.2 MB/s** | **1.7 MB/s** |
|
|
97
|
+
| | **Roxify WAV** | **794 ms** | **480 ms** | **1.3 MB/s** | **2.1 MB/s** |
|
|
98
|
+
| JSON 1 MB | zip -9 | 79 ms | 20 ms | 12.7 MB/s | 50.5 MB/s |
|
|
99
|
+
| | 7z LZMA -9 | 197 ms | 26 ms | 5.1 MB/s | 37.9 MB/s |
|
|
100
|
+
| | **Roxify PNG** | **1.14 s** | **755 ms** | **0.9 MB/s** | **1.3 MB/s** |
|
|
101
|
+
| | **Roxify WAV** | **1.49 s** | **518 ms** | **0.7 MB/s** | **1.9 MB/s** |
|
|
102
|
+
| Text 10 MB | zip -9 | 1.21 s | 70 ms | 8.2 MB/s | 143.8 MB/s |
|
|
103
|
+
| | 7z LZMA -9 | 5.05 s | 99 ms | 2.0 MB/s | 100.8 MB/s |
|
|
104
|
+
| | **Roxify PNG** | **9.05 s** | **4.53 s** | **1.1 MB/s** | **2.2 MB/s** |
|
|
105
|
+
| | **Roxify WAV** | **9.22 s** | **2.59 s** | **1.1 MB/s** | **3.9 MB/s** |
|
|
91
106
|
|
|
92
107
|
> 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.
|
|
93
108
|
|
|
@@ -95,69 +110,69 @@ All measurements were taken on Linux x64 (Intel i7-6700K @ 4.0 GHz, 32 GB RAM) w
|
|
|
95
110
|
|
|
96
111
|
Direct API calls (no CLI startup overhead):
|
|
97
112
|
|
|
98
|
-
| Size
|
|
99
|
-
|
|
100
|
-
| 1 KB
|
|
101
|
-
| 10 KB
|
|
102
|
-
| 100 KB | PNG
|
|
103
|
-
| 500 KB | PNG
|
|
104
|
-
| 1 MB
|
|
105
|
-
| 5 MB
|
|
106
|
-
| 10 MB
|
|
107
|
-
| 1 KB
|
|
108
|
-
| 10 KB
|
|
109
|
-
| 100 KB | WAV
|
|
110
|
-
| 500 KB | WAV
|
|
111
|
-
| 1 MB
|
|
112
|
-
| 5 MB
|
|
113
|
-
| 10 MB
|
|
113
|
+
| Size | Container | Encode | Decode | Enc Throughput | Dec Throughput | Output | Ratio | Integrity |
|
|
114
|
+
| ------ | --------- | ------ | ------- | -------------- | -------------- | --------- | ------ | --------- |
|
|
115
|
+
| 1 KB | PNG | 9 ms | 12 ms | 0.1 MB/s | 0.1 MB/s | 1.14 KB | 114.3% | ✓ |
|
|
116
|
+
| 10 KB | PNG | 18 ms | 34 ms | 0.5 MB/s | 0.3 MB/s | 10.32 KB | 103.2% | ✓ |
|
|
117
|
+
| 100 KB | PNG | 52 ms | 109 ms | 1.9 MB/s | 0.9 MB/s | 100.52 KB | 100.5% | ✓ |
|
|
118
|
+
| 500 KB | PNG | 339 ms | 541 ms | 1.4 MB/s | 0.9 MB/s | 502.64 KB | 100.5% | ✓ |
|
|
119
|
+
| 1 MB | PNG | 875 ms | 1.24 s | 1.1 MB/s | 0.8 MB/s | 1.00 MB | 100.3% | ✓ |
|
|
120
|
+
| 5 MB | PNG | 3.39 s | 4.12 s | 1.5 MB/s | 1.2 MB/s | 5.01 MB | 100.2% | ✓ |
|
|
121
|
+
| 10 MB | PNG | 6.84 s | 12.28 s | 1.5 MB/s | 0.8 MB/s | 10.01 MB | 100.1% | ✓ |
|
|
122
|
+
| 1 KB | WAV | 2 ms | 2 ms | 0.6 MB/s | 0.6 MB/s | 1.08 KB | 107.5% | ✓ |
|
|
123
|
+
| 10 KB | WAV | 4 ms | 5 ms | 2.3 MB/s | 1.8 MB/s | 10.08 KB | 100.8% | ✓ |
|
|
124
|
+
| 100 KB | WAV | 39 ms | 28 ms | 2.5 MB/s | 3.5 MB/s | 100.08 KB | 100.1% | ✓ |
|
|
125
|
+
| 500 KB | WAV | 172 ms | 190 ms | 2.8 MB/s | 2.6 MB/s | 500.09 KB | 100.0% | ✓ |
|
|
126
|
+
| 1 MB | WAV | 452 ms | 276 ms | 2.2 MB/s | 3.6 MB/s | 1.00 MB | 100.0% | ✓ |
|
|
127
|
+
| 5 MB | WAV | 2.70 s | 1.65 s | 1.8 MB/s | 3.0 MB/s | 5.00 MB | 100.0% | ✓ |
|
|
128
|
+
| 10 MB | WAV | 4.81 s | 2.56 s | 2.1 MB/s | 3.9 MB/s | 10.00 MB | 100.0% | ✓ |
|
|
114
129
|
|
|
115
130
|
> WAV container is **2–4× faster** than PNG for decoding at large sizes, and produces slightly smaller output thanks to simpler framing.
|
|
116
131
|
|
|
117
132
|
### Reed-Solomon ECC Throughput
|
|
118
133
|
|
|
119
|
-
| Size
|
|
120
|
-
|
|
121
|
-
| 1 KB
|
|
122
|
-
| 10 KB
|
|
123
|
-
| 100 KB | 49 ms
|
|
124
|
-
| 1 MB
|
|
134
|
+
| Size | Encode | Decode | Enc Throughput | Dec Throughput | Overhead |
|
|
135
|
+
| ------ | ------ | ------ | -------------- | -------------- | -------- |
|
|
136
|
+
| 1 KB | 6 ms | 4 ms | 0.2 MB/s | 0.2 MB/s | 125.7% |
|
|
137
|
+
| 10 KB | 7 ms | 6 ms | 1.3 MB/s | 1.5 MB/s | 119.6% |
|
|
138
|
+
| 100 KB | 49 ms | 45 ms | 2.0 MB/s | 2.1 MB/s | 118.8% |
|
|
139
|
+
| 1 MB | 483 ms | 377 ms | 2.1 MB/s | 2.7 MB/s | 118.6% |
|
|
125
140
|
|
|
126
141
|
### Lossy-Resilient Encoding
|
|
127
142
|
|
|
128
143
|
#### Robust Image (QR-code-style, block size 4×4)
|
|
129
144
|
|
|
130
145
|
| Data Size | Encode Time | Output (PNG) |
|
|
131
|
-
|
|
132
|
-
| 32 B
|
|
133
|
-
| 128 B
|
|
134
|
-
| 512 B
|
|
135
|
-
| 1 KB
|
|
136
|
-
| 2 KB
|
|
146
|
+
| --------- | ----------- | ------------ |
|
|
147
|
+
| 32 B | 32 ms | 122 KB |
|
|
148
|
+
| 128 B | 39 ms | 122 KB |
|
|
149
|
+
| 512 B | 76 ms | 316 KB |
|
|
150
|
+
| 1 KB | 139 ms | 508 KB |
|
|
151
|
+
| 2 KB | 251 ms | 986 KB |
|
|
137
152
|
|
|
138
153
|
#### Robust Audio (MFSK 8-channel, medium ECC)
|
|
139
154
|
|
|
140
155
|
| Data Size | Encode | Decode | Output (WAV) | Integrity |
|
|
141
|
-
|
|
142
|
-
| 10 B
|
|
143
|
-
| 32 B
|
|
144
|
-
| 64 B
|
|
145
|
-
| 128 B
|
|
146
|
-
| 256 B
|
|
156
|
+
| --------- | ------ | ------ | ------------ | --------- |
|
|
157
|
+
| 10 B | 33 ms | 44 ms | 1.35 MB | ✓ |
|
|
158
|
+
| 32 B | 19 ms | 31 ms | 1.35 MB | ✓ |
|
|
159
|
+
| 64 B | 22 ms | 24 ms | 1.35 MB | ✓ |
|
|
160
|
+
| 128 B | 21 ms | 28 ms | 1.35 MB | ✓ |
|
|
161
|
+
| 256 B | 40 ms | 45 ms | 2.59 MB | ✓ |
|
|
147
162
|
|
|
148
163
|
### Data Integrity Verification
|
|
149
164
|
|
|
150
165
|
All encode/decode roundtrips produce bit-exact output, verified by SHA-256:
|
|
151
166
|
|
|
152
|
-
| Test Case
|
|
153
|
-
|
|
154
|
-
| Empty buffer (0 B)
|
|
155
|
-
| Single byte (1 B)
|
|
156
|
-
| All byte values (256 B) | ✓
|
|
157
|
-
| 1 KB text
|
|
158
|
-
| 100 KB random
|
|
159
|
-
| 1 MB random
|
|
160
|
-
| 5 MB random
|
|
167
|
+
| Test Case | PNG | WAV |
|
|
168
|
+
| ----------------------- | --- | --- |
|
|
169
|
+
| Empty buffer (0 B) | ✓ | ✓ |
|
|
170
|
+
| Single byte (1 B) | ✓ | ✓ |
|
|
171
|
+
| All byte values (256 B) | ✓ | ✓ |
|
|
172
|
+
| 1 KB text | ✓ | ✓ |
|
|
173
|
+
| 100 KB random | ✓ | ✓ |
|
|
174
|
+
| 1 MB random | ✓ | ✓ |
|
|
175
|
+
| 5 MB random | ✓ | ✓ |
|
|
161
176
|
|
|
162
177
|
**14 / 14 integrity tests passed** across both containers.
|
|
163
178
|
|
|
@@ -175,12 +190,12 @@ All encode/decode roundtrips produce bit-exact output, verified by SHA-256:
|
|
|
175
190
|
|
|
176
191
|
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:
|
|
177
192
|
|
|
178
|
-
| Tool
|
|
179
|
-
|
|
180
|
-
| zip
|
|
181
|
-
| tar/gzip | `tar -cf - \| gzip -9`
|
|
182
|
-
| 7z
|
|
183
|
-
| Roxify
|
|
193
|
+
| Tool | Command / Setting |
|
|
194
|
+
| -------- | --------------------------- |
|
|
195
|
+
| zip | `zip -r -q -9` |
|
|
196
|
+
| tar/gzip | `tar -cf - \| gzip -9` |
|
|
197
|
+
| 7z | `7z a -mx=9` (LZMA2 ultra) |
|
|
198
|
+
| Roxify | Zstd level 19, compact mode |
|
|
184
199
|
|
|
185
200
|
To reproduce:
|
|
186
201
|
|
|
@@ -222,14 +237,14 @@ rox encode input.zip output.png
|
|
|
222
237
|
rox encode <input> [output] [options]
|
|
223
238
|
```
|
|
224
239
|
|
|
225
|
-
| Option
|
|
226
|
-
|
|
227
|
-
| `-p, --passphrase <pass>` | Encrypt with AES-256-GCM
|
|
228
|
-
| `-m, --mode <mode>`
|
|
229
|
-
| `-q, --quality <0-11>`
|
|
230
|
-
| `-e, --encrypt <type>`
|
|
231
|
-
| `--no-compress`
|
|
232
|
-
| `-o, --output <path>`
|
|
240
|
+
| Option | Description | Default |
|
|
241
|
+
| ------------------------- | ----------------------------------------------- | -------------------------- |
|
|
242
|
+
| `-p, --passphrase <pass>` | Encrypt with AES-256-GCM | none |
|
|
243
|
+
| `-m, --mode <mode>` | Encoding mode: `screenshot`, `compact` | `screenshot` |
|
|
244
|
+
| `-q, --quality <0-11>` | Compression effort (0 = fastest, 11 = smallest) | `1` |
|
|
245
|
+
| `-e, --encrypt <type>` | Encryption method: `auto`, `aes`, `xor`, `none` | `aes` if passphrase is set |
|
|
246
|
+
| `--no-compress` | Disable compression entirely | false |
|
|
247
|
+
| `-o, --output <path>` | Explicit output file path | auto-generated |
|
|
233
248
|
|
|
234
249
|
### Decoding
|
|
235
250
|
|
|
@@ -237,11 +252,11 @@ rox encode <input> [output] [options]
|
|
|
237
252
|
rox decode <input> [output] [options]
|
|
238
253
|
```
|
|
239
254
|
|
|
240
|
-
| Option
|
|
241
|
-
|
|
242
|
-
| `-p, --passphrase <pass>` | Decryption passphrase
|
|
243
|
-
| `-o, --output <path>`
|
|
244
|
-
| `--dict <file>`
|
|
255
|
+
| Option | Description | Default |
|
|
256
|
+
| ------------------------- | ------------------------------------------ | --------------------------- |
|
|
257
|
+
| `-p, --passphrase <pass>` | Decryption passphrase | none |
|
|
258
|
+
| `-o, --output <path>` | Output file path | auto-detected from metadata |
|
|
259
|
+
| `--dict <file>` | Zstd dictionary for improved decompression | none |
|
|
245
260
|
|
|
246
261
|
### Examples
|
|
247
262
|
|
|
@@ -334,22 +349,22 @@ const png = await encodeBinaryToPng(largeBuffer, {
|
|
|
334
349
|
|
|
335
350
|
```typescript
|
|
336
351
|
interface EncodeOptions {
|
|
337
|
-
compression?: 'zstd';
|
|
338
|
-
compressionLevel?: number;
|
|
339
|
-
passphrase?: string;
|
|
340
|
-
dict?: Buffer;
|
|
341
|
-
name?: string;
|
|
342
|
-
mode?: 'screenshot';
|
|
352
|
+
compression?: 'zstd'; // Compression algorithm
|
|
353
|
+
compressionLevel?: number; // Zstd compression level (0-19)
|
|
354
|
+
passphrase?: string; // Encryption passphrase
|
|
355
|
+
dict?: Buffer; // Zstd dictionary for improved ratios
|
|
356
|
+
name?: string; // Original filename stored in metadata
|
|
357
|
+
mode?: 'screenshot'; // Encoding mode
|
|
343
358
|
encrypt?: 'auto' | 'aes' | 'xor' | 'none';
|
|
344
359
|
output?: 'auto' | 'png' | 'rox'; // Output format
|
|
345
|
-
includeName?: boolean;
|
|
346
|
-
includeFileList?: boolean;
|
|
360
|
+
includeName?: boolean; // Include filename in PNG metadata
|
|
361
|
+
includeFileList?: boolean; // Include file manifest in PNG
|
|
347
362
|
fileList?: Array<string | { name: string; size: number }>;
|
|
348
|
-
skipOptimization?: boolean;
|
|
349
|
-
lossyResilient?: boolean;
|
|
350
|
-
eccLevel?: EccLevel;
|
|
351
|
-
robustBlockSize?: number;
|
|
352
|
-
container?: 'image' | 'sound';
|
|
363
|
+
skipOptimization?: boolean; // Skip PNG optimization pass
|
|
364
|
+
lossyResilient?: boolean; // Enable lossy-resilient encoding (RS ECC)
|
|
365
|
+
eccLevel?: EccLevel; // 'low' | 'medium' | 'quartile' | 'high'
|
|
366
|
+
robustBlockSize?: number; // 2–8 pixels per data block (lossy image)
|
|
367
|
+
container?: 'image' | 'sound'; // Output container format
|
|
353
368
|
onProgress?: (info: ProgressInfo) => void;
|
|
354
369
|
showProgress?: boolean;
|
|
355
370
|
verbose?: boolean;
|
|
@@ -360,9 +375,9 @@ interface EncodeOptions {
|
|
|
360
375
|
|
|
361
376
|
```typescript
|
|
362
377
|
interface DecodeOptions {
|
|
363
|
-
passphrase?: string;
|
|
364
|
-
outPath?: string;
|
|
365
|
-
files?: string[];
|
|
378
|
+
passphrase?: string; // Decryption passphrase
|
|
379
|
+
outPath?: string; // Output directory for unpacked files
|
|
380
|
+
files?: string[]; // Extract only specific files from archive
|
|
366
381
|
onProgress?: (info: ProgressInfo) => void;
|
|
367
382
|
showProgress?: boolean;
|
|
368
383
|
verbose?: boolean;
|
|
@@ -373,10 +388,10 @@ interface DecodeOptions {
|
|
|
373
388
|
|
|
374
389
|
```typescript
|
|
375
390
|
interface DecodeResult {
|
|
376
|
-
buf?: Buffer;
|
|
377
|
-
meta?: { name?: string };
|
|
378
|
-
files?: PackedFile[];
|
|
379
|
-
correctedErrors?: number;
|
|
391
|
+
buf?: Buffer; // Decoded binary payload
|
|
392
|
+
meta?: { name?: string }; // Metadata (original filename)
|
|
393
|
+
files?: PackedFile[]; // Unpacked directory entries, if applicable
|
|
394
|
+
correctedErrors?: number; // RS errors corrected (lossy-resilient mode)
|
|
380
395
|
}
|
|
381
396
|
```
|
|
382
397
|
|
|
@@ -384,10 +399,10 @@ interface DecodeResult {
|
|
|
384
399
|
|
|
385
400
|
## Encoding Modes
|
|
386
401
|
|
|
387
|
-
| Mode
|
|
388
|
-
|
|
402
|
+
| Mode | Description | Use Case |
|
|
403
|
+
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
389
404
|
| `screenshot` | Encodes data as RGB pixels in a standard PNG. The image looks like a gradient or noise pattern and survives re-uploads and social media processing. | Sharing on image-only platforms, bypassing file-type filters |
|
|
390
|
-
| `compact`
|
|
405
|
+
| `compact` | Minimal 1x1 PNG with data embedded in a custom ancillary chunk (`rXDT`). Produces the smallest possible output. | Programmatic use, archival, maximum compression ratio |
|
|
391
406
|
|
|
392
407
|
### Stretch-Resilient Decoding
|
|
393
408
|
|
|
@@ -410,10 +425,10 @@ rox decode zoomed-screenshot.png -o output/
|
|
|
410
425
|
|
|
411
426
|
Roxify supports two encryption methods:
|
|
412
427
|
|
|
413
|
-
| Method | Algorithm
|
|
414
|
-
|
|
415
|
-
| `aes`
|
|
416
|
-
| `xor`
|
|
428
|
+
| Method | Algorithm | Strength | Use Case |
|
|
429
|
+
| ------ | -------------------------------------------- | ---------------------------------------------- | -------------------------------------- |
|
|
430
|
+
| `aes` | AES-256-GCM with PBKDF2 (100,000 iterations) | Cryptographically secure, authenticated | Sensitive data, confidential documents |
|
|
431
|
+
| `xor` | XOR cipher with passphrase-derived key | Obfuscation only, not cryptographically secure | Casual deterrent against inspection |
|
|
417
432
|
|
|
418
433
|
When `encrypt` is set to `auto` (the default when a passphrase is provided), AES is selected.
|
|
419
434
|
|
|
@@ -432,12 +447,12 @@ Enable `lossyResilient: true` to produce output that survives lossy compression.
|
|
|
432
447
|
|
|
433
448
|
### Error Correction Levels
|
|
434
449
|
|
|
435
|
-
| Level
|
|
436
|
-
|
|
437
|
-
| `low`
|
|
438
|
-
| `medium`
|
|
439
|
-
| `quartile` |
|
|
440
|
-
| `high`
|
|
450
|
+
| Level | Parity Symbols | Overhead | Correctable Errors |
|
|
451
|
+
| ---------- | -------------: | -------: | -----------------: |
|
|
452
|
+
| `low` | 20 / block | ~10% | ~4% |
|
|
453
|
+
| `medium` | 40 / block | ~19% | ~9% |
|
|
454
|
+
| `quartile` | 64 / block | ~33% | ~15% |
|
|
455
|
+
| `high` | 128 / block | ~100% | ~25% |
|
|
441
456
|
|
|
442
457
|
### Example
|
|
443
458
|
|
|
@@ -446,7 +461,7 @@ Enable `lossyResilient: true` to produce output that survives lossy compression.
|
|
|
446
461
|
const png = await encodeBinaryToPng(data, {
|
|
447
462
|
lossyResilient: true,
|
|
448
463
|
eccLevel: 'quartile',
|
|
449
|
-
robustBlockSize: 4,
|
|
464
|
+
robustBlockSize: 4, // 4×4 pixels per data bit
|
|
450
465
|
});
|
|
451
466
|
|
|
452
467
|
// Audio that survives MP3 compression
|
|
@@ -498,12 +513,12 @@ const wav = await encodeBinaryToPng(data, {
|
|
|
498
513
|
|
|
499
514
|
The `compressionLevel` option (CLI: `-q`) controls the trade-off between speed and output size:
|
|
500
515
|
|
|
501
|
-
| Level | Speed
|
|
502
|
-
|
|
503
|
-
| 0
|
|
504
|
-
| 1
|
|
505
|
-
| 5
|
|
506
|
-
| 11
|
|
516
|
+
| Level | Speed | Ratio | Recommendation |
|
|
517
|
+
| ----- | -------- | -------- | ----------------------------------------- |
|
|
518
|
+
| 0 | Fastest | Largest | Files over 100 MB, real-time workflows |
|
|
519
|
+
| 1 | Fast | Good | Default; general-purpose use |
|
|
520
|
+
| 5 | Moderate | Better | Archival of medium-sized datasets |
|
|
521
|
+
| 11 | Slowest | Smallest | Small files under 1 MB, long-term storage |
|
|
507
522
|
|
|
508
523
|
### Native Module
|
|
509
524
|
|
|
@@ -533,12 +548,12 @@ const png = await encodeBinaryToPng(data, { dict });
|
|
|
533
548
|
|
|
534
549
|
Roxify ships prebuilt native modules for the following targets:
|
|
535
550
|
|
|
536
|
-
| Platform | Architecture
|
|
537
|
-
|
|
538
|
-
| Linux
|
|
539
|
-
| macOS
|
|
540
|
-
| macOS
|
|
541
|
-
| Windows
|
|
551
|
+
| Platform | Architecture | Binary Name |
|
|
552
|
+
| -------- | --------------------- | ------------------------------------------------ |
|
|
553
|
+
| Linux | x86_64 | `libroxify_native-x86_64-unknown-linux-gnu.node` |
|
|
554
|
+
| macOS | x86_64 | `libroxify_native-x86_64-apple-darwin.node` |
|
|
555
|
+
| macOS | ARM64 (Apple Silicon) | `libroxify_native-aarch64-apple-darwin.node` |
|
|
556
|
+
| Windows | x86_64 | `roxify_native-x86_64-pc-windows-msvc.node` |
|
|
542
557
|
|
|
543
558
|
The correct binary is resolved automatically at runtime. If no binary is found for the current platform, Roxify falls back silently to the pure JavaScript implementation.
|
|
544
559
|
|
|
@@ -630,35 +645,35 @@ Input --> Detect Format --> Demodulate/Read Blocks --> De-interleave --> RS ECC
|
|
|
630
645
|
|
|
631
646
|
### Rust Modules
|
|
632
647
|
|
|
633
|
-
| Module
|
|
634
|
-
|
|
635
|
-
| `core.rs`
|
|
636
|
-
| `encoder.rs`
|
|
637
|
-
| `packer.rs`
|
|
638
|
-
| `crypto.rs`
|
|
639
|
-
| `archive.rs`
|
|
640
|
-
| `reconstitution.rs` | Screenshot detection and automatic crop to recover encoded data
|
|
641
|
-
| `audio.rs`
|
|
642
|
-
| `bwt.rs`
|
|
643
|
-
| `rans.rs`
|
|
644
|
-
| `hybrid.rs`
|
|
645
|
-
| `pool.rs`
|
|
646
|
-
| `image_utils.rs`
|
|
647
|
-
| `png_utils.rs`
|
|
648
|
-
| `progress.rs`
|
|
648
|
+
| Module | Responsibility |
|
|
649
|
+
| ------------------- | ---------------------------------------------------------------------- |
|
|
650
|
+
| `core.rs` | Pixel scanning, CRC32, Adler32, delta coding, Zstd compress/decompress |
|
|
651
|
+
| `encoder.rs` | PNG payload encoding with marker pixels and metadata chunks |
|
|
652
|
+
| `packer.rs` | Directory tree serialization and streaming deserialization |
|
|
653
|
+
| `crypto.rs` | AES-256-GCM encryption and PBKDF2 key derivation |
|
|
654
|
+
| `archive.rs` | Tar-based archiving with optional Zstd compression |
|
|
655
|
+
| `reconstitution.rs` | Screenshot detection and automatic crop to recover encoded data |
|
|
656
|
+
| `audio.rs` | WAV container encoding and decoding (PCM byte packing) |
|
|
657
|
+
| `bwt.rs` | Parallel Burrows-Wheeler Transform |
|
|
658
|
+
| `rans.rs` | rANS (Asymmetric Numeral Systems) entropy coder |
|
|
659
|
+
| `hybrid.rs` | Block-based orchestration of BWT, context mixing, and rANS |
|
|
660
|
+
| `pool.rs` | Buffer pooling and zero-copy memory management |
|
|
661
|
+
| `image_utils.rs` | Image resizing, pixel format conversion, metadata extraction |
|
|
662
|
+
| `png_utils.rs` | Low-level PNG chunk read/write operations |
|
|
663
|
+
| `progress.rs` | Progress tracking for long-running compression/decompression |
|
|
649
664
|
|
|
650
665
|
### TypeScript Modules
|
|
651
666
|
|
|
652
|
-
| Module
|
|
653
|
-
|
|
654
|
-
| `ecc.ts`
|
|
667
|
+
| Module | Responsibility |
|
|
668
|
+
| ----------------- | --------------------------------------------------------------------- |
|
|
669
|
+
| `ecc.ts` | Reed-Solomon GF(256) codec, block ECC, interleaving |
|
|
655
670
|
| `robust-audio.ts` | MFSK audio modulation/demodulation, Goertzel detection, sync preamble |
|
|
656
|
-
| `robust-image.ts` | QR-code-like block encoding, finder patterns, majority voting
|
|
657
|
-
| `encoder.ts`
|
|
658
|
-
| `decoder.ts`
|
|
659
|
-
| `audio.ts`
|
|
660
|
-
| `helpers.ts`
|
|
661
|
-
| `zstd.ts`
|
|
671
|
+
| `robust-image.ts` | QR-code-like block encoding, finder patterns, majority voting |
|
|
672
|
+
| `encoder.ts` | High-level encoding orchestration (standard + lossy-resilient) |
|
|
673
|
+
| `decoder.ts` | High-level decoding with automatic format detection |
|
|
674
|
+
| `audio.ts` | Standard WAV container (8-bit PCM) |
|
|
675
|
+
| `helpers.ts` | Delta coding, XOR cipher, palette generation |
|
|
676
|
+
| `zstd.ts` | Parallel Zstd compression via native module |
|
|
662
677
|
|
|
663
678
|
---
|
|
664
679
|
|
|
@@ -684,11 +699,11 @@ try {
|
|
|
684
699
|
}
|
|
685
700
|
```
|
|
686
701
|
|
|
687
|
-
| Error
|
|
688
|
-
|
|
689
|
-
| `Incorrect passphrase`
|
|
690
|
-
| `not a valid PNG`
|
|
691
|
-
| `Passphrase required`
|
|
702
|
+
| Error | Cause |
|
|
703
|
+
| --------------------------- | ------------------------------------------------- |
|
|
704
|
+
| `Incorrect passphrase` | Wrong password provided for decryption |
|
|
705
|
+
| `not a valid PNG` | Input buffer is not a PNG or lacks Roxify markers |
|
|
706
|
+
| `Passphrase required` | File is encrypted but no passphrase was supplied |
|
|
692
707
|
| `Image too large to decode` | PNG dimensions exceed the in-process memory limit |
|
|
693
708
|
|
|
694
709
|
---
|
|
@@ -716,7 +731,7 @@ Contributions are welcome. Please open an issue to discuss proposed changes befo
|
|
|
716
731
|
|
|
717
732
|
## License
|
|
718
733
|
|
|
719
|
-
|
|
734
|
+
This project is licensed under the **Roxify Proprietary Open Source License (RPOSL)**. The source code is freely available for personal, educational, and research use. All commercial rights are exclusively reserved to the author. See [LICENSE](LICENSE) for details.
|
|
720
735
|
|
|
721
736
|
---
|
|
722
737
|
|
package/dist/cli.js
CHANGED
|
@@ -66,6 +66,7 @@ Commands:
|
|
|
66
66
|
Options:
|
|
67
67
|
--image Use PNG container (default)
|
|
68
68
|
--sound Use WAV audio container (smaller overhead, faster)
|
|
69
|
+
--bwt-ans Use BWT-ANS compression instead of Zstd
|
|
69
70
|
-p, --passphrase <pass> Use passphrase (AES-256-GCM)
|
|
70
71
|
-m, --mode <mode> Mode: screenshot (default)
|
|
71
72
|
-e, --encrypt <type> auto|aes|xor|none
|
|
@@ -135,6 +136,10 @@ function parseArgs(args) {
|
|
|
135
136
|
parsed.forceTs = true;
|
|
136
137
|
i++;
|
|
137
138
|
}
|
|
139
|
+
else if (key === 'bwt-ans') {
|
|
140
|
+
parsed.compression = 'bwt-ans';
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
138
143
|
else if (key === 'lossy-resilient') {
|
|
139
144
|
parsed.lossyResilient = true;
|
|
140
145
|
i++;
|
|
@@ -292,7 +297,7 @@ async function encodeCommand(args) {
|
|
|
292
297
|
catch (e) {
|
|
293
298
|
anyInputDir = false;
|
|
294
299
|
}
|
|
295
|
-
if (isRustBinaryAvailable() && !parsed.forceTs && containerMode !== 'sound') {
|
|
300
|
+
if (isRustBinaryAvailable() && !parsed.forceTs && containerMode !== 'sound' && parsed.compression !== 'bwt-ans') {
|
|
296
301
|
try {
|
|
297
302
|
console.log(`Encoding to ${resolvedOutput} (Using native Rust encoder)\n`);
|
|
298
303
|
const startTime = Date.now();
|
|
@@ -395,6 +400,8 @@ async function encodeCommand(args) {
|
|
|
395
400
|
options.verbose = true;
|
|
396
401
|
if (parsed.noCompress)
|
|
397
402
|
options.compression = 'none';
|
|
403
|
+
if (parsed.compression === 'bwt-ans')
|
|
404
|
+
options.compression = 'bwt-ans';
|
|
398
405
|
if (parsed.passphrase) {
|
|
399
406
|
options.passphrase = parsed.passphrase;
|
|
400
407
|
options.encrypt = parsed.encrypt || 'aes';
|
package/dist/utils/constants.js
CHANGED
|
@@ -19,6 +19,7 @@ export const MARKER_START = MARKER_COLORS;
|
|
|
19
19
|
export const MARKER_END = [...MARKER_COLORS].reverse();
|
|
20
20
|
export const COMPRESSION_MARKERS = {
|
|
21
21
|
zstd: [{ r: 0, g: 255, b: 0 }],
|
|
22
|
+
'bwt-ans': [{ r: 0, g: 128, b: 255 }],
|
|
22
23
|
};
|
|
23
24
|
export const FORMAT_MARKERS = {
|
|
24
25
|
png: { r: 0, g: 255, b: 255 },
|
package/dist/utils/decoder.js
CHANGED
|
@@ -11,7 +11,7 @@ import { native } from './native.js';
|
|
|
11
11
|
import { cropAndReconstitute } from './reconstitution.js';
|
|
12
12
|
import { decodeRobustAudio, isRobustAudioWav } from './robust-audio.js';
|
|
13
13
|
import { decodeRobustImage, isRobustImage } from './robust-image.js';
|
|
14
|
-
import { parallelZstdDecompress
|
|
14
|
+
import { parallelZstdDecompress } from './zstd.js';
|
|
15
15
|
function isColorMatch(r1, g1, b1, r2, g2, b2) {
|
|
16
16
|
return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2) < 50;
|
|
17
17
|
}
|
|
@@ -142,7 +142,16 @@ export function unstretchImage(rawRGB, width, height, tolerance = 0) {
|
|
|
142
142
|
}
|
|
143
143
|
return { data, width: logicalW, height: logicalH };
|
|
144
144
|
}
|
|
145
|
+
const RBW1_MAGIC = Buffer.from('RBW1');
|
|
145
146
|
async function tryDecompress(payload, onProgress) {
|
|
147
|
+
if (payload.length >= 4 && payload.subarray(0, 4).equals(RBW1_MAGIC) && native?.hybridDecompress) {
|
|
148
|
+
if (onProgress)
|
|
149
|
+
onProgress({ phase: 'decompress_start', total: 1 });
|
|
150
|
+
const result = Buffer.from(native.hybridDecompress(payload));
|
|
151
|
+
if (onProgress)
|
|
152
|
+
onProgress({ phase: 'decompress_done', loaded: 1, total: 1 });
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
146
155
|
return await parallelZstdDecompress(payload, onProgress);
|
|
147
156
|
}
|
|
148
157
|
function detectImageFormat(buf) {
|
|
@@ -466,7 +475,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
466
475
|
if (opts.onProgress)
|
|
467
476
|
opts.onProgress({ phase: 'decompress_start' });
|
|
468
477
|
try {
|
|
469
|
-
payload = await
|
|
478
|
+
payload = await tryDecompress(payload, (info) => {
|
|
470
479
|
if (opts.onProgress)
|
|
471
480
|
opts.onProgress(info);
|
|
472
481
|
});
|
|
@@ -660,7 +669,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
660
669
|
const rawPayload = logicalData.slice(idx, idx + payloadLen);
|
|
661
670
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
662
671
|
try {
|
|
663
|
-
payload = await
|
|
672
|
+
payload = await tryDecompress(payload, (info) => {
|
|
664
673
|
if (opts.onProgress)
|
|
665
674
|
opts.onProgress(info);
|
|
666
675
|
});
|