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/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
  [![npm version](https://img.shields.io/npm/v/roxify.svg)](https://www.npmjs.com/package/roxify)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![License: RPOSL](https://img.shields.io/badge/License-RPOSL-red.svg)](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 | Original | zip -9 | gzip -9 | 7z LZMA2 -9 | Roxify PNG | Roxify WAV |
64
- |---|---|---|---|---|---|---|
65
- | 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%)** |
66
- | 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%)** |
67
- | 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%) |
68
- | 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%) |
69
- | 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%)** |
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%) | 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 | Tool | Encode | Decode | Enc Throughput | Dec Throughput |
77
- |---|---|---|---|---|---|
78
- | Text 1 MB | zip -9 | 112 ms | 36 ms | 8.9 MB/s | 27.6 MB/s |
79
- | | gzip -9 | 146 ms | 38 ms | 6.9 MB/s | 26.0 MB/s |
80
- | | 7z LZMA -9 | 303 ms | 21 ms | 3.3 MB/s | 46.6 MB/s |
81
- | | **Roxify PNG** | **859 ms** | **577 ms** | **1.2 MB/s** | **1.7 MB/s** |
82
- | | **Roxify WAV** | **794 ms** | **480 ms** | **1.3 MB/s** | **2.1 MB/s** |
83
- | JSON 1 MB | zip -9 | 79 ms | 20 ms | 12.7 MB/s | 50.5 MB/s |
84
- | | 7z LZMA -9 | 197 ms | 26 ms | 5.1 MB/s | 37.9 MB/s |
85
- | | **Roxify PNG** | **1.14 s** | **755 ms** | **0.9 MB/s** | **1.3 MB/s** |
86
- | | **Roxify WAV** | **1.49 s** | **518 ms** | **0.7 MB/s** | **1.9 MB/s** |
87
- | Text 10 MB | zip -9 | 1.21 s | 70 ms | 8.2 MB/s | 143.8 MB/s |
88
- | | 7z LZMA -9 | 5.05 s | 99 ms | 2.0 MB/s | 100.8 MB/s |
89
- | | **Roxify PNG** | **9.05 s** | **4.53 s** | **1.1 MB/s** | **2.2 MB/s** |
90
- | | **Roxify WAV** | **9.22 s** | **2.59 s** | **1.1 MB/s** | **3.9 MB/s** |
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 | Container | Encode | Decode | Enc Throughput | Dec Throughput | Output | Ratio | Integrity |
99
- |---|---|---|---|---|---|---|---|---|
100
- | 1 KB | PNG | 9 ms | 12 ms | 0.1 MB/s | 0.1 MB/s | 1.14 KB | 114.3% | ✓ |
101
- | 10 KB | PNG | 18 ms | 34 ms | 0.5 MB/s | 0.3 MB/s | 10.32 KB | 103.2% | ✓ |
102
- | 100 KB | PNG | 52 ms | 109 ms | 1.9 MB/s | 0.9 MB/s | 100.52 KB | 100.5% | ✓ |
103
- | 500 KB | PNG | 339 ms | 541 ms | 1.4 MB/s | 0.9 MB/s | 502.64 KB | 100.5% | ✓ |
104
- | 1 MB | PNG | 875 ms | 1.24 s | 1.1 MB/s | 0.8 MB/s | 1.00 MB | 100.3% | ✓ |
105
- | 5 MB | PNG | 3.39 s | 4.12 s | 1.5 MB/s | 1.2 MB/s | 5.01 MB | 100.2% | ✓ |
106
- | 10 MB | PNG | 6.84 s | 12.28 s | 1.5 MB/s | 0.8 MB/s | 10.01 MB | 100.1% | ✓ |
107
- | 1 KB | WAV | 2 ms | 2 ms | 0.6 MB/s | 0.6 MB/s | 1.08 KB | 107.5% | ✓ |
108
- | 10 KB | WAV | 4 ms | 5 ms | 2.3 MB/s | 1.8 MB/s | 10.08 KB | 100.8% | ✓ |
109
- | 100 KB | WAV | 39 ms | 28 ms | 2.5 MB/s | 3.5 MB/s | 100.08 KB | 100.1% | ✓ |
110
- | 500 KB | WAV | 172 ms | 190 ms | 2.8 MB/s | 2.6 MB/s | 500.09 KB | 100.0% | ✓ |
111
- | 1 MB | WAV | 452 ms | 276 ms | 2.2 MB/s | 3.6 MB/s | 1.00 MB | 100.0% | ✓ |
112
- | 5 MB | WAV | 2.70 s | 1.65 s | 1.8 MB/s | 3.0 MB/s | 5.00 MB | 100.0% | ✓ |
113
- | 10 MB | WAV | 4.81 s | 2.56 s | 2.1 MB/s | 3.9 MB/s | 10.00 MB | 100.0% | ✓ |
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 | Encode | Decode | Enc Throughput | Dec Throughput | Overhead |
120
- |---|---|---|---|---|---|
121
- | 1 KB | 6 ms | 4 ms | 0.2 MB/s | 0.2 MB/s | 125.7% |
122
- | 10 KB | 7 ms | 6 ms | 1.3 MB/s | 1.5 MB/s | 119.6% |
123
- | 100 KB | 49 ms | 45 ms | 2.0 MB/s | 2.1 MB/s | 118.8% |
124
- | 1 MB | 483 ms | 377 ms | 2.1 MB/s | 2.7 MB/s | 118.6% |
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 | 32 ms | 122 KB |
133
- | 128 B | 39 ms | 122 KB |
134
- | 512 B | 76 ms | 316 KB |
135
- | 1 KB | 139 ms | 508 KB |
136
- | 2 KB | 251 ms | 986 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 | 33 ms | 44 ms | 1.35 MB | ✓ |
143
- | 32 B | 19 ms | 31 ms | 1.35 MB | ✓ |
144
- | 64 B | 22 ms | 24 ms | 1.35 MB | ✓ |
145
- | 128 B | 21 ms | 28 ms | 1.35 MB | ✓ |
146
- | 256 B | 40 ms | 45 ms | 2.59 MB | ✓ |
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 | PNG | WAV |
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 | Command / Setting |
179
- |---|---|
180
- | zip | `zip -r -q -9` |
181
- | tar/gzip | `tar -cf - \| gzip -9` |
182
- | 7z | `7z a -mx=9` (LZMA2 ultra) |
183
- | Roxify | Zstd level 19, compact mode |
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 | Description | Default |
226
- |---|---|---|
227
- | `-p, --passphrase <pass>` | Encrypt with AES-256-GCM | none |
228
- | `-m, --mode <mode>` | Encoding mode: `screenshot`, `compact` | `screenshot` |
229
- | `-q, --quality <0-11>` | Compression effort (0 = fastest, 11 = smallest) | `1` |
230
- | `-e, --encrypt <type>` | Encryption method: `auto`, `aes`, `xor`, `none` | `aes` if passphrase is set |
231
- | `--no-compress` | Disable compression entirely | false |
232
- | `-o, --output <path>` | Explicit output file path | auto-generated |
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 | Description | Default |
241
- |---|---|---|
242
- | `-p, --passphrase <pass>` | Decryption passphrase | none |
243
- | `-o, --output <path>` | Output file path | auto-detected from metadata |
244
- | `--dict <file>` | Zstd dictionary for improved decompression | none |
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'; // Compression algorithm
338
- compressionLevel?: number; // Zstd compression level (0-19)
339
- passphrase?: string; // Encryption passphrase
340
- dict?: Buffer; // Zstd dictionary for improved ratios
341
- name?: string; // Original filename stored in metadata
342
- mode?: 'screenshot'; // Encoding mode
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; // Include filename in PNG metadata
346
- includeFileList?: boolean; // Include file manifest in PNG
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; // Skip PNG optimization pass
349
- lossyResilient?: boolean; // Enable lossy-resilient encoding (RS ECC)
350
- eccLevel?: EccLevel; // 'low' | 'medium' | 'quartile' | 'high'
351
- robustBlockSize?: number; // 2–8 pixels per data block (lossy image)
352
- container?: 'image' | 'sound'; // Output container format
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; // Decryption passphrase
364
- outPath?: string; // Output directory for unpacked files
365
- files?: string[]; // Extract only specific files from archive
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; // Decoded binary payload
377
- meta?: { name?: string }; // Metadata (original filename)
378
- files?: PackedFile[]; // Unpacked directory entries, if applicable
379
- correctedErrors?: number; // RS errors corrected (lossy-resilient mode)
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 | Description | Use Case |
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` | Minimal 1x1 PNG with data embedded in a custom ancillary chunk (`rXDT`). Produces the smallest possible output. | Programmatic use, archival, maximum compression ratio |
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 | Strength | Use Case |
414
- |---|---|---|---|
415
- | `aes` | AES-256-GCM with PBKDF2 (100,000 iterations) | Cryptographically secure, authenticated | Sensitive data, confidential documents |
416
- | `xor` | XOR cipher with passphrase-derived key | Obfuscation only, not cryptographically secure | Casual deterrent against inspection |
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 | Parity Symbols | Overhead | Correctable Errors |
436
- |-------|---------------:|---------:|-------------------:|
437
- | `low` | 20 / block | ~10% | ~4% |
438
- | `medium` | 40 / block | ~19% | ~9% |
439
- | `quartile` | 64 / block | ~33% | ~15% |
440
- | `high` | 128 / block | ~100% | ~25% |
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, // 4×4 pixels per data bit
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 | Ratio | Recommendation |
502
- |---|---|---|---|
503
- | 0 | Fastest | Largest | Files over 100 MB, real-time workflows |
504
- | 1 | Fast | Good | Default; general-purpose use |
505
- | 5 | Moderate | Better | Archival of medium-sized datasets |
506
- | 11 | Slowest | Smallest | Small files under 1 MB, long-term storage |
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 | Binary Name |
537
- |---|---|---|
538
- | Linux | x86_64 | `libroxify_native-x86_64-unknown-linux-gnu.node` |
539
- | macOS | x86_64 | `libroxify_native-x86_64-apple-darwin.node` |
540
- | macOS | ARM64 (Apple Silicon) | `libroxify_native-aarch64-apple-darwin.node` |
541
- | Windows | x86_64 | `roxify_native-x86_64-pc-windows-msvc.node` |
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 | Responsibility |
634
- |---|---|
635
- | `core.rs` | Pixel scanning, CRC32, Adler32, delta coding, Zstd compress/decompress |
636
- | `encoder.rs` | PNG payload encoding with marker pixels and metadata chunks |
637
- | `packer.rs` | Directory tree serialization and streaming deserialization |
638
- | `crypto.rs` | AES-256-GCM encryption and PBKDF2 key derivation |
639
- | `archive.rs` | Tar-based archiving with optional Zstd compression |
640
- | `reconstitution.rs` | Screenshot detection and automatic crop to recover encoded data |
641
- | `audio.rs` | WAV container encoding and decoding (PCM byte packing) |
642
- | `bwt.rs` | Parallel Burrows-Wheeler Transform |
643
- | `rans.rs` | rANS (Asymmetric Numeral Systems) entropy coder |
644
- | `hybrid.rs` | Block-based orchestration of BWT, context mixing, and rANS |
645
- | `pool.rs` | Buffer pooling and zero-copy memory management |
646
- | `image_utils.rs` | Image resizing, pixel format conversion, metadata extraction |
647
- | `png_utils.rs` | Low-level PNG chunk read/write operations |
648
- | `progress.rs` | Progress tracking for long-running compression/decompression |
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 | Responsibility |
653
- |---|---|
654
- | `ecc.ts` | Reed-Solomon GF(256) codec, block ECC, interleaving |
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` | High-level encoding orchestration (standard + lossy-resilient) |
658
- | `decoder.ts` | High-level decoding with automatic format detection |
659
- | `audio.ts` | Standard WAV container (8-bit PCM) |
660
- | `helpers.ts` | Delta coding, XOR cipher, palette generation |
661
- | `zstd.ts` | Parallel Zstd compression via native module |
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 | Cause |
688
- |---|---|
689
- | `Incorrect passphrase` | Wrong password provided for decryption |
690
- | `not a valid PNG` | Input buffer is not a PNG or lacks Roxify markers |
691
- | `Passphrase required` | File is encrypted but no passphrase was supplied |
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
- MIT. See [LICENSE](LICENSE) for details.
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';
@@ -29,6 +29,11 @@ export declare const COMPRESSION_MARKERS: {
29
29
  g: number;
30
30
  b: number;
31
31
  }[];
32
+ 'bwt-ans': {
33
+ r: number;
34
+ g: number;
35
+ b: number;
36
+ }[];
32
37
  };
33
38
  export declare const FORMAT_MARKERS: {
34
39
  png: {
@@ -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 },
@@ -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, tryZstdDecompress } from './zstd.js';
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 tryZstdDecompress(payload, (info) => {
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 tryZstdDecompress(payload, (info) => {
672
+ payload = await tryDecompress(payload, (info) => {
664
673
  if (opts.onProgress)
665
674
  opts.onProgress(info);
666
675
  });