117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
![]() |
|
||
|
import { Buffer } from 'buffer';
|
||
|
const pako = require('pako');
|
||
|
|
||
|
interface PNGChunk {
|
||
|
length: number;
|
||
|
type: string;
|
||
|
data: Buffer;
|
||
|
crc: Buffer;
|
||
|
}
|
||
|
|
||
|
export class QuestLog {
|
||
|
private static PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||
|
private static readonly formatChecker = /^[\x00-\x7F]*$/;
|
||
|
static isValidMessage(message: string): boolean {
|
||
|
if (!message || message.length === 0) {
|
||
|
return false;
|
||
|
}
|
||
|
return QuestLog.formatChecker.test(message);
|
||
|
}
|
||
|
|
||
|
static extractMessageFromUint8Array(uint8Array:Uint8Array): string {
|
||
|
return QuestLog.extractMessageFromBuffer(Buffer.from(uint8Array));
|
||
|
}
|
||
|
/**
|
||
|
* Extracts hidden message from a PNG file
|
||
|
*/
|
||
|
static extractMessageFromBuffer(fileData:Buffer): string {
|
||
|
// const fileData = await fs.readFile(imagePath);
|
||
|
|
||
|
if (!this.isPNG(fileData)) {
|
||
|
throw new Error('File is not a valid PNG');
|
||
|
}
|
||
|
|
||
|
const chunks = this.parseChunks(fileData);
|
||
|
console.log("chunks length:",chunks.length);
|
||
|
const idatChunk = chunks.find(chunk => chunk.type === 'IDAT');
|
||
|
const ihdrChunk = chunks.find(chunk => chunk.type === 'IHDR');
|
||
|
|
||
|
if (!idatChunk || !ihdrChunk) {
|
||
|
throw new Error('Invalid PNG structure');
|
||
|
}
|
||
|
|
||
|
console.log("Decompress data");
|
||
|
// Decompress the image data
|
||
|
const decompressedData = pako.inflate(idatChunk.data);
|
||
|
|
||
|
let binaryMessage = '';
|
||
|
let currentByte = '';
|
||
|
let message = '';
|
||
|
|
||
|
console.log("read width");
|
||
|
const width = ihdrChunk.data.readUInt32BE(0) * 3;
|
||
|
|
||
|
console.log("read compressed data");
|
||
|
for (let i = 0; i < decompressedData.length; i++) {
|
||
|
// Skip filter byte at the start of each scanline
|
||
|
if (i % (width + 1) === 0) continue;
|
||
|
|
||
|
const bit = decompressedData[i] & 1;
|
||
|
currentByte += bit;
|
||
|
|
||
|
if (currentByte.length === 8) {
|
||
|
const charCode = parseInt(currentByte, 2);
|
||
|
if (charCode === 0) break; // Found null terminator
|
||
|
|
||
|
message += String.fromCharCode(charCode);
|
||
|
currentByte = '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(message.endsWith("ED")){
|
||
|
message = message.substring(0, message.length - 2);
|
||
|
}
|
||
|
if (!this.isValidMessage(message)) {
|
||
|
return null;
|
||
|
}
|
||
|
console.log("mSg", message);
|
||
|
return message;
|
||
|
}
|
||
|
|
||
|
private static isPNG(buffer: Buffer): boolean {
|
||
|
return buffer.slice(0, 8).equals(this.PNG_SIGNATURE);
|
||
|
}
|
||
|
|
||
|
private static parseChunks(buffer: Buffer): PNGChunk[] {
|
||
|
const chunks: PNGChunk[] = [];
|
||
|
let offset = 8; // Skip PNG signature
|
||
|
|
||
|
while (offset < buffer.length) {
|
||
|
const length = buffer.readUInt32BE(offset);
|
||
|
const type = buffer.toString('ascii', offset + 4, offset + 8);
|
||
|
const data = buffer.slice(offset + 8, offset + 8 + length);
|
||
|
const crc = buffer.slice(
|
||
|
offset + 8 + length,
|
||
|
offset + 8 + length + 4
|
||
|
);
|
||
|
|
||
|
chunks.push({ length, type, data, crc });
|
||
|
offset += 12 + length;
|
||
|
}
|
||
|
|
||
|
return chunks;
|
||
|
}
|
||
|
|
||
|
private static serializeChunk(chunk: PNGChunk): Buffer {
|
||
|
const lengthBuf = Buffer.alloc(4);
|
||
|
lengthBuf.writeUInt32BE(chunk.data.length);
|
||
|
|
||
|
return Buffer.concat([
|
||
|
lengthBuf,
|
||
|
Buffer.from(chunk.type),
|
||
|
chunk.data,
|
||
|
chunk.crc
|
||
|
]);
|
||
|
}
|
||
|
}
|