Where the data lives
PNG stores metadata in three ancillary chunk types. Each chunk is length (4) · type (4) · data · CRC (4). Text chunks carry a null-terminated Latin-1 keyword followed by the value:
| Chunk | Encoding | Layout after keyword |
|---|---|---|
| tEXt | Latin-1 | value |
| zTXt | Latin-1, deflate | compression method (1 byte) · zlib stream |
| iTXt | UTF-8, optional deflate | compression flag · method · language tag\0 · translated keyword\0 · value |
Keys written by popular AI tools
| Tool | Keyword | Value |
|---|---|---|
| ComfyUI | prompt | JSON — the executed API graph (node id → class_type, inputs) |
| ComfyUI | workflow | JSON — the editable canvas graph (nodes, links, groups) |
| Automatic1111 / Forge | parameters | Text — prompt, `Negative prompt:`, then `Steps:, Sampler:, CFG scale:, Seed:, Size:, Model hash:, Model:` |
| InvokeAI | invokeai_metadata | JSON — prompt, seed, model, scheduler |
| SwarmUI | parameters | JSON — `sui_image_params` object |
| Fooocus | parameters / fooocus_scheme | JSON — prompt, negative, styles, seed, base model |
| Generic | Software, Comment, Description, XML:com.adobe.xmp | Encoder name, free text, XMP packet |
Copy-paste parser (JavaScript)
Works in every modern browser and in Node 18+ (Web Streams). Returns a plain object keyed by chunk keyword.
// Minimal PNG text-chunk reader (browser or Node ≥ 18)
const SIG = [137, 80, 78, 71, 13, 10, 26, 10];
const latin1 = new TextDecoder('latin1'), utf8 = new TextDecoder('utf-8');
async function inflate(bytes) {
const s = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate'));
return new Uint8Array(await new Response(s).arrayBuffer());
}
export async function readPngText(buf) { // buf: Uint8Array
if (!SIG.every((b, i) => buf[i] === b)) throw new Error('not a PNG');
const out = {}; let pos = 8;
while (pos + 8 <= buf.length) {
const len = new DataView(buf.buffer, buf.byteOffset + pos).getUint32(0);
const type = latin1.decode(buf.subarray(pos + 4, pos + 8));
const data = buf.subarray(pos + 8, pos + 8 + len);
if (type === 'tEXt') {
const n = data.indexOf(0);
out[latin1.decode(data.subarray(0, n))] = latin1.decode(data.subarray(n + 1));
} else if (type === 'zTXt') {
const n = data.indexOf(0);
out[latin1.decode(data.subarray(0, n))] = latin1.decode(await inflate(data.subarray(n + 2)));
} else if (type === 'iTXt') {
const p = data.indexOf(0), compressed = data[p + 1] === 1;
let q = data.indexOf(0, p + 3); q = data.indexOf(0, q + 1);
const payload = data.subarray(q + 1);
out[latin1.decode(data.subarray(0, p))] = utf8.decode(compressed ? await inflate(payload) : payload);
}
if (type === 'IEND') break;
pos += 12 + len; // length + type + data + CRC
}
return out; // { prompt, workflow, parameters, ... }
} To get ComfyUI prompts, JSON.parse(out.prompt) and collect inputs.text from every node whose class_type contains CLIPTextEncode. For A1111/Forge, split out.parameters on "\nNegative prompt:" and the final settings line.
Hosted JSON endpoint
PNGMeta parses files client-side and does not currently expose a public server-side extraction endpoint. The snippet above is dependency-free and runs in browsers, Node, Deno, Bun and Cloudflare Workers, so you can embed the same logic in any pipeline or bot.
Not a developer? Just upload a PNG and read the result, or check the FAQ.