import { getApiBase } from "./api"; /** Displayable base64 (not a UI placeholder). */ export function isUsableImageBase64(b64) { return b64 && typeof b64 === "string" && !b64.startsWith("[") && b64.length > 100; } /** Resolve relative API paths to absolute URLs. */ export function resolveImageUrl(url) { if (!url || typeof url !== "string") return null; if (url.startsWith("data:")) { if (url.includes("[image:") || url.endsWith("base64,") || url.length < 120) return null; return url; } if (url.startsWith("http://") || url.startsWith("https://")) return url; const apiBase = getApiBase().replace(/\/$/, ""); const path = url.startsWith("/") ? url : `/${url}`; if (path.startsWith("/generated-image/")) { return `${apiBase}${path}`; } if (path.startsWith("/api/")) { return `${apiBase.replace(/\/api$/, "")}${path}`; } return `${apiBase}${path}`; } /** Build data URL from raw base64 + mime. */ export function base64ToDataUrl(b64, mime = "image/png") { if (!isUsableImageBase64(b64)) return null; return `data:${mime || "image/png"};base64,${b64}`; } /** `/generated-image/{id}?t=…` → `/generated-image/{id}/b64?t=…` */ export function generatedImageB64Endpoint(imageUrl) { const resolved = resolveImageUrl(imageUrl); if (!resolved?.includes("/generated-image/")) return null; return resolved.replace(/\/generated-image\/([^/?]+)/, "/generated-image/$1/b64"); } export function collectImageFields(input) { if (!input || typeof input !== "object") { return { image_base64: null, image_url: null, mime: "image/png", title: null, has_image: false }; } return { image_base64: input.image_base64 || null, image_url: input.image_url || null, mime: input.mime || "image/png", title: input.title || input.prompt || input.subject || null, has_image: Boolean(input.has_image), }; } export function mergeImageFields(...sources) { const out = { image_base64: null, image_url: null, mime: "image/png", title: null, has_image: false }; for (const src of sources) { const f = collectImageFields(src); if (isUsableImageBase64(f.image_base64)) out.image_base64 = f.image_base64; if (f.image_url) out.image_url = f.image_url; if (f.mime && f.mime !== "image/png") out.mime = f.mime; if (f.title) out.title = f.title; if (f.has_image) out.has_image = true; } return out; } /** Best display src without network — data URL or resolved http(s) URL for . */ export function resolveImageDisplaySrc({ image_base64, image_url, mime = "image/png" }) { const dataUrl = base64ToDataUrl(image_base64, mime); if (dataUrl) return dataUrl; return resolveImageUrl(image_url); } /** Fetch JSON base64 fallback when direct URL fails. */ export async function fetchImageB64DataUrl(imageUrl, mime = "image/png") { const endpoint = generatedImageB64Endpoint(imageUrl); if (!endpoint) return null; try { const resp = await fetch(endpoint, { mode: "cors", credentials: "omit" }); if (!resp.ok) return null; const data = await resp.json(); return base64ToDataUrl(data?.image_base64, data?.mime || mime); } catch { return null; } } async function blobSrcFromDataUrl(dataUrl) { const resp = await fetch(dataUrl); if (!resp.ok) return null; const blob = await resp.blob(); if (!blob || blob.size < 500) return null; return URL.createObjectURL(blob); } /** Load a displayable src for — blob URL preferred over huge data: URLs. */ export async function loadRenderableImageSrc(fields) { const mime = fields?.mime || "image/png"; const dataUrl = base64ToDataUrl(fields?.image_base64, mime); if (dataUrl) { try { const blobSrc = await blobSrcFromDataUrl(dataUrl); if (blobSrc) return { src: blobSrc, revoke: true }; } catch { /* try URL fallbacks */ } } const url = resolveImageUrl(fields?.image_url); if (url) { try { const resp = await fetch(url, { mode: "cors", credentials: "omit" }); if (resp.ok) { const blob = await resp.blob(); if (blob.size > 500 && (blob.type.startsWith("image/") || blob.size > 2000)) { return { src: URL.createObjectURL(blob), revoke: true }; } } } catch { /* try /b64 */ } const fromJson = await fetchImageB64DataUrl(url, mime); if (fromJson) { try { const blobSrc = await blobSrcFromDataUrl(fromJson); if (blobSrc) return { src: blobSrc, revoke: true }; } catch { return { src: fromJson, revoke: false }; } } } if (dataUrl) return { src: dataUrl, revoke: false }; return null; }