roxify 1.6.6 → 1.6.7

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.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Lossy-Resilient Image Encoding (PNG container).
3
+ *
4
+ * Encodes binary data using QR-code-inspired techniques:
5
+ * - Large pixel blocks (configurable 2×2 to 8×8) for JPEG/WebP resilience.
6
+ * - Binary encoding (black/white) with threshold detection.
7
+ * - Finder patterns at corners for automatic alignment.
8
+ * - Reed-Solomon error correction with configurable redundancy.
9
+ * - Byte-level interleaving to spread burst errors across RS blocks.
10
+ *
11
+ * The resulting image looks like a structured data pattern (similar to
12
+ * a QR code) and can be recovered even after JPEG recompression at
13
+ * quality levels as low as 30–50.
14
+ */
15
+ import { EccLevel } from './ecc.js';
16
+ export interface RobustImageEncodeOptions {
17
+ /** Pixel size per data block (2–8). Higher = more lossy resilience. Default: 4. */
18
+ blockSize?: number;
19
+ /** Error correction level. Default: 'medium'. */
20
+ eccLevel?: EccLevel;
21
+ }
22
+ export interface RobustImageDecodeResult {
23
+ data: Buffer;
24
+ correctedErrors: number;
25
+ }
26
+ /**
27
+ * Encode binary data into a lossy-resilient PNG image.
28
+ *
29
+ * The output uses QR-code-like techniques:
30
+ * - Finder patterns for alignment after re-encoding.
31
+ * - Large pixel blocks for JPEG/WebP resilience.
32
+ * - Reed-Solomon ECC for automatic error correction.
33
+ *
34
+ * @param data - Raw data to encode.
35
+ * @param opts - Encoding options.
36
+ * @returns PNG image as a Buffer.
37
+ */
38
+ export declare function encodeRobustImage(data: Buffer, opts?: RobustImageEncodeOptions): Buffer;
39
+ /**
40
+ * Decode binary data from a lossy-resilient PNG image.
41
+ *
42
+ * Handles images that have been re-encoded through JPEG/WebP at various
43
+ * quality levels. The Reed-Solomon ECC layer corrects any bit errors
44
+ * introduced by lossy compression.
45
+ *
46
+ * @param png - PNG (or raw RGB) image buffer.
47
+ * @returns Decoded data and error correction stats.
48
+ */
49
+ export declare function decodeRobustImage(png: Buffer): RobustImageDecodeResult;
50
+ /**
51
+ * Check if a PNG buffer contains a robust-image-encoded payload.
52
+ * Looks for finder patterns in the corners.
53
+ */
54
+ export declare function isRobustImage(png: Buffer): boolean;
@@ -0,0 +1,515 @@
1
+ /**
2
+ * Lossy-Resilient Image Encoding (PNG container).
3
+ *
4
+ * Encodes binary data using QR-code-inspired techniques:
5
+ * - Large pixel blocks (configurable 2×2 to 8×8) for JPEG/WebP resilience.
6
+ * - Binary encoding (black/white) with threshold detection.
7
+ * - Finder patterns at corners for automatic alignment.
8
+ * - Reed-Solomon error correction with configurable redundancy.
9
+ * - Byte-level interleaving to spread burst errors across RS blocks.
10
+ *
11
+ * The resulting image looks like a structured data pattern (similar to
12
+ * a QR code) and can be recovered even after JPEG recompression at
13
+ * quality levels as low as 30–50.
14
+ */
15
+ import { eccDecode, eccEncode } from './ecc.js';
16
+ import { native } from './native.js';
17
+ // ─── Configuration ──────────────────────────────────────────────────────────
18
+ /** Finder pattern (7×7 blocks, like QR codes). */
19
+ const FINDER_SIZE = 7;
20
+ /** Alignment pattern (5×5 blocks). */
21
+ const ALIGNMENT_SIZE = 5;
22
+ /** Quiet zone around finder patterns (blocks). */
23
+ const QUIET_ZONE = 1;
24
+ /** Header area width in blocks (next to top-left finder). */
25
+ const HEADER_BLOCKS = 20;
26
+ /** Magic bytes for robust image format. */
27
+ const ROBUST_IMG_MAGIC = Buffer.from('RBI1');
28
+ // ─── Finder Pattern ─────────────────────────────────────────────────────────
29
+ /**
30
+ * Generate a 7×7 finder pattern (same as QR code finder).
31
+ * Returns a 2D boolean grid (true = black, false = white).
32
+ */
33
+ function finderPattern() {
34
+ const p = [];
35
+ for (let y = 0; y < FINDER_SIZE; y++) {
36
+ const row = [];
37
+ for (let x = 0; x < FINDER_SIZE; x++) {
38
+ const border = y === 0 || y === 6 || x === 0 || x === 6;
39
+ const inner = y >= 2 && y <= 4 && x >= 2 && x <= 4;
40
+ row.push(border || inner);
41
+ }
42
+ p.push(row);
43
+ }
44
+ return p;
45
+ }
46
+ /**
47
+ * Generate a 5×5 alignment pattern.
48
+ */
49
+ function alignmentPattern() {
50
+ const p = [];
51
+ for (let y = 0; y < ALIGNMENT_SIZE; y++) {
52
+ const row = [];
53
+ for (let x = 0; x < ALIGNMENT_SIZE; x++) {
54
+ const border = y === 0 || y === 4 || x === 0 || x === 4;
55
+ const center = y === 2 && x === 2;
56
+ row.push(border || center);
57
+ }
58
+ p.push(row);
59
+ }
60
+ return p;
61
+ }
62
+ /**
63
+ * Compute grid layout: place finder patterns, quiet zones, and determine
64
+ * which block positions are available for data.
65
+ * Optimized: uses flat boolean array instead of Set<string> for reserved lookup.
66
+ */
67
+ function computeLayout(dataBlocks) {
68
+ // Minimum grid to fit data + finder patterns + quiet zones
69
+ const finderFootprint = FINDER_SIZE + QUIET_ZONE;
70
+ // Start with a square grid and expand if needed
71
+ let side = Math.ceil(Math.sqrt(dataBlocks + 4 * finderFootprint * finderFootprint));
72
+ side = Math.max(side, finderFootprint * 2 + 4); // minimum for finders
73
+ const gridW = side;
74
+ const gridH = side;
75
+ // Determine reserved regions (finder patterns + quiet zones) using flat array
76
+ const reserved = new Uint8Array(gridW * gridH);
77
+ // Top-left finder
78
+ for (let y = 0; y < finderFootprint; y++) {
79
+ for (let x = 0; x < finderFootprint; x++) {
80
+ reserved[y * gridW + x] = 1;
81
+ }
82
+ }
83
+ // Top-right finder
84
+ for (let y = 0; y < finderFootprint; y++) {
85
+ for (let x = gridW - finderFootprint; x < gridW; x++) {
86
+ reserved[y * gridW + x] = 1;
87
+ }
88
+ }
89
+ // Bottom-left finder
90
+ for (let y = gridH - finderFootprint; y < gridH; y++) {
91
+ for (let x = 0; x < finderFootprint; x++) {
92
+ reserved[y * gridW + x] = 1;
93
+ }
94
+ }
95
+ // Bottom-right finder
96
+ for (let y = gridH - finderFootprint; y < gridH; y++) {
97
+ for (let x = gridW - finderFootprint; x < gridW; x++) {
98
+ reserved[y * gridW + x] = 1;
99
+ }
100
+ }
101
+ // Collect available data positions (row-by-row)
102
+ const dataPositions = [];
103
+ for (let y = 0; y < gridH; y++) {
104
+ const rowBase = y * gridW;
105
+ for (let x = 0; x < gridW; x++) {
106
+ if (!reserved[rowBase + x]) {
107
+ dataPositions.push([x, y]);
108
+ }
109
+ }
110
+ }
111
+ return { gridW, gridH, dataPositions };
112
+ }
113
+ // ─── Block Rendering ────────────────────────────────────────────────────────
114
+ /**
115
+ * Render the block grid into an RGB pixel buffer.
116
+ * Optimized: fills row spans instead of individual pixels.
117
+ *
118
+ * @param grid - 2D grid of block values (0 = black, 255 = white).
119
+ * @param blockSize - Pixel size of each block (e.g., 4 = 4×4 pixels per block).
120
+ * @returns { rgb, width, height } - Raw RGB buffer and pixel dimensions.
121
+ */
122
+ function renderGrid(grid, gridW, gridH, blockSize) {
123
+ const width = gridW * blockSize;
124
+ const height = gridH * blockSize;
125
+ const rgb = Buffer.alloc(width * height * 3);
126
+ const stride = width * 3;
127
+ for (let by = 0; by < gridH; by++) {
128
+ const row = grid[by];
129
+ // Build one pixel row for this block row
130
+ const firstRowOffset = by * blockSize * stride;
131
+ for (let bx = 0; bx < gridW; bx++) {
132
+ const val = row[bx];
133
+ const pxStart = firstRowOffset + bx * blockSize * 3;
134
+ // Fill one row of pixel span for this block
135
+ for (let dx = 0; dx < blockSize; dx++) {
136
+ const off = pxStart + dx * 3;
137
+ rgb[off] = val;
138
+ rgb[off + 1] = val;
139
+ rgb[off + 2] = val;
140
+ }
141
+ }
142
+ // Copy the first pixel row to the remaining (blockSize - 1) rows
143
+ const srcStart = firstRowOffset;
144
+ for (let dy = 1; dy < blockSize; dy++) {
145
+ rgb.copy(rgb, firstRowOffset + dy * stride, srcStart, srcStart + stride);
146
+ }
147
+ }
148
+ return { rgb, width, height };
149
+ }
150
+ /**
151
+ * Place a finder pattern on the grid at a given position.
152
+ */
153
+ function placeFinderPattern(grid, startX, startY) {
154
+ const fp = finderPattern();
155
+ for (let y = 0; y < FINDER_SIZE; y++) {
156
+ for (let x = 0; x < FINDER_SIZE; x++) {
157
+ grid[startY + y][startX + x] = fp[y][x] ? 0 : 255;
158
+ }
159
+ }
160
+ }
161
+ // ─── Read Blocks from Image ─────────────────────────────────────────────────
162
+ /**
163
+ * Read block values from an RGB pixel buffer using majority voting.
164
+ * Each block's pixels are averaged, then thresholded to 0 or 255.
165
+ */
166
+ function readBlocks(rgb, width, height, blockSize, gridW, gridH) {
167
+ const grid = [];
168
+ for (let by = 0; by < gridH; by++) {
169
+ const row = new Uint8Array(gridW);
170
+ for (let bx = 0; bx < gridW; bx++) {
171
+ let sum = 0;
172
+ let count = 0;
173
+ for (let dy = 0; dy < blockSize; dy++) {
174
+ for (let dx = 0; dx < blockSize; dx++) {
175
+ const py = by * blockSize + dy;
176
+ const px = bx * blockSize + dx;
177
+ if (py < height && px < width) {
178
+ const idx = (py * width + px) * 3;
179
+ // Average RGB to grayscale
180
+ sum += (rgb[idx] + rgb[idx + 1] + rgb[idx + 2]) / 3;
181
+ count++;
182
+ }
183
+ }
184
+ }
185
+ // Threshold at midpoint (128)
186
+ row[bx] = count > 0 && sum / count > 128 ? 255 : 0;
187
+ }
188
+ grid.push(row);
189
+ }
190
+ return grid;
191
+ }
192
+ /**
193
+ * Detect finder patterns and extract grid parameters from an image.
194
+ * Returns the estimated block size and grid dimensions.
195
+ */
196
+ function detectFinderPatterns(rgb, width, height) {
197
+ // Try each candidate block size (2–8) and look for finder patterns
198
+ for (let bs = 2; bs <= 8; bs++) {
199
+ const gw = Math.floor(width / bs);
200
+ const gh = Math.floor(height / bs);
201
+ if (gw < FINDER_SIZE * 2 + 4 || gh < FINDER_SIZE * 2 + 4)
202
+ continue;
203
+ // Check top-left corner for finder pattern
204
+ const fp = finderPattern();
205
+ let matchCount = 0;
206
+ let totalChecked = 0;
207
+ for (let fy = 0; fy < FINDER_SIZE; fy++) {
208
+ for (let fx = 0; fx < FINDER_SIZE; fx++) {
209
+ const expected = fp[fy][fx] ? 0 : 255;
210
+ // Sample center of block
211
+ const py = fy * bs + Math.floor(bs / 2);
212
+ const px = fx * bs + Math.floor(bs / 2);
213
+ if (py >= height || px >= width)
214
+ continue;
215
+ const idx = (py * width + px) * 3;
216
+ const gray = (rgb[idx] + rgb[idx + 1] + rgb[idx + 2]) / 3;
217
+ const actual = gray > 128 ? 255 : 0;
218
+ totalChecked++;
219
+ if (actual === expected)
220
+ matchCount++;
221
+ }
222
+ }
223
+ // Require ≥80% match for finder detection
224
+ if (totalChecked > 0 && matchCount / totalChecked >= 0.8) {
225
+ return { blockSize: bs, gridW: gw, gridH: gh };
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+ // ─── Header Encoding ────────────────────────────────────────────────────────
231
+ /**
232
+ * Encode header bits into a sequence of data positions.
233
+ * Header: [4B magic] [1B blockSize] [1B eccLevel] [4B dataLen] [2B gridW] [2B gridH]
234
+ * Total: 14 bytes = 112 bits.
235
+ */
236
+ function encodeHeader(blockSize, eccLevel, dataLen, gridW, gridH) {
237
+ const header = Buffer.alloc(14);
238
+ ROBUST_IMG_MAGIC.copy(header, 0);
239
+ header[4] = blockSize;
240
+ header[5] = eccLevel;
241
+ header.writeUInt32BE(dataLen, 6);
242
+ header.writeUInt16BE(gridW, 10);
243
+ header.writeUInt16BE(gridH, 12);
244
+ return new Uint8Array(header);
245
+ }
246
+ function decodeHeader(bytes) {
247
+ if (bytes.length < 14)
248
+ return null;
249
+ const buf = Buffer.from(bytes);
250
+ if (!buf.subarray(0, 4).equals(ROBUST_IMG_MAGIC))
251
+ return null;
252
+ return {
253
+ blockSize: buf[4],
254
+ eccLevel: buf[5],
255
+ dataLen: buf.readUInt32BE(6),
256
+ gridW: buf.readUInt16BE(10),
257
+ gridH: buf.readUInt16BE(12),
258
+ };
259
+ }
260
+ // ─── Bit/Byte Packing ──────────────────────────────────────────────────────
261
+ /**
262
+ * Convert bytes to bits (MSB first within each byte).
263
+ */
264
+ function bytesToBits(data) {
265
+ const bits = new Uint8Array(data.length * 8);
266
+ for (let i = 0; i < data.length; i++) {
267
+ for (let bit = 7; bit >= 0; bit--) {
268
+ bits[i * 8 + (7 - bit)] = (data[i] >> bit) & 1;
269
+ }
270
+ }
271
+ return bits;
272
+ }
273
+ /**
274
+ * Convert bits back to bytes (MSB first).
275
+ */
276
+ function bitsToBytes(bits) {
277
+ const numBytes = Math.ceil(bits.length / 8);
278
+ const bytes = new Uint8Array(numBytes);
279
+ for (let i = 0; i < numBytes; i++) {
280
+ let byte = 0;
281
+ for (let bit = 0; bit < 8; bit++) {
282
+ const idx = i * 8 + bit;
283
+ if (idx < bits.length && bits[idx]) {
284
+ byte |= 1 << (7 - bit);
285
+ }
286
+ }
287
+ bytes[i] = byte;
288
+ }
289
+ return bytes;
290
+ }
291
+ const ECC_LEVEL_MAP = {
292
+ low: 0,
293
+ medium: 1,
294
+ quartile: 2,
295
+ high: 3,
296
+ };
297
+ const ECC_LEVEL_REVERSE = {
298
+ 0: 'low',
299
+ 1: 'medium',
300
+ 2: 'quartile',
301
+ 3: 'high',
302
+ };
303
+ /**
304
+ * Encode binary data into a lossy-resilient PNG image.
305
+ *
306
+ * The output uses QR-code-like techniques:
307
+ * - Finder patterns for alignment after re-encoding.
308
+ * - Large pixel blocks for JPEG/WebP resilience.
309
+ * - Reed-Solomon ECC for automatic error correction.
310
+ *
311
+ * @param data - Raw data to encode.
312
+ * @param opts - Encoding options.
313
+ * @returns PNG image as a Buffer.
314
+ */
315
+ export function encodeRobustImage(data, opts = {}) {
316
+ const blockSize = opts.blockSize ?? 4;
317
+ const eccLevel = opts.eccLevel ?? 'medium';
318
+ if (blockSize < 2 || blockSize > 8) {
319
+ throw new Error(`Block size must be 2–8, got ${blockSize}`);
320
+ }
321
+ // 1. Protect with ECC
322
+ const protectedData = eccEncode(data, eccLevel);
323
+ // 2. Convert header + protected data to bits
324
+ const headerBytes = encodeHeader(blockSize, ECC_LEVEL_MAP[eccLevel], data.length, 0, // gridW placeholder (filled after layout)
325
+ 0);
326
+ // Total payload: header (14 bytes) + protected data
327
+ const payload = Buffer.concat([Buffer.from(headerBytes), protectedData]);
328
+ const bits = bytesToBits(new Uint8Array(payload));
329
+ // 3. Compute layout
330
+ const layout = computeLayout(bits.length);
331
+ // Update header with actual grid dimensions
332
+ const headerFinal = encodeHeader(blockSize, ECC_LEVEL_MAP[eccLevel], data.length, layout.gridW, layout.gridH);
333
+ const payloadFinal = Buffer.concat([Buffer.from(headerFinal), protectedData]);
334
+ const bitsFinal = bytesToBits(new Uint8Array(payloadFinal));
335
+ // Re-layout with correct size if needed
336
+ const finalLayout = computeLayout(bitsFinal.length);
337
+ if (finalLayout.dataPositions.length < bitsFinal.length) {
338
+ throw new Error(`Data too large for image: need ${bitsFinal.length} blocks, have ${finalLayout.dataPositions.length}`);
339
+ }
340
+ // 4. Build the block grid
341
+ const { gridW, gridH, dataPositions } = finalLayout;
342
+ const grid = [];
343
+ for (let y = 0; y < gridH; y++) {
344
+ grid.push(new Uint8Array(gridW).fill(255)); // white background
345
+ }
346
+ // Place finder patterns at 4 corners
347
+ placeFinderPattern(grid, 0, 0);
348
+ placeFinderPattern(grid, gridW - FINDER_SIZE, 0);
349
+ placeFinderPattern(grid, 0, gridH - FINDER_SIZE);
350
+ placeFinderPattern(grid, gridW - FINDER_SIZE, gridH - FINDER_SIZE);
351
+ // Place data bits
352
+ for (let i = 0; i < bitsFinal.length && i < dataPositions.length; i++) {
353
+ const [bx, by] = dataPositions[i];
354
+ grid[by][bx] = bitsFinal[i] ? 0 : 255; // 1 = black, 0 = white
355
+ }
356
+ // 5. Render to pixels
357
+ const { rgb, width, height } = renderGrid(grid, gridW, gridH, blockSize);
358
+ // 6. Encode as PNG
359
+ if (native?.rgbToPng) {
360
+ return Buffer.from(native.rgbToPng(rgb, width, height));
361
+ }
362
+ // Fallback: manual PNG generation (minimal, no compression)
363
+ return manualPngEncode(rgb, width, height);
364
+ }
365
+ /**
366
+ * Decode binary data from a lossy-resilient PNG image.
367
+ *
368
+ * Handles images that have been re-encoded through JPEG/WebP at various
369
+ * quality levels. The Reed-Solomon ECC layer corrects any bit errors
370
+ * introduced by lossy compression.
371
+ *
372
+ * @param png - PNG (or raw RGB) image buffer.
373
+ * @returns Decoded data and error correction stats.
374
+ */
375
+ export function decodeRobustImage(png) {
376
+ // 1. Get raw pixels
377
+ let width;
378
+ let height;
379
+ let rgb;
380
+ if (native?.sharpMetadata && native?.sharpToRaw) {
381
+ const meta = native.sharpMetadata(png);
382
+ width = meta.width;
383
+ height = meta.height;
384
+ const raw = native.sharpToRaw(png);
385
+ rgb = Buffer.from(raw.pixels);
386
+ }
387
+ else {
388
+ throw new Error('Robust image decoding requires the native module (sharpMetadata + sharpToRaw)');
389
+ }
390
+ // 2. Detect finder patterns to determine block size and grid
391
+ const detection = detectFinderPatterns(rgb, width, height);
392
+ if (!detection) {
393
+ throw new Error('Could not detect finder patterns — image may be too corrupted');
394
+ }
395
+ const { blockSize, gridW, gridH } = detection;
396
+ // 3. Read block grid
397
+ const grid = readBlocks(rgb, width, height, blockSize, gridW, gridH);
398
+ // 4. Compute layout (same algorithm as encoding)
399
+ const finderFootprint = FINDER_SIZE + QUIET_ZONE;
400
+ const reserved = new Uint8Array(gridW * gridH);
401
+ for (let y = 0; y < finderFootprint; y++) {
402
+ for (let x = 0; x < finderFootprint; x++) {
403
+ reserved[y * gridW + x] = 1;
404
+ }
405
+ }
406
+ for (let y = 0; y < finderFootprint; y++) {
407
+ for (let x = gridW - finderFootprint; x < gridW; x++) {
408
+ reserved[y * gridW + x] = 1;
409
+ }
410
+ }
411
+ for (let y = gridH - finderFootprint; y < gridH; y++) {
412
+ for (let x = 0; x < finderFootprint; x++) {
413
+ reserved[y * gridW + x] = 1;
414
+ }
415
+ }
416
+ for (let y = gridH - finderFootprint; y < gridH; y++) {
417
+ for (let x = gridW - finderFootprint; x < gridW; x++) {
418
+ reserved[y * gridW + x] = 1;
419
+ }
420
+ }
421
+ const dataPositions = [];
422
+ for (let y = 0; y < gridH; y++) {
423
+ const rowBase = y * gridW;
424
+ for (let x = 0; x < gridW; x++) {
425
+ if (!reserved[rowBase + x]) {
426
+ dataPositions.push([x, y]);
427
+ }
428
+ }
429
+ }
430
+ // 5. Extract bits from data positions
431
+ const bits = new Uint8Array(dataPositions.length);
432
+ for (let i = 0; i < dataPositions.length; i++) {
433
+ const [bx, by] = dataPositions[i];
434
+ if (by < grid.length && bx < grid[by].length) {
435
+ bits[i] = grid[by][bx] === 0 ? 1 : 0; // black = 1, white = 0
436
+ }
437
+ }
438
+ // 6. Convert bits to bytes
439
+ const allBytes = bitsToBytes(bits);
440
+ // 7. Parse header (first 14 bytes)
441
+ const header = decodeHeader(allBytes);
442
+ if (!header) {
443
+ throw new Error('Invalid robust image header — data may be corrupted');
444
+ }
445
+ // 8. Extract ECC-protected payload
446
+ const eccData = Buffer.from(allBytes.subarray(14));
447
+ // 9. Decode ECC
448
+ const { data, totalCorrected } = eccDecode(eccData);
449
+ // 10. Trim to original length
450
+ return {
451
+ data: data.subarray(0, header.dataLen),
452
+ correctedErrors: totalCorrected,
453
+ };
454
+ }
455
+ /**
456
+ * Check if a PNG buffer contains a robust-image-encoded payload.
457
+ * Looks for finder patterns in the corners.
458
+ */
459
+ export function isRobustImage(png) {
460
+ try {
461
+ if (!native?.sharpMetadata || !native?.sharpToRaw)
462
+ return false;
463
+ const meta = native.sharpMetadata(png);
464
+ const raw = native.sharpToRaw(png);
465
+ const rgb = Buffer.from(raw.pixels);
466
+ return detectFinderPatterns(rgb, meta.width, meta.height) !== null;
467
+ }
468
+ catch {
469
+ return false;
470
+ }
471
+ }
472
+ // ─── Fallback PNG Encoder ───────────────────────────────────────────────────
473
+ /**
474
+ * Minimal PNG encoder for when the native module is unavailable.
475
+ * Uses uncompressed IDAT (zlib stored blocks).
476
+ */
477
+ function manualPngEncode(rgb, width, height) {
478
+ const zlib = require('zlib');
479
+ // Build raw image data with filter byte (0 = None) per row
480
+ const bytesPerRow = width * 3;
481
+ const rawData = Buffer.alloc(height * (1 + bytesPerRow));
482
+ for (let y = 0; y < height; y++) {
483
+ rawData[y * (1 + bytesPerRow)] = 0; // filter: None
484
+ rgb.copy(rawData, y * (1 + bytesPerRow) + 1, y * bytesPerRow, (y + 1) * bytesPerRow);
485
+ }
486
+ const deflated = zlib.deflateSync(rawData, { level: 0 });
487
+ // PNG signature
488
+ const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
489
+ // IHDR chunk
490
+ const ihdr = Buffer.alloc(13);
491
+ ihdr.writeUInt32BE(width, 0);
492
+ ihdr.writeUInt32BE(height, 4);
493
+ ihdr[8] = 8; // bit depth
494
+ ihdr[9] = 2; // color type: RGB
495
+ ihdr[10] = 0; // compression
496
+ ihdr[11] = 0; // filter
497
+ ihdr[12] = 0; // interlace
498
+ function pngChunk(type, data) {
499
+ const typeBuf = Buffer.from(type, 'ascii');
500
+ const length = Buffer.alloc(4);
501
+ length.writeUInt32BE(data.length, 0);
502
+ const combined = Buffer.concat([typeBuf, data]);
503
+ const { crc32: crc32Fn } = require('./crc.js');
504
+ const crcVal = crc32Fn(data, crc32Fn(typeBuf));
505
+ const crcBuf = Buffer.alloc(4);
506
+ crcBuf.writeUInt32BE(crcVal >>> 0, 0);
507
+ return Buffer.concat([length, combined, crcBuf]);
508
+ }
509
+ return Buffer.concat([
510
+ sig,
511
+ pngChunk('IHDR', ihdr),
512
+ pngChunk('IDAT', deflated),
513
+ pngChunk('IEND', Buffer.alloc(0)),
514
+ ]);
515
+ }
@@ -1,4 +1,5 @@
1
1
  import { PackedFile } from '../pack.js';
2
+ import type { EccLevel } from './ecc.js';
2
3
  export interface EncodeOptions {
3
4
  compression?: 'zstd';
4
5
  compressionLevel?: number;
@@ -11,6 +12,27 @@ export interface EncodeOptions {
11
12
  _skipAuto?: boolean;
12
13
  output?: 'auto' | 'png' | 'rox';
13
14
  outputFormat?: 'png' | 'webp';
15
+ /** Container format: 'image' (PNG, default) or 'sound' (WAV) */
16
+ container?: 'image' | 'sound';
17
+ /**
18
+ * Enable lossy-resilient encoding. When true, the output survives lossy
19
+ * compression (MP3/AAC for audio, JPEG/WebP for image) using QR-code-like
20
+ * error correction and block-based encoding.
21
+ */
22
+ lossyResilient?: boolean;
23
+ /**
24
+ * Error correction level for lossy-resilient mode.
25
+ * - 'low': ~10% redundancy, corrects ~4% errors
26
+ * - 'medium': ~19% redundancy, corrects ~9% errors (default)
27
+ * - 'quartile': ~33% redundancy, corrects ~15% errors
28
+ * - 'high': ~100% redundancy, corrects ~25% errors
29
+ */
30
+ eccLevel?: EccLevel;
31
+ /**
32
+ * Block size for lossy-resilient image mode (2–8 pixels per data block).
33
+ * Larger blocks survive heavier lossy compression. Default: 4.
34
+ */
35
+ robustBlockSize?: number;
14
36
  includeName?: boolean;
15
37
  includeFileList?: boolean;
16
38
  fileList?: Array<string | {
@@ -33,6 +55,8 @@ export interface DecodeResult {
33
55
  name?: string;
34
56
  };
35
57
  files?: PackedFile[];
58
+ /** Number of symbol errors corrected by Reed-Solomon ECC (lossy-resilient mode). */
59
+ correctedErrors?: number;
36
60
  }
37
61
  export interface DecodeOptions {
38
62
  passphrase?: string;
@@ -46,6 +46,31 @@ export async function compressStream(stream, level = 19, onProgress, dict) {
46
46
  }
47
47
  export async function parallelZstdCompress(payload, level = 19, onProgress, dict) {
48
48
  const chunkSize = 8 * 1024 * 1024;
49
+ // For small payloads (< chunkSize), concatenate and compress as single frame
50
+ // to avoid multi-chunk overhead (16+ bytes header per chunk boundary).
51
+ let flat = null;
52
+ if (Array.isArray(payload)) {
53
+ const totalLen = payload.reduce((a, b) => a + b.length, 0);
54
+ if (totalLen <= chunkSize) {
55
+ flat = Buffer.concat(payload);
56
+ }
57
+ }
58
+ else {
59
+ flat = payload;
60
+ }
61
+ if (flat && flat.length <= chunkSize) {
62
+ if (onProgress)
63
+ onProgress(0, 1);
64
+ if (!nativeZstdCompress && !nativeZstdCompressWithDict) {
65
+ throw new Error('Native zstd compression not available');
66
+ }
67
+ const result = Buffer.from(nativeZstdCompressWithDict && dict ?
68
+ nativeZstdCompressWithDict(flat, level, dict)
69
+ : nativeZstdCompress(flat, level));
70
+ if (onProgress)
71
+ onProgress(1, 1);
72
+ return [result];
73
+ }
49
74
  const chunks = [];
50
75
  if (Array.isArray(payload)) {
51
76
  for (const p of payload) {
@@ -60,17 +85,6 @@ export async function parallelZstdCompress(payload, level = 19, onProgress, dict
60
85
  }
61
86
  }
62
87
  else {
63
- if (payload.length <= chunkSize) {
64
- if (onProgress)
65
- onProgress(0, 1);
66
- if (!nativeZstdCompress) {
67
- throw new Error('Native zstd compression not available');
68
- }
69
- const result = Buffer.from(nativeZstdCompress(payload, level));
70
- if (onProgress)
71
- onProgress(1, 1);
72
- return [result];
73
- }
74
88
  for (let i = 0; i < payload.length; i += chunkSize) {
75
89
  chunks.push(payload.subarray(i, Math.min(i + chunkSize, payload.length)));
76
90
  }
@@ -132,7 +146,7 @@ export async function parallelZstdDecompress(payload, onProgress) {
132
146
  const decompressedChunks = [];
133
147
  for (let i = 0; i < numChunks; i++) {
134
148
  const size = chunkSizes[i];
135
- const chunk = payload.slice(offset, offset + size);
149
+ const chunk = payload.subarray(offset, offset + size);
136
150
  offset += size;
137
151
  if (!nativeZstdDecompress) {
138
152
  throw new Error('Native zstd decompression not available');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.6.6",
3
+ "version": "1.6.7",
4
4
  "type": "module",
5
5
  "description": "Ultra-lightweight PNG steganography with native Rust acceleration. Encode binary data into PNG images with zstd compression.",
6
6
  "main": "dist/index.js",
Binary file