roxify 1.6.5 → 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.
@@ -1,10 +1,13 @@
1
1
  import { createCipheriv, pbkdf2Sync, randomBytes } from 'crypto';
2
2
  import * as zlib from 'zlib';
3
3
  import { unpackBuffer } from '../pack.js';
4
+ import { bytesToWav } from './audio.js';
4
5
  import { COMPRESSION_MARKERS, ENC_AES, ENC_NONE, ENC_XOR, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PIXEL_MAGIC_BLOCK, PNG_HEADER, } from './constants.js';
5
6
  import { crc32 } from './crc.js';
6
7
  import { colorsToBytes } from './helpers.js';
7
8
  import { native } from './native.js';
9
+ import { encodeRobustAudio } from './robust-audio.js';
10
+ import { encodeRobustImage } from './robust-image.js';
8
11
  import { parallelZstdCompress } from './zstd.js';
9
12
  /**
10
13
  * Encode a buffer or array of buffers into a PNG image (ROX format).
@@ -56,6 +59,105 @@ export async function encodeBinaryToPng(input, opts = {}) {
56
59
  };
57
60
  }
58
61
  }
62
+ const compressionLevel = opts.compressionLevel ?? 19;
63
+ // ─── Lossy-resilient encoding fast path ────────────────────────────────────
64
+ // When lossyResilient is true, use QR-code-style block encoding with
65
+ // Reed-Solomon FEC. This produces output that survives lossy compression.
66
+ if (opts.lossyResilient) {
67
+ const inputBuf = Array.isArray(input) ? Buffer.concat(input) : input;
68
+ if (opts.onProgress)
69
+ opts.onProgress({ phase: 'compress_start', total: inputBuf.length });
70
+ if (opts.container === 'sound') {
71
+ // Robust audio encoding (multi-tone FSK + RS ECC)
72
+ const result = encodeRobustAudio(inputBuf, {
73
+ eccLevel: opts.eccLevel ?? 'medium',
74
+ });
75
+ if (opts.onProgress)
76
+ opts.onProgress({ phase: 'done' });
77
+ progressBar?.stop();
78
+ return result;
79
+ }
80
+ else {
81
+ // Robust image encoding (QR-code-like blocks + RS ECC)
82
+ const result = encodeRobustImage(inputBuf, {
83
+ blockSize: opts.robustBlockSize ?? 4,
84
+ eccLevel: opts.eccLevel ?? 'medium',
85
+ });
86
+ if (opts.onProgress)
87
+ opts.onProgress({ phase: 'done' });
88
+ progressBar?.stop();
89
+ return result;
90
+ }
91
+ }
92
+ // --- Native encoder fast path: let Rust handle compression/encryption/PNG ---
93
+ // This must be checked BEFORE TS compression to avoid double-compression.
94
+ if (typeof native.nativeEncodePngWithNameAndFilelist === 'function' &&
95
+ opts.includeFileList &&
96
+ opts.fileList) {
97
+ const fileName = opts.name || undefined;
98
+ const inputBuf = Array.isArray(input) ? Buffer.concat(input) : input;
99
+ let sizeMap = null;
100
+ try {
101
+ const unpack = unpackBuffer(inputBuf);
102
+ if (unpack) {
103
+ sizeMap = {};
104
+ for (const ef of unpack.files)
105
+ sizeMap[ef.path] = ef.buf.length;
106
+ }
107
+ }
108
+ catch (e) { }
109
+ const normalized = opts.fileList.map((f) => {
110
+ if (typeof f === 'string')
111
+ return { name: f, size: sizeMap && sizeMap[f] ? sizeMap[f] : 0 };
112
+ if (f && typeof f === 'object') {
113
+ if (f.name)
114
+ return { name: f.name, size: f.size ?? 0 };
115
+ if (f.path)
116
+ return { name: f.path, size: f.size ?? 0 };
117
+ }
118
+ return { name: String(f), size: 0 };
119
+ });
120
+ const fileListJson = JSON.stringify(normalized);
121
+ if (opts.onProgress)
122
+ opts.onProgress({ phase: 'compress_start', total: inputBuf.length });
123
+ // ── WAV container (--sound) via native Rust encoder ──
124
+ if (opts.container === 'sound') {
125
+ if (typeof native.nativeEncodeWavWithEncryptionNameAndFilelist === 'function' &&
126
+ opts.passphrase && opts.encrypt && opts.encrypt !== 'auto') {
127
+ const result = native.nativeEncodeWavWithEncryptionNameAndFilelist(inputBuf, compressionLevel, opts.passphrase, opts.encrypt, fileName, fileListJson);
128
+ if (opts.onProgress)
129
+ opts.onProgress({ phase: 'done' });
130
+ progressBar?.stop();
131
+ return Buffer.from(result);
132
+ }
133
+ else if (typeof native.nativeEncodeWavWithNameAndFilelist === 'function') {
134
+ const result = native.nativeEncodeWavWithNameAndFilelist(inputBuf, compressionLevel, fileName, fileListJson);
135
+ if (opts.onProgress)
136
+ opts.onProgress({ phase: 'done' });
137
+ progressBar?.stop();
138
+ return Buffer.from(result);
139
+ }
140
+ // fallthrough to TS WAV path below if native WAV not available
141
+ }
142
+ // ── PNG container (default) via native Rust encoder ──
143
+ if (opts.container !== 'sound') {
144
+ if (opts.passphrase && opts.encrypt && opts.encrypt !== 'auto') {
145
+ const result = native.nativeEncodePngWithEncryptionNameAndFilelist(inputBuf, compressionLevel, opts.passphrase, opts.encrypt, fileName, fileListJson);
146
+ if (opts.onProgress)
147
+ opts.onProgress({ phase: 'done' });
148
+ progressBar?.stop();
149
+ return Buffer.from(result);
150
+ }
151
+ else {
152
+ const result = native.nativeEncodePngWithNameAndFilelist(inputBuf, compressionLevel, fileName, fileListJson);
153
+ if (opts.onProgress)
154
+ opts.onProgress({ phase: 'done' });
155
+ progressBar?.stop();
156
+ return Buffer.from(result);
157
+ }
158
+ }
159
+ }
160
+ // --- TypeScript compression/encryption pipeline ---
59
161
  let payloadInput;
60
162
  let totalLen = 0;
61
163
  if (Array.isArray(input)) {
@@ -68,7 +170,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
68
170
  }
69
171
  if (opts.onProgress)
70
172
  opts.onProgress({ phase: 'compress_start', total: totalLen });
71
- const compressionLevel = opts.compressionLevel ?? 19;
72
173
  let payload = await parallelZstdCompress(payloadInput, compressionLevel, (loaded, total) => {
73
174
  if (opts.onProgress) {
74
175
  opts.onProgress({
@@ -77,7 +178,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
77
178
  total,
78
179
  });
79
180
  }
80
- });
181
+ }, opts.dict);
81
182
  if (opts.onProgress)
82
183
  opts.onProgress({ phase: 'compress_done', loaded: payload.length });
83
184
  if (Array.isArray(input)) {
@@ -148,48 +249,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
148
249
  const payloadTotalLen = payload.reduce((a, b) => a + b.length, 0);
149
250
  if (opts.onProgress)
150
251
  opts.onProgress({ phase: 'meta_prep_done', loaded: payloadTotalLen });
151
- if (typeof native.nativeEncodePngWithNameAndFilelist === 'function' &&
152
- opts.includeFileList &&
153
- opts.fileList) {
154
- const fileName = opts.name || undefined;
155
- let sizeMap = null;
156
- if (!Array.isArray(input)) {
157
- try {
158
- const unpack = unpackBuffer(input);
159
- if (unpack) {
160
- sizeMap = {};
161
- for (const ef of unpack.files)
162
- sizeMap[ef.path] = ef.buf.length;
163
- }
164
- }
165
- catch (e) { }
166
- }
167
- const normalized = opts.fileList.map((f) => {
168
- if (typeof f === 'string')
169
- return { name: f, size: sizeMap && sizeMap[f] ? sizeMap[f] : 0 };
170
- if (f && typeof f === 'object') {
171
- if (f.name)
172
- return { name: f.name, size: f.size ?? 0 };
173
- if (f.path)
174
- return { name: f.path, size: f.size ?? 0 };
175
- }
176
- return { name: String(f), size: 0 };
177
- });
178
- const fileListJson = JSON.stringify(normalized);
179
- const flatPayload = Buffer.concat(payload);
180
- if (opts.passphrase && opts.encrypt && opts.encrypt !== 'auto') {
181
- const result = native.nativeEncodePngWithEncryptionNameAndFilelist(flatPayload, compressionLevel, opts.passphrase, opts.encrypt, fileName, fileListJson);
182
- if (opts.onProgress)
183
- opts.onProgress({ phase: 'done' });
184
- return Buffer.from(result);
185
- }
186
- else {
187
- const result = native.nativeEncodePngWithNameAndFilelist(flatPayload, compressionLevel, fileName, fileListJson);
188
- if (opts.onProgress)
189
- opts.onProgress({ phase: 'done' });
190
- return Buffer.from(result);
191
- }
192
- }
193
252
  const metaParts = [];
194
253
  const includeName = opts.includeName === undefined ? true : !!opts.includeName;
195
254
  if (includeName && opts.name) {
@@ -233,10 +292,57 @@ export async function encodeBinaryToPng(input, opts = {}) {
233
292
  if (opts.output === 'rox') {
234
293
  return Buffer.concat([MAGIC, ...meta]);
235
294
  }
295
+ // ─── WAV container (TS fallback path) ──────────────────────────────────────
296
+ if (opts.container === 'sound') {
297
+ const nameBuf = opts.name ? Buffer.from(opts.name, 'utf8') : Buffer.alloc(0);
298
+ const nameLen = nameBuf.length;
299
+ const payloadLenBuf = Buffer.alloc(4);
300
+ payloadLenBuf.writeUInt32BE(payloadTotalLen, 0);
301
+ const version = 1;
302
+ let wavPayload = [
303
+ PIXEL_MAGIC,
304
+ Buffer.from([version]),
305
+ Buffer.from([nameLen]),
306
+ nameBuf,
307
+ payloadLenBuf,
308
+ ...payload,
309
+ ];
310
+ if (opts.includeFileList && opts.fileList) {
311
+ let sizeMapW = null;
312
+ if (!Array.isArray(input)) {
313
+ try {
314
+ const unpack = unpackBuffer(input);
315
+ if (unpack) {
316
+ sizeMapW = {};
317
+ for (const ef of unpack.files)
318
+ sizeMapW[ef.path] = ef.buf.length;
319
+ }
320
+ }
321
+ catch (e) { }
322
+ }
323
+ const normalizedW = opts.fileList.map((f) => {
324
+ if (typeof f === 'string')
325
+ return { name: f, size: sizeMapW && sizeMapW[f] ? sizeMapW[f] : 0 };
326
+ if (f && typeof f === 'object') {
327
+ if (f.name)
328
+ return { name: f.name, size: f.size ?? 0 };
329
+ if (f.path)
330
+ return { name: f.path, size: f.size ?? 0 };
331
+ }
332
+ return { name: String(f), size: 0 };
333
+ });
334
+ const jsonBufW = Buffer.from(JSON.stringify(normalizedW), 'utf8');
335
+ const lenBufW = Buffer.alloc(4);
336
+ lenBufW.writeUInt32BE(jsonBufW.length, 0);
337
+ wavPayload = [...wavPayload, Buffer.from('rXFL', 'utf8'), lenBufW, jsonBufW];
338
+ }
339
+ const wavData = bytesToWav(Buffer.concat(wavPayload));
340
+ payload.length = 0;
341
+ progressBar?.stop();
342
+ return wavData;
343
+ }
236
344
  {
237
- const nameBuf = opts.name
238
- ? Buffer.from(opts.name, 'utf8')
239
- : Buffer.alloc(0);
345
+ const nameBuf = opts.name ? Buffer.from(opts.name, 'utf8') : Buffer.alloc(0);
240
346
  const nameLen = nameBuf.length;
241
347
  const payloadLenBuf = Buffer.alloc(4);
242
348
  payloadLenBuf.writeUInt32BE(payloadTotalLen, 0);
@@ -282,8 +388,8 @@ export async function encodeBinaryToPng(input, opts = {}) {
282
388
  const dataWithoutMarkers = [pixelMagic, ...metaPixel];
283
389
  const dataWithoutMarkersLen = dataWithoutMarkers.reduce((a, b) => a + b.length, 0);
284
390
  const padding = (3 - (dataWithoutMarkersLen % 3)) % 3;
285
- const paddedData = padding > 0
286
- ? [...dataWithoutMarkers, Buffer.alloc(padding)]
391
+ const paddedData = padding > 0 ?
392
+ [...dataWithoutMarkers, Buffer.alloc(padding)]
287
393
  : dataWithoutMarkers;
288
394
  const markerStartBytes = colorsToBytes(MARKER_START);
289
395
  const compressionMarkerBytes = colorsToBytes(COMPRESSION_MARKERS.zstd);
@@ -1,8 +1,7 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { createRequire } from 'module';
3
3
  import { arch, platform } from 'os';
4
- import { dirname, resolve } from 'path';
5
- import { fileURLToPath } from 'url';
4
+ import { join, resolve } from 'path';
6
5
  function getNativeModule() {
7
6
  let moduleDir;
8
7
  let nativeRequire;
@@ -11,18 +10,18 @@ function getNativeModule() {
11
10
  nativeRequire = require;
12
11
  }
13
12
  else {
14
- // ESM: derive module directory from this file's URL and create a require based on it
15
- moduleDir = dirname(fileURLToPath(import.meta.url));
13
+ moduleDir = process.cwd();
16
14
  try {
17
15
  nativeRequire = require;
18
16
  }
19
17
  catch {
20
- nativeRequire = createRequire(import.meta.url);
18
+ nativeRequire = createRequire(process.cwd() + '/package.json');
21
19
  }
22
20
  }
23
21
  function getNativePath() {
24
22
  const platformMap = {
25
23
  linux: 'x86_64-unknown-linux-gnu',
24
+ darwin: arch() === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin',
26
25
  win32: 'x86_64-pc-windows-gnu',
27
26
  };
28
27
  const platformAltMap = {
@@ -30,6 +29,7 @@ function getNativeModule() {
30
29
  };
31
30
  const extMap = {
32
31
  linux: 'so',
32
+ darwin: 'dylib',
33
33
  win32: 'node',
34
34
  };
35
35
  const currentPlatform = platform();
@@ -39,18 +39,12 @@ function getNativeModule() {
39
39
  if (!target || !ext) {
40
40
  throw new Error(`Unsupported platform: ${currentPlatform}`);
41
41
  }
42
- const targets = targetAlt ? [target, targetAlt] : [target];
43
- const candidates = [];
44
- const pushIf = (...paths) => {
45
- for (const p of paths)
46
- candidates.push(p);
47
- };
48
- // Try multiple locations relative to the compiled JS file (dist),
49
- // package root and common 'dist' directories used by packaging tools.
50
- for (const t of targets) {
51
- pushIf(resolve(moduleDir, `../roxify_native-${t}.node`), resolve(moduleDir, `../libroxify_native-${t}.node`), resolve(moduleDir, `../../roxify_native-${t}.node`), resolve(moduleDir, `../../libroxify_native-${t}.node`), resolve(moduleDir, `../dist/roxify_native-${t}.node`), resolve(moduleDir, `../../dist/roxify_native-${t}.node`));
52
- }
53
- pushIf(resolve(moduleDir, '../roxify_native.node'), resolve(moduleDir, '../libroxify_native.node'), resolve(moduleDir, '../../roxify_native.node'), resolve(moduleDir, '../../libroxify_native.node'), resolve(moduleDir, '../dist/roxify_native.node'), resolve(moduleDir, '../../dist/roxify_native.node'));
42
+ const prebuiltPath = join(moduleDir, '../../roxify_native.node');
43
+ const prebuiltLibPath = join(moduleDir, '../../libroxify_native.node');
44
+ const bundlePath = join(moduleDir, '../roxify_native.node');
45
+ const bundleLibPath = join(moduleDir, '../libroxify_native.node');
46
+ const bundlePathWithTarget = join(moduleDir, `../roxify_native-${target}.node`);
47
+ const bundleLibPathWithTarget = join(moduleDir, `../libroxify_native-${target}.node`);
54
48
  let root = moduleDir && moduleDir !== '.' ? moduleDir : process.cwd();
55
49
  while (root.length > 1 &&
56
50
  !existsSync(resolve(root, 'package.json')) &&
@@ -60,13 +54,38 @@ function getNativeModule() {
60
54
  break;
61
55
  root = parent;
62
56
  }
57
+ const bundleNode = resolve(moduleDir, '../roxify_native.node');
58
+ const bundleLibNode = resolve(moduleDir, '../libroxify_native.node');
59
+ const bundleNodeWithTarget = resolve(moduleDir, `../roxify_native-${target}.node`);
60
+ const bundleLibNodeWithTarget = resolve(moduleDir, `../libroxify_native-${target}.node`);
61
+ const repoNode = resolve(root, 'roxify_native.node');
62
+ const repoLibNode = resolve(root, 'libroxify_native.node');
63
+ const repoNodeWithTarget = resolve(root, `roxify_native-${target}.node`);
64
+ const repoLibNodeWithTarget = resolve(root, `libroxify_native-${target}.node`);
65
+ const targetNode = resolve(root, 'target/release/roxify_native.node');
66
+ const targetSo = resolve(root, 'target/release/roxify_native.so');
67
+ const targetLibSo = resolve(root, 'target/release/libroxify_native.so');
68
+ const nodeModulesNode = resolve(root, 'node_modules/roxify/roxify_native.node');
69
+ const nodeModulesNodeWithTarget = resolve(root, `node_modules/roxify/roxify_native-${target}.node`);
70
+ const prebuiltNode = resolve(moduleDir, '../../roxify_native.node');
71
+ const prebuiltLibNode = resolve(moduleDir, '../../libroxify_native.node');
72
+ const prebuiltNodeWithTarget = resolve(moduleDir, `../../roxify_native-${target}.node`);
73
+ const prebuiltLibNodeWithTarget = resolve(moduleDir, `../../libroxify_native-${target}.node`);
74
+ // Support multiple possible OS triples (e.g. windows-gnu and windows-msvc)
75
+ const targets = targetAlt ? [target, targetAlt] : [target];
76
+ const candidates = [];
63
77
  for (const t of targets) {
64
- pushIf(resolve(root, `roxify_native-${t}.node`), resolve(root, `libroxify_native-${t}.node`), resolve(root, `dist/roxify_native-${t}.node`));
78
+ const bundleNodeWithT = resolve(moduleDir, `../roxify_native-${t}.node`);
79
+ const bundleLibNodeWithT = resolve(moduleDir, `../libroxify_native-${t}.node`);
80
+ const repoNodeWithT = resolve(root, `roxify_native-${t}.node`);
81
+ const repoLibNodeWithT = resolve(root, `libroxify_native-${t}.node`);
82
+ const nodeModulesNodeWithT = resolve(root, `node_modules/roxify/roxify_native-${t}.node`);
83
+ const prebuiltNodeWithT = resolve(moduleDir, `../../roxify_native-${t}.node`);
84
+ const prebuiltLibNodeWithT = resolve(moduleDir, `../../libroxify_native-${t}.node`);
85
+ candidates.push(bundleLibNodeWithT, bundleNodeWithT, repoLibNodeWithT, repoNodeWithT, nodeModulesNodeWithT, prebuiltLibNodeWithT, prebuiltNodeWithT);
65
86
  }
66
- pushIf(resolve(root, 'roxify_native.node'), resolve(root, 'libroxify_native.node'), resolve(root, 'dist/roxify_native.node'), resolve(root, 'target/release/roxify_native.node'), resolve(root, 'target/release/libroxify_native.so'), resolve(root, 'target/release/roxify_native.so'), resolve(root, 'node_modules/roxify/roxify_native.node'));
67
- // Remove duplicates while preserving order
68
- const uniqueCandidates = [...new Set(candidates)];
69
- for (const c of uniqueCandidates) {
87
+ candidates.push(bundleLibNode, bundleNode, repoLibNode, repoNode, targetNode, targetLibSo, targetSo, nodeModulesNode, prebuiltLibNode, prebuiltNode);
88
+ for (const c of candidates) {
70
89
  try {
71
90
  if (!existsSync(c))
72
91
  continue;
@@ -84,11 +103,9 @@ function getNativeModule() {
84
103
  }
85
104
  return c;
86
105
  }
87
- catch (e) {
88
- // ignore errors while checking candidates
89
- }
106
+ catch { }
90
107
  }
91
- throw new Error(`Native module not found for ${currentPlatform}-${arch()}. Checked: ${uniqueCandidates.join(' ')}`);
108
+ throw new Error(`Native module not found for ${currentPlatform}-${arch()}. Checked: ${candidates.join(' ')}`);
92
109
  }
93
110
  return nativeRequire(getNativePath());
94
111
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Lossy-Resilient Audio Encoding (WAV container).
3
+ *
4
+ * Encodes binary data as multi-frequency tones (OFDM-like) that survive
5
+ * lossy audio compression (MP3, AAC, OGG Vorbis). The output sounds like
6
+ * a series of harmonious chords — not white noise.
7
+ *
8
+ * Architecture:
9
+ * 1. Data is protected with Reed-Solomon ECC (configurable level).
10
+ * 2. Each byte is transmitted as 8 simultaneous frequency channels
11
+ * (one bit per channel: tone present = 1, absent = 0).
12
+ * 3. Raised-cosine windowing prevents spectral splatter.
13
+ * 4. A chirp sync preamble enables frame alignment after lossy re-encoding.
14
+ *
15
+ * Frequency plan:
16
+ * 8 carriers at 600, 900, 1200, 1500, 1800, 2100, 2400, 2700 Hz.
17
+ * All within the 300–3400 Hz band preserved by most lossy codecs.
18
+ *
19
+ * Throughput: ~17 bytes/sec raw (with default symbol timing).
20
+ */
21
+ import { EccLevel } from './ecc.js';
22
+ export interface RobustAudioEncodeOptions {
23
+ /** Error correction level. Default: 'medium'. */
24
+ eccLevel?: EccLevel;
25
+ }
26
+ export interface RobustAudioDecodeResult {
27
+ data: Buffer;
28
+ correctedErrors: number;
29
+ }
30
+ /**
31
+ * Encode binary data into a lossy-resilient WAV file.
32
+ *
33
+ * The output uses multi-frequency tones (not white noise) and includes
34
+ * Reed-Solomon error correction for recovery after MP3/AAC/OGG compression.
35
+ *
36
+ * @param data - Raw data to encode.
37
+ * @param opts - Encoding options.
38
+ * @returns WAV file as a Buffer.
39
+ */
40
+ export declare function encodeRobustAudio(data: Buffer, opts?: RobustAudioEncodeOptions): Buffer;
41
+ /**
42
+ * Decode binary data from a lossy-resilient WAV file.
43
+ *
44
+ * Handles WAV files that have been re-encoded through lossy codecs.
45
+ *
46
+ * @param wav - WAV file buffer (16-bit PCM preferred, 8-bit also accepted).
47
+ * @returns Decoded data and error correction stats.
48
+ */
49
+ export declare function decodeRobustAudio(wav: Buffer): RobustAudioDecodeResult;
50
+ /**
51
+ * Check if a buffer looks like a robust-audio-encoded WAV.
52
+ * Detects the sync preamble signature.
53
+ */
54
+ export declare function isRobustAudioWav(buf: Buffer): boolean;