/** * Pure helpers for image entries rendered by the admin and gallery pages. * * Image rows come from the database (`width`, `height`, `file_size` are stored * from a POST body and may be strings) or from the output-folder scan (`id` is * a path digest, `path` is the file). Everything these helpers return for HTML * is already escaped, so callers can drop it into text or a quoted attribute. * * Loadable in the browser (window.ImageHelpers) and in Node for tests. */ (function (root) { "use strict"; const escapeHtml = typeof module !== "undefined" && module.exports ? require("./html-escape.js").escapeHtml : root.escapeHtml; const UNKNOWN_SIZE = "Unknown size"; /** * "640×480 | 1.2 MB" for a card body or a viewer caption, escaped. * * @param {{width?: unknown, height?: unknown, file_size?: unknown}|null|undefined} image * @param {{formatFileSize?: (bytes: number) => string, separator?: string}} [options] * @returns {string} escaped text; "Unknown size" when a dimension is missing */ function formatImageCaption(image, { formatFileSize, separator = " | " } = {}) { const entry = image || {}; const parts = []; if (entry.width && entry.height) { parts.push(`${escapeHtml(entry.width)}×${escapeHtml(entry.height)}`); } else { parts.push(UNKNOWN_SIZE); } const bytes = Number(entry.file_size); if (formatFileSize && Number.isFinite(bytes) && bytes > 0) { parts.push(escapeHtml(formatFileSize(bytes))); } return parts.join(escapeHtml(separator)); } /** * A complete standalone page showing a workflow as pretty-printed JSON. * Written into a blank same-origin window, so the JSON must not be able * to close the
and inject markup.
*
* @param {unknown} workflow
* @returns {string} HTML document
*/
function workflowDocumentHtml(workflow) {
const json = escapeHtml(JSON.stringify(workflow, null, 2));
return [
"",
"",
"ComfyUI Workflow Data ",
"",
"ComfyUI Workflow JSON
",
`${json}`,
"",
"",
].join("\n");
}
/**
* Body for POST /prompt_manager/autotag/single.
*
* Database rows (admin review, /images/all) have an integer id the server
* looks up; output-folder entries (gallery review, /images/output) have a
* path digest as id, which would 404, so those are addressed by path.
*
* @param {{id?: unknown, path?: unknown}} image
* @param {{modelType: string, generalThreshold?: number, characterThreshold?: number, prompt?: string}} settings
* @returns {object} JSON-serialisable request body
*/
function autotagSingleBody(image, settings) {
const entry = image || {};
const target =
typeof entry.path === "string" && entry.path && !Number.isInteger(entry.id)
? { path: entry.path }
: { image_id: entry.id };
if ("path" in target && Number.isInteger(entry.root_index)) {
target.root_index = entry.root_index;
}
const modelType = String(settings.modelType || "");
const params = modelType.startsWith("wd14")
? {
general_threshold: settings.generalThreshold,
character_threshold: settings.characterThreshold,
}
: { prompt: settings.prompt };
return { ...target, model_type: modelType, ...params };
}
/**
* Next source to try after an
fails. Cards show the thumbnail first;
* when it fails (stale cache, half-written file) the original is worth one
* attempt. Returns null when there is nothing left to try.
* @param {string} currentSrc
* @param {DOMStringMap|object|undefined} dataset `data-original`, `data-fell-back`
*/
function fallbackImageSource(currentSrc, dataset) {
if (!dataset || dataset.fellBack) return null;
const original = dataset.original;
if (!original || original === currentSrc) return null;
return original;
}
function metadataLookupUrl(imageUrl) {
try {
const url = new URL(imageUrl, 'http://prompt-manager.invalid');
const marker = '/prompt_manager/images/serve/';
const index = url.pathname.indexOf(marker);
if (index < 0) return null;
const relativePath = decodeURIComponent(url.pathname.slice(index + marker.length));
const query = new URLSearchParams();
if (url.searchParams.has('root')) query.set('root', url.searchParams.get('root'));
const suffix = query.size ? `?${query}` : '';
return `/prompt_manager/images/prompt/${encodeURIComponent(relativePath)}${suffix}`;
} catch {
return null;
}
}
const api = { formatImageCaption, workflowDocumentHtml, autotagSingleBody, fallbackImageSource, metadataLookupUrl };
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.ImageHelpers = api;
})(typeof window !== "undefined" ? window : globalThis);