roxify 1.0.0 → 1.1.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 +49 -320
- package/dist/cli.js +1 -1
- package/dist/index.js +102 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,368 +1,97 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
> Encode binary data into PNG images and decode them back. Fast, efficient, with optional encryption.
|
|
1
|
+
# roxify
|
|
4
2
|
|
|
5
3
|
[](https://www.npmjs.com/package/roxify)
|
|
6
|
-
[](LICENSE)
|
|
7
|
-
|
|
8
|
-
## Features
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
- 🔒 **Secure**: AES-256-GCM encryption support with PBKDF2 key derivation
|
|
12
|
-
- 🎨 **Multiple modes**: Compact, chunk, pixel, and screenshot modes
|
|
13
|
-
- 📦 **CLI & API**: Use as command-line tool or JavaScript library
|
|
14
|
-
- 🔄 **Lossless**: Perfect roundtrip encoding/decoding
|
|
15
|
-
- 📊 **Efficient**: Typically 20-30% of original size with compression
|
|
16
|
-
- 📖 **Full TSDoc**: Complete TypeScript documentation
|
|
5
|
+
Encode binary data into PNG images and decode them back. Supports CLI and programmatic API (Node.js ESM).
|
|
17
6
|
|
|
18
|
-
|
|
7
|
+
Roxify is a compact, color-based alternative to QR codes, designed specifically for digital-only use (not for printing). It encodes data using color channels (rather than monochrome patterns) for higher density, and is optimized for decoding from approximate screenshots — including nearest-neighbour resize/stretch and solid or gradient backgrounds. It is not intended for printed media and is not resilient to lossy compression or heavy image filtering.
|
|
19
8
|
|
|
20
|
-
|
|
21
|
-
- 📗 **[JavaScript SDK](./JAVASCRIPT_SDK.md)** - Programmatic API reference with examples
|
|
22
|
-
- 📙 **[Quick Start](#quick-start)** - Get started in 2 minutes
|
|
9
|
+
Roxified PNGs are often more space-efficient than ZIP archives for similar payloads and provide a visual indication of the embedded data size.
|
|
23
10
|
|
|
24
11
|
## Installation
|
|
25
12
|
|
|
26
|
-
### As CLI tool (npx)
|
|
27
|
-
|
|
28
|
-
No installation needed! Use directly with npx:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npx rox encode input.zip output.png
|
|
32
|
-
npx rox decode output.png original.zip
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### As library
|
|
36
|
-
|
|
37
13
|
```bash
|
|
38
14
|
npm install roxify
|
|
39
15
|
```
|
|
40
16
|
|
|
41
17
|
## CLI Usage
|
|
42
18
|
|
|
43
|
-
### Quick Start
|
|
44
|
-
|
|
45
19
|
```bash
|
|
46
|
-
|
|
47
|
-
npx rox encode document.pdf document.png
|
|
48
|
-
|
|
49
|
-
# Decode it back
|
|
50
|
-
npx rox decode document.png document.pdf
|
|
51
|
-
|
|
52
|
-
# With encryption
|
|
53
|
-
npx rox encode secret.zip secret.png -p mypassword
|
|
54
|
-
npx rox decode secret.png secret.zip -p mypassword
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### CLI Commands
|
|
58
|
-
|
|
59
|
-
#### `encode` - Encode file to PNG
|
|
20
|
+
npx rox encode <inputName>.ext (<outputName>.png)
|
|
60
21
|
|
|
61
|
-
|
|
62
|
-
npx rox encode <input> [output] [options]
|
|
22
|
+
npx rox decode <inputName>.png (<outputName>.ext)
|
|
63
23
|
```
|
|
64
24
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- `-p, --passphrase <pass>` - Encrypt with passphrase (AES-256-GCM)
|
|
68
|
-
- `-m, --mode <mode>` - Encoding mode: `compact|chunk|pixel|screenshot` (default: `screenshot`)
|
|
69
|
-
- `-q, --quality <0-11>` - Brotli compression quality (default: `1`)
|
|
70
|
-
- `0` = fastest, largest
|
|
71
|
-
- `11` = slowest, smallest
|
|
72
|
-
- `-e, --encrypt <type>` - Encryption: `auto|aes|xor|none` (default: `aes` if passphrase)
|
|
73
|
-
- `--no-compress` - Disable compression
|
|
74
|
-
- `-o, --output <path>` - Output file path
|
|
75
|
-
|
|
76
|
-
**Examples:**
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
# Basic encoding
|
|
80
|
-
npx rox encode data.bin output.png
|
|
81
|
-
|
|
82
|
-
# Fast compression for large files
|
|
83
|
-
npx rox encode large-video.mp4 output.png -q 0
|
|
25
|
+
If no output name is provided:
|
|
84
26
|
|
|
85
|
-
|
|
86
|
-
|
|
27
|
+
- Encoding: output defaults to `<inputName>.png`.
|
|
28
|
+
- Decoding: if the image contains the original filename it will be restored; otherwise the output will be `decoded.bin`.
|
|
87
29
|
|
|
88
|
-
|
|
89
|
-
npx rox encode secret.pdf secure.png -p "my secure password"
|
|
30
|
+
**Commands:**
|
|
90
31
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# Screenshot mode (recommended, looks like a real image)
|
|
95
|
-
npx rox encode archive.tar.gz screenshot.png -m screenshot
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
#### `decode` - Decode PNG to file
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
npx rox decode <input> [output] [options]
|
|
102
|
-
```
|
|
32
|
+
- `encode <input> [output]` — Encode file to PNG
|
|
33
|
+
- `decode <input> [output]` — Decode PNG to file
|
|
103
34
|
|
|
104
35
|
**Options:**
|
|
105
36
|
|
|
106
|
-
- `-p, --passphrase <pass>`
|
|
107
|
-
- `-
|
|
108
|
-
|
|
109
|
-
|
|
37
|
+
- `-p, --passphrase <pass>` — Encrypt with AES-256-GCM
|
|
38
|
+
- `-m, --mode <mode>` — Encoding mode: `screenshot` (default), `pixel`, `compact`, `chunk`
|
|
39
|
+
- `-q, --quality <0-11>` — Brotli compression quality (default: 11)
|
|
40
|
+
- `--no-compress` — Disable compression
|
|
41
|
+
- `-v, --verbose` — Show detailed errors
|
|
110
42
|
|
|
111
|
-
|
|
112
|
-
# Basic decoding
|
|
113
|
-
npx rox decode encoded.png output.bin
|
|
114
|
-
|
|
115
|
-
# Auto-detect filename from metadata
|
|
116
|
-
npx rox decode encoded.png
|
|
117
|
-
|
|
118
|
-
# With decryption
|
|
119
|
-
npx rox decode encrypted.png output.pdf -p "my secure password"
|
|
120
|
-
```
|
|
43
|
+
Run `npx rox help` for full options.
|
|
121
44
|
|
|
122
|
-
##
|
|
45
|
+
## API Usage
|
|
123
46
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
47
|
+
```js
|
|
127
48
|
import { encodeBinaryToPng, decodePngToBinary } from 'roxify';
|
|
128
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
129
|
-
|
|
130
|
-
// Encode
|
|
131
|
-
const input = readFileSync('input.zip');
|
|
132
|
-
const png = await encodeBinaryToPng(input, {
|
|
133
|
-
mode: 'screenshot',
|
|
134
|
-
name: 'input.zip',
|
|
135
|
-
});
|
|
136
|
-
writeFileSync('output.png', png);
|
|
137
|
-
|
|
138
|
-
// Decode
|
|
139
|
-
const encoded = readFileSync('output.png');
|
|
140
|
-
const result = await decodePngToBinary(encoded);
|
|
141
|
-
writeFileSync(result.meta?.name || 'output.bin', result.buf);
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### With Encryption
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
// Encode with AES-256-GCM
|
|
148
|
-
const png = await encodeBinaryToPng(input, {
|
|
149
|
-
mode: 'screenshot',
|
|
150
|
-
passphrase: 'my-secret-password',
|
|
151
|
-
encrypt: 'aes',
|
|
152
|
-
name: 'secret.zip',
|
|
153
|
-
});
|
|
154
49
|
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
passphrase: 'my-secret-password',
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### Fast Compression
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
// Optimize for speed (recommended for large files)
|
|
165
|
-
const png = await encodeBinaryToPng(largeBuffer, {
|
|
50
|
+
const data = Buffer.from('Hello world');
|
|
51
|
+
const png = await encodeBinaryToPng(data, {
|
|
166
52
|
mode: 'screenshot',
|
|
167
|
-
|
|
168
|
-
name: 'large-file.bin',
|
|
53
|
+
name: 'message.txt',
|
|
169
54
|
});
|
|
170
55
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
brQuality: 11, // Best compression
|
|
175
|
-
name: 'config.json',
|
|
176
|
-
});
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Encoding Modes
|
|
180
|
-
|
|
181
|
-
#### `screenshot` (Recommended)
|
|
182
|
-
|
|
183
|
-
Encodes data as RGB pixel values, optimized for screenshot-like appearance. Best balance of size and compatibility.
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
const png = await encodeBinaryToPng(data, { mode: 'screenshot' });
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
#### `compact` (Smallest)
|
|
190
|
-
|
|
191
|
-
Minimal 1x1 PNG with data in custom chunk. Fastest and smallest.
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
const png = await encodeBinaryToPng(data, { mode: 'compact' });
|
|
56
|
+
const { buf, meta } = await decodePngToBinary(png);
|
|
57
|
+
console.log(buf.toString('utf8'));
|
|
58
|
+
console.log(meta?.name);
|
|
195
59
|
```
|
|
196
60
|
|
|
197
|
-
|
|
61
|
+
**API:**
|
|
198
62
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
const png = await encodeBinaryToPng(data, { mode: 'pixel' });
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
#### `chunk`
|
|
206
|
-
|
|
207
|
-
Standard PNG with data in custom rXDT chunk.
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
const png = await encodeBinaryToPng(data, { mode: 'chunk' });
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
## API Reference
|
|
214
|
-
|
|
215
|
-
### `encodeBinaryToPng(input, options)`
|
|
216
|
-
|
|
217
|
-
Encodes binary data into a PNG image.
|
|
218
|
-
|
|
219
|
-
**Parameters:**
|
|
220
|
-
|
|
221
|
-
- `input: Buffer` - The binary data to encode
|
|
222
|
-
- `options?: EncodeOptions` - Encoding options
|
|
223
|
-
|
|
224
|
-
**Returns:** `Promise<Buffer>` - The encoded PNG
|
|
63
|
+
- `encodeBinaryToPng(input: Buffer, opts?: EncodeOptions): Promise<Buffer>`
|
|
64
|
+
- `decodePngToBinary(pngBuf: Buffer): Promise<{ buf: Buffer, meta?: { name?: string } }>`
|
|
225
65
|
|
|
226
66
|
**Options:**
|
|
227
67
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// Passphrase for encryption
|
|
234
|
-
passphrase?: string;
|
|
235
|
-
|
|
236
|
-
// Original filename to embed
|
|
237
|
-
name?: string;
|
|
68
|
+
- `mode` — `'screenshot'` | `'pixel'` | `'compact'` | `'chunk'` (default: `'screenshot'`)
|
|
69
|
+
- `name` — Original filename (embedded as metadata)
|
|
70
|
+
- `passphrase` — Encryption passphrase (uses AES-256-GCM)
|
|
71
|
+
- `compression` — `'br'` | `'none'` (default: `'br'`)
|
|
72
|
+
- `brQuality` — Brotli quality 0-11 (default: 4)
|
|
238
73
|
|
|
239
|
-
|
|
240
|
-
mode?: 'compact' | 'chunk' | 'pixel' | 'screenshot';
|
|
241
|
-
|
|
242
|
-
// Encryption method
|
|
243
|
-
encrypt?: 'auto' | 'aes' | 'xor' | 'none';
|
|
244
|
-
|
|
245
|
-
// Output format
|
|
246
|
-
output?: 'auto' | 'png' | 'rox';
|
|
247
|
-
|
|
248
|
-
// Include filename in metadata (default: true)
|
|
249
|
-
includeName?: boolean;
|
|
250
|
-
|
|
251
|
-
// Brotli quality 0-11 (default: 1)
|
|
252
|
-
brQuality?: number;
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### `decodePngToBinary(pngBuf, options)`
|
|
257
|
-
|
|
258
|
-
Decodes a PNG image back to binary data.
|
|
259
|
-
|
|
260
|
-
**Parameters:**
|
|
261
|
-
|
|
262
|
-
- `pngBuf: Buffer` - The PNG image to decode
|
|
263
|
-
- `options?: DecodeOptions` - Decoding options
|
|
264
|
-
|
|
265
|
-
**Returns:** `Promise<DecodeResult>` - The decoded data and metadata
|
|
266
|
-
|
|
267
|
-
**Options:**
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
interface DecodeOptions {
|
|
271
|
-
// Passphrase for decryption
|
|
272
|
-
passphrase?: string;
|
|
273
|
-
}
|
|
274
|
-
```
|
|
74
|
+
## Example: Express Endpoint
|
|
275
75
|
|
|
276
|
-
|
|
76
|
+
```js
|
|
77
|
+
import express from 'express';
|
|
78
|
+
import { encodeBinaryToPng } from 'roxify';
|
|
277
79
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
meta?: {
|
|
285
|
-
// Original filename
|
|
286
|
-
name?: string;
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## Performance Tips
|
|
292
|
-
|
|
293
|
-
### For Large Files (>10 MB)
|
|
294
|
-
|
|
295
|
-
```bash
|
|
296
|
-
# Use quality 0 for fastest encoding
|
|
297
|
-
npx rox encode large.bin output.png -q 0
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
const png = await encodeBinaryToPng(largeFile, {
|
|
302
|
-
mode: 'screenshot',
|
|
303
|
-
brQuality: 0, // 10-20x faster than default
|
|
80
|
+
const app = express();
|
|
81
|
+
app.get('/payload.png', async (req, res) => {
|
|
82
|
+
const payload = Buffer.from('Embedded data');
|
|
83
|
+
const png = await encodeBinaryToPng(payload, { mode: 'screenshot' });
|
|
84
|
+
res.setHeader('Content-Type', 'image/png');
|
|
85
|
+
res.send(png);
|
|
304
86
|
});
|
|
87
|
+
app.listen(3000);
|
|
305
88
|
```
|
|
306
89
|
|
|
307
|
-
|
|
90
|
+
## Requirements
|
|
308
91
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
npx rox encode small.json output.png -q 11 -m compact
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
```typescript
|
|
315
|
-
const png = await encodeBinaryToPng(smallFile, {
|
|
316
|
-
mode: 'compact',
|
|
317
|
-
brQuality: 11, // Best compression ratio
|
|
318
|
-
});
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Benchmark Results
|
|
322
|
-
|
|
323
|
-
File: 3.8 MB binary
|
|
324
|
-
|
|
325
|
-
- **Quality 0**: ~500-800ms, output ~1.2 MB
|
|
326
|
-
- **Quality 1** (default): ~1-2s, output ~800 KB
|
|
327
|
-
- **Quality 5**: ~8-12s, output ~750 KB
|
|
328
|
-
- **Quality 11**: ~20-30s, output ~720 KB
|
|
329
|
-
|
|
330
|
-
## Error Handling
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
try {
|
|
334
|
-
const result = await decodePngToBinary(encoded, {
|
|
335
|
-
passphrase: 'wrong-password',
|
|
336
|
-
});
|
|
337
|
-
} catch (err) {
|
|
338
|
-
if (err.message.includes('Incorrect passphrase')) {
|
|
339
|
-
console.error('Wrong password!');
|
|
340
|
-
} else if (err.message.includes('Invalid ROX format')) {
|
|
341
|
-
console.error('Not a valid RoxCompressor PNG');
|
|
342
|
-
} else {
|
|
343
|
-
console.error('Decode failed:', err.message);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
## Security
|
|
349
|
-
|
|
350
|
-
- **AES-256-GCM**: Authenticated encryption with 100,000 PBKDF2 iterations
|
|
351
|
-
- **XOR cipher**: Simple obfuscation (not cryptographically secure)
|
|
352
|
-
- **No encryption**: Data is compressed but not encrypted
|
|
353
|
-
|
|
354
|
-
⚠️ **Warning**: Use strong passphrases for sensitive data. The `xor` encryption mode is not secure and should only be used for obfuscation.
|
|
92
|
+
- Node.js 18+ (ESM)
|
|
93
|
+
- Native dependencies: `sharp` (auto-installed)
|
|
355
94
|
|
|
356
95
|
## License
|
|
357
96
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
## Contributing
|
|
361
|
-
|
|
362
|
-
Contributions welcome! Please open an issue or PR on GitHub.
|
|
363
|
-
|
|
364
|
-
## Links
|
|
365
|
-
|
|
366
|
-
- [GitHub Repository](https://github.com/RoxasYTB/RoxCompressor)
|
|
367
|
-
- [npm Package](https://www.npmjs.com/package/roxify)
|
|
368
|
-
- [Report Issues](https://github.com/RoxasYTB/RoxCompressor/issues)
|
|
97
|
+
This package is proprietary (UNLICENSED). The repository remains private; the package is published to npm for distribution. If there is significant community interest, it may be open-sourced in the future.
|
package/dist/cli.js
CHANGED
|
@@ -173,7 +173,7 @@ async function decodeCommand(args) {
|
|
|
173
173
|
catch (e) {
|
|
174
174
|
console.log('Could not generate reconst PNG:', e.message);
|
|
175
175
|
}
|
|
176
|
-
console.log(`Reading: ${resolvedInputPath}`);
|
|
176
|
+
console.log(`Reading: ${resolvedInputPath.replace('_reconst.png', '.png')}`);
|
|
177
177
|
const options = {};
|
|
178
178
|
if (parsed.passphrase) {
|
|
179
179
|
options.passphrase = parsed.passphrase;
|
package/dist/index.js
CHANGED
|
@@ -108,6 +108,19 @@ async function loadRaw(imgInput) {
|
|
|
108
108
|
return { data, info };
|
|
109
109
|
}
|
|
110
110
|
export async function cropAndReconstitute(input) {
|
|
111
|
+
async function loadRaw(imgInput) {
|
|
112
|
+
const { data, info } = await sharp(imgInput)
|
|
113
|
+
.ensureAlpha()
|
|
114
|
+
.raw()
|
|
115
|
+
.toBuffer({ resolveWithObject: true });
|
|
116
|
+
return { data, info };
|
|
117
|
+
}
|
|
118
|
+
function idxFor(x, y, width) {
|
|
119
|
+
return (y * width + x) * 4;
|
|
120
|
+
}
|
|
121
|
+
function eqRGB(a, b) {
|
|
122
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
123
|
+
}
|
|
111
124
|
const { data, info } = await loadRaw(input);
|
|
112
125
|
const w = info.width;
|
|
113
126
|
const h = info.height;
|
|
@@ -183,8 +196,8 @@ export async function cropAndReconstitute(input) {
|
|
|
183
196
|
const sy1 = Math.min(startPoint.y, endPoint.y);
|
|
184
197
|
const sx2 = Math.max(startPoint.x, endPoint.x);
|
|
185
198
|
const sy2 = Math.max(startPoint.y, endPoint.y);
|
|
186
|
-
const cropW = sx2 - sx1;
|
|
187
|
-
const cropH = sy2 - sy1;
|
|
199
|
+
const cropW = sx2 - sx1 + 1;
|
|
200
|
+
const cropH = sy2 - sy1 + 1;
|
|
188
201
|
if (cropW <= 0 || cropH <= 0)
|
|
189
202
|
throw new Error('Invalid crop dimensions');
|
|
190
203
|
const cropped = await sharp(input)
|
|
@@ -212,19 +225,84 @@ export async function cropAndReconstitute(input) {
|
|
|
212
225
|
return false;
|
|
213
226
|
return true;
|
|
214
227
|
}
|
|
215
|
-
const
|
|
228
|
+
const newWidth = cw;
|
|
229
|
+
const newHeight = ch + 1;
|
|
230
|
+
const out = Buffer.alloc(newWidth * newHeight * 4, 0);
|
|
231
|
+
for (let i = 0; i < out.length; i += 4)
|
|
232
|
+
out[i + 3] = 255;
|
|
216
233
|
for (let y = 0; y < ch; y++) {
|
|
234
|
+
for (let x = 0; x < cw; x++) {
|
|
235
|
+
const srcI = ((y * cw + x) * 4) | 0;
|
|
236
|
+
const dstI = ((y * newWidth + x) * 4) | 0;
|
|
237
|
+
out[dstI] = cdata[srcI];
|
|
238
|
+
out[dstI + 1] = cdata[srcI + 1];
|
|
239
|
+
out[dstI + 2] = cdata[srcI + 2];
|
|
240
|
+
out[dstI + 3] = cdata[srcI + 3];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (cw >= 3) {
|
|
244
|
+
const targetY = ch - 1;
|
|
245
|
+
for (let x = cw - 3; x < cw; x++) {
|
|
246
|
+
const i = ((targetY * newWidth + x) * 4) | 0;
|
|
247
|
+
out[i] = 0;
|
|
248
|
+
out[i + 1] = 0;
|
|
249
|
+
out[i + 2] = 0;
|
|
250
|
+
out[i + 3] = 255;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const targetY = ch - 1;
|
|
255
|
+
for (let x = 0; x < cw; x++) {
|
|
256
|
+
const i = ((targetY * newWidth + x) * 4) | 0;
|
|
257
|
+
out[i] = 0;
|
|
258
|
+
out[i + 1] = 0;
|
|
259
|
+
out[i + 2] = 0;
|
|
260
|
+
out[i + 3] = 255;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const lastY = ch;
|
|
264
|
+
for (let x = 0; x < newWidth; x++) {
|
|
265
|
+
const i = ((lastY * newWidth + x) * 4) | 0;
|
|
266
|
+
out[i] = 0;
|
|
267
|
+
out[i + 1] = 0;
|
|
268
|
+
out[i + 2] = 0;
|
|
269
|
+
out[i + 3] = 255;
|
|
270
|
+
}
|
|
271
|
+
if (newWidth >= 3) {
|
|
272
|
+
const bgrStart = newWidth - 3;
|
|
273
|
+
let i = ((lastY * newWidth + bgrStart) * 4) | 0;
|
|
274
|
+
out[i] = 0;
|
|
275
|
+
out[i + 1] = 0;
|
|
276
|
+
out[i + 2] = 255;
|
|
277
|
+
out[i + 3] = 255;
|
|
278
|
+
i = ((lastY * newWidth + bgrStart + 1) * 4) | 0;
|
|
279
|
+
out[i] = 0;
|
|
280
|
+
out[i + 1] = 255;
|
|
281
|
+
out[i + 2] = 0;
|
|
282
|
+
out[i + 3] = 255;
|
|
283
|
+
i = ((lastY * newWidth + bgrStart + 2) * 4) | 0;
|
|
284
|
+
out[i] = 255;
|
|
285
|
+
out[i + 1] = 0;
|
|
286
|
+
out[i + 2] = 0;
|
|
287
|
+
out[i + 3] = 255;
|
|
288
|
+
}
|
|
289
|
+
function getPixel(x, y) {
|
|
290
|
+
const i = ((y * newWidth + x) * 4) | 0;
|
|
291
|
+
return [out[i], out[i + 1], out[i + 2], out[i + 3]];
|
|
292
|
+
}
|
|
293
|
+
const compressedLines = [];
|
|
294
|
+
for (let y = 0; y < newHeight; y++) {
|
|
217
295
|
const line = [];
|
|
218
296
|
let x = 0;
|
|
219
|
-
while (x <
|
|
220
|
-
const current =
|
|
297
|
+
while (x < newWidth) {
|
|
298
|
+
const current = getPixel(x, y);
|
|
221
299
|
if (current[0] === 0 && current[1] === 0 && current[2] === 0) {
|
|
222
300
|
x++;
|
|
223
301
|
continue;
|
|
224
302
|
}
|
|
225
303
|
line.push(current);
|
|
226
304
|
let nx = x + 1;
|
|
227
|
-
while (nx <
|
|
305
|
+
while (nx < newWidth && eq(getPixel(nx, y), current))
|
|
228
306
|
nx++;
|
|
229
307
|
x = nx;
|
|
230
308
|
}
|
|
@@ -246,90 +324,25 @@ export async function cropAndReconstitute(input) {
|
|
|
246
324
|
.png()
|
|
247
325
|
.toBuffer();
|
|
248
326
|
}
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
for (let i = 0; i <
|
|
253
|
-
|
|
327
|
+
const finalWidth = Math.max(...compressedLines.map((l) => l.length));
|
|
328
|
+
const finalHeight = compressedLines.length;
|
|
329
|
+
const finalOut = Buffer.alloc(finalWidth * finalHeight * 4, 0);
|
|
330
|
+
for (let i = 0; i < finalOut.length; i += 4)
|
|
331
|
+
finalOut[i + 3] = 255;
|
|
254
332
|
for (let y = 0; y < compressedLines.length; y++) {
|
|
255
333
|
const line = compressedLines[y];
|
|
256
|
-
const
|
|
257
|
-
const startX = 0;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
out[i + 1] = line[x][1];
|
|
265
|
-
out[i + 2] = line[x][2];
|
|
266
|
-
out[i + 3] = line[x][3] === 0 ? 255 : line[x][3];
|
|
267
|
-
}
|
|
268
|
-
if (isSecondToLast) {
|
|
269
|
-
for (let x = effectiveLength; x < Math.min(effectiveLength + 3, newWidth); x++) {
|
|
270
|
-
const i = ((y * newWidth + x) * 4) | 0;
|
|
271
|
-
out[i] = 0;
|
|
272
|
-
out[i + 1] = 0;
|
|
273
|
-
out[i + 2] = 0;
|
|
274
|
-
out[i + 3] = 255;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const secondToLastY = newHeight - 2;
|
|
279
|
-
let secondToLastIsBlack = true;
|
|
280
|
-
for (let x = 0; x < newWidth; x++) {
|
|
281
|
-
const i = ((secondToLastY * newWidth + x) * 4) | 0;
|
|
282
|
-
if (out[i] !== 0 || out[i + 1] !== 0 || out[i + 2] !== 0) {
|
|
283
|
-
secondToLastIsBlack = false;
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
let finalHeight = newHeight;
|
|
288
|
-
let finalOut = out;
|
|
289
|
-
if (secondToLastIsBlack) {
|
|
290
|
-
finalHeight = newHeight - 1;
|
|
291
|
-
finalOut = Buffer.alloc(newWidth * finalHeight * 4, 0);
|
|
292
|
-
for (let i = 0; i < finalOut.length; i += 4)
|
|
293
|
-
finalOut[i + 3] = 255;
|
|
294
|
-
for (let y = 0; y < newHeight - 2; y++) {
|
|
295
|
-
for (let x = 0; x < newWidth; x++) {
|
|
296
|
-
const srcI = ((y * newWidth + x) * 4) | 0;
|
|
297
|
-
const dstI = ((y * newWidth + x) * 4) | 0;
|
|
298
|
-
finalOut[dstI] = out[srcI];
|
|
299
|
-
finalOut[dstI + 1] = out[srcI + 1];
|
|
300
|
-
finalOut[dstI + 2] = out[srcI + 2];
|
|
301
|
-
finalOut[dstI + 3] = out[srcI + 3];
|
|
302
|
-
}
|
|
334
|
+
const isLastLine = y === compressedLines.length - 1;
|
|
335
|
+
const startX = isLastLine ? finalWidth - line.length : 0;
|
|
336
|
+
for (let x = 0; x < line.length; x++) {
|
|
337
|
+
const i = ((y * finalWidth + startX + x) * 4) | 0;
|
|
338
|
+
finalOut[i] = line[x][0];
|
|
339
|
+
finalOut[i + 1] = line[x][1];
|
|
340
|
+
finalOut[i + 2] = line[x][2];
|
|
341
|
+
finalOut[i + 3] = line[x][3] === 0 ? 255 : line[x][3];
|
|
303
342
|
}
|
|
304
343
|
}
|
|
305
|
-
const lastY = finalHeight - 1;
|
|
306
|
-
for (let x = 0; x < newWidth; x++) {
|
|
307
|
-
const i = ((lastY * newWidth + x) * 4) | 0;
|
|
308
|
-
finalOut[i] = 0;
|
|
309
|
-
finalOut[i + 1] = 0;
|
|
310
|
-
finalOut[i + 2] = 0;
|
|
311
|
-
finalOut[i + 3] = 255;
|
|
312
|
-
}
|
|
313
|
-
if (newWidth >= 3) {
|
|
314
|
-
const bgrStart = newWidth - 3;
|
|
315
|
-
let i = ((lastY * newWidth + bgrStart) * 4) | 0;
|
|
316
|
-
finalOut[i] = 0;
|
|
317
|
-
finalOut[i + 1] = 0;
|
|
318
|
-
finalOut[i + 2] = 255;
|
|
319
|
-
finalOut[i + 3] = 255;
|
|
320
|
-
i = ((lastY * newWidth + bgrStart + 1) * 4) | 0;
|
|
321
|
-
finalOut[i] = 0;
|
|
322
|
-
finalOut[i + 1] = 255;
|
|
323
|
-
finalOut[i + 2] = 0;
|
|
324
|
-
finalOut[i + 3] = 255;
|
|
325
|
-
i = ((lastY * newWidth + bgrStart + 2) * 4) | 0;
|
|
326
|
-
finalOut[i] = 255;
|
|
327
|
-
finalOut[i + 1] = 0;
|
|
328
|
-
finalOut[i + 2] = 0;
|
|
329
|
-
finalOut[i + 3] = 255;
|
|
330
|
-
}
|
|
331
344
|
return sharp(finalOut, {
|
|
332
|
-
raw: { width:
|
|
345
|
+
raw: { width: finalWidth, height: finalHeight, channels: 4 },
|
|
333
346
|
})
|
|
334
347
|
.png()
|
|
335
348
|
.toBuffer();
|
|
@@ -442,7 +455,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
442
455
|
const spaceInLastRow = pixelsInLastRow === 0 ? logicalWidth : logicalWidth - pixelsInLastRow;
|
|
443
456
|
const needsExtraRow = spaceInLastRow < MARKER_END.length;
|
|
444
457
|
const logicalHeight = needsExtraRow ? dataRows + 1 : dataRows;
|
|
445
|
-
const scale =
|
|
458
|
+
const scale = 2;
|
|
446
459
|
const width = logicalWidth * scale;
|
|
447
460
|
const height = logicalHeight * scale;
|
|
448
461
|
const raw = Buffer.alloc(width * height * bytesPerPixel);
|
|
@@ -489,10 +502,10 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
489
502
|
raw: { width, height, channels: 3 },
|
|
490
503
|
})
|
|
491
504
|
.png({
|
|
492
|
-
compressionLevel:
|
|
505
|
+
compressionLevel: 9,
|
|
493
506
|
palette: false,
|
|
494
|
-
effort:
|
|
495
|
-
adaptiveFiltering:
|
|
507
|
+
effort: 10,
|
|
508
|
+
adaptiveFiltering: true,
|
|
496
509
|
})
|
|
497
510
|
.toBuffer();
|
|
498
511
|
}
|