117 lines
3.5 KiB
TypeScript
Raw Normal View History

2025-07-23 15:02:40 +04:00
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
]);
}
}