dist-mcp / helpers / readPngMetadata.js
dist-mcp / helpers / readPngMetadata.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.readPngGenerationMeta = readPngGenerationMeta;
exports.formatGenerationMeta = formatGenerationMeta;
const fs_1 = __importDefault(require("fs"));
/**
* Read Draw Things generation metadata from a PNG's XMP iTXt chunk.
* Returns null if the file is not a PNG, has no XMP chunk, or the JSON cannot
* be parsed.
*/
function readPngGenerationMeta(filePath) {
let buf;
try {
buf = fs_1.default.readFileSync(filePath);
}
catch {
return null;
}
// Verify PNG signature: \x89 P N G \r \n \x1a \n
if (buf.length < 8 || buf[0] !== 0x89 || buf[1] !== 0x50 || buf[2] !== 0x4e || buf[3] !== 0x47) {
return null;
}
let offset = 8; // skip 8-byte PNG signature
while (offset + 12 <= buf.length) {
const chunkLen = buf.readUInt32BE(offset);
const chunkType = buf.toString("ascii", offset + 4, offset + 8);
const dataStart = offset + 8;
const dataEnd = dataStart + chunkLen;
if (dataEnd + 4 > buf.length)
break;
if (chunkType === "IEND")
break;
if (chunkType === "iTXt") {
const data = buf.slice(dataStart, dataEnd);
const kwEnd = data.indexOf(0);
if (kwEnd >= 0 && data.toString("ascii", 0, kwEnd) === "XML:com.adobe.xmp") {
const comprFlag = data[kwEnd + 1]; // 0 = uncompressed, 1 = compressed
if (comprFlag !== 0) {
offset = dataEnd + 4;
continue;
}
// Skip: comprFlag(1) + comprMethod(1) = offset + 2 after kwEnd
let pos = kwEnd + 3;
// Skip language tag (null-terminated)
while (pos < data.length && data[pos] !== 0)
pos++;
pos++;
// Skip translated keyword (null-terminated)
while (pos < data.length && data[pos] !== 0)
pos++;
pos++;
// Remaining bytes are the XMP text
const xmpText = data.toString("utf8", pos);
return extractMetaFromXmp(xmpText);
}
}
offset = dataEnd + 4; // advance past chunk + 4-byte CRC
}
return null;
}
function extractMetaFromXmp(xmp) {
// The JSON lives inside <exif:UserComment><rdf:Alt><rdf:li ...>JSON</rdf:li></rdf:Alt></exif:UserComment>
// (Draw Things does NOT use CDATA here.)
const match = xmp.match(/<exif:UserComment>[\s\S]*?<rdf:li[^>]*>([\s\S]*?)<\/rdf:li>/);
if (!match)
return null;
let raw;
try {
// XML text content may have been escaped (e.g. " for ") by older writer builds.
// Unescape the five standard XML entities before JSON.parse.
const jsonText = match[1].trim()
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/&/g, "&");
raw = JSON.parse(jsonText);
}
catch {
return null;
}
// Draw Things writes key "model"; our pngMetadata.ts writer also uses "model".
const loras = Array.isArray(raw.lora)
? raw.lora
.filter((l) => l && typeof l.model === "string")
.map((l) => ({
file: String(l.model),
weight: typeof l.weight === "number" ? l.weight : undefined,
}))
: undefined;
const sources = Array.isArray(raw.sources)
? raw.sources.filter((s) => typeof s === "string")
: undefined;
return {
prompt: typeof raw.c === "string" && raw.c ? raw.c : undefined,
negativePrompt: typeof raw.uc === "string" && raw.uc ? raw.uc : undefined,
model: typeof raw.model === "string" && raw.model ? raw.model : undefined,
sampler: typeof raw.sampler === "string" && raw.sampler ? raw.sampler : undefined,
steps: typeof raw.steps === "number" ? raw.steps : undefined,
guidanceScale: typeof raw.scale === "number" ? raw.scale : undefined,
seed: typeof raw.seed === "number" ? raw.seed : undefined,
seedMode: typeof raw.seed_mode === "string" && raw.seed_mode ? raw.seed_mode : undefined,
shift: typeof raw.shift === "number" ? raw.shift : undefined,
size: typeof raw.size === "string" && raw.size ? raw.size : undefined,
strength: typeof raw.strength === "number" ? raw.strength : undefined,
loras: loras?.length ? loras : undefined,
sources: sources?.length ? sources : undefined,
mode: typeof raw.mode === "string" && raw.mode ? raw.mode : undefined,
generatedBy: typeof raw.generated_by === "string" && raw.generated_by ? raw.generated_by : undefined,
};
}
function formatGenerationMeta(meta) {
const lines = [" GENERATION METADATA:"];
if (meta.prompt)
lines.push(` Prompt: ${meta.prompt}`);
if (meta.negativePrompt)
lines.push(` Negative Prompt: ${meta.negativePrompt}`);
if (meta.model)
lines.push(` Model: ${meta.model}`);
const techParts = [];
if (meta.sampler)
techParts.push(`Sampler: ${meta.sampler}`);
if (typeof meta.steps === "number")
techParts.push(`Steps: ${meta.steps}`);
if (typeof meta.guidanceScale === "number")
techParts.push(`Guidance Scale: ${meta.guidanceScale}`);
if (typeof meta.seed === "number")
techParts.push(`Seed: ${meta.seed}`);
if (techParts.length > 0)
lines.push(` ${techParts.join(" ")}`);
if (meta.size)
lines.push(` Size: ${meta.size}`);
if (typeof meta.strength === "number" && meta.strength !== 1.0) {
lines.push(` Strength: ${meta.strength}`);
}
if (meta.loras && meta.loras.length > 0) {
const loraStr = meta.loras
.map((l) => (l.weight != null ? `${l.file} (${l.weight})` : l.file))
.join(", ");
lines.push(` LoRA: ${loraStr}`);
}
if (meta.sources && meta.sources.length > 0) {
lines.push(` Source(s): ${meta.sources.join(", ")}`);
}
return lines.join("\n");
}
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.readPngGenerationMeta = readPngGenerationMeta;
exports.formatGenerationMeta = formatGenerationMeta;
const fs_1 = __importDefault(require("fs"));
/**
* Read Draw Things generation metadata from a PNG's XMP iTXt chunk.
* Returns null if the file is not a PNG, has no XMP chunk, or the JSON cannot
* be parsed.
*/
function readPngGenerationMeta(filePath) {
let buf;
try {
buf = fs_1.default.readFileSync(filePath);
}
catch {
return null;
}
// Verify PNG signature: \x89 P N G \r \n \x1a \n
if (buf.length < 8 || buf[0] !== 0x89 || buf[1] !== 0x50 || buf[2] !== 0x4e || buf[3] !== 0x47) {
return null;
}
let offset = 8; // skip 8-byte PNG signature
while (offset + 12 <= buf.length) {
const chunkLen = buf.readUInt32BE(offset);
const chunkType = buf.toString("ascii", offset + 4, offset + 8);
const dataStart = offset + 8;
const dataEnd = dataStart + chunkLen;
if (dataEnd + 4 > buf.length)
break;
if (chunkType === "IEND")
break;
if (chunkType === "iTXt") {
const data = buf.slice(dataStart, dataEnd);
const kwEnd = data.indexOf(0);
if (kwEnd >= 0 && data.toString("ascii", 0, kwEnd) === "XML:com.adobe.xmp") {
const comprFlag = data[kwEnd + 1]; // 0 = uncompressed, 1 = compressed
if (comprFlag !== 0) {
offset = dataEnd + 4;
continue;
}
// Skip: comprFlag(1) + comprMethod(1) = offset + 2 after kwEnd
let pos = kwEnd + 3;
// Skip language tag (null-terminated)
while (pos < data.length && data[pos] !== 0)
pos++;
pos++;
// Skip translated keyword (null-terminated)
while (pos < data.length && data[pos] !== 0)
pos++;
pos++;
// Remaining bytes are the XMP text
const xmpText = data.toString("utf8", pos);
return extractMetaFromXmp(xmpText);
}
}
offset = dataEnd + 4; // advance past chunk + 4-byte CRC
}
return null;
}
function extractMetaFromXmp(xmp) {
// The JSON lives inside <exif:UserComment><rdf:Alt><rdf:li ...>JSON</rdf:li></rdf:Alt></exif:UserComment>
// (Draw Things does NOT use CDATA here.)
const match = xmp.match(/<exif:UserComment>[\s\S]*?<rdf:li[^>]*>([\s\S]*?)<\/rdf:li>/);
if (!match)
return null;
let raw;
try {
// XML text content may have been escaped (e.g. " for ") by older writer builds.
// Unescape the five standard XML entities before JSON.parse.
const jsonText = match[1].trim()
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/&/g, "&");
raw = JSON.parse(jsonText);
}
catch {
return null;
}
// Draw Things writes key "model"; our pngMetadata.ts writer also uses "model".
const loras = Array.isArray(raw.lora)
? raw.lora
.filter((l) => l && typeof l.model === "string")
.map((l) => ({
file: String(l.model),
weight: typeof l.weight === "number" ? l.weight : undefined,
}))
: undefined;
const sources = Array.isArray(raw.sources)
? raw.sources.filter((s) => typeof s === "string")
: undefined;
return {
prompt: typeof raw.c === "string" && raw.c ? raw.c : undefined,
negativePrompt: typeof raw.uc === "string" && raw.uc ? raw.uc : undefined,
model: typeof raw.model === "string" && raw.model ? raw.model : undefined,
sampler: typeof raw.sampler === "string" && raw.sampler ? raw.sampler : undefined,
steps: typeof raw.steps === "number" ? raw.steps : undefined,
guidanceScale: typeof raw.scale === "number" ? raw.scale : undefined,
seed: typeof raw.seed === "number" ? raw.seed : undefined,
seedMode: typeof raw.seed_mode === "string" && raw.seed_mode ? raw.seed_mode : undefined,
shift: typeof raw.shift === "number" ? raw.shift : undefined,
size: typeof raw.size === "string" && raw.size ? raw.size : undefined,
strength: typeof raw.strength === "number" ? raw.strength : undefined,
loras: loras?.length ? loras : undefined,
sources: sources?.length ? sources : undefined,
mode: typeof raw.mode === "string" && raw.mode ? raw.mode : undefined,
generatedBy: typeof raw.generated_by === "string" && raw.generated_by ? raw.generated_by : undefined,
};
}
function formatGenerationMeta(meta) {
const lines = [" GENERATION METADATA:"];
if (meta.prompt)
lines.push(` Prompt: ${meta.prompt}`);
if (meta.negativePrompt)
lines.push(` Negative Prompt: ${meta.negativePrompt}`);
if (meta.model)
lines.push(` Model: ${meta.model}`);
const techParts = [];
if (meta.sampler)
techParts.push(`Sampler: ${meta.sampler}`);
if (typeof meta.steps === "number")
techParts.push(`Steps: ${meta.steps}`);
if (typeof meta.guidanceScale === "number")
techParts.push(`Guidance Scale: ${meta.guidanceScale}`);
if (typeof meta.seed === "number")
techParts.push(`Seed: ${meta.seed}`);
if (techParts.length > 0)
lines.push(` ${techParts.join(" ")}`);
if (meta.size)
lines.push(` Size: ${meta.size}`);
if (typeof meta.strength === "number" && meta.strength !== 1.0) {
lines.push(` Strength: ${meta.strength}`);
}
if (meta.loras && meta.loras.length > 0) {
const loraStr = meta.loras
.map((l) => (l.weight != null ? `${l.file} (${l.weight})` : l.file))
.join(", ");
lines.push(` LoRA: ${loraStr}`);
}
if (meta.sources && meta.sources.length > 0) {
lines.push(` Source(s): ${meta.sources.join(", ")}`);
}
return lines.join("\n");
}