File size: 4,632 Bytes
dd87944
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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 <img src>. */
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 <img> — 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;
}