Project Files
src / pngText.ts
import * as fs from "node:fs";
import { inflateSync } from "node:zlib";
/** PNG magic; exported for PNG writers. */
export const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
/** zTXt inflate output cap (zip-bomb mitigation). */
export const PNG_ZTXT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
function decodePngTextChunk(data: Buffer): { keyword: string; text: string } | null {
const z = data.indexOf(0);
if (z < 0) return null;
const keyword = data.subarray(0, z).toString("latin1");
const text = data.subarray(z + 1).toString("latin1");
return { keyword, text };
}
function decodePngZtxtChunk(data: Buffer): { keyword: string; text: string } | null {
const z = data.indexOf(0);
if (z < 0 || data.length < z + 2) return null;
const keyword = data.subarray(0, z).toString("latin1");
const compMethod = data[z + 1];
const compressed = data.subarray(z + 2);
if (compMethod !== 0) {
return { keyword, text: `<unsupported zTXt compression method ${compMethod}>` };
}
try {
const raw = inflateSync(compressed, { maxOutputLength: PNG_ZTXT_MAX_OUTPUT_BYTES });
return { keyword, text: raw.toString("latin1") };
} catch (e) {
return { keyword, text: `<zlib decompress failed: ${String(e)}>` };
}
}
export function parsePngTextAndZtxt(filePath: string): {
tEXt: Array<{ keyword: string; text: string }>;
zTXt: Array<{ keyword: string; text: string }>;
} {
const out = { tEXt: [] as Array<{ keyword: string; text: string }>, zTXt: [] as Array<{ keyword: string; text: string }> };
let data: Buffer;
try {
data = fs.readFileSync(filePath);
} catch {
return out;
}
if (data.length < 8 || !data.subarray(0, 8).equals(PNG_SIGNATURE)) return out;
let pos = 8;
while (pos + 8 <= data.length) {
const length = data.readUInt32BE(pos);
const ctype = data.subarray(pos + 4, pos + 8).toString("ascii");
const chunkStart = pos + 8;
const chunkEnd = chunkStart + length;
if (chunkEnd + 4 > data.length) break;
const chunkData = data.subarray(chunkStart, chunkEnd);
pos = chunkEnd + 4;
if (ctype === "IEND") break;
if (ctype === "tEXt") {
const entry = decodePngTextChunk(chunkData);
if (entry) out.tEXt.push(entry);
} else if (ctype === "zTXt") {
const entry = decodePngZtxtChunk(chunkData);
if (entry) out.zTXt.push(entry);
}
}
return out;
}