roxify 1.1.2 → 1.1.4

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.
Files changed (3) hide show
  1. package/dist/cli.js +4 -20
  2. package/dist/index.js +193 -179
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, writeFileSync } from 'fs';
3
- import { basename, dirname, join, resolve } from 'path';
4
- import sharp from 'sharp';
5
- import { cropAndReconstitute, DataFormatError, decodePngToBinary, encodeBinaryToPng, IncorrectPassphraseError, PassphraseRequiredError, } from './index.js';
6
- const VERSION = '1.0.4';
3
+ import { basename, dirname, resolve } from 'path';
4
+ import { DataFormatError, decodePngToBinary, encodeBinaryToPng, IncorrectPassphraseError, PassphraseRequiredError, } from './index.js';
5
+ const VERSION = '1.1.4';
7
6
  function showHelp() {
8
7
  console.log(`
9
8
  ROX CLI — Encode/decode binary in PNG
@@ -167,7 +166,6 @@ async function decodeCommand(args) {
167
166
  try {
168
167
  const inputBuffer = readFileSync(resolvedInput);
169
168
  console.log(`Reading: ${resolvedInput}`);
170
- const info = await sharp(inputBuffer).metadata();
171
169
  const options = {};
172
170
  if (parsed.passphrase) {
173
171
  options.passphrase = parsed.passphrase;
@@ -177,21 +175,7 @@ async function decodeCommand(args) {
177
175
  }
178
176
  console.log(`Decoding...`);
179
177
  const startDecode = Date.now();
180
- if (parsed.verbose)
181
- options.verbose = true;
182
- const doubledBuffer = await sharp(inputBuffer)
183
- .resize({
184
- width: info.width * 2,
185
- height: info.height * 2,
186
- kernel: 'nearest',
187
- })
188
- .png()
189
- .toBuffer();
190
- if (options.debugDir) {
191
- writeFileSync(join(options.debugDir, 'doubled.png'), doubledBuffer);
192
- }
193
- const reconstructedBuffer = await cropAndReconstitute(doubledBuffer, options.debugDir);
194
- const result = await decodePngToBinary(reconstructedBuffer, options);
178
+ const result = await decodePngToBinary(inputBuffer, options);
195
179
  const decodeTime = Date.now() - startDecode;
196
180
  const resolvedOutput = parsed.output || outputPath || result.meta?.name || 'decoded.bin';
197
181
  writeFileSync(resolvedOutput, result.buf);
package/dist/index.js CHANGED
@@ -148,9 +148,8 @@ export async function cropAndReconstitute(input, debugDir) {
148
148
  await sharp(doubledBuffer).toFile(join(debugDir, 'doubled.png'));
149
149
  }
150
150
  const { data: doubledData, info: doubledInfo } = await loadRaw(doubledBuffer);
151
- const w = doubledInfo.width;
152
- const h = doubledInfo.height;
153
- function at(x, y) {
151
+ const w = doubledInfo.width, h = doubledInfo.height;
152
+ const at = (x, y) => {
154
153
  const i = idxFor(x, y, w);
155
154
  return [
156
155
  doubledData[i],
@@ -158,178 +157,110 @@ export async function cropAndReconstitute(input, debugDir) {
158
157
  doubledData[i + 2],
159
158
  doubledData[i + 3],
160
159
  ];
161
- }
162
- let startPoint = null;
163
- for (let y = 0; y < h && !startPoint; y++) {
164
- for (let x = 0; x < w; x++) {
165
- const p = at(x, y);
166
- if (p[0] !== 255 || p[1] !== 0 || p[2] !== 0)
167
- continue;
168
- let nx = x + 1;
169
- while (nx < w && eqRGB(at(nx, y), p))
170
- nx++;
171
- if (nx >= w)
172
- continue;
173
- const a = at(nx, y);
174
- let nx2 = nx + 1;
175
- while (nx2 < w && eqRGB(at(nx2, y), a))
176
- nx2++;
177
- if (nx2 >= w)
178
- continue;
179
- const b = at(nx2, y);
180
- const isRgb = a[0] === 0 &&
181
- a[1] === 255 &&
182
- a[2] === 0 &&
183
- b[0] === 0 &&
184
- b[1] === 0 &&
185
- b[2] === 255;
186
- if (isRgb) {
187
- startPoint = { x, y, type: 'rgb' };
188
- break;
189
- }
190
- }
191
- }
192
- let endPoint = null;
193
- for (let y = h - 1; y >= 0 && !endPoint; y--) {
194
- for (let x = w - 1; x >= 0; x--) {
195
- const p = at(x, y);
196
- if (p[0] !== 255 || p[1] !== 0 || p[2] !== 0)
197
- continue;
198
- let nx = x - 1;
199
- while (nx >= 0 && eqRGB(at(nx, y), p))
200
- nx--;
201
- if (nx < 0)
202
- continue;
203
- const a = at(nx, y);
204
- let nx2 = nx - 1;
205
- while (nx2 >= 0 && eqRGB(at(nx2, y), a))
206
- nx2--;
207
- if (nx2 < 0)
208
- continue;
209
- const b = at(nx2, y);
210
- const isRgbReverse = a[0] === 0 &&
211
- a[1] === 255 &&
212
- a[2] === 0 &&
213
- b[0] === 0 &&
214
- b[1] === 0 &&
215
- b[2] === 255;
216
- if (isRgbReverse) {
217
- endPoint = { x, y, type: 'bgr' };
218
- break;
160
+ };
161
+ const findPattern = (startX, startY, dirX, dirY, pattern) => {
162
+ for (let y = startY; y >= 0 && y < h; y += dirY) {
163
+ for (let x = startX; x >= 0 && x < w; x += dirX) {
164
+ const p = at(x, y);
165
+ if (p[0] !== 255 || p[1] !== 0 || p[2] !== 0)
166
+ continue;
167
+ let nx = x + dirX;
168
+ while (nx >= 0 && nx < w && eqRGB(at(nx, y), p))
169
+ nx += dirX;
170
+ if (nx < 0 || nx >= w)
171
+ continue;
172
+ const a = at(nx, y);
173
+ let nx2 = nx + dirX;
174
+ while (nx2 >= 0 && nx2 < w && eqRGB(at(nx2, y), a))
175
+ nx2 += dirX;
176
+ if (nx2 < 0 || nx2 >= w)
177
+ continue;
178
+ const b = at(nx2, y);
179
+ if (a[0] === pattern[0][0] &&
180
+ a[1] === pattern[0][1] &&
181
+ a[2] === pattern[0][2] &&
182
+ b[0] === pattern[1][0] &&
183
+ b[1] === pattern[1][1] &&
184
+ b[2] === pattern[1][2]) {
185
+ return { x, y };
186
+ }
219
187
  }
220
188
  }
221
- }
222
- if (!startPoint)
223
- throw new Error('Start pattern (RGB) not found');
224
- if (!endPoint)
225
- throw new Error('End pattern (BGR) not found');
226
- const sx1 = Math.min(startPoint.x, endPoint.x);
227
- const sy1 = Math.min(startPoint.y, endPoint.y);
228
- const sx2 = Math.max(startPoint.x, endPoint.x);
229
- const sy2 = Math.max(startPoint.y, endPoint.y);
230
- const cropW = sx2 - sx1 + 1;
231
- const cropH = sy2 - sy1 + 1;
189
+ return null;
190
+ };
191
+ const startPoint = findPattern(0, 0, 1, 1, [
192
+ [0, 255, 0],
193
+ [0, 0, 255],
194
+ ]);
195
+ const endPoint = findPattern(w - 1, h - 1, -1, -1, [
196
+ [0, 255, 0],
197
+ [0, 0, 255],
198
+ ]);
199
+ if (!startPoint || !endPoint)
200
+ throw new Error('Patterns not found');
201
+ const sx1 = Math.min(startPoint.x, endPoint.x), sy1 = Math.min(startPoint.y, endPoint.y);
202
+ const sx2 = Math.max(startPoint.x, endPoint.x), sy2 = Math.max(startPoint.y, endPoint.y);
203
+ const cropW = sx2 - sx1 + 1, cropH = sy2 - sy1 + 1;
232
204
  if (cropW <= 0 || cropH <= 0)
233
205
  throw new Error('Invalid crop dimensions');
234
206
  const cropped = await sharp(doubledBuffer)
235
207
  .extract({ left: sx1, top: sy1, width: cropW, height: cropH })
236
208
  .png()
237
209
  .toBuffer();
238
- const { data: cdata, info: cinfo } = await sharp(cropped)
239
- .ensureAlpha()
240
- .raw()
241
- .toBuffer({ resolveWithObject: true });
242
- const cw = cinfo.width;
243
- const ch = cinfo.height;
244
- function cat(x, y) {
245
- const i = idxFor(x, y, cw);
246
- return [cdata[i], cdata[i + 1], cdata[i + 2], cdata[i + 3]];
247
- }
248
- function eq(a, b) {
249
- return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
250
- }
251
- function lineEq(l1, l2) {
252
- if (l1.length !== l2.length)
253
- return false;
254
- for (let i = 0; i < l1.length; i++)
255
- if (!eq(l1[i], l2[i]))
256
- return false;
257
- return true;
258
- }
259
- const newWidth = cw;
260
- const newHeight = ch + 1;
210
+ const { data: cdata, info: cinfo } = await loadRaw(cropped);
211
+ const cw = cinfo.width, ch = cinfo.height;
212
+ const newWidth = cw, newHeight = ch + 1;
261
213
  const out = Buffer.alloc(newWidth * newHeight * 4, 0);
262
214
  for (let i = 0; i < out.length; i += 4)
263
215
  out[i + 3] = 255;
264
216
  for (let y = 0; y < ch; y++) {
265
217
  for (let x = 0; x < cw; x++) {
266
- const srcI = ((y * cw + x) * 4) | 0;
267
- const dstI = ((y * newWidth + x) * 4) | 0;
218
+ const srcI = (y * cw + x) * 4;
219
+ const dstI = (y * newWidth + x) * 4;
268
220
  out[dstI] = cdata[srcI];
269
221
  out[dstI + 1] = cdata[srcI + 1];
270
222
  out[dstI + 2] = cdata[srcI + 2];
271
223
  out[dstI + 3] = cdata[srcI + 3];
272
224
  }
273
225
  }
274
- const targetY = ch - 1;
275
- for (let x = 0; x < cw; x++) {
276
- const i = ((targetY * newWidth + x) * 4) | 0;
277
- out[i] = 0;
278
- out[i + 1] = 0;
279
- out[i + 2] = 0;
280
- out[i + 3] = 255;
281
- }
282
- const lastY = ch;
283
226
  for (let x = 0; x < newWidth; x++) {
284
- const i = ((lastY * newWidth + x) * 4) | 0;
285
- out[i] = 0;
286
- out[i + 1] = 0;
287
- out[i + 2] = 0;
227
+ const i = ((ch - 1) * newWidth + x) * 4;
228
+ out[i] = out[i + 1] = out[i + 2] = 0;
288
229
  out[i + 3] = 255;
230
+ const j = (ch * newWidth + x) * 4;
231
+ out[j] = out[j + 1] = out[j + 2] = 0;
232
+ out[j + 3] = 255;
289
233
  }
290
234
  if (newWidth >= 3) {
291
235
  const bgrStart = newWidth - 3;
292
- let i = ((lastY * newWidth + bgrStart) * 4) | 0;
293
- out[i] = 0;
294
- out[i + 1] = 0;
295
- out[i + 2] = 255;
296
- out[i + 3] = 255;
297
- i = ((lastY * newWidth + bgrStart + 1) * 4) | 0;
298
- out[i] = 0;
299
- out[i + 1] = 255;
300
- out[i + 2] = 0;
301
- out[i + 3] = 255;
302
- i = ((lastY * newWidth + bgrStart + 2) * 4) | 0;
303
- out[i] = 255;
304
- out[i + 1] = 0;
305
- out[i + 2] = 0;
306
- out[i + 3] = 255;
236
+ const bgr = [
237
+ [0, 0, 255],
238
+ [0, 255, 0],
239
+ [255, 0, 0],
240
+ ];
241
+ for (let k = 0; k < 3; k++) {
242
+ const i = (ch * newWidth + bgrStart + k) * 4;
243
+ out[i] = bgr[k][0];
244
+ out[i + 1] = bgr[k][1];
245
+ out[i + 2] = bgr[k][2];
246
+ out[i + 3] = 255;
247
+ }
307
248
  }
308
- function getPixel(x, y) {
309
- const i = ((y * newWidth + x) * 4) | 0;
249
+ const getPixel = (x, y) => {
250
+ const i = (y * newWidth + x) * 4;
310
251
  return [out[i], out[i + 1], out[i + 2], out[i + 3]];
311
- }
252
+ };
312
253
  const compressedLines = [];
313
254
  for (let y = 0; y < newHeight; y++) {
314
255
  const line = [];
315
- let x = 0;
316
- while (x < newWidth) {
317
- const current = getPixel(x, y);
318
- if (current[0] === 0 && current[1] === 0 && current[2] === 0) {
319
- x++;
320
- continue;
321
- }
322
- line.push(current);
323
- let nx = x + 1;
324
- while (nx < newWidth && eq(getPixel(nx, y), current))
325
- nx++;
326
- x = nx;
327
- }
328
- if (line.length === 0)
329
- continue;
330
- if (compressedLines.length === 0 ||
331
- !lineEq(compressedLines[compressedLines.length - 1], line))
256
+ for (let x = 0; x < newWidth; x++)
257
+ line.push(getPixel(x, y));
258
+ const isAllBlack = line.every((p) => p[0] === 0 && p[1] === 0 && p[2] === 0 && p[3] === 255);
259
+ if (!isAllBlack &&
260
+ (compressedLines.length === 0 ||
261
+ !line.every((p, i) => p.every((v, j) => v === compressedLines[compressedLines.length - 1][i][j])))) {
332
262
  compressedLines.push(line);
263
+ }
333
264
  }
334
265
  if (compressedLines.length === 0) {
335
266
  return sharp({
@@ -343,49 +274,116 @@ export async function cropAndReconstitute(input, debugDir) {
343
274
  .png()
344
275
  .toBuffer();
345
276
  }
346
- const finalWidth = Math.max(...compressedLines.map((l) => l.length));
347
- const finalHeight = compressedLines.length;
348
- const finalOut = Buffer.alloc(finalWidth * finalHeight * 4, 0);
277
+ let finalWidth = newWidth, finalHeight = compressedLines.length;
278
+ let finalOut = Buffer.alloc(finalWidth * finalHeight * 4, 0);
349
279
  for (let i = 0; i < finalOut.length; i += 4)
350
280
  finalOut[i + 3] = 255;
351
- for (let y = 0; y < compressedLines.length; y++) {
352
- const line = compressedLines[y];
353
- const isLastLine = y === compressedLines.length - 1;
354
- const startX = isLastLine ? finalWidth - line.length : 0;
355
- for (let x = 0; x < line.length; x++) {
356
- const i = ((y * finalWidth + startX + x) * 4) | 0;
357
- finalOut[i] = line[x][0];
358
- finalOut[i + 1] = line[x][1];
359
- finalOut[i + 2] = line[x][2];
360
- finalOut[i + 3] = line[x][3] === 0 ? 255 : line[x][3];
281
+ for (let y = 0; y < finalHeight; y++) {
282
+ for (let x = 0; x < finalWidth; x++) {
283
+ const i = (y * finalWidth + x) * 4;
284
+ finalOut[i] = compressedLines[y][x][0];
285
+ finalOut[i + 1] = compressedLines[y][x][1];
286
+ finalOut[i + 2] = compressedLines[y][x][2];
287
+ finalOut[i + 3] = compressedLines[y][x][3] || 255;
361
288
  }
362
289
  }
363
- if (finalHeight >= 2) {
364
- const secondLastY = finalHeight - 2;
290
+ if (finalHeight >= 1 && finalWidth >= 3) {
291
+ const lastY = finalHeight - 1;
292
+ for (let k = 0; k < 3; k++) {
293
+ const i = (lastY * finalWidth + finalWidth - 3 + k) * 4;
294
+ finalOut[i] = finalOut[i + 1] = finalOut[i + 2] = 0;
295
+ finalOut[i + 3] = 255;
296
+ }
297
+ }
298
+ if (finalWidth >= 2) {
299
+ const kept = [];
365
300
  for (let x = 0; x < finalWidth; x++) {
366
- const i = ((secondLastY * finalWidth + x) * 4) | 0;
367
- const r = finalOut[i];
368
- const g = finalOut[i + 1];
369
- const b = finalOut[i + 2];
370
- if ((r === 255 && g === 0 && b === 0) ||
371
- (r === 0 && g === 255 && b === 0) ||
372
- (r === 0 && g === 0 && b === 255)) {
373
- finalOut[i] = 0;
374
- finalOut[i + 1] = 0;
375
- finalOut[i + 2] = 0;
301
+ if (kept.length === 0) {
302
+ kept.push(x);
303
+ continue;
304
+ }
305
+ const prevX = kept[kept.length - 1];
306
+ let same = true;
307
+ for (let y = 0; y < finalHeight; y++) {
308
+ const ia = (y * finalWidth + prevX) * 4, ib = (y * finalWidth + x) * 4;
309
+ if (finalOut[ia] !== finalOut[ib] ||
310
+ finalOut[ia + 1] !== finalOut[ib + 1] ||
311
+ finalOut[ia + 2] !== finalOut[ib + 2] ||
312
+ finalOut[ia + 3] !== finalOut[ib + 3]) {
313
+ same = false;
314
+ break;
315
+ }
316
+ }
317
+ if (!same)
318
+ kept.push(x);
319
+ }
320
+ if (kept.length !== finalWidth) {
321
+ const newFinalWidth = kept.length;
322
+ const newOut = Buffer.alloc(newFinalWidth * finalHeight * 4, 0);
323
+ for (let i = 0; i < newOut.length; i += 4)
324
+ newOut[i + 3] = 255;
325
+ for (let nx = 0; nx < kept.length; nx++) {
326
+ const sx = kept[nx];
327
+ for (let y = 0; y < finalHeight; y++) {
328
+ const srcI = (y * finalWidth + sx) * 4, dstI = (y * newFinalWidth + nx) * 4;
329
+ newOut[dstI] = finalOut[srcI];
330
+ newOut[dstI + 1] = finalOut[srcI + 1];
331
+ newOut[dstI + 2] = finalOut[srcI + 2];
332
+ newOut[dstI + 3] = finalOut[srcI + 3];
333
+ }
334
+ }
335
+ finalOut = newOut;
336
+ finalWidth = newFinalWidth;
337
+ }
338
+ }
339
+ if (finalHeight >= 2 && finalWidth >= 3) {
340
+ const secondLastY = finalHeight - 2;
341
+ const bgrSeq = [
342
+ [0, 0, 255],
343
+ [0, 255, 0],
344
+ [255, 0, 0],
345
+ ];
346
+ let hasBGR = true;
347
+ for (let k = 0; k < 3; k++) {
348
+ const i = (secondLastY * finalWidth + finalWidth - 3 + k) * 4;
349
+ if (finalOut[i] !== bgrSeq[k][0] ||
350
+ finalOut[i + 1] !== bgrSeq[k][1] ||
351
+ finalOut[i + 2] !== bgrSeq[k][2]) {
352
+ hasBGR = false;
353
+ break;
354
+ }
355
+ }
356
+ if (hasBGR) {
357
+ for (let k = 0; k < 3; k++) {
358
+ const i = (secondLastY * finalWidth + finalWidth - 3 + k) * 4;
359
+ finalOut[i] = finalOut[i + 1] = finalOut[i + 2] = 0;
376
360
  finalOut[i + 3] = 255;
377
361
  }
378
362
  }
379
363
  }
380
- const resultBuffer = await sharp(finalOut, {
364
+ if (finalHeight >= 1 && finalWidth >= 1) {
365
+ const lastYFinal = finalHeight - 1;
366
+ const bgrSeq = [
367
+ [0, 0, 255],
368
+ [0, 255, 0],
369
+ [255, 0, 0],
370
+ ];
371
+ for (let k = 0; k < 3; k++) {
372
+ const sx = finalWidth - 3 + k;
373
+ if (sx >= 0) {
374
+ const i = (lastYFinal * finalWidth + sx) * 4;
375
+ finalOut[i] = bgrSeq[k][0];
376
+ finalOut[i + 1] = bgrSeq[k][1];
377
+ finalOut[i + 2] = bgrSeq[k][2];
378
+ finalOut[i + 3] = 255;
379
+ }
380
+ }
381
+ }
382
+ return sharp(finalOut, {
381
383
  raw: { width: finalWidth, height: finalHeight, channels: 4 },
382
384
  })
383
385
  .png()
384
386
  .toBuffer();
385
- if (debugDir) {
386
- await sharp(resultBuffer).toFile(join(debugDir, 'reconstructed.png'));
387
- }
388
- return resultBuffer;
389
387
  }
390
388
  /**
391
389
  * Encode a Buffer into a PNG wrapper. Supports optional compression and
@@ -652,8 +650,24 @@ export async function encodeBinaryToPng(input, opts = {}) {
652
650
  * @public
653
651
  */
654
652
  export async function decodePngToBinary(pngBuf, opts = {}) {
655
- if (pngBuf.slice(0, MAGIC.length).equals(MAGIC)) {
656
- const d = pngBuf.slice(MAGIC.length);
653
+ let processedBuf = pngBuf;
654
+ try {
655
+ const info = await sharp(pngBuf).metadata();
656
+ if (info.width && info.height) {
657
+ const doubledBuffer = await sharp(pngBuf)
658
+ .resize({
659
+ width: info.width * 2,
660
+ height: info.height * 2,
661
+ kernel: 'nearest',
662
+ })
663
+ .png()
664
+ .toBuffer();
665
+ processedBuf = await cropAndReconstitute(doubledBuffer, opts.debugDir);
666
+ }
667
+ }
668
+ catch (e) { }
669
+ if (processedBuf.slice(0, MAGIC.length).equals(MAGIC)) {
670
+ const d = processedBuf.slice(MAGIC.length);
657
671
  const nameLen = d[0];
658
672
  let idx = 1;
659
673
  let name;
@@ -680,7 +694,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
680
694
  }
681
695
  let chunks = [];
682
696
  try {
683
- const chunksRaw = extract(pngBuf);
697
+ const chunksRaw = extract(processedBuf);
684
698
  chunks = chunksRaw.map((c) => ({
685
699
  name: c.name,
686
700
  data: Buffer.isBuffer(c.data)
@@ -733,7 +747,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
733
747
  return { buf: payload, meta: { name } };
734
748
  }
735
749
  try {
736
- const { data, info } = await sharp(pngBuf)
750
+ const { data, info } = await sharp(processedBuf)
737
751
  .ensureAlpha()
738
752
  .raw()
739
753
  .toBuffer({ resolveWithObject: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Encode binary data into PNG images with Zstd compression and decode them back. Supports CLI and programmatic API (Node.js ESM).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",