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 ]); } }