PNG tEXt metadata extractor — NDJSON output for jq pipelines.
Extracts tEXt chunks from PNG files and outputs one JSON object per line (NDJSON). No image decoding — reads only binary chunk headers for speed.
- VDSL — search/aggregate
vdslrecipe chunks embedded by the VDSL image generation platform - ComfyUI — extract
prompt/workflowchunks - General — any arbitrary tEXt keyword
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/ynishi/pngmetagrep/releases/latest/download/pngmetagrep-installer.sh | shPlatforms: x86_64-linux, aarch64-linux, x86_64-macos, aarch64-macos
cargo install pngmetagrep# Extract all tEXt chunks from PNGs under a directory
pngmetagrep ./images
# Specify chunk keywords (repeatable)
pngmetagrep ./images --chunk prompt --chunk workflow
# Regex filter on JSON output
pngmetagrep ./images -e '"seed":\s*42'
# Case-insensitive filter
pngmetagrep ./images -e 'landscape' -i
# Print matching file paths only (no JSON)
pngmetagrep ./images -e 'portrait' -l
# Limit parallel threads
pngmetagrep ./images -j 4
# Pipe to jq
pngmetagrep ./images | jq 'select(.seed == 42)'| Flag | Description |
|---|---|
--chunk <KEY> |
Text chunk keyword to extract (repeatable, default: all) |
-e <REGEX> |
Regex filter applied to serialized JSON output |
-i |
Case-insensitive matching for -e |
-l |
Print matching file paths only (no JSON) |
-j <N> |
Number of parallel threads (default: CPU count) |
Single chunk whose value is a JSON object — path is merged flat:
{"path":"images/001.png","_v":1,"seed":42,"model":"sd-xl"}Multiple chunks or non-object values — nested by keyword:
{"path":"images/002.png","prompt":{...},"workflow":{...}}tEXt, zTXt and iTXt are all extracted — Pillow and similar writers
store large metadata compressed, and those chunks are searchable by -e
too.
A truncated or otherwise malformed PNG does not silently look like a file with no metadata. Whatever chunks were readable are still printed on stdout, and the defect goes to stderr with its byte offset:
pngmetagrep: images/broken.png: truncated PNG: chunk at offset 1181085 declares 12476 bytes, 8179 available
stdout therefore stays clean NDJSON for the pipeline downstream. Chunks sitting past the damage are unreachable — writers differ on where text chunks go, so a damaged file may yield all, some, or none of its metadata.
| Crate | Role |
|---|---|
pngmeta |
Chunk-level PNG reader/writer — spans, text chunks, malformed-input guarantees |
pngmetagrep-core |
Text chunk extraction + match strategy selection |
pngmetagrep (CLI) |
Parallel CLI built on clap + rayon + walkdir |
// Map the file without reading image data: each span carries type,
// offset and length, and the payload is read only when asked for.
let mut reader = pngmeta::ChunkReader::open(std::path::Path::new("a.png"))?;
while let Some(span) = reader.next_span()? {
println!("{} at {} ({} bytes)", span.kind, span.offset, span.length);
}
// Or hand it bytes you already have, borrowing each payload.
let bytes = std::fs::read("a.png")?;
for item in pngmeta::chunk_spans(&bytes)? {
let (span, payload) = item?;
if span.kind.is_text() {
// ...
}
}
// Text chunks decoded — tEXt, zTXt and iTXt alike.
for entry in pngmeta::read_text_entries_at(std::path::Path::new("a.png"))? {
println!("{} = {}", entry.keyword, entry.text);
}
// The same walk as an iterator, when the readable entries of a damaged
// file are worth more than failing on it.
let readable: Vec<_> = pngmeta::text_entries_from_bytes(&bytes)?
.flatten()
.collect();zTXt and compressed iTXt need the inflate feature:
pngmeta = { version = "0.2", features = ["inflate"] }Malformed input never panics: a truncated file reports Error::Truncated
rather than looking like a file with no metadata, an oversized length
field is refused before allocating, and non-PNG input is identifiable via
Error::is_not_png() without consulting the file extension.
Pre-built binaries are distributed via cargo-dist. Tag push (v*.*.*) triggers GitHub Actions to build and publish to Releases.
cargo release patch --executeMIT