roxify 1.5.2 → 1.5.3
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/utils/decoder.js +210 -1
- package/dist/utils/reconstitution.js +24 -1
- package/package.json +1 -1
package/dist/utils/decoder.js
CHANGED
|
@@ -347,8 +347,11 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
347
347
|
logicalData = rawRGB;
|
|
348
348
|
}
|
|
349
349
|
else {
|
|
350
|
+
console.log('DEBUG: about to call cropAndReconstitute, debugDir=', opts.debugDir);
|
|
350
351
|
const reconstructed = await cropAndReconstitute(processedBuf, opts.debugDir);
|
|
352
|
+
console.log('DEBUG: cropAndReconstitute returned, reconstructed len=', reconstructed.length);
|
|
351
353
|
const rawData = native.sharpToRaw(reconstructed);
|
|
354
|
+
console.log('DEBUG: rawData from reconstructed:', rawData.width, 'x', rawData.height, 'pixels=', Math.floor(rawData.pixels.length / 3));
|
|
352
355
|
logicalWidth = rawData.width;
|
|
353
356
|
logicalHeight = rawData.height;
|
|
354
357
|
logicalData = Buffer.from(rawData.pixels);
|
|
@@ -596,6 +599,16 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
596
599
|
const markerEndBytes = colorsToBytes(MARKER_END);
|
|
597
600
|
console.log('DEBUG: MARKER_END index:', pixelBytes.indexOf(markerEndBytes));
|
|
598
601
|
}
|
|
602
|
+
if (opts.debugDir) {
|
|
603
|
+
try {
|
|
604
|
+
console.log('DEBUG: writing extracted pixel bytes to', opts.debugDir);
|
|
605
|
+
writeFileSync(join(opts.debugDir, 'extracted-pixel-bytes.bin'), pixelBytes);
|
|
606
|
+
writeFileSync(join(opts.debugDir, 'extracted-pixel-head.hex'), pixelBytes.slice(0, 512).toString('hex'));
|
|
607
|
+
}
|
|
608
|
+
catch (e) {
|
|
609
|
+
console.log('DEBUG: failed writing extracted bytes', e?.message ?? e);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
599
612
|
}
|
|
600
613
|
try {
|
|
601
614
|
let idx = 0;
|
|
@@ -663,7 +676,203 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
663
676
|
throw new IncorrectPassphraseError(`Incorrect passphrase (screenshot mode, zstd failed: ` +
|
|
664
677
|
errMsg +
|
|
665
678
|
')');
|
|
666
|
-
|
|
679
|
+
// Fallback: try reconstituting the image and re-extracting the pixels
|
|
680
|
+
try {
|
|
681
|
+
if (process.env.ROX_DEBUG)
|
|
682
|
+
console.log('DEBUG: decompress failed, attempting cropAndReconstitute fallback');
|
|
683
|
+
const reconstructed = await cropAndReconstitute(processedBuf, opts.debugDir);
|
|
684
|
+
const raw2 = native.sharpToRaw(reconstructed);
|
|
685
|
+
let logicalData2 = Buffer.from(raw2.pixels);
|
|
686
|
+
let logicalWidth2 = raw2.width;
|
|
687
|
+
let logicalHeight2 = raw2.height;
|
|
688
|
+
// find startIdx2 (linear)
|
|
689
|
+
let startIdx2 = -1;
|
|
690
|
+
const totalPixels2 = (logicalData2.length / 3) | 0;
|
|
691
|
+
for (let i2 = 0; i2 <= totalPixels2 - MARKER_START.length; i2++) {
|
|
692
|
+
let match2 = true;
|
|
693
|
+
for (let mi2 = 0; mi2 < MARKER_START.length && match2; mi2++) {
|
|
694
|
+
const offset2 = (i2 + mi2) * 3;
|
|
695
|
+
if (logicalData2[offset2] !== MARKER_START[mi2].r ||
|
|
696
|
+
logicalData2[offset2 + 1] !== MARKER_START[mi2].g ||
|
|
697
|
+
logicalData2[offset2 + 2] !== MARKER_START[mi2].b) {
|
|
698
|
+
match2 = false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (match2) {
|
|
702
|
+
startIdx2 = i2;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (startIdx2 === -1) {
|
|
707
|
+
// try 2D scan
|
|
708
|
+
let found2D2 = false;
|
|
709
|
+
for (let y = 0; y < logicalHeight2 && !found2D2; y++) {
|
|
710
|
+
for (let x = 0; x <= logicalWidth2 - MARKER_START.length && !found2D2; x++) {
|
|
711
|
+
let match = true;
|
|
712
|
+
for (let mi = 0; mi < MARKER_START.length && match; mi++) {
|
|
713
|
+
const idx = (y * logicalWidth2 + (x + mi)) * 3;
|
|
714
|
+
if (idx + 2 >= logicalData2.length ||
|
|
715
|
+
logicalData2[idx] !== MARKER_START[mi].r ||
|
|
716
|
+
logicalData2[idx + 1] !== MARKER_START[mi].g ||
|
|
717
|
+
logicalData2[idx + 2] !== MARKER_START[mi].b) {
|
|
718
|
+
match = false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (match) {
|
|
722
|
+
// compute rectangle
|
|
723
|
+
let endX = x + MARKER_START.length - 1;
|
|
724
|
+
let endY = y;
|
|
725
|
+
for (let scanY = y; scanY < logicalHeight2; scanY++) {
|
|
726
|
+
let rowHasData = false;
|
|
727
|
+
for (let scanX = x; scanX < logicalWidth2; scanX++) {
|
|
728
|
+
const scanIdx = (scanY * logicalWidth2 + scanX) * 3;
|
|
729
|
+
if (scanIdx + 2 < logicalData2.length) {
|
|
730
|
+
const r = logicalData2[scanIdx];
|
|
731
|
+
const g = logicalData2[scanIdx + 1];
|
|
732
|
+
const b = logicalData2[scanIdx + 2];
|
|
733
|
+
const isBackground = (r === 100 && g === 120 && b === 110) ||
|
|
734
|
+
(r === 0 && g === 0 && b === 0) ||
|
|
735
|
+
(r >= 50 &&
|
|
736
|
+
r <= 220 &&
|
|
737
|
+
g >= 50 &&
|
|
738
|
+
g <= 220 &&
|
|
739
|
+
b >= 50 &&
|
|
740
|
+
b <= 220 &&
|
|
741
|
+
Math.abs(r - g) < 70 &&
|
|
742
|
+
Math.abs(r - b) < 70 &&
|
|
743
|
+
Math.abs(g - b) < 70);
|
|
744
|
+
if (!isBackground) {
|
|
745
|
+
rowHasData = true;
|
|
746
|
+
if (scanX > endX)
|
|
747
|
+
endX = scanX;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (rowHasData) {
|
|
752
|
+
endY = scanY;
|
|
753
|
+
}
|
|
754
|
+
else if (scanY > y) {
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const rectWidth = endX - x + 1;
|
|
759
|
+
const rectHeight = endY - y + 1;
|
|
760
|
+
const newDataLen = rectWidth * rectHeight * 3;
|
|
761
|
+
const newData = Buffer.allocUnsafe(newDataLen);
|
|
762
|
+
let writeIdx = 0;
|
|
763
|
+
for (let ry = y; ry <= endY; ry++) {
|
|
764
|
+
for (let rx = x; rx <= endX; rx++) {
|
|
765
|
+
const idx = (ry * logicalWidth2 + rx) * 3;
|
|
766
|
+
newData[writeIdx++] = logicalData2[idx];
|
|
767
|
+
newData[writeIdx++] = logicalData2[idx + 1];
|
|
768
|
+
newData[writeIdx++] = logicalData2[idx + 2];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
logicalData2 = newData;
|
|
772
|
+
logicalWidth2 = rectWidth;
|
|
773
|
+
logicalHeight2 = rectHeight;
|
|
774
|
+
startIdx2 = 0;
|
|
775
|
+
found2D2 = true;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (!found2D2)
|
|
780
|
+
throw new DataFormatError('Screenshot fallback failed: START not found');
|
|
781
|
+
}
|
|
782
|
+
// compute endStartPixel2
|
|
783
|
+
const curTotalPixels2 = (logicalData2.length / 3) | 0;
|
|
784
|
+
const lastLineStart2 = (logicalHeight2 - 1) * logicalWidth2;
|
|
785
|
+
const endMarkerStartCol2 = logicalWidth2 - MARKER_END.length;
|
|
786
|
+
let endStartPixel2 = -1;
|
|
787
|
+
if (lastLineStart2 + endMarkerStartCol2 < curTotalPixels2) {
|
|
788
|
+
let matchEnd2 = true;
|
|
789
|
+
for (let mi = 0; mi < MARKER_END.length && matchEnd2; mi++) {
|
|
790
|
+
const pixelIdx = lastLineStart2 + endMarkerStartCol2 + mi;
|
|
791
|
+
if (pixelIdx >= curTotalPixels2) {
|
|
792
|
+
matchEnd2 = false;
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
const offset = pixelIdx * 3;
|
|
796
|
+
if (logicalData2[offset] !== MARKER_END[mi].r ||
|
|
797
|
+
logicalData2[offset + 1] !== MARKER_END[mi].g ||
|
|
798
|
+
logicalData2[offset + 2] !== MARKER_END[mi].b) {
|
|
799
|
+
matchEnd2 = false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (matchEnd2) {
|
|
803
|
+
endStartPixel2 =
|
|
804
|
+
lastLineStart2 + endMarkerStartCol2 - startIdx2;
|
|
805
|
+
if (process.env.ROX_DEBUG) {
|
|
806
|
+
console.log('DEBUG: Found END marker in fallback at last line');
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (endStartPixel2 === -1) {
|
|
811
|
+
if (process.env.ROX_DEBUG) {
|
|
812
|
+
console.log('DEBUG: END marker not found in fallback; using end of grid');
|
|
813
|
+
}
|
|
814
|
+
endStartPixel2 = curTotalPixels2 - startIdx2;
|
|
815
|
+
}
|
|
816
|
+
const dataPixelCount2 = endStartPixel2 - (MARKER_START.length + 1);
|
|
817
|
+
const pixelBytes2 = Buffer.allocUnsafe(dataPixelCount2 * 3);
|
|
818
|
+
for (let i2 = 0; i2 < dataPixelCount2; i2++) {
|
|
819
|
+
const srcOffset = (startIdx2 + MARKER_START.length + 1 + i2) * 3;
|
|
820
|
+
const dstOffset = i2 * 3;
|
|
821
|
+
pixelBytes2[dstOffset] = logicalData2[srcOffset];
|
|
822
|
+
pixelBytes2[dstOffset + 1] = logicalData2[srcOffset + 1];
|
|
823
|
+
pixelBytes2[dstOffset + 2] = logicalData2[srcOffset + 2];
|
|
824
|
+
}
|
|
825
|
+
// try decompressing fallback payload
|
|
826
|
+
const foundPX = pixelBytes2.indexOf(PIXEL_MAGIC);
|
|
827
|
+
if (process.env.ROX_DEBUG)
|
|
828
|
+
console.log('DEBUG: PIXEL_MAGIC index in fallback:', foundPX);
|
|
829
|
+
if (pixelBytes2.length >= PIXEL_MAGIC.length) {
|
|
830
|
+
let ii = 0;
|
|
831
|
+
const at0 = pixelBytes2
|
|
832
|
+
.slice(0, PIXEL_MAGIC.length)
|
|
833
|
+
.equals(PIXEL_MAGIC);
|
|
834
|
+
if (at0)
|
|
835
|
+
ii = PIXEL_MAGIC.length;
|
|
836
|
+
else {
|
|
837
|
+
const found = pixelBytes2.indexOf(PIXEL_MAGIC);
|
|
838
|
+
if (found !== -1)
|
|
839
|
+
ii = found + PIXEL_MAGIC.length;
|
|
840
|
+
}
|
|
841
|
+
if (ii > 0) {
|
|
842
|
+
const version2 = pixelBytes2[ii++];
|
|
843
|
+
const nameLen2 = pixelBytes2[ii++];
|
|
844
|
+
const payloadLen2 = pixelBytes2.readUInt32BE(ii + nameLen2);
|
|
845
|
+
const rawPayload2 = pixelBytes2.slice(ii + nameLen2 + 4, ii + nameLen2 + 4 + payloadLen2);
|
|
846
|
+
let payload2 = tryDecryptIfNeeded(rawPayload2, opts.passphrase);
|
|
847
|
+
payload2 = await tryDecompress(payload2, (info) => {
|
|
848
|
+
if (opts.onProgress)
|
|
849
|
+
opts.onProgress(info);
|
|
850
|
+
});
|
|
851
|
+
if (!payload2.slice(0, MAGIC.length).equals(MAGIC)) {
|
|
852
|
+
throw new DataFormatError('Screenshot fallback failed: missing ROX1 magic after decompression');
|
|
853
|
+
}
|
|
854
|
+
payload2 = payload2.slice(MAGIC.length);
|
|
855
|
+
if (opts.files) {
|
|
856
|
+
const unpacked2 = unpackBuffer(payload2, opts.files);
|
|
857
|
+
if (unpacked2) {
|
|
858
|
+
if (opts.onProgress)
|
|
859
|
+
opts.onProgress({ phase: 'done' });
|
|
860
|
+
progressBar?.stop();
|
|
861
|
+
return { files: unpacked2.files, meta: { name } };
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (opts.onProgress)
|
|
865
|
+
opts.onProgress({ phase: 'done' });
|
|
866
|
+
progressBar?.stop();
|
|
867
|
+
return { buf: payload2, meta: { name } };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
throw new DataFormatError('Screenshot mode zstd decompression failed: ' + errMsg);
|
|
871
|
+
}
|
|
872
|
+
catch (e2) {
|
|
873
|
+
// If fallback fails, rethrow original error
|
|
874
|
+
throw new DataFormatError(`Screenshot mode zstd decompression failed: ` + errMsg);
|
|
875
|
+
}
|
|
667
876
|
}
|
|
668
877
|
if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
|
|
669
878
|
throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
1
3
|
import { native } from './native.js';
|
|
2
4
|
export async function cropAndReconstitute(input, debugDir) {
|
|
3
|
-
|
|
5
|
+
const out = Buffer.from(native.cropAndReconstitute(input));
|
|
6
|
+
if (debugDir) {
|
|
7
|
+
try {
|
|
8
|
+
const meta = native.sharpMetadata(input);
|
|
9
|
+
const doubled = native.sharpResizeImage(input, meta.width * 2, meta.height * 2, 'nearest');
|
|
10
|
+
console.log('DEBUG: writing doubled.png to', debugDir);
|
|
11
|
+
writeFileSync(join(debugDir, 'doubled.png'), Buffer.from(doubled));
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
console.log('DEBUG: failed to write doubled.png', e?.message ?? e);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
console.log('DEBUG: writing reconstructed.png and reconstructed-pixels.bin to', debugDir);
|
|
18
|
+
writeFileSync(join(debugDir, 'reconstructed.png'), out);
|
|
19
|
+
const raw = native.sharpToRaw(out);
|
|
20
|
+
writeFileSync(join(debugDir, 'reconstructed-pixels.bin'), Buffer.from(raw.pixels));
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
console.log('DEBUG: failed to write reconstructed artifacts', e?.message ?? e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
4
27
|
}
|
package/package.json
CHANGED