roxify 1.2.6 → 1.2.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.
package/dist/cli.js CHANGED
@@ -304,7 +304,7 @@ async function encodeCommand(args) {
304
304
  for await (const chunk of inputData) {
305
305
  chunks.push(chunk);
306
306
  }
307
- inputBuffer = Buffer.concat(chunks);
307
+ inputBuffer = chunks;
308
308
  }
309
309
  else {
310
310
  inputBuffer = inputData;
package/dist/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export * from './utils/reconstitution.js';
10
10
  export * from './utils/types.js';
11
11
  export * from './utils/zstd.js';
12
12
  export { decodeMinPng, encodeMinPng } from './minpng.js';
13
- export { packPaths, unpackBuffer } from './pack.js';
13
+ export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
package/dist/index.js CHANGED
@@ -10,4 +10,4 @@ export * from './utils/reconstitution.js';
10
10
  export * from './utils/types.js';
11
11
  export * from './utils/zstd.js';
12
12
  export { decodeMinPng, encodeMinPng } from './minpng.js';
13
- export { packPaths, unpackBuffer } from './pack.js';
13
+ export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
package/dist/pack.d.ts CHANGED
@@ -10,6 +10,10 @@ export interface VFSIndexEntry {
10
10
  offset: number;
11
11
  size: number;
12
12
  }
13
+ export declare function packPathsToParts(paths: string[], baseDir?: string, onProgress?: (readBytes: number, totalBytes: number, currentFile?: string) => void): {
14
+ parts: Buffer[];
15
+ list: string[];
16
+ };
13
17
  export declare function packPaths(paths: string[], baseDir?: string, onProgress?: (readBytes: number, totalBytes: number, currentFile?: string) => void): {
14
18
  buf: Buffer;
15
19
  list: string[];
package/dist/pack.js CHANGED
@@ -14,7 +14,7 @@ function* collectFilesGenerator(paths) {
14
14
  }
15
15
  }
16
16
  }
17
- export function packPaths(paths, baseDir, onProgress) {
17
+ export function packPathsToParts(paths, baseDir, onProgress) {
18
18
  const files = [];
19
19
  for (const f of collectFilesGenerator(paths)) {
20
20
  files.push(f);
@@ -48,6 +48,10 @@ export function packPaths(paths, baseDir, onProgress) {
48
48
  header.writeUInt32BE(0x524f5850, 0);
49
49
  header.writeUInt32BE(files.length, 4);
50
50
  parts.unshift(header);
51
+ return { parts, list };
52
+ }
53
+ export function packPaths(paths, baseDir, onProgress) {
54
+ const { parts, list } = packPathsToParts(paths, baseDir, onProgress);
51
55
  return { buf: Buffer.concat(parts), list };
52
56
  }
53
57
  export function unpackBuffer(buf, fileList) {
package/dist/utils/crc.js CHANGED
@@ -16,7 +16,7 @@ export function crc32(buf, previous = 0) {
16
16
  for (let i = 0; i < buf.length; i++) {
17
17
  crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
18
18
  }
19
- return crc ^ 0xffffffff;
19
+ return (crc ^ 0xffffffff) >>> 0;
20
20
  }
21
21
  export function adler32(buf, prev = 1) {
22
22
  let s1 = prev & 0xffff;
@@ -92,7 +92,7 @@ export async function decodePngToBinary(input, opts = {}) {
92
92
  try {
93
93
  const info = await sharp(pngBuf).metadata();
94
94
  if (info.width && info.height) {
95
- const MAX_RAW_BYTES = 150 * 1024 * 1024;
95
+ const MAX_RAW_BYTES = 1200 * 1024 * 1024;
96
96
  const rawBytesEstimate = info.width * info.height * 4;
97
97
  if (rawBytesEstimate > MAX_RAW_BYTES) {
98
98
  throw new DataFormatError(`Image too large to decode in-process (${Math.round(rawBytesEstimate / 1024 / 1024)} MB). Increase Node heap or use a smaller image/compact mode.`);
@@ -225,17 +225,44 @@ export async function decodePngToBinary(input, opts = {}) {
225
225
  return { buf: payload, meta: { name } };
226
226
  }
227
227
  try {
228
- const { data, info } = await sharp(processedBuf)
229
- .ensureAlpha()
230
- .raw()
231
- .toBuffer({ resolveWithObject: true });
232
- const currentWidth = info.width;
233
- const currentHeight = info.height;
234
- const rawRGB = Buffer.alloc(currentWidth * currentHeight * 3);
235
- for (let i = 0; i < currentWidth * currentHeight; i++) {
236
- rawRGB[i * 3] = data[i * 4];
237
- rawRGB[i * 3 + 1] = data[i * 4 + 1];
238
- rawRGB[i * 3 + 2] = data[i * 4 + 2];
228
+ const metadata = await sharp(processedBuf).metadata();
229
+ const currentWidth = metadata.width;
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)
238
+ .extract({
239
+ left: 0,
240
+ top: startRow,
241
+ width: currentWidth,
242
+ height: chunkHeight,
243
+ })
244
+ .raw()
245
+ .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];
257
+ }
258
+ }
259
+ if (opts.onProgress) {
260
+ opts.onProgress({
261
+ phase: 'extract_pixels',
262
+ loaded: endRow,
263
+ total: currentHeight,
264
+ });
265
+ }
239
266
  }
240
267
  const firstPixels = [];
241
268
  for (let i = 0; i < Math.min(MARKER_START.length, rawRGB.length / 3); i++) {
@@ -278,29 +305,25 @@ export async function decodePngToBinary(input, opts = {}) {
278
305
  else {
279
306
  const reconstructed = await cropAndReconstitute(processedBuf, opts.debugDir);
280
307
  const { data: rdata, info: rinfo } = await sharp(reconstructed)
281
- .ensureAlpha()
282
308
  .raw()
283
309
  .toBuffer({ resolveWithObject: true });
284
310
  logicalWidth = rinfo.width;
285
311
  logicalHeight = rinfo.height;
286
312
  logicalData = Buffer.alloc(rinfo.width * rinfo.height * 3);
287
- for (let i = 0; i < rinfo.width * rinfo.height; i++) {
288
- logicalData[i * 3] = rdata[i * 4];
289
- logicalData[i * 3 + 1] = rdata[i * 4 + 1];
290
- logicalData[i * 3 + 2] = rdata[i * 4 + 2];
313
+ if (rinfo.channels === 3) {
314
+ rdata.copy(logicalData);
315
+ }
316
+ else if (rinfo.channels === 4) {
317
+ for (let i = 0; i < logicalWidth * logicalHeight; i++) {
318
+ logicalData[i * 3] = rdata[i * 4];
319
+ logicalData[i * 3 + 1] = rdata[i * 4 + 1];
320
+ logicalData[i * 3 + 2] = rdata[i * 4 + 2];
321
+ }
291
322
  }
292
323
  }
293
324
  if (process.env.ROX_DEBUG) {
294
325
  console.log('DEBUG: Logical grid reconstructed:', logicalWidth, 'x', logicalHeight, '=', logicalWidth * logicalHeight, 'pixels');
295
326
  }
296
- const finalGrid = [];
297
- for (let i = 0; i < logicalData.length; i += 3) {
298
- finalGrid.push({
299
- r: logicalData[i],
300
- g: logicalData[i + 1],
301
- b: logicalData[i + 2],
302
- });
303
- }
304
327
  if (hasPixelMagic) {
305
328
  if (logicalData.length < 8 + PIXEL_MAGIC.length) {
306
329
  throw new DataFormatError('Pixel mode data too short');
@@ -337,15 +360,15 @@ export async function decodePngToBinary(input, opts = {}) {
337
360
  payload = payload.slice(MAGIC.length);
338
361
  return { buf: payload, meta: { name } };
339
362
  }
363
+ const totalPixels = (logicalData.length / 3) | 0;
340
364
  let startIdx = -1;
341
- for (let i = 0; i <= finalGrid.length - MARKER_START.length; i++) {
365
+ for (let i = 0; i <= totalPixels - MARKER_START.length; i++) {
342
366
  let match = true;
343
367
  for (let mi = 0; mi < MARKER_START.length && match; mi++) {
344
- const p = finalGrid[i + mi];
345
- if (!p ||
346
- p.r !== MARKER_START[mi].r ||
347
- p.g !== MARKER_START[mi].g ||
348
- p.b !== MARKER_START[mi].b) {
368
+ const offset = (i + mi) * 3;
369
+ if (logicalData[offset] !== MARKER_START[mi].r ||
370
+ logicalData[offset + 1] !== MARKER_START[mi].g ||
371
+ logicalData[offset + 2] !== MARKER_START[mi].b) {
349
372
  match = false;
350
373
  }
351
374
  }
@@ -356,7 +379,7 @@ export async function decodePngToBinary(input, opts = {}) {
356
379
  }
357
380
  if (startIdx === -1) {
358
381
  if (process.env.ROX_DEBUG) {
359
- console.log('DEBUG: MARKER_START not found in grid of', finalGrid.length, 'pixels');
382
+ console.log('DEBUG: MARKER_START not found in grid of', totalPixels, 'pixels');
360
383
  console.log('DEBUG: Trying 2D scan for START marker...');
361
384
  }
362
385
  let found2D = false;
@@ -417,17 +440,20 @@ export async function decodePngToBinary(input, opts = {}) {
417
440
  if (process.env.ROX_DEBUG) {
418
441
  console.log(`DEBUG: Extracted rectangle: ${rectWidth}x${rectHeight} from (${x},${y})`);
419
442
  }
420
- finalGrid.length = 0;
443
+ const newDataLen = rectWidth * rectHeight * 3;
444
+ const newData = Buffer.allocUnsafe(newDataLen);
445
+ let writeIdx = 0;
421
446
  for (let ry = y; ry <= endY; ry++) {
422
447
  for (let rx = x; rx <= endX; rx++) {
423
448
  const idx = (ry * logicalWidth + rx) * 3;
424
- finalGrid.push({
425
- r: logicalData[idx],
426
- g: logicalData[idx + 1],
427
- b: logicalData[idx + 2],
428
- });
449
+ newData[writeIdx++] = logicalData[idx];
450
+ newData[writeIdx++] = logicalData[idx + 1];
451
+ newData[writeIdx++] = logicalData[idx + 2];
429
452
  }
430
453
  }
454
+ logicalData = newData;
455
+ logicalWidth = rectWidth;
456
+ logicalHeight = rectHeight;
431
457
  startIdx = 0;
432
458
  found2D = true;
433
459
  }
@@ -435,34 +461,43 @@ export async function decodePngToBinary(input, opts = {}) {
435
461
  }
436
462
  if (!found2D) {
437
463
  if (process.env.ROX_DEBUG) {
438
- console.log('DEBUG: First 20 pixels:', finalGrid
439
- .slice(0, 20)
440
- .map((p) => `(${p.r},${p.g},${p.b})`)
441
- .join(' '));
464
+ const first20 = [];
465
+ for (let i = 0; i < Math.min(20, totalPixels); i++) {
466
+ const offset = i * 3;
467
+ first20.push(`(${logicalData[offset]},${logicalData[offset + 1]},${logicalData[offset + 2]})`);
468
+ }
469
+ console.log('DEBUG: First 20 pixels:', first20.join(' '));
442
470
  }
443
471
  throw new Error('Marker START not found - image format not supported');
444
472
  }
445
473
  }
446
474
  if (process.env.ROX_DEBUG && startIdx === 0) {
447
- console.log(`DEBUG: MARKER_START at index ${startIdx}, grid size: ${finalGrid.length}`);
475
+ console.log(`DEBUG: MARKER_START at index ${startIdx}, grid size: ${totalPixels}`);
448
476
  }
449
- const gridFromStart = finalGrid.slice(startIdx);
450
- if (gridFromStart.length < MARKER_START.length + MARKER_END.length) {
477
+ const dataStartPixel = startIdx + MARKER_START.length + 1;
478
+ const curTotalPixels = (logicalData.length / 3) | 0;
479
+ if (curTotalPixels < dataStartPixel + MARKER_END.length) {
451
480
  if (process.env.ROX_DEBUG) {
452
- console.log('DEBUG: gridFromStart too small:', gridFromStart.length, 'pixels');
481
+ console.log('DEBUG: grid too small:', curTotalPixels, 'pixels');
453
482
  }
454
483
  throw new Error('Marker START or END not found - image format not supported');
455
484
  }
456
485
  for (let i = 0; i < MARKER_START.length; i++) {
457
- if (gridFromStart[i].r !== MARKER_START[i].r ||
458
- gridFromStart[i].g !== MARKER_START[i].g ||
459
- gridFromStart[i].b !== MARKER_START[i].b) {
486
+ const offset = (startIdx + i) * 3;
487
+ if (logicalData[offset] !== MARKER_START[i].r ||
488
+ logicalData[offset + 1] !== MARKER_START[i].g ||
489
+ logicalData[offset + 2] !== MARKER_START[i].b) {
460
490
  throw new Error('Marker START not found - image format not supported');
461
491
  }
462
492
  }
463
493
  let compression = 'zstd';
464
- if (gridFromStart.length > MARKER_START.length) {
465
- const compPixel = gridFromStart[MARKER_START.length];
494
+ if (curTotalPixels > startIdx + MARKER_START.length) {
495
+ const compOffset = (startIdx + MARKER_START.length) * 3;
496
+ const compPixel = {
497
+ r: logicalData[compOffset],
498
+ g: logicalData[compOffset + 1],
499
+ b: logicalData[compOffset + 2],
500
+ };
466
501
  if (compPixel.r === 0 && compPixel.g === 255 && compPixel.b === 0) {
467
502
  compression = 'zstd';
468
503
  }
@@ -473,47 +508,51 @@ export async function decodePngToBinary(input, opts = {}) {
473
508
  if (process.env.ROX_DEBUG) {
474
509
  console.log(`DEBUG: Detected compression: ${compression}`);
475
510
  }
476
- let endStartIdx = -1;
511
+ let endStartPixel = -1;
477
512
  const lastLineStart = (logicalHeight - 1) * logicalWidth;
478
513
  const endMarkerStartCol = logicalWidth - MARKER_END.length;
479
- if (lastLineStart + endMarkerStartCol < finalGrid.length) {
514
+ if (lastLineStart + endMarkerStartCol < curTotalPixels) {
480
515
  let matchEnd = true;
481
516
  for (let mi = 0; mi < MARKER_END.length && matchEnd; mi++) {
482
- const idx = lastLineStart + endMarkerStartCol + mi;
483
- if (idx >= finalGrid.length) {
517
+ const pixelIdx = lastLineStart + endMarkerStartCol + mi;
518
+ if (pixelIdx >= curTotalPixels) {
484
519
  matchEnd = false;
485
520
  break;
486
521
  }
487
- const p = finalGrid[idx];
488
- if (p.r !== MARKER_END[mi].r ||
489
- p.g !== MARKER_END[mi].g ||
490
- p.b !== MARKER_END[mi].b) {
522
+ const offset = pixelIdx * 3;
523
+ if (logicalData[offset] !== MARKER_END[mi].r ||
524
+ logicalData[offset + 1] !== MARKER_END[mi].g ||
525
+ logicalData[offset + 2] !== MARKER_END[mi].b) {
491
526
  matchEnd = false;
492
527
  }
493
528
  }
494
529
  if (matchEnd) {
495
- endStartIdx = lastLineStart + endMarkerStartCol - startIdx;
530
+ endStartPixel = lastLineStart + endMarkerStartCol - startIdx;
496
531
  if (process.env.ROX_DEBUG) {
497
532
  console.log(`DEBUG: Found END marker at last line, col ${endMarkerStartCol}`);
498
533
  }
499
534
  }
500
535
  }
501
- if (endStartIdx === -1) {
536
+ if (endStartPixel === -1) {
502
537
  if (process.env.ROX_DEBUG) {
503
538
  console.log('DEBUG: END marker not found at expected position');
504
- console.log('DEBUG: Last line pixels:', finalGrid
505
- .slice(Math.max(0, lastLineStart), finalGrid.length)
506
- .map((p) => `(${p.r},${p.g},${p.b})`)
507
- .join(' '));
508
- }
509
- endStartIdx = gridFromStart.length;
510
- }
511
- const dataGrid = gridFromStart.slice(MARKER_START.length + 1, endStartIdx);
512
- const pixelBytes = Buffer.alloc(dataGrid.length * 3);
513
- for (let i = 0; i < dataGrid.length; i++) {
514
- pixelBytes[i * 3] = dataGrid[i].r;
515
- pixelBytes[i * 3 + 1] = dataGrid[i].g;
516
- pixelBytes[i * 3 + 2] = dataGrid[i].b;
539
+ const lastLinePixels = [];
540
+ for (let i = Math.max(0, lastLineStart); i < curTotalPixels && i < lastLineStart + 20; i++) {
541
+ const offset = i * 3;
542
+ lastLinePixels.push(`(${logicalData[offset]},${logicalData[offset + 1]},${logicalData[offset + 2]})`);
543
+ }
544
+ console.log('DEBUG: Last line pixels:', lastLinePixels.join(' '));
545
+ }
546
+ endStartPixel = curTotalPixels - startIdx;
547
+ }
548
+ const dataPixelCount = endStartPixel - (MARKER_START.length + 1);
549
+ const pixelBytes = Buffer.allocUnsafe(dataPixelCount * 3);
550
+ for (let i = 0; i < dataPixelCount; i++) {
551
+ const srcOffset = (dataStartPixel + i) * 3;
552
+ const dstOffset = i * 3;
553
+ pixelBytes[dstOffset] = logicalData[srcOffset];
554
+ pixelBytes[dstOffset + 1] = logicalData[srcOffset + 1];
555
+ pixelBytes[dstOffset + 2] = logicalData[srcOffset + 2];
517
556
  }
518
557
  if (process.env.ROX_DEBUG) {
519
558
  console.log('DEBUG: extracted len', pixelBytes.length);
@@ -1,4 +1,4 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
3
  import { EncodeOptions } from './types.js';
4
- export declare function encodeBinaryToPng(input: Buffer, opts?: EncodeOptions): Promise<Buffer>;
4
+ export declare function encodeBinaryToPng(input: Buffer | Buffer[], opts?: EncodeOptions): Promise<Buffer>;
@@ -1,11 +1,11 @@
1
1
  import cliProgress from 'cli-progress';
2
2
  import { createCipheriv, pbkdf2Sync, randomBytes } from 'crypto';
3
- import encode from 'png-chunks-encode';
4
3
  import sharp from 'sharp';
5
4
  import * as zlib from 'zlib';
6
5
  import { unpackBuffer } from '../pack.js';
7
- import { COMPRESSION_MARKERS, ENC_AES, ENC_NONE, ENC_XOR, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PNG_HEADER, PNG_HEADER_HEX, } from './constants.js';
8
- import { applyXor, colorsToBytes } from './helpers.js';
6
+ import { COMPRESSION_MARKERS, ENC_AES, ENC_NONE, ENC_XOR, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PNG_HEADER, } from './constants.js';
7
+ import { crc32 } from './crc.js';
8
+ import { colorsToBytes } from './helpers.js';
9
9
  import { optimizePngBuffer } from './optimization.js';
10
10
  import { parallelZstdCompress } from './zstd.js';
11
11
  export async function encodeBinaryToPng(input, opts = {}) {
@@ -41,11 +41,19 @@ export async function encodeBinaryToPng(input, opts = {}) {
41
41
  };
42
42
  }
43
43
  }
44
- let payload = Buffer.concat([MAGIC, input]);
44
+ let payloadInput;
45
+ let totalLen = 0;
46
+ if (Array.isArray(input)) {
47
+ payloadInput = [MAGIC, ...input];
48
+ totalLen = MAGIC.length + input.reduce((a, b) => a + b.length, 0);
49
+ }
50
+ else {
51
+ payloadInput = [MAGIC, input];
52
+ totalLen = MAGIC.length + input.length;
53
+ }
45
54
  if (opts.onProgress)
46
- opts.onProgress({ phase: 'compress_start', total: payload.length });
47
- const deltaEncoded = payload;
48
- payload = await parallelZstdCompress(deltaEncoded, 19, (loaded, total) => {
55
+ opts.onProgress({ phase: 'compress_start', total: totalLen });
56
+ let payload = await parallelZstdCompress(payloadInput, 15, (loaded, total) => {
49
57
  if (opts.onProgress) {
50
58
  opts.onProgress({
51
59
  phase: 'compress_progress',
@@ -56,6 +64,9 @@ export async function encodeBinaryToPng(input, opts = {}) {
56
64
  });
57
65
  if (opts.onProgress)
58
66
  opts.onProgress({ phase: 'compress_done', loaded: payload.length });
67
+ if (Array.isArray(input)) {
68
+ input.length = 0;
69
+ }
59
70
  if (opts.passphrase && !opts.encrypt) {
60
71
  opts.encrypt = 'aes';
61
72
  }
@@ -83,29 +94,44 @@ export async function encodeBinaryToPng(input, opts = {}) {
83
94
  const PBKDF2_ITERS = 1000000;
84
95
  const key = pbkdf2Sync(opts.passphrase, salt, PBKDF2_ITERS, 32, 'sha256');
85
96
  const cipher = createCipheriv('aes-256-gcm', key, iv);
86
- const enc = Buffer.concat([cipher.update(payload), cipher.final()]);
97
+ const encParts = [];
98
+ for (const chunk of payload) {
99
+ encParts.push(cipher.update(chunk));
100
+ }
101
+ encParts.push(cipher.final());
87
102
  const tag = cipher.getAuthTag();
88
- payload = Buffer.concat([Buffer.from([ENC_AES]), salt, iv, tag, enc]);
103
+ payload = [Buffer.from([ENC_AES]), salt, iv, tag, ...encParts];
89
104
  if (opts.onProgress)
90
105
  opts.onProgress({ phase: 'encrypt_done' });
91
106
  }
92
107
  else if (encChoice === 'xor') {
93
- const xored = applyXor(payload, opts.passphrase);
94
- payload = Buffer.concat([Buffer.from([ENC_XOR]), xored]);
108
+ const xoredParts = [];
109
+ let offset = 0;
110
+ const keyBuf = Buffer.from(opts.passphrase, 'utf8');
111
+ for (const chunk of payload) {
112
+ const out = Buffer.alloc(chunk.length);
113
+ for (let i = 0; i < chunk.length; i++) {
114
+ out[i] = chunk[i] ^ keyBuf[(offset + i) % keyBuf.length];
115
+ }
116
+ offset += chunk.length;
117
+ xoredParts.push(out);
118
+ }
119
+ payload = [Buffer.from([ENC_XOR]), ...xoredParts];
95
120
  if (opts.onProgress)
96
121
  opts.onProgress({ phase: 'encrypt_done' });
97
122
  }
98
123
  else if (encChoice === 'none') {
99
- payload = Buffer.concat([Buffer.from([ENC_NONE]), payload]);
124
+ payload = [Buffer.from([ENC_NONE]), ...payload];
100
125
  if (opts.onProgress)
101
126
  opts.onProgress({ phase: 'encrypt_done' });
102
127
  }
103
128
  }
104
129
  else {
105
- payload = Buffer.concat([Buffer.from([ENC_NONE]), payload]);
130
+ payload = [Buffer.from([ENC_NONE]), ...payload];
106
131
  }
132
+ const payloadTotalLen = payload.reduce((a, b) => a + b.length, 0);
107
133
  if (opts.onProgress)
108
- opts.onProgress({ phase: 'meta_prep_done', loaded: payload.length });
134
+ opts.onProgress({ phase: 'meta_prep_done', loaded: payloadTotalLen });
109
135
  const metaParts = [];
110
136
  const includeName = opts.includeName === undefined ? true : !!opts.includeName;
111
137
  if (includeName && opts.name) {
@@ -116,19 +142,20 @@ export async function encodeBinaryToPng(input, opts = {}) {
116
142
  else {
117
143
  metaParts.push(Buffer.from([0]));
118
144
  }
119
- metaParts.push(payload);
120
- let meta = Buffer.concat(metaParts);
145
+ let meta = [...metaParts, ...payload];
121
146
  if (opts.includeFileList && opts.fileList) {
122
147
  let sizeMap = null;
123
- try {
124
- const unpack = unpackBuffer(input);
125
- if (unpack) {
126
- sizeMap = {};
127
- for (const ef of unpack.files)
128
- sizeMap[ef.path] = ef.buf.length;
148
+ if (!Array.isArray(input)) {
149
+ try {
150
+ const unpack = unpackBuffer(input);
151
+ if (unpack) {
152
+ sizeMap = {};
153
+ for (const ef of unpack.files)
154
+ sizeMap[ef.path] = ef.buf.length;
155
+ }
129
156
  }
157
+ catch (e) { }
130
158
  }
131
- catch (e) { }
132
159
  const normalized = opts.fileList.map((f) => {
133
160
  if (typeof f === 'string')
134
161
  return { name: f, size: sizeMap && sizeMap[f] ? sizeMap[f] : 0 };
@@ -143,10 +170,10 @@ export async function encodeBinaryToPng(input, opts = {}) {
143
170
  const jsonBuf = Buffer.from(JSON.stringify(normalized), 'utf8');
144
171
  const lenBuf = Buffer.alloc(4);
145
172
  lenBuf.writeUInt32BE(jsonBuf.length, 0);
146
- meta = Buffer.concat([meta, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf]);
173
+ meta = [...meta, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf];
147
174
  }
148
175
  if (opts.output === 'rox') {
149
- return Buffer.concat([MAGIC, meta]);
176
+ return Buffer.concat([MAGIC, ...meta]);
150
177
  }
151
178
  {
152
179
  const nameBuf = opts.name
@@ -154,26 +181,28 @@ export async function encodeBinaryToPng(input, opts = {}) {
154
181
  : Buffer.alloc(0);
155
182
  const nameLen = nameBuf.length;
156
183
  const payloadLenBuf = Buffer.alloc(4);
157
- payloadLenBuf.writeUInt32BE(payload.length, 0);
184
+ payloadLenBuf.writeUInt32BE(payloadTotalLen, 0);
158
185
  const version = 1;
159
- let metaPixel = Buffer.concat([
186
+ let metaPixel = [
160
187
  Buffer.from([version]),
161
188
  Buffer.from([nameLen]),
162
189
  nameBuf,
163
190
  payloadLenBuf,
164
- payload,
165
- ]);
191
+ ...payload,
192
+ ];
166
193
  if (opts.includeFileList && opts.fileList) {
167
194
  let sizeMap2 = null;
168
- try {
169
- const unpack = unpackBuffer(input);
170
- if (unpack) {
171
- sizeMap2 = {};
172
- for (const ef of unpack.files)
173
- sizeMap2[ef.path] = ef.buf.length;
195
+ if (!Array.isArray(input)) {
196
+ try {
197
+ const unpack = unpackBuffer(input);
198
+ if (unpack) {
199
+ sizeMap2 = {};
200
+ for (const ef of unpack.files)
201
+ sizeMap2[ef.path] = ef.buf.length;
202
+ }
174
203
  }
204
+ catch (e) { }
175
205
  }
176
- catch (e) { }
177
206
  const normalized = opts.fileList.map((f) => {
178
207
  if (typeof f === 'string')
179
208
  return { name: f, size: sizeMap2 && sizeMap2[f] ? sizeMap2[f] : 0 };
@@ -188,27 +217,24 @@ export async function encodeBinaryToPng(input, opts = {}) {
188
217
  const jsonBuf = Buffer.from(JSON.stringify(normalized), 'utf8');
189
218
  const lenBuf = Buffer.alloc(4);
190
219
  lenBuf.writeUInt32BE(jsonBuf.length, 0);
191
- metaPixel = Buffer.concat([
192
- metaPixel,
193
- Buffer.from('rXFL', 'utf8'),
194
- lenBuf,
195
- jsonBuf,
196
- ]);
220
+ metaPixel = [...metaPixel, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf];
197
221
  }
198
- const dataWithoutMarkers = Buffer.concat([PIXEL_MAGIC, metaPixel]);
199
- const padding = (3 - (dataWithoutMarkers.length % 3)) % 3;
222
+ const dataWithoutMarkers = [PIXEL_MAGIC, ...metaPixel];
223
+ const dataWithoutMarkersLen = dataWithoutMarkers.reduce((a, b) => a + b.length, 0);
224
+ const padding = (3 - (dataWithoutMarkersLen % 3)) % 3;
200
225
  const paddedData = padding > 0
201
- ? Buffer.concat([dataWithoutMarkers, Buffer.alloc(padding)])
226
+ ? [...dataWithoutMarkers, Buffer.alloc(padding)]
202
227
  : dataWithoutMarkers;
203
228
  const markerStartBytes = colorsToBytes(MARKER_START);
204
229
  const compressionMarkerBytes = colorsToBytes(COMPRESSION_MARKERS.zstd);
205
- const dataWithMarkers = Buffer.concat([
230
+ const dataWithMarkers = [
206
231
  markerStartBytes,
207
232
  compressionMarkerBytes,
208
- paddedData,
209
- ]);
233
+ ...paddedData,
234
+ ];
210
235
  const bytesPerPixel = 3;
211
- const dataPixels = Math.ceil(dataWithMarkers.length / 3);
236
+ const dataWithMarkersLen = dataWithMarkers.reduce((a, b) => a + b.length, 0);
237
+ const dataPixels = Math.ceil(dataWithMarkersLen / 3);
212
238
  const totalPixels = dataPixels + MARKER_END.length;
213
239
  const maxWidth = 16384;
214
240
  let side = Math.ceil(Math.sqrt(totalPixels));
@@ -227,8 +253,37 @@ export async function encodeBinaryToPng(input, opts = {}) {
227
253
  const scale = 1;
228
254
  const width = logicalWidth * scale;
229
255
  const height = logicalHeight * scale;
230
- const raw = Buffer.alloc(width * height * bytesPerPixel);
256
+ const LARGE_IMAGE_PIXELS = 50000000;
257
+ const useManualPng = width * height > LARGE_IMAGE_PIXELS || !!process.env.ROX_FAST_PNG;
258
+ let raw;
259
+ let stride = 0;
260
+ if (useManualPng) {
261
+ stride = width * 3 + 1;
262
+ raw = Buffer.alloc(height * stride);
263
+ }
264
+ else {
265
+ raw = Buffer.alloc(width * height * bytesPerPixel);
266
+ }
267
+ let currentBufIdx = 0;
268
+ let currentBufOffset = 0;
269
+ const getNextByte = () => {
270
+ while (currentBufIdx < dataWithMarkers.length) {
271
+ const buf = dataWithMarkers[currentBufIdx];
272
+ if (currentBufOffset < buf.length) {
273
+ return buf[currentBufOffset++];
274
+ }
275
+ currentBufIdx++;
276
+ currentBufOffset = 0;
277
+ }
278
+ return 0;
279
+ };
231
280
  for (let ly = 0; ly < logicalHeight; ly++) {
281
+ if (useManualPng) {
282
+ for (let sy = 0; sy < scale; sy++) {
283
+ const py = ly * scale + sy;
284
+ raw[py * stride] = 0;
285
+ }
286
+ }
232
287
  for (let lx = 0; lx < logicalWidth; lx++) {
233
288
  const linearIdx = ly * logicalWidth + lx;
234
289
  let r = 0, g = 0, b = 0;
@@ -240,107 +295,126 @@ export async function encodeBinaryToPng(input, opts = {}) {
240
295
  b = MARKER_END[markerIdx].b;
241
296
  }
242
297
  else if (linearIdx < dataPixels) {
243
- const srcIdx = linearIdx * 3;
244
- r = srcIdx < dataWithMarkers.length ? dataWithMarkers[srcIdx] : 0;
245
- g =
246
- srcIdx + 1 < dataWithMarkers.length
247
- ? dataWithMarkers[srcIdx + 1]
248
- : 0;
249
- b =
250
- srcIdx + 2 < dataWithMarkers.length
251
- ? dataWithMarkers[srcIdx + 2]
252
- : 0;
298
+ r = getNextByte();
299
+ g = getNextByte();
300
+ b = getNextByte();
253
301
  }
254
302
  for (let sy = 0; sy < scale; sy++) {
255
303
  for (let sx = 0; sx < scale; sx++) {
256
304
  const px = lx * scale + sx;
257
305
  const py = ly * scale + sy;
258
- const dstIdx = (py * width + px) * 3;
259
- raw[dstIdx] = r;
260
- raw[dstIdx + 1] = g;
261
- raw[dstIdx + 2] = b;
306
+ if (useManualPng) {
307
+ const dstIdx = py * stride + 1 + px * 3;
308
+ raw[dstIdx] = r;
309
+ raw[dstIdx + 1] = g;
310
+ raw[dstIdx + 2] = b;
311
+ }
312
+ else {
313
+ const dstIdx = (py * width + px) * 3;
314
+ raw[dstIdx] = r;
315
+ raw[dstIdx + 1] = g;
316
+ raw[dstIdx + 2] = b;
317
+ }
262
318
  }
263
319
  }
264
320
  }
265
321
  }
322
+ payload.length = 0;
323
+ dataWithMarkers.length = 0;
324
+ metaPixel.length = 0;
325
+ meta.length = 0;
326
+ paddedData.length = 0;
327
+ dataWithoutMarkers.length = 0;
266
328
  if (opts.onProgress)
267
- opts.onProgress({ phase: 'png_gen', loaded: 0, total: 100 });
268
- let loaded = 0;
269
- const progressInterval = setInterval(() => {
270
- loaded = Math.min(loaded + 2, 98);
271
- if (opts.onProgress)
272
- opts.onProgress({ phase: 'png_gen', loaded, total: 100 });
273
- }, 50);
329
+ opts.onProgress({ phase: 'png_gen', loaded: 0, total: height });
274
330
  let bufScr;
275
- const LARGE_IMAGE_PIXELS = 50000000;
276
- if (width * height > LARGE_IMAGE_PIXELS || process.env.ROX_FAST_PNG) {
277
- const idatData = zlib.deflateSync(raw, {
278
- level: 6,
331
+ if (useManualPng) {
332
+ const bytesPerRow = width * 3;
333
+ const scanlinesData = Buffer.alloc(height * (1 + bytesPerRow));
334
+ const progressStep = Math.max(1, Math.floor(height / 20));
335
+ for (let row = 0; row < height; row++) {
336
+ scanlinesData[row * (1 + bytesPerRow)] = 0;
337
+ const srcStart = row * stride + 1;
338
+ const dstStart = row * (1 + bytesPerRow) + 1;
339
+ raw.copy(scanlinesData, dstStart, srcStart, srcStart + bytesPerRow);
340
+ if (opts.onProgress && row % progressStep === 0) {
341
+ opts.onProgress({ phase: 'png_gen', loaded: row, total: height });
342
+ }
343
+ }
344
+ if (opts.onProgress)
345
+ opts.onProgress({ phase: 'png_compress', loaded: 0, total: 100 });
346
+ const idatData = zlib.deflateSync(scanlinesData, {
347
+ level: 3,
279
348
  memLevel: 8,
280
349
  strategy: zlib.constants.Z_DEFAULT_STRATEGY,
281
350
  });
282
- const ihdrData2 = Buffer.alloc(13);
283
- ihdrData2.writeUInt32BE(width, 0);
284
- ihdrData2.writeUInt32BE(height, 4);
285
- ihdrData2[8] = 8;
286
- ihdrData2[9] = 2;
287
- ihdrData2[10] = 0;
288
- ihdrData2[11] = 0;
289
- ihdrData2[12] = 0;
290
- const chunks2 = [];
291
- chunks2.push({ name: 'IHDR', data: ihdrData2 });
292
- chunks2.push({ name: 'IDAT', data: idatData });
293
- chunks2.push({ name: 'IEND', data: Buffer.alloc(0) });
294
- const tmp = Buffer.from(encode(chunks2));
295
- bufScr =
296
- tmp.slice(0, 8).toString('hex') === PNG_HEADER_HEX
297
- ? tmp
298
- : Buffer.concat([PNG_HEADER, tmp]);
351
+ raw = Buffer.alloc(0);
352
+ const ihdrData = Buffer.alloc(13);
353
+ ihdrData.writeUInt32BE(width, 0);
354
+ ihdrData.writeUInt32BE(height, 4);
355
+ ihdrData[8] = 8;
356
+ ihdrData[9] = 2;
357
+ ihdrData[10] = 0;
358
+ ihdrData[11] = 0;
359
+ ihdrData[12] = 0;
360
+ const ihdrType = Buffer.from('IHDR', 'utf8');
361
+ const ihdrCrc = crc32(ihdrData, crc32(ihdrType));
362
+ const ihdrCrcBuf = Buffer.alloc(4);
363
+ ihdrCrcBuf.writeUInt32BE(ihdrCrc, 0);
364
+ const ihdrLen = Buffer.alloc(4);
365
+ ihdrLen.writeUInt32BE(ihdrData.length, 0);
366
+ const idatType = Buffer.from('IDAT', 'utf8');
367
+ const idatCrc = crc32(idatData, crc32(idatType));
368
+ const idatCrcBuf = Buffer.alloc(4);
369
+ idatCrcBuf.writeUInt32BE(idatCrc, 0);
370
+ const idatLen = Buffer.alloc(4);
371
+ idatLen.writeUInt32BE(idatData.length, 0);
372
+ const iendType = Buffer.from('IEND', 'utf8');
373
+ const iendCrc = crc32(Buffer.alloc(0), crc32(iendType));
374
+ const iendCrcBuf = Buffer.alloc(4);
375
+ iendCrcBuf.writeUInt32BE(iendCrc, 0);
376
+ const iendLen = Buffer.alloc(4);
377
+ iendLen.writeUInt32BE(0, 0);
378
+ bufScr = Buffer.concat([
379
+ PNG_HEADER,
380
+ ihdrLen,
381
+ ihdrType,
382
+ ihdrData,
383
+ ihdrCrcBuf,
384
+ idatLen,
385
+ idatType,
386
+ idatData,
387
+ idatCrcBuf,
388
+ iendLen,
389
+ iendType,
390
+ iendCrcBuf,
391
+ ]);
299
392
  }
300
393
  else {
301
394
  bufScr = await sharp(raw, {
302
395
  raw: { width, height, channels: 3 },
303
396
  })
304
397
  .png({
305
- compressionLevel: 6,
398
+ compressionLevel: 3,
306
399
  palette: false,
307
400
  effort: 1,
308
401
  adaptiveFiltering: false,
309
402
  })
310
403
  .toBuffer();
311
404
  }
405
+ raw = Buffer.alloc(0);
312
406
  if (opts.onProgress)
313
- opts.onProgress({ phase: 'png_gen', loaded: 100, total: 100 });
407
+ opts.onProgress({ phase: 'png_compress', loaded: 100, total: 100 });
314
408
  if (opts.onProgress)
315
409
  opts.onProgress({ phase: 'optimizing', loaded: 0, total: 100 });
316
- let optInterval = null;
317
- if (opts.onProgress) {
318
- let optLoaded = 0;
319
- optInterval = setInterval(() => {
320
- optLoaded = Math.min(optLoaded + 5, 95);
321
- opts.onProgress?.({
322
- phase: 'optimizing',
323
- loaded: optLoaded,
324
- total: 100,
325
- });
326
- }, 100);
327
- }
328
410
  try {
329
411
  const optimized = await optimizePngBuffer(bufScr, true);
330
- clearInterval(progressInterval);
331
- if (optInterval) {
332
- clearInterval(optInterval);
333
- optInterval = null;
334
- }
335
412
  if (opts.onProgress)
336
413
  opts.onProgress({ phase: 'optimizing', loaded: 100, total: 100 });
337
414
  progressBar?.stop();
338
415
  return optimized;
339
416
  }
340
417
  catch (e) {
341
- clearInterval(progressInterval);
342
- if (optInterval)
343
- clearInterval(optInterval);
344
418
  progressBar?.stop();
345
419
  return bufScr;
346
420
  }
@@ -7,6 +7,10 @@ import extract from 'png-chunks-extract';
7
7
  import * as zlib from 'zlib';
8
8
  import { PNG_HEADER, PNG_HEADER_HEX } from './constants.js';
9
9
  export async function optimizePngBuffer(pngBuf, fast = false) {
10
+ const MAX_OPTIMIZE_SIZE = 50 * 1024 * 1024;
11
+ if (pngBuf.length > MAX_OPTIMIZE_SIZE) {
12
+ return pngBuf;
13
+ }
10
14
  const runCommandAsync = (cmd, args, timeout = 120000) => {
11
15
  return new Promise((resolve) => {
12
16
  try {
@@ -4,7 +4,7 @@ export declare function compressStream(stream: AsyncGenerator<Buffer>, level?: n
4
4
  chunks: Buffer[];
5
5
  totalLength: number;
6
6
  }>;
7
- export declare function parallelZstdCompress(payload: Buffer, level?: number, onProgress?: (loaded: number, total: number) => void): Promise<Buffer>;
7
+ export declare function parallelZstdCompress(payload: Buffer | Buffer[], level?: number, onProgress?: (loaded: number, total: number) => void): Promise<Buffer[]>;
8
8
  export declare function parallelZstdDecompress(payload: Buffer, onProgress?: (info: {
9
9
  phase: string;
10
10
  loaded?: number;
@@ -26,22 +26,35 @@ export async function compressStream(stream, level = 19, onProgress) {
26
26
  }
27
27
  export async function parallelZstdCompress(payload, level = 19, onProgress) {
28
28
  const chunkSize = 8 * 1024 * 1024;
29
- if (payload.length <= chunkSize) {
30
- if (onProgress)
31
- onProgress(0, 1);
32
- const result = await zstdCompress(payload, level);
33
- if (onProgress)
34
- onProgress(1, 1);
35
- return Buffer.from(result);
36
- }
37
29
  const chunks = [];
38
- for (let i = 0; i < payload.length; i += chunkSize) {
39
- const chunk = payload.subarray(i, Math.min(i + chunkSize, payload.length));
40
- chunks.push(chunk);
30
+ if (Array.isArray(payload)) {
31
+ for (const p of payload) {
32
+ if (p.length <= chunkSize) {
33
+ chunks.push(p);
34
+ }
35
+ else {
36
+ for (let i = 0; i < p.length; i += chunkSize) {
37
+ chunks.push(p.subarray(i, Math.min(i + chunkSize, p.length)));
38
+ }
39
+ }
40
+ }
41
+ }
42
+ else {
43
+ if (payload.length <= chunkSize) {
44
+ if (onProgress)
45
+ onProgress(0, 1);
46
+ const result = await zstdCompress(payload, level);
47
+ if (onProgress)
48
+ onProgress(1, 1);
49
+ return [Buffer.from(result)];
50
+ }
51
+ for (let i = 0; i < payload.length; i += chunkSize) {
52
+ chunks.push(payload.subarray(i, Math.min(i + chunkSize, payload.length)));
53
+ }
41
54
  }
42
55
  const totalChunks = chunks.length;
43
56
  let completedChunks = 0;
44
- const concurrency = Math.max(1, Math.min(8, cpus().length));
57
+ const concurrency = Math.max(1, Math.min(4, cpus().length));
45
58
  const compressedChunks = new Array(totalChunks);
46
59
  let idx = 0;
47
60
  const worker = async () => {
@@ -65,7 +78,7 @@ export async function parallelZstdCompress(payload, level = 19, onProgress) {
65
78
  const header = Buffer.alloc(8);
66
79
  header.writeUInt32BE(0x5a535444, 0);
67
80
  header.writeUInt32BE(compressedChunks.length, 4);
68
- return Buffer.concat([header, chunkSizes, ...compressedChunks]);
81
+ return [header, chunkSizes, ...compressedChunks];
69
82
  }
70
83
  export async function parallelZstdDecompress(payload, onProgress) {
71
84
  if (payload.length < 8) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
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",