@enslo/sd-metadata
🌐 日本語版はこちら
A TypeScript library to read and write metadata embedded in AI-generated images.
Features
- Multi-format Support: PNG (tEXt / iTXt / eXIf), JPEG (COM / Exif), WebP (Exif), plus MP4 / MOV and WebM / MKV videos (read-only)
- Simple API:
parse(),write(),embed(),stringify()— four functions cover all image use cases;parseVideo()adds videos - Stealth PNGInfo Recovery: recovers metadata hidden in pixel LSBs by NovelAI and the stealth-pnginfo extensions, even after image hosts strip the regular metadata
- LoRA Aware: lists the LoRAs applied to an image, read from A1111 tags, tool JSON and ComfyUI loader nodes
- TypeScript Native: Written in TypeScript with full type definitions included
- Zero Dependencies: Works in Node.js and browsers without any external dependencies
- Format Conversion: Seamlessly convert metadata between PNG, JPEG, and WebP
- Metadata Preservation: Preserves original metadata structure when converting formats (e.g., PNG → JPEG → PNG maintains all original data)
- AI Provenance Detection: identifies images and videos that commercial AI services (OpenAI, Google, xAI, Meta) declared as their own, through C2PA, XMP or signature markers — detection only, no signature verification
Installation
npm install @enslo/sd-metadata
Quick Start
import { parse } from '@enslo/sd-metadata';
// `imageBytes` is a Uint8Array or ArrayBuffer (from fs, fetch, a file input, ...)
const result = await parse(imageBytes);
if (result.status === 'success') {
console.log('Tool:', result.metadata.software); // 'novelai', 'comfyui', ...
console.log('Prompt:', result.metadata.prompt);
console.log('Size:', result.metadata.width, 'x', result.metadata.height);
}
See Usage for Node.js, browser, and userscript entry points, and Advanced Examples for the other result statuses (vendor, unrecognized, empty, invalid).
Tool Support
| Tool | PNG | JPEG | WebP |
|---|---|---|---|
| NovelAI * | ✅ | 🔄️ | ✅ |
| ComfyUI * | ✅ | 🔄️ | ✅ |
| Stable Diffusion WebUI | ✅ | ✅ | ✅ |
| Forge | ✅ | ✅ | ✅ |
| Forge Classic | ✅ | ✅ | ✅ |
| Forge Neo | ✅ | ✅ | ✅ |
| reForge | ✅ | ✅ | ✅ |
| EasyReforge | ✅ | ✅ | ✅ |
| SD.Next | ✅ | ✅ | ✅ |
| InvokeAI | ✅ | 🔄️ | 🔄️ |
| SwarmUI * | ✅ | ✅ | ✅ |
| Civitai | ✅ | ✅ | ⚠️ |
| TensorArt | ✅ | 🔄️ | 🔄️ |
| Stability Matrix | ✅ | 🔄️ | 🔄️ |
| HuggingFace Space | ✅ | 🔄️ | 🔄️ |
| Fooocus | ✅ | ✅ | ✅ |
| Ruined Fooocus | ✅ | 🔄️ | 🔄️ |
| Easy Diffusion | ✅ | ✅ | ✅ |
| Draw Things | ✅ | 🔄️ | 🔄️ |
| Dezgo | ✅ | ✅ | ✅ |
| KusArt | ✅ | 🔄️ | 🔄️ |
Legend:
- ✅ Fully Supported - Formats natively supported by the tool, verified with sample files
- 🔄️ Extended Support - Formats not natively supported by the tool, but sd-metadata enables read/write through custom format conversion. Supports round-trip conversion back to native formats.
- ⚠️ Experimental - Implemented by analyzing reference code or documentation, not verified with actual sample files. May not extract all metadata fields correctly.
Extended-support examples
- Stability Matrix (native: PNG only) → sd-metadata enables JPEG/WebP support
- NovelAI (native: PNG, WebP) → sd-metadata enables JPEG support
When you convert from a native format to an extended format and back (e.g., PNG → JPEG → PNG), all metadata is preserved.
[!NOTE] * Tools with format-specific behaviors — see below.
[!TIP] Help us expand tool support! We're actively collecting sample images from experimental combinations (Civitai WebP) and unsupported tools. If you have sample images generated by these or other AI tools, please consider contributing them! See CONTRIBUTING.md for details.
Videos
parseVideo() reads MP4 / MOV (QuickTime) and WebM / MKV (Matroska) files. Generation metadata goes through the same parsers as images, so a tool that writes its usual text into a video tag is reported as the same software. Verified with sample files from:
- ComfyUI — the built-in
SaveVideonode and VideoHelperSuite'sVideo Combine(MP4, WebM, MKV; its ProRes MOV output carries no metadata) - Forge Neo — Wan 2.2 videos (MP4, MKV)
Videos are read-only: write() and embed() take images. See Video Files.
Format-Specific Behaviors
Some tools have specific behaviors when converting between formats:
- ComfyUI JPEG/WebP: Reading supports multiple node formats (e.g., the built-in
Save Animated WEBPandsave-image-extended). Writing always matches the built-inSave Animated WEBPnode'sMake/ModelEXIF tags byte-for-byte — the layout ComfyUI itself reads natively for WebP drag-and-drop — dropping any PNG chunk other thanprompt/workflow(e.g.parametersfrom third-party save nodes). - NovelAI WebP: Automatically corrects corrupted UTF-8 in the Description field. WebP → PNG → WebP round-trip produces valid, readable metadata but with minor text corrections.
- SwarmUI PNG→JPEG/WebP: Native SwarmUI JPEG/WebP files do not include node information. When converting from PNG, this library preserves the ComfyUI workflow in the
Makefield for complete metadata retention (extended support).
Usage
[!NOTE] All examples use ESM syntax. CommonJS users can replace
importwithrequire:const { parse } = require('@enslo/sd-metadata');
Node.js Usage
import { parse, stringify } from '@enslo/sd-metadata';
import { readFileSync } from 'fs';
const imageData = readFileSync('image.png');
const result = await parse(imageData);
if (result.status === 'success') {
console.log('Tool:', result.metadata.software); // 'novelai', 'comfyui', etc.
console.log('Prompt:', result.metadata.prompt);
console.log('Model:', result.metadata.model?.name);
console.log('Size:', result.metadata.width, 'x', result.metadata.height);
}
// Format as human-readable text (works with any status)
const text = stringify(result);
if (text) {
console.log(text);
}
Browser Usage
import { parse, softwareLabels } from '@enslo/sd-metadata';
// Handle file input
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const arrayBuffer = await file.arrayBuffer();
const result = await parse(arrayBuffer);
if (result.status === 'success') {
document.getElementById('tool').textContent = softwareLabels[result.metadata.software];
document.getElementById('prompt').textContent = result.metadata.prompt;
document.getElementById('model').textContent = result.metadata.model?.name || 'N/A';
}
});
Userscript Usage
For userscripts (Tampermonkey, Violentmonkey, etc.), load the IIFE build via @require:
// ==UserScript==
// @name My Script
// @namespace https://example.com
// @require https://cdn.jsdelivr.net/npm/@enslo/[email protected]/dist/index.global.js
// ==/UserScript==
const response = await fetch(imageUrl);
const arrayBuffer = await response.arrayBuffer();
const result = await sdMetadata.parse(arrayBuffer);
if (result.status === 'success') {
console.log('Tool:', result.metadata.software);
console.log('Prompt:', result.metadata.prompt);
}
[!TIP] Always pin to a specific version in
@requirefor stability.
Stealth PNGInfo Recovery
NovelAI (by default) and the stealth-pnginfo family of extensions (A1111/Forge, ComfyUI) hide a copy of the generation metadata in the least-significant bits of the pixels, where it survives when image hosts strip the metadata chunks. parse() recovers it automatically: when an image carries no readable metadata, it scans the pixels as a fallback, and the recovered result carries stealth: true. An image that declares its vendor (a ChatGPT or Gemini PNG with a C2PA manifest) is not scanned, as those services write no stealth data:
import { parse } from '@enslo/sd-metadata';
const result = await parse(strippedImage);
if (result.status === 'success') {
console.log(result.stealth); // true — recovered from pixels
console.log(result.metadata.prompt);
}
- All four variants are read — alpha- or RGB-channel embedding, compressed or uncompressed — for both NovelAI's JSON payloads and the extensions' plain-infotext payloads.
- WebP: NovelAI embeds stealth data in its lossless WebP exports too. Browsers decode the pixels automatically; in Node.js, Bun or Deno pass a decoder through the
decodePixelsoption (e.g. backed by sharp), otherwise the WebP scan is skipped. See "Stealth recovery on servers" under Advanced Examples. - Rescue: a recovered result feeds
write()unchanged, restoring the original tool's native metadata to the stripped file. See "Rescuing stripped metadata" under Advanced Examples. - Servers: the scan inflates and unfilters the whole image (about 50 ms for a 1-megapixel PNG, 200 ms for 4 megapixels, on Node.js 24). Decompression is capped at what the header declares, 256 MiB of pixels and 16 MiB of payload, so a crafted file cannot claim gigabytes, but on untrusted uploads limit the file size and dimensions before calling
parse(), or useread()where recovery is not wanted. - The scan needs
DecompressionStream(Node.js 18+, Bun 1.4+, Deno, all modern browsers); without itparse()degrades to chunk-based reading. Writing stealth data is intentionally out of scope.
Video Files
parseVideo() reads generation metadata from MP4 / MOV and WebM / MKV files, with the same result statuses as parse(). Pass a Blob (a browser File, or Node's fs.openAsBlob()) and only the byte ranges that hold metadata are read, so a large file is never loaded whole:
import { parseVideo, stringify } from '@enslo/sd-metadata';
import { openAsBlob } from 'node:fs';
const result = await parseVideo(await openAsBlob('output.mp4')); // or a File from an <input>
if (result.status === 'success') {
console.log(result.metadata.software); // 'comfyui', 'forge-neo', ...
console.log(result.metadata.prompt);
console.log(result.raw.format); // 'mp4' | 'mov' | 'webm' | 'mkv'
console.log(result.raw.tags); // the file's text tags, in file order
}
console.log(stringify(result)); // A1111-format text, as for images
Videos from commercial services (Google Veo, xAI Grok Imagine, Meta Muse, OpenAI Sora) are reported with the vendor status — see "AI Provenance Detection" under Advanced Examples. Writing metadata into videos is not supported.
Advanced Examples
Format Conversion
Convert metadata between different image formats:
import { parse, write } from '@enslo/sd-metadata';
// Read metadata from PNG
const pngData = readFileSync('comfyui-output.png');
const parseResult = await parse(pngData);
if (parseResult.status === 'success') {
// Convert PNG to JPEG (using your preferred image processing library)
const jpegImageData = convertToJpeg(pngData); // Pseudo-code: use sharp, canvas, etc.
// Embed the metadata into the JPEG
const result = write(jpegImageData, parseResult);
if (result.ok) {
writeFileSync('output.jpg', result.value);
console.log('Image converted to JPEG with metadata preserved');
}
}
Tip: This library handles metadata read/write only. For actual image format conversion (decoding/encoding pixels), use image processing libraries like sharp, jimp, or browser Canvas API.
Handling Different Result Types
import { originLabel, parse } from '@enslo/sd-metadata';
const result = await parse(imageData);
switch (result.status) {
case 'success':
// Metadata successfully parsed
console.log(`Generated by ${result.metadata.software}`);
console.log(`Prompt: ${result.metadata.prompt}`);
break;
case 'vendor':
// A commercial AI service declared the image as its own — see "AI Provenance Detection" below
console.log(`Made by ${originLabel(result)} (unverified)`); // e.g. "xAI Grok Imagine"
break;
case 'unrecognized':
// Metadata exists but format is not recognized
console.log('Unknown metadata format');
// You can still access raw metadata for debugging:
console.log('Raw chunks:', result.raw);
break;
case 'empty':
// No metadata found
console.log('No metadata in this image');
break;
case 'invalid':
// Corrupted or invalid image data
console.log('Error:', result.message);
break;
}
Handling Unrecognized Metadata
When working with metadata from unsupported tools:
import { parse, write } from '@enslo/sd-metadata';
const source = await parse(unknownImage);
// source.status === 'unrecognized'
// Write to target image
// - Same format (e.g., PNG → PNG): metadata preserved as-is
// - Cross-format (e.g., PNG → JPEG): metadata dropped with warning
const result = write(targetImage, source);
if (result.ok) {
saveFile('output.png', result.value);
if (result.warning) {
// Metadata was dropped during cross-format conversion
console.warn('Metadata was dropped:', result.warning.reason);
}
}
AI Provenance Detection (Vendor Declarations)
Commercial AI services embed a declaration that they made the image — a signed C2PA "Content Credentials" manifest, an IPTC DigitalSourceType in an XMP packet, or a vendor's own signature — instead of generation parameters. parse() and parseVideo() return { status: 'vendor', info } for these files:
import { originLabel, parse, productLabel, vendorLabels } from '@enslo/sd-metadata';
const result = await parse(imageData);
if (result.status === 'vendor') {
console.log('Vendor:', vendorLabels[result.info.vendor]); // "OpenAI", "Google", "xAI", "Meta"
console.log('Service:', productLabel(result.info)); // "GPT Image", "Gemini", "Grok Imagine", "Muse"
console.log('Declared in:', result.info.source); // 'c2pa' | 'xmp' | 'signature'
console.log('As written:', result.info.product); // e.g. "ChatGPT", or undefined
console.log(originLabel(result)); // "xAI Grok Imagine" — the two joined
}
| Vendor | Service | Declaration | Images | Videos |
|---|---|---|---|---|
| OpenAI | GPT Image (ChatGPT), Sora | C2PA | PNG | MP4 |
| Gemini, Veo | C2PA | PNG, JPEG | MP4 | |
| xAI | Grok Imagine | C2PA, signature | JPEG | MP4 |
| Meta | Muse | XMP | JPEG, WebP | MP4 |
productLabel() names the service: a known product value (ChatGPT, gpt-image, ...) is mapped to its service name, and a declaration without one falls back to the vendor's main service for the media (images: GPT Image, Gemini, Grok Imagine, Muse; videos: Sora, Veo, Grok Imagine, Muse). The declared value stays in info.product.
Detection only — not proof. No signature is verified, so a
vendorresult is forgeable and cannot prove authenticity. These markers are also easily stripped (screenshots, re-uploads, re-encoding), so the absence of avendorresult does not prove an image is not AI-generated.
Reading LoRAs
metadata.loras lists the LoRAs that affected the image, in the order the tool recorded them, and is absent when there are none:
import { parse } from '@enslo/sd-metadata';
const result = await parse(imageData);
if (result.status === 'success') {
for (const lora of result.metadata.loras ?? []) {
console.log(lora.name); // exactly as the tool wrote it: file stem, path, URN or display name
console.log(lora.weight); // UNet / model strength, when recorded
console.log(lora.clipWeight); // text-encoder strength, only when recorded separately
console.log(lora.hash); // as written (AutoV2, sha256, "blake3:..."), never normalized
}
}
The list comes from <lora:…> tags and Lora hashes in A1111-style text, from the LoRA lists tools write in their JSON (SwarmUI, InvokeAI, Fooocus, Easy Diffusion, Draw Things, TensorArt, Dezgo, KusArt, ...), and from ComfyUI loader nodes — only the loaders the sampler actually depends on, so a bypassed or unconnected loader is not listed.
Stealth recovery on servers (WebP)
PNG pixels are decoded by the library itself, but WebP decoding depends on the runtime. Browsers use the platform decoder (WebCodecs ImageDecoder, falling back to createImageBitmap + OffscreenCanvas) with no configuration. In Node.js, Bun or Deno supply the pixels via decodePixels, e.g. backed by sharp; without it the WebP stealth scan is skipped gracefully:
import { parse } from '@enslo/sd-metadata';
import sharp from 'sharp';
const result = await parse(webpData, {
decodePixels: async (data) => {
const { data: pixels, info } = await sharp(data)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
return { data: new Uint8Array(pixels), width: info.width, height: info.height };
},
});
Rescuing stripped metadata
A stealth-recovered result feeds write() directly, restoring the original tool's native metadata to the stripped file — no separate API needed:
import { parse, write } from '@enslo/sd-metadata';
const rescued = await parse(strippedImage);
if (rescued.status === 'success') {
const restored = write(strippedImage, rescued);
if (restored.ok) {
// restored.value now carries regular metadata again,
// readable by any tool — stealth data stays untouched in the pixels.
}
}
Removing Metadata
To strip all metadata from an image:
import { write } from '@enslo/sd-metadata';
const result = write(imageData, { status: 'empty' });
if (result.ok) {
writeFileSync('clean-image.png', result.value);
}
Embedding Custom Metadata
Create and embed custom metadata in A1111 format:
import { embed } from '@enslo/sd-metadata';
const metadata = {
prompt: 'masterpiece, best quality, 1girl',
negativePrompt: 'lowres, bad quality',
width: 512,
height: 768,
sampling: {
steps: 20,
sampler: 'Euler a',
cfg: 7,
seed: '12345', // seeds are strings — some tools use 64-bit values
},
model: { name: 'model.safetensors' },
loras: [{ name: 'detail_tweaker', weight: 0.8 }], // written as <lora:detail_tweaker:0.8>
};
// Write to any image format (PNG, JPEG, WebP)
const result = embed(imageData, metadata);
if (result.ok) {
writeFileSync('output.png', result.value);
}
You can also add arbitrary key-value pairs to the settings line with extras:
const result = embed(imageData, {
...metadata,
extras: { Version: 'v1.10.0', 'Lora hashes': 'abc123' },
});
Tip: If an extras key matches a structured field (e.g.,
Steps), the extras value overrides the structured value at its original position. New keys are appended at the end.
Since EmbedMetadata is a subset of all GenerationMetadata variants, you can pass parsed metadata directly — including NovelAI with its characterPrompts:
import { parse, embed } from '@enslo/sd-metadata';
const result = await parse(novelaiPng);
if (result.status === 'success') {
// NovelAI metadata (or any other tool) works as-is
const output = embed(blankJpeg, result.metadata);
}
API Reference
parse(input: Uint8Array | ArrayBuffer, options?: ReadOptions): Promise<ParseResult>
Reads and parses metadata from an image file. When an image carries no readable metadata, additionally scans the pixels for Stealth PNGInfo (see Stealth PNGInfo Recovery); the scan only runs in that case, and not for images with a vendor declaration, so images with regular metadata pay no extra cost.
Parameters:
input- Image file data (PNG, JPEG, or WebP)options- Optional read optionsstrict?: boolean(default:false) — Whentrue, dimensions (width/height) are taken strictly from metadata only. Whenfalse, missing dimensions are extracted from image headers.decodePixels?: (data, format) => Promise<RgbaPixels | null>— Decodes WebP pixels for the stealth scan where the platform cannot (servers). Called lazily, only when a WebP carries no readable metadata. See "Stealth recovery on servers" under Advanced Examples.
Returns:
{ status: 'success', metadata, raw, stealth? }- Successfully parsedmetadata: Unified metadata object (seeGenerationMetadata)raw: Original format-specific data (chunks/segments)stealth:truewhen the metadata was recovered from pixel LSBs
{ status: 'vendor', info }- A commercial AI service (OpenAI, Google, xAI, Meta) declared the image as its own; there are no generation parameters to readinfo:VendorInfo—vendor,product(as declared, if any),source('c2pa' | 'xmp' | 'signature') andmedia: 'image'. Detection only — nothing is verified.
{ status: 'unrecognized', raw, stealth? }- Image has metadata but not from a known AI toolraw: Original metadata preserved for conversionstealth:truewhen the raw data was recovered from pixel LSBs
{ status: 'empty' }- No metadata found in the image{ status: 'invalid', message? }- Corrupted or unsupported image formatmessage: Optional error description
parseVideo(input: Uint8Array | ArrayBuffer | Blob, options?: VideoReadOptions): Promise<VideoParseResult>
Reads and parses metadata from a video file (MP4 / MOV, WebM / MKV). See Video Files.
Parameters:
input- Video file data, or aBlob/File. ABlobis read in small ranges — only the boxes and elements that hold metadata — instead of being loaded whole.options- Optional read optionsstrict?: boolean(default:false) — Whentrue, dimensions are taken strictly from metadata only. Whenfalse, missing dimensions are taken from the video track.
Returns:
The same statuses as parse(), with VideoRawMetadata (format and the file's text tags) in place of RawMetadata and media: 'video' in a vendor info. Videos carry no stealth data, and a VideoParseResult cannot be passed to write().
read(input: Uint8Array | ArrayBuffer, options?: ReadOptions): ParseResult
[!WARNING] Deprecated — use
parse()instead.read()remains available for callers that need a synchronous, chunk-only read, but it cannot recover Stealth PNGInfo.
Identical to parse() except that it is synchronous and never scans pixels.
write(input: Uint8Array | ArrayBuffer, metadata: ParseResult): WriteResult
Writes metadata to an image file. The target's existing metadata is replaced, not merged: its text chunks, EXIF and XMP are rebuilt from metadata, so camera EXIF such as Orientation, DateTime or Make / Model is not carried over.
Parameters:
input- Target image file data (PNG, JPEG, or WebP)metadata-ParseResultfromparse()status: 'success'or'empty'- Can write directlystatus: 'unrecognized'- Same format: writes as-is; Cross-format: drops metadata with warning- Stealth-recovered results work as-is — writing restores the original tool's native metadata (see "Rescuing stripped metadata" under Advanced Examples)
Returns:
{ ok: true, value: Uint8Array, warning?: WriteWarning }- Successfully writtenwarningis set when some or all of the metadata was left out:reasonis'unrecognizedCrossFormat'(unrecognized metadata into another format),'conversionEmpty'(the tool's converter found nothing the target format can hold) or'segmentTooLarge'(a JPEG COM / XMP segment over 65,535 bytes was skipped;messagenames it)
{ ok: false, error: { type, message? } }- Failed.typeis one of:'unsupportedFormat': Target image is not PNG, JPEG, or WebP'conversionFailed': Metadata conversion failed (e.g., incompatible format)'writeFailed': Failed to embed metadata into the image, or the input cannot be written back
embed(input: Uint8Array | ArrayBuffer, metadata: EmbedMetadata | GenerationMetadata): WriteResult
Embeds custom metadata into an image in SD WebUI (A1111) format. As with write(), the target's existing metadata (text chunks, EXIF, XMP) is replaced.
Parameters:
input- Target image file data (PNG, JPEG, or WebP)metadata-EmbedMetadataorGenerationMetadatato embed (useextrasfield for custom settings)
Returns:
{ ok: true, value: Uint8Array }- Successfully written (returns new image data){ ok: false, error: { type, message? } }- Failed.typeis one of:'unsupportedFormat': Target image is not PNG, JPEG, or WebP'writeFailed': Failed to embed metadata into the image
Use cases:
- Creating custom metadata for programmatically generated images
- Converting metadata from other tools to WebUI-compatible format
- Building applications that output WebUI-readable metadata
stringify(input: ParseResult | VideoParseResult | EmbedMetadata | GenerationMetadata): string
Converts metadata to a human-readable string.
Parameters:
input- One of:ParseResultfromparse()orVideoParseResultfromparseVideo()— selects best representation based on statusEmbedMetadataorGenerationMetadata— formats as A1111 text directly
Returns:
ParseResult/VideoParseResult:success→ A1111-format text. For tools that write A1111 text themselves (sd-webui, Forge family, reForge, SD.Next, ...) the original text is returned as the tool wrote it, so keys the library does not model survive; for other tools the text is rebuilt from the metadata, LoRAs includedvendor→<Vendor> <Service>(e.g.OpenAI GPT Image), or the service alone when the vendor is unknownunrecognized→ Raw metadata as plain textempty/invalid→ Empty string
EmbedMetadata/GenerationMetadata→ A1111-format text
Use cases:
- Displaying generation parameters in image viewers or galleries
- Copying metadata to clipboard as readable text
- Logging or debugging parsed metadata
- Previewing
EmbedMetadatabefore embedding
originLabel(input: ParseResult | VideoParseResult | GenerationMetadata | VendorInfo): string
Display name of whatever made a file, in one call: the software of a success result (as in softwareLabels), or the vendor and its service of a vendor result (as in vendorLabels and productLabel(), the service alone when the vendor is unknown). Any other status gives an empty string. The metadata or the vendor declaration can also be passed on its own.
import { originLabel, parse } from '@enslo/sd-metadata';
const result = await parse(imageData);
console.log(originLabel(result));
// => "NovelAI", "ComfyUI", "OpenAI GPT Image", "xAI Grok Imagine", ... or "" when there is nothing to name
softwareLabels: Record<GenerationSoftware, string>
A read-only mapping from GenerationSoftware identifiers to their human-readable display names.
import { softwareLabels } from '@enslo/sd-metadata';
const result = await parse(imageData);
if (result.status === 'success') {
console.log(softwareLabels[result.metadata.software]);
// => "NovelAI", "ComfyUI", "Stable Diffusion WebUI", etc.
}
vendorLabels: Record<Vendor, string>
A read-only mapping from Vendor identifiers to company names.
import { vendorLabels } from '@enslo/sd-metadata';
const result = await parse(imageData);
if (result.status === 'vendor') {
console.log(vendorLabels[result.info.vendor]);
// => "OpenAI", "Google", "xAI", "Meta", "Unknown"
}
productLabel(info: VendorInfo): string
Display name of the AI service that made a vendor image or video. A known declared product (ChatGPT, gpt-image, Grok Imagine, ...) is mapped to its service name; a declaration without a product falls back to the vendor's main service for the media (images: GPT Image, Gemini, Grok Imagine, Muse; videos: Sora, Veo, Grok Imagine, Muse). An unknown product value is returned as written.
import { productLabel, vendorLabels } from '@enslo/sd-metadata';
const result = await parse(imageData);
if (result.status === 'vendor') {
console.log(`${vendorLabels[result.info.vendor]} ${productLabel(result.info)}`);
// => "OpenAI GPT Image", "Google Gemini", "xAI Grok Imagine", "Meta Muse"
}
Type Reference
This section provides an overview of the main types. For complete type definitions, see Type Documentation.
ParseResult
The result of the parse() (and deprecated read()) function. It uses a discriminated union with a status field. stealth is set to true by parse() when the result was recovered from pixel LSBs (Stealth PNGInfo).
type ParseResult =
| { status: 'success'; metadata: GenerationMetadata; raw: RawMetadata; stealth?: boolean }
| { status: 'vendor'; info: VendorInfo }
| { status: 'unrecognized'; raw: RawMetadata; stealth?: boolean }
| { status: 'empty' }
| { status: 'invalid'; message?: string };
VideoParseResult
The result of parseVideo(), with the same statuses. raw holds the container format and the file's text tags instead of chunks or segments.
type VideoParseResult =
| { status: 'success'; metadata: GenerationMetadata; raw: VideoRawMetadata }
| { status: 'vendor'; info: VendorInfo }
| { status: 'unrecognized'; raw: VideoRawMetadata }
| { status: 'empty' }
| { status: 'invalid'; message?: string };
GenerationMetadata
Unified metadata structure returned by parse() and parseVideo() — a discriminated union of 3 metadata types, distinguished by the software field:
type GenerationMetadata =
| NovelAIMetadata // 'novelai' — adds V4 character-placement fields
| ComfyUIMetadata // 'comfyui' | 'tensorart' | 'stability-matrix' | 'swarmui' — adds the workflow graph (nodes)
| StandardMetadata; // 'sd-webui', 'forge', 'invokeai', 'dezgo', ... — baseline fields only
All variants share the BaseMetadata fields: prompt, negativePrompt, width, height, and the optional model / sampling / hires / upscale settings and loras list. Narrow on the software discriminator to access tool-specific fields:
if (result.status === 'success') {
const metadata = result.metadata;
console.log('Prompt:', metadata.prompt);
console.log('Model:', metadata.model?.name);
console.log('Seed:', metadata.sampling?.seed); // a string
if (metadata.software === 'novelai') {
// TypeScript knows this is NovelAIMetadata
console.log('Character prompts:', metadata.characterPrompts);
}
}
For the complete definitions of every exported type — including BaseMetadata, EmbedMetadata, RawMetadata, VendorInfo, LoraSettings, GenerationSoftware, and the settings types — see the Type Documentation.
Development
See the Contributing Guide for development setup and guidelines.
License
MIT