roxify 1.3.1 → 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 +4 -3
- package/dist/roxify-cli +0 -0
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.js +1 -0
- package/dist/utils/decoder.js +96 -29
- package/dist/utils/encoder.js +182 -126
- package/dist/utils/helpers.d.ts +7 -0
- package/dist/utils/helpers.js +40 -0
- package/dist/utils/rust-cli-wrapper.js +43 -7
- package/dist/utils/types.d.ts +2 -0
- package/libroxify_native.node +0 -0
- package/package.json +9 -2
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.
|
|
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,
|
|
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:
|
|
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;
|
package/dist/utils/constants.js
CHANGED
|
@@ -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;
|
package/dist/utils/decoder.js
CHANGED
|
@@ -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
|
-
|
|
232
|
-
let
|
|
233
|
-
|
|
234
|
-
|
|
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:
|
|
241
|
-
width: currentWidth,
|
|
242
|
-
height:
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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 (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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) {
|
package/dist/utils/encoder.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
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
|
}
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/helpers.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -5,16 +5,52 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
7
|
function findRustBinary() {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'
|
|
8
|
+
const candidates = [];
|
|
9
|
+
const binNames = process.platform === 'win32'
|
|
10
|
+
? ['roxify-cli.exe', 'roxify_cli.exe', 'roxify_native.exe']
|
|
11
|
+
: ['roxify-cli', 'roxify_cli', 'roxify_native'];
|
|
12
|
+
// Possible locations relative to this file (works in repo and in packaged dist)
|
|
13
|
+
const relativeDirs = [
|
|
14
|
+
join(__dirname, '..', '..', 'target', 'release'),
|
|
15
|
+
join(__dirname, '..', '..', 'dist'),
|
|
16
|
+
join(__dirname, '..'),
|
|
17
|
+
join(__dirname, '..', '..'),
|
|
12
18
|
];
|
|
13
|
-
for (const
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
for (const dir of relativeDirs) {
|
|
20
|
+
for (const name of binNames) {
|
|
21
|
+
candidates.push(join(dir, name));
|
|
16
22
|
}
|
|
17
23
|
}
|
|
24
|
+
// Common global paths
|
|
25
|
+
if (process.platform !== 'win32') {
|
|
26
|
+
candidates.push('/usr/local/bin/roxify_native');
|
|
27
|
+
candidates.push('/usr/bin/roxify_native');
|
|
28
|
+
}
|
|
29
|
+
for (const p of candidates) {
|
|
30
|
+
try {
|
|
31
|
+
if (existsSync(p))
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
catch (e) { }
|
|
35
|
+
}
|
|
36
|
+
// Search in PATH for common binary names
|
|
37
|
+
try {
|
|
38
|
+
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
39
|
+
const { execSync } = require('child_process');
|
|
40
|
+
for (const name of binNames) {
|
|
41
|
+
try {
|
|
42
|
+
const out = execSync(`${which} ${name}`, { encoding: 'utf-8' })
|
|
43
|
+
.split('\n')[0]
|
|
44
|
+
.trim();
|
|
45
|
+
if (out && existsSync(out))
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
// ignore
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) { }
|
|
18
54
|
return null;
|
|
19
55
|
}
|
|
20
56
|
export function isRustBinaryAvailable() {
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -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;
|
package/libroxify_native.node
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.
|
|
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/
|
|
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": [
|