roxify 1.3.2 → 1.4.0

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/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { basename, dirname, join, resolve } from 'path';
6
6
  import { DataFormatError, decodePngToBinary, encodeBinaryToPng, hasPassphraseInPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
7
7
  import { packPathsGenerator, unpackBuffer } from './pack.js';
8
8
  import { encodeWithRustCLI, isRustBinaryAvailable, } from './utils/rust-cli-wrapper.js';
9
- const VERSION = '1.3.2';
9
+ const VERSION = '1.4.0';
10
10
  async function readLargeFile(filePath) {
11
11
  const st = statSync(filePath);
12
12
  if (st.size <= 2 * 1024 * 1024 * 1024) {
@@ -211,7 +211,7 @@ async function encodeCommand(args) {
211
211
  });
212
212
  }, 500);
213
213
  const encryptType = parsed.encrypt === 'xor' ? 'xor' : 'aes';
214
- await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput, 3, parsed.passphrase, encryptType);
214
+ await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput, 19, parsed.passphrase, encryptType);
215
215
  clearInterval(progressInterval);
216
216
  const encodeTime = Date.now() - startTime;
217
217
  encodeBar.update(100, {
@@ -283,8 +283,9 @@ async function encodeCommand(args) {
283
283
  Object.assign(options, {
284
284
  mode,
285
285
  name: parsed.outputName || 'archive',
286
- skipOptimization: true,
286
+ skipOptimization: false,
287
287
  compressionLevel: 19,
288
+ outputFormat: 'auto',
288
289
  });
289
290
  if (parsed.verbose)
290
291
  options.verbose = true;
package/dist/roxify-cli CHANGED
Binary file
@@ -3,6 +3,7 @@
3
3
  export declare const CHUNK_TYPE = "rXDT";
4
4
  export declare const MAGIC: Buffer;
5
5
  export declare const PIXEL_MAGIC: Buffer;
6
+ export declare const PIXEL_MAGIC_BLOCK: Buffer;
6
7
  export declare const ENC_NONE = 0;
7
8
  export declare const ENC_AES = 1;
8
9
  export declare const ENC_XOR = 2;
@@ -1,6 +1,7 @@
1
1
  export const CHUNK_TYPE = 'rXDT';
2
2
  export const MAGIC = Buffer.from('ROX1');
3
3
  export const PIXEL_MAGIC = Buffer.from('PXL1');
4
+ export const PIXEL_MAGIC_BLOCK = Buffer.from('BLK2');
4
5
  export const ENC_NONE = 0;
5
6
  export const ENC_AES = 1;
6
7
  export const ENC_XOR = 2;
@@ -3,7 +3,7 @@ import { readFileSync } from 'fs';
3
3
  import extract from 'png-chunks-extract';
4
4
  import sharp from 'sharp';
5
5
  import { unpackBuffer } from '../pack.js';
6
- import { CHUNK_TYPE, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PNG_HEADER, } from './constants.js';
6
+ import { CHUNK_TYPE, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PIXEL_MAGIC_BLOCK, PNG_HEADER, } from './constants.js';
7
7
  import { DataFormatError, IncorrectPassphraseError, PassphraseRequiredError, } from './errors.js';
8
8
  import { colorsToBytes, deltaDecode, tryDecryptIfNeeded } from './helpers.js';
9
9
  import { cropAndReconstitute } from './reconstitution.js';
@@ -228,40 +228,93 @@ export async function decodePngToBinary(input, opts = {}) {
228
228
  const metadata = await sharp(processedBuf).metadata();
229
229
  const currentWidth = metadata.width;
230
230
  const currentHeight = metadata.height;
231
- const rawRGB = Buffer.allocUnsafe(currentWidth * currentHeight * 3);
232
- let writeOffset = 0;
233
- const rowsPerChunk = 2000;
234
- for (let startRow = 0; startRow < currentHeight; startRow += rowsPerChunk) {
235
- const endRow = Math.min(startRow + rowsPerChunk, currentHeight);
236
- const chunkHeight = endRow - startRow;
237
- const { data: chunkData, info: chunkInfo } = await sharp(processedBuf)
231
+ let rawRGB = Buffer.alloc(0);
232
+ let isBlockEncoded = false;
233
+ if (currentWidth % 2 === 0 && currentHeight % 2 === 0) {
234
+ const { data: testData } = await sharp(processedBuf)
238
235
  .extract({
239
236
  left: 0,
240
- top: startRow,
241
- width: currentWidth,
242
- height: chunkHeight,
237
+ top: 0,
238
+ width: Math.min(4, currentWidth),
239
+ height: Math.min(4, currentHeight),
243
240
  })
244
241
  .raw()
245
242
  .toBuffer({ resolveWithObject: true });
246
- const channels = chunkInfo.channels;
247
- const pixelsInChunk = currentWidth * chunkHeight;
248
- if (channels === 3) {
249
- chunkData.copy(rawRGB, writeOffset);
250
- writeOffset += pixelsInChunk * 3;
251
- }
252
- else if (channels === 4) {
253
- for (let i = 0; i < pixelsInChunk; i++) {
254
- rawRGB[writeOffset++] = chunkData[i * 4];
255
- rawRGB[writeOffset++] = chunkData[i * 4 + 1];
256
- rawRGB[writeOffset++] = chunkData[i * 4 + 2];
243
+ let hasBlockPattern = true;
244
+ for (let y = 0; y < Math.min(2, currentHeight / 2); y++) {
245
+ for (let x = 0; x < Math.min(2, currentWidth / 2); x++) {
246
+ const px00 = (y * 2 * Math.min(4, currentWidth) + x * 2) * 3;
247
+ const px01 = (y * 2 * Math.min(4, currentWidth) + (x * 2 + 1)) * 3;
248
+ const px10 = ((y * 2 + 1) * Math.min(4, currentWidth) + x * 2) * 3;
249
+ const px11 = ((y * 2 + 1) * Math.min(4, currentWidth) + (x * 2 + 1)) * 3;
250
+ if (testData[px00] !== testData[px01] ||
251
+ testData[px00] !== testData[px10] ||
252
+ testData[px00] !== testData[px11] ||
253
+ testData[px00 + 1] !== testData[px01 + 1] ||
254
+ testData[px00 + 1] !== testData[px10 + 1] ||
255
+ testData[px00 + 1] !== testData[px11 + 1]) {
256
+ hasBlockPattern = false;
257
+ break;
258
+ }
257
259
  }
260
+ if (!hasBlockPattern)
261
+ break;
258
262
  }
259
- if (opts.onProgress) {
260
- opts.onProgress({
261
- phase: 'extract_pixels',
262
- loaded: endRow,
263
- total: currentHeight,
264
- });
263
+ if (hasBlockPattern) {
264
+ isBlockEncoded = true;
265
+ const blocksWide = currentWidth / 2;
266
+ const blocksHigh = currentHeight / 2;
267
+ rawRGB = Buffer.alloc(blocksWide * blocksHigh * 3);
268
+ let outIdx = 0;
269
+ for (let by = 0; by < blocksHigh; by++) {
270
+ for (let bx = 0; bx < blocksWide; bx++) {
271
+ const { data: blockData } = await sharp(processedBuf)
272
+ .extract({ left: bx * 2, top: by * 2, width: 1, height: 1 })
273
+ .raw()
274
+ .toBuffer({ resolveWithObject: true });
275
+ rawRGB[outIdx++] = blockData[0];
276
+ rawRGB[outIdx++] = blockData[1];
277
+ rawRGB[outIdx++] = blockData[2];
278
+ }
279
+ }
280
+ }
281
+ }
282
+ if (!isBlockEncoded) {
283
+ rawRGB = Buffer.allocUnsafe(currentWidth * currentHeight * 3);
284
+ let writeOffset = 0;
285
+ const rowsPerChunk = 2000;
286
+ for (let startRow = 0; startRow < currentHeight; startRow += rowsPerChunk) {
287
+ const endRow = Math.min(startRow + rowsPerChunk, currentHeight);
288
+ const chunkHeight = endRow - startRow;
289
+ const { data: chunkData, info: chunkInfo } = await sharp(processedBuf)
290
+ .extract({
291
+ left: 0,
292
+ top: startRow,
293
+ width: currentWidth,
294
+ height: chunkHeight,
295
+ })
296
+ .raw()
297
+ .toBuffer({ resolveWithObject: true });
298
+ const channels = chunkInfo.channels;
299
+ const pixelsInChunk = currentWidth * chunkHeight;
300
+ if (channels === 3) {
301
+ chunkData.copy(rawRGB, writeOffset);
302
+ writeOffset += pixelsInChunk * 3;
303
+ }
304
+ else if (channels === 4) {
305
+ for (let i = 0; i < pixelsInChunk; i++) {
306
+ rawRGB[writeOffset++] = chunkData[i * 4];
307
+ rawRGB[writeOffset++] = chunkData[i * 4 + 1];
308
+ rawRGB[writeOffset++] = chunkData[i * 4 + 2];
309
+ }
310
+ }
311
+ if (opts.onProgress) {
312
+ opts.onProgress({
313
+ phase: 'extract_pixels',
314
+ loaded: endRow,
315
+ total: currentHeight,
316
+ });
317
+ }
265
318
  }
266
319
  }
267
320
  const firstPixels = [];
@@ -285,6 +338,7 @@ export async function decodePngToBinary(input, opts = {}) {
285
338
  }
286
339
  }
287
340
  let hasPixelMagic = false;
341
+ let hasBlockMagic = false;
288
342
  if (rawRGB.length >= 8 + PIXEL_MAGIC.length) {
289
343
  const widthFromDim = rawRGB.readUInt32BE(0);
290
344
  const heightFromDim = rawRGB.readUInt32BE(4);
@@ -293,11 +347,14 @@ export async function decodePngToBinary(input, opts = {}) {
293
347
  rawRGB.slice(8, 8 + PIXEL_MAGIC.length).equals(PIXEL_MAGIC)) {
294
348
  hasPixelMagic = true;
295
349
  }
350
+ else if (rawRGB.slice(8, 8 + PIXEL_MAGIC_BLOCK.length).equals(PIXEL_MAGIC_BLOCK)) {
351
+ hasBlockMagic = true;
352
+ }
296
353
  }
297
354
  let logicalWidth;
298
355
  let logicalHeight;
299
356
  let logicalData;
300
- if (hasMarkerStart || hasPixelMagic) {
357
+ if (hasMarkerStart || hasPixelMagic || hasBlockMagic) {
301
358
  logicalWidth = currentWidth;
302
359
  logicalHeight = currentHeight;
303
360
  logicalData = rawRGB;
@@ -569,14 +626,24 @@ export async function decodePngToBinary(input, opts = {}) {
569
626
  let idx = 0;
570
627
  if (pixelBytes.length >= PIXEL_MAGIC.length) {
571
628
  const at0 = pixelBytes.slice(0, PIXEL_MAGIC.length).equals(PIXEL_MAGIC);
629
+ const at0Block = pixelBytes
630
+ .slice(0, PIXEL_MAGIC_BLOCK.length)
631
+ .equals(PIXEL_MAGIC_BLOCK);
572
632
  if (at0) {
573
633
  idx = PIXEL_MAGIC.length;
574
634
  }
635
+ else if (at0Block) {
636
+ idx = PIXEL_MAGIC_BLOCK.length;
637
+ }
575
638
  else {
576
639
  const found = pixelBytes.indexOf(PIXEL_MAGIC);
640
+ const foundBlock = pixelBytes.indexOf(PIXEL_MAGIC_BLOCK);
577
641
  if (found !== -1) {
578
642
  idx = found + PIXEL_MAGIC.length;
579
643
  }
644
+ else if (foundBlock !== -1) {
645
+ idx = foundBlock + PIXEL_MAGIC_BLOCK.length;
646
+ }
580
647
  }
581
648
  }
582
649
  if (idx > 0) {
@@ -3,7 +3,7 @@ import { createCipheriv, pbkdf2Sync, randomBytes } from 'crypto';
3
3
  import sharp from 'sharp';
4
4
  import * as zlib from 'zlib';
5
5
  import { unpackBuffer } from '../pack.js';
6
- import { COMPRESSION_MARKERS, ENC_AES, ENC_NONE, ENC_XOR, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PNG_HEADER, } from './constants.js';
6
+ import { COMPRESSION_MARKERS, ENC_AES, ENC_NONE, ENC_XOR, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PIXEL_MAGIC_BLOCK, PNG_HEADER, } from './constants.js';
7
7
  import { crc32 } from './crc.js';
8
8
  import { colorsToBytes } from './helpers.js';
9
9
  import { optimizePngBuffer } from './optimization.js';
@@ -220,7 +220,9 @@ export async function encodeBinaryToPng(input, opts = {}) {
220
220
  lenBuf.writeUInt32BE(jsonBuf.length, 0);
221
221
  metaPixel = [...metaPixel, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf];
222
222
  }
223
- const dataWithoutMarkers = [PIXEL_MAGIC, ...metaPixel];
223
+ const useBlockEncoding = opts.useBlockEncoding ?? true;
224
+ const pixelMagic = useBlockEncoding ? PIXEL_MAGIC_BLOCK : PIXEL_MAGIC;
225
+ const dataWithoutMarkers = [pixelMagic, ...metaPixel];
224
226
  const dataWithoutMarkersLen = dataWithoutMarkers.reduce((a, b) => a + b.length, 0);
225
227
  const padding = (3 - (dataWithoutMarkersLen % 3)) % 3;
226
228
  const paddedData = padding > 0
@@ -233,56 +235,188 @@ export async function encodeBinaryToPng(input, opts = {}) {
233
235
  compressionMarkerBytes,
234
236
  ...paddedData,
235
237
  ];
236
- const bytesPerPixel = 3;
237
238
  const dataWithMarkersLen = dataWithMarkers.reduce((a, b) => a + b.length, 0);
238
- const dataPixels = Math.ceil(dataWithMarkersLen / 3);
239
- const totalPixels = dataPixels + MARKER_END.length;
240
- const maxWidth = 16384;
241
- let side = Math.ceil(Math.sqrt(totalPixels));
242
- if (side < MARKER_END.length)
243
- side = MARKER_END.length;
244
- let logicalWidth;
245
- let logicalHeight;
246
- if (side <= maxWidth) {
247
- logicalWidth = side;
248
- logicalHeight = side;
249
- }
250
- else {
251
- logicalWidth = Math.min(maxWidth, totalPixels);
252
- logicalHeight = Math.ceil(totalPixels / logicalWidth);
253
- }
254
- const scale = 1;
255
- const width = logicalWidth * scale;
256
- const height = logicalHeight * scale;
257
- const LARGE_IMAGE_PIXELS = 10000000;
258
- const useManualPng = width * height > LARGE_IMAGE_PIXELS || !!process.env.ROX_FAST_PNG;
259
- let raw;
260
- let stride = 0;
261
- if (useManualPng) {
262
- stride = width * 3 + 1;
263
- raw = Buffer.alloc(height * stride);
239
+ let width;
240
+ let height;
241
+ let bufScr;
242
+ if (useBlockEncoding) {
264
243
  const flatData = Buffer.concat(dataWithMarkers);
265
- const markerEndBytes = Buffer.alloc(MARKER_END.length * 3);
266
- for (let i = 0; i < MARKER_END.length; i++) {
267
- markerEndBytes[i * 3] = MARKER_END[i].r;
268
- markerEndBytes[i * 3 + 1] = MARKER_END[i].g;
269
- markerEndBytes[i * 3 + 2] = MARKER_END[i].b;
270
- }
271
- const totalDataBytes = logicalWidth * logicalHeight * 3;
272
- const fullData = Buffer.alloc(totalDataBytes);
273
- const markerStartPos = (logicalHeight - 1) * logicalWidth * 3 +
274
- (logicalWidth - MARKER_END.length) * 3;
275
- flatData.copy(fullData, 0, 0, Math.min(flatData.length, markerStartPos));
276
- markerEndBytes.copy(fullData, markerStartPos);
277
- for (let row = 0; row < height; row++) {
278
- raw[row * stride] = 0;
279
- fullData.copy(raw, row * stride + 1, row * width * 3, (row + 1) * width * 3);
244
+ const blocksPerRow = Math.ceil(Math.sqrt(flatData.length));
245
+ const numRows = Math.ceil(flatData.length / blocksPerRow);
246
+ width = blocksPerRow * 2;
247
+ height = numRows * 2;
248
+ const rgbBuffer = Buffer.alloc(width * height * 3);
249
+ for (let i = 0; i < flatData.length; i++) {
250
+ const blockRow = Math.floor(i / blocksPerRow);
251
+ const blockCol = i % blocksPerRow;
252
+ const pixelRow = blockRow * 2;
253
+ const pixelCol = blockCol * 2;
254
+ const byte = flatData[i];
255
+ for (let dy = 0; dy < 2; dy++) {
256
+ for (let dx = 0; dx < 2; dx++) {
257
+ const px = (pixelRow + dy) * width + (pixelCol + dx);
258
+ rgbBuffer[px * 3] = byte;
259
+ rgbBuffer[px * 3 + 1] = byte;
260
+ rgbBuffer[px * 3 + 2] = byte;
261
+ }
262
+ }
280
263
  }
264
+ bufScr = await sharp(rgbBuffer, {
265
+ raw: { width, height, channels: 3 },
266
+ })
267
+ .png({
268
+ compressionLevel: 9,
269
+ adaptiveFiltering: true,
270
+ effort: 9,
271
+ })
272
+ .toBuffer();
281
273
  }
282
274
  else {
283
- raw = Buffer.alloc(width * height * bytesPerPixel);
284
- const flatData = Buffer.concat(dataWithMarkers);
285
- flatData.copy(raw, 0, 0, Math.min(flatData.length, raw.length));
275
+ const bytesPerPixel = 3;
276
+ const dataPixels = Math.ceil(dataWithMarkersLen / 3);
277
+ const totalPixels = dataPixels + MARKER_END.length;
278
+ const maxWidth = 16384;
279
+ let side = Math.ceil(Math.sqrt(totalPixels));
280
+ if (side < MARKER_END.length)
281
+ side = MARKER_END.length;
282
+ let logicalWidth;
283
+ let logicalHeight;
284
+ if (side <= maxWidth) {
285
+ logicalWidth = side;
286
+ logicalHeight = side;
287
+ }
288
+ else {
289
+ logicalWidth = Math.min(maxWidth, totalPixels);
290
+ logicalHeight = Math.ceil(totalPixels / logicalWidth);
291
+ }
292
+ const scale = 1;
293
+ width = logicalWidth * scale;
294
+ height = logicalHeight * scale;
295
+ const LARGE_IMAGE_PIXELS = 10000000;
296
+ const useManualPng = (width * height > LARGE_IMAGE_PIXELS || !!process.env.ROX_FAST_PNG) &&
297
+ opts.outputFormat !== 'webp';
298
+ if (process.env.ROX_DEBUG) {
299
+ console.log(`[DEBUG] Width=${width}, Height=${height}, Pixels=${width * height}`);
300
+ console.log(`[DEBUG] outputFormat=${opts.outputFormat}, useManualPng=${useManualPng}`);
301
+ }
302
+ let raw;
303
+ let stride = 0;
304
+ if (useManualPng) {
305
+ stride = width * 3 + 1;
306
+ raw = Buffer.alloc(height * stride);
307
+ const flatData = Buffer.concat(dataWithMarkers);
308
+ const markerEndBytes = Buffer.alloc(MARKER_END.length * 3);
309
+ for (let i = 0; i < MARKER_END.length; i++) {
310
+ markerEndBytes[i * 3] = MARKER_END[i].r;
311
+ markerEndBytes[i * 3 + 1] = MARKER_END[i].g;
312
+ markerEndBytes[i * 3 + 2] = MARKER_END[i].b;
313
+ }
314
+ const totalDataBytes = logicalWidth * logicalHeight * 3;
315
+ const fullData = Buffer.alloc(totalDataBytes);
316
+ const markerStartPos = (logicalHeight - 1) * logicalWidth * 3 +
317
+ (logicalWidth - MARKER_END.length) * 3;
318
+ flatData.copy(fullData, 0, 0, Math.min(flatData.length, markerStartPos));
319
+ markerEndBytes.copy(fullData, markerStartPos);
320
+ for (let row = 0; row < height; row++) {
321
+ raw[row * stride] = 0;
322
+ fullData.copy(raw, row * stride + 1, row * width * 3, (row + 1) * width * 3);
323
+ }
324
+ }
325
+ else {
326
+ raw = Buffer.alloc(width * height * 3);
327
+ const flatData = Buffer.concat(dataWithMarkers);
328
+ flatData.copy(raw, 0, 0, Math.min(flatData.length, raw.length));
329
+ }
330
+ if (opts.onProgress)
331
+ opts.onProgress({ phase: 'png_gen', loaded: 0, total: height });
332
+ if (useManualPng) {
333
+ const bytesPerRow = width * 3;
334
+ const scanlinesData = Buffer.alloc(height * (1 + bytesPerRow));
335
+ const progressStep = Math.max(1, Math.floor(height / 20));
336
+ for (let row = 0; row < height; row++) {
337
+ scanlinesData[row * (1 + bytesPerRow)] = 0;
338
+ const srcStart = row * stride + 1;
339
+ const dstStart = row * (1 + bytesPerRow) + 1;
340
+ raw.copy(scanlinesData, dstStart, srcStart, srcStart + bytesPerRow);
341
+ if (opts.onProgress && row % progressStep === 0) {
342
+ opts.onProgress({ phase: 'png_gen', loaded: row, total: height });
343
+ }
344
+ }
345
+ if (opts.onProgress)
346
+ opts.onProgress({ phase: 'png_compress', loaded: 0, total: 100 });
347
+ const idatData = zlib.deflateSync(scanlinesData, {
348
+ level: 0,
349
+ memLevel: 8,
350
+ strategy: zlib.constants.Z_FILTERED,
351
+ });
352
+ raw = Buffer.alloc(0);
353
+ const ihdrData = Buffer.alloc(13);
354
+ ihdrData.writeUInt32BE(width, 0);
355
+ ihdrData.writeUInt32BE(height, 4);
356
+ ihdrData[8] = 8;
357
+ ihdrData[9] = 2;
358
+ ihdrData[10] = 0;
359
+ ihdrData[11] = 0;
360
+ ihdrData[12] = 0;
361
+ const ihdrType = Buffer.from('IHDR', 'utf8');
362
+ const ihdrCrc = crc32(ihdrData, crc32(ihdrType));
363
+ const ihdrCrcBuf = Buffer.alloc(4);
364
+ ihdrCrcBuf.writeUInt32BE(ihdrCrc, 0);
365
+ const ihdrLen = Buffer.alloc(4);
366
+ ihdrLen.writeUInt32BE(ihdrData.length, 0);
367
+ const idatType = Buffer.from('IDAT', 'utf8');
368
+ const idatCrc = crc32(idatData, crc32(idatType));
369
+ const idatCrcBuf = Buffer.alloc(4);
370
+ idatCrcBuf.writeUInt32BE(idatCrc, 0);
371
+ const idatLen = Buffer.alloc(4);
372
+ idatLen.writeUInt32BE(idatData.length, 0);
373
+ const iendType = Buffer.from('IEND', 'utf8');
374
+ const iendCrc = crc32(Buffer.alloc(0), crc32(iendType));
375
+ const iendCrcBuf = Buffer.alloc(4);
376
+ iendCrcBuf.writeUInt32BE(iendCrc, 0);
377
+ const iendLen = Buffer.alloc(4);
378
+ iendLen.writeUInt32BE(0, 0);
379
+ bufScr = Buffer.concat([
380
+ PNG_HEADER,
381
+ ihdrLen,
382
+ ihdrType,
383
+ ihdrData,
384
+ ihdrCrcBuf,
385
+ idatLen,
386
+ idatType,
387
+ idatData,
388
+ idatCrcBuf,
389
+ iendLen,
390
+ iendType,
391
+ iendCrcBuf,
392
+ ]);
393
+ }
394
+ else {
395
+ const outputFormat = opts.outputFormat || 'png';
396
+ if (outputFormat === 'webp') {
397
+ bufScr = await sharp(raw, {
398
+ raw: { width, height, channels: 3 },
399
+ })
400
+ .webp({
401
+ lossless: true,
402
+ quality: 100,
403
+ effort: 6,
404
+ })
405
+ .toBuffer();
406
+ }
407
+ else {
408
+ bufScr = await sharp(raw, {
409
+ raw: { width, height, channels: 3 },
410
+ })
411
+ .png({
412
+ compressionLevel: 3,
413
+ palette: false,
414
+ effort: 1,
415
+ adaptiveFiltering: false,
416
+ })
417
+ .toBuffer();
418
+ }
419
+ }
286
420
  }
287
421
  payload.length = 0;
288
422
  dataWithMarkers.length = 0;
@@ -290,87 +424,9 @@ export async function encodeBinaryToPng(input, opts = {}) {
290
424
  meta.length = 0;
291
425
  paddedData.length = 0;
292
426
  dataWithoutMarkers.length = 0;
293
- if (opts.onProgress)
294
- opts.onProgress({ phase: 'png_gen', loaded: 0, total: height });
295
- let bufScr;
296
- if (useManualPng) {
297
- const bytesPerRow = width * 3;
298
- const scanlinesData = Buffer.alloc(height * (1 + bytesPerRow));
299
- const progressStep = Math.max(1, Math.floor(height / 20));
300
- for (let row = 0; row < height; row++) {
301
- scanlinesData[row * (1 + bytesPerRow)] = 0;
302
- const srcStart = row * stride + 1;
303
- const dstStart = row * (1 + bytesPerRow) + 1;
304
- raw.copy(scanlinesData, dstStart, srcStart, srcStart + bytesPerRow);
305
- if (opts.onProgress && row % progressStep === 0) {
306
- opts.onProgress({ phase: 'png_gen', loaded: row, total: height });
307
- }
308
- }
309
- if (opts.onProgress)
310
- opts.onProgress({ phase: 'png_compress', loaded: 0, total: 100 });
311
- const idatData = zlib.deflateSync(scanlinesData, {
312
- level: 0,
313
- memLevel: 8,
314
- strategy: zlib.constants.Z_FILTERED,
315
- });
316
- raw = Buffer.alloc(0);
317
- const ihdrData = Buffer.alloc(13);
318
- ihdrData.writeUInt32BE(width, 0);
319
- ihdrData.writeUInt32BE(height, 4);
320
- ihdrData[8] = 8;
321
- ihdrData[9] = 2;
322
- ihdrData[10] = 0;
323
- ihdrData[11] = 0;
324
- ihdrData[12] = 0;
325
- const ihdrType = Buffer.from('IHDR', 'utf8');
326
- const ihdrCrc = crc32(ihdrData, crc32(ihdrType));
327
- const ihdrCrcBuf = Buffer.alloc(4);
328
- ihdrCrcBuf.writeUInt32BE(ihdrCrc, 0);
329
- const ihdrLen = Buffer.alloc(4);
330
- ihdrLen.writeUInt32BE(ihdrData.length, 0);
331
- const idatType = Buffer.from('IDAT', 'utf8');
332
- const idatCrc = crc32(idatData, crc32(idatType));
333
- const idatCrcBuf = Buffer.alloc(4);
334
- idatCrcBuf.writeUInt32BE(idatCrc, 0);
335
- const idatLen = Buffer.alloc(4);
336
- idatLen.writeUInt32BE(idatData.length, 0);
337
- const iendType = Buffer.from('IEND', 'utf8');
338
- const iendCrc = crc32(Buffer.alloc(0), crc32(iendType));
339
- const iendCrcBuf = Buffer.alloc(4);
340
- iendCrcBuf.writeUInt32BE(iendCrc, 0);
341
- const iendLen = Buffer.alloc(4);
342
- iendLen.writeUInt32BE(0, 0);
343
- bufScr = Buffer.concat([
344
- PNG_HEADER,
345
- ihdrLen,
346
- ihdrType,
347
- ihdrData,
348
- ihdrCrcBuf,
349
- idatLen,
350
- idatType,
351
- idatData,
352
- idatCrcBuf,
353
- iendLen,
354
- iendType,
355
- iendCrcBuf,
356
- ]);
357
- }
358
- else {
359
- bufScr = await sharp(raw, {
360
- raw: { width, height, channels: 3 },
361
- })
362
- .png({
363
- compressionLevel: 3,
364
- palette: false,
365
- effort: 1,
366
- adaptiveFiltering: false,
367
- })
368
- .toBuffer();
369
- }
370
- raw = Buffer.alloc(0);
371
427
  if (opts.onProgress)
372
428
  opts.onProgress({ phase: 'png_compress', loaded: 100, total: 100 });
373
- if (opts.skipOptimization) {
429
+ if (opts.skipOptimization || opts.outputFormat === 'webp') {
374
430
  progressBar?.stop();
375
431
  return bufScr;
376
432
  }
@@ -9,3 +9,10 @@ export declare function deltaEncode(data: Buffer): Buffer;
9
9
  export declare function deltaDecode(data: Buffer): Buffer;
10
10
  export declare function applyXor(buf: Buffer, passphrase: string): Buffer;
11
11
  export declare function tryDecryptIfNeeded(buf: Buffer, passphrase?: string): Buffer;
12
+ export declare function generatePalette256(): Buffer;
13
+ export declare function encodeDataToBlocks2x2(data: Buffer): {
14
+ buffer: Buffer;
15
+ width: number;
16
+ height: number;
17
+ };
18
+ export declare function decodeBlocksToData(indexedBuffer: Buffer, width: number, height: number): Buffer;
@@ -110,3 +110,43 @@ export function tryDecryptIfNeeded(buf, passphrase) {
110
110
  }
111
111
  return buf;
112
112
  }
113
+ export function generatePalette256() {
114
+ const palette = Buffer.alloc(256 * 3);
115
+ for (let i = 0; i < 256; i++) {
116
+ palette[i * 3] = i;
117
+ palette[i * 3 + 1] = (i * 127) & 0xff;
118
+ palette[i * 3 + 2] = 255 - i;
119
+ }
120
+ return palette;
121
+ }
122
+ export function encodeDataToBlocks2x2(data) {
123
+ const totalBytes = data.length;
124
+ const totalBlocks = totalBytes;
125
+ const blocksPerRow = Math.ceil(Math.sqrt(totalBlocks));
126
+ const numRows = Math.ceil(totalBlocks / blocksPerRow);
127
+ const pixelWidth = blocksPerRow * 2;
128
+ const pixelHeight = numRows * 2;
129
+ const indexedBuffer = Buffer.alloc(pixelWidth * pixelHeight);
130
+ for (let i = 0; i < totalBytes; i++) {
131
+ const blockX = (i % blocksPerRow) * 2;
132
+ const blockY = Math.floor(i / blocksPerRow) * 2;
133
+ const value = data[i];
134
+ indexedBuffer[blockY * pixelWidth + blockX] = value;
135
+ indexedBuffer[blockY * pixelWidth + blockX + 1] = value;
136
+ indexedBuffer[(blockY + 1) * pixelWidth + blockX] = value;
137
+ indexedBuffer[(blockY + 1) * pixelWidth + blockX + 1] = value;
138
+ }
139
+ return { buffer: indexedBuffer, width: pixelWidth, height: pixelHeight };
140
+ }
141
+ export function decodeBlocksToData(indexedBuffer, width, height) {
142
+ const blocksPerRow = width / 2;
143
+ const numRows = height / 2;
144
+ const totalBlocks = blocksPerRow * numRows;
145
+ const data = Buffer.alloc(totalBlocks);
146
+ for (let i = 0; i < totalBlocks; i++) {
147
+ const blockX = (i % blocksPerRow) * 2;
148
+ const blockY = Math.floor(i / blocksPerRow) * 2;
149
+ data[i] = indexedBuffer[blockY * width + blockX];
150
+ }
151
+ return data;
152
+ }
@@ -10,6 +10,7 @@ export interface EncodeOptions {
10
10
  encrypt?: 'auto' | 'aes' | 'xor' | 'none';
11
11
  _skipAuto?: boolean;
12
12
  output?: 'auto' | 'png' | 'rox';
13
+ outputFormat?: 'png' | 'webp';
13
14
  includeName?: boolean;
14
15
  includeFileList?: boolean;
15
16
  fileList?: Array<string | {
@@ -17,6 +18,7 @@ export interface EncodeOptions {
17
18
  size: number;
18
19
  }>;
19
20
  skipOptimization?: boolean;
21
+ useBlockEncoding?: boolean;
20
22
  onProgress?: (info: {
21
23
  phase: string;
22
24
  loaded?: number;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Encode binary data into PNG images and decode them back. CLI and programmatic API with native Rust acceleration.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,14 @@
22
22
  "build:cli": "cargo build --release --bin roxify_native && cp target/release/roxify_native dist/roxify-cli",
23
23
  "build:all": "npm run build:native && npm run build && npm run build:cli",
24
24
  "prepublishOnly": "npm run build:all",
25
- "test": "node test/test-*.js",
25
+ "test": "npm run build && node test/run-all-tests.js",
26
+ "test:final": "node test/test-final-complete.js",
27
+ "test:benchmark": "node test/benchmark-large-data.js",
28
+ "test:stress": "node test/stress-test-datatypes.js",
29
+ "test:compare": "node test/compare-png-webp.js",
30
+ "test:formats": "node test/benchmark-image-formats.js",
31
+ "test:optimize": "node test/benchmark-auto-optimize.cjs",
32
+ "test:predict": "node test/test-fast-prediction.cjs",
26
33
  "cli": "node dist/cli.js"
27
34
  },
28
35
  "keywords": [