@enslo/sd-metadata 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,98 +4,139 @@ var Result = {
4
4
  error: (error) => ({ ok: false, error })
5
5
  };
6
6
 
7
- // src/converters/utils.ts
8
- var createTextChunk = (keyword, text) => text !== void 0 ? [{ type: "tEXt", keyword, text }] : [];
9
- var createITxtChunk = (keyword, text) => text !== void 0 ? [
10
- {
11
- type: "iTXt",
12
- keyword,
13
- compressionFlag: 0,
14
- compressionMethod: 0,
15
- languageTag: "",
16
- translatedKeyword: "",
17
- text
7
+ // src/parsers/a1111.ts
8
+ function parseA1111(entries) {
9
+ const parametersEntry = entries.find(
10
+ (e) => e.keyword === "parameters" || e.keyword === "Comment"
11
+ );
12
+ if (!parametersEntry) {
13
+ return Result.error({ type: "unsupportedFormat" });
18
14
  }
19
- ] : [];
20
- var findSegment = (segments, type) => segments.find((s) => s.source.type === type);
21
- var stringify = (value) => {
22
- if (value === void 0) return void 0;
23
- return typeof value === "string" ? value : JSON.stringify(value);
24
- };
25
-
26
- // src/converters/chunk-encoding.ts
27
- var CHUNK_ENCODING_STRATEGIES = {
28
- // Dynamic selection tools
29
- a1111: "dynamic",
30
- forge: "dynamic",
31
- "forge-neo": "dynamic",
32
- "sd-webui": "dynamic",
33
- invokeai: "dynamic",
34
- novelai: "dynamic",
35
- "sd-next": "dynamic",
36
- easydiffusion: "dynamic",
37
- blind: "dynamic",
38
- // Unicode escape tools (spec-compliant)
39
- comfyui: "text-unicode-escape",
40
- swarmui: "text-unicode-escape",
41
- fooocus: "text-unicode-escape",
42
- "ruined-fooocus": "text-unicode-escape",
43
- "hf-space": "text-unicode-escape",
44
- // Raw UTF-8 tools (non-compliant but compatible)
45
- "stability-matrix": "text-utf8-raw",
46
- tensorart: "text-utf8-raw"
47
- };
48
- function getEncodingStrategy(tool) {
49
- return CHUNK_ENCODING_STRATEGIES[tool] ?? "text-unicode-escape";
50
- }
51
- function escapeUnicode(text) {
52
- return text.replace(/[\u0100-\uffff]/g, (char) => {
53
- const code = char.charCodeAt(0).toString(16).padStart(4, "0");
54
- return `\\u${code}`;
55
- });
56
- }
57
- function hasNonLatin1(text) {
58
- return /[^\x00-\xFF]/.test(text);
15
+ const text = parametersEntry.text;
16
+ const hasAIMarkers = text.includes("Steps:") || text.includes("Sampler:") || text.includes("Negative prompt:");
17
+ if (!hasAIMarkers) {
18
+ return Result.error({ type: "unsupportedFormat" });
19
+ }
20
+ const { prompt, negativePrompt, settings } = parseParametersText(text);
21
+ const settingsMap = parseSettings(settings);
22
+ const size = settingsMap.get("Size") ?? "0x0";
23
+ const [width, height] = parseSize(size);
24
+ const version = settingsMap.get("Version");
25
+ const app = settingsMap.get("App");
26
+ const software = detectSoftwareVariant(version, app);
27
+ const metadata = {
28
+ software,
29
+ prompt,
30
+ negativePrompt,
31
+ width,
32
+ height
33
+ };
34
+ const modelName = settingsMap.get("Model");
35
+ const modelHash = settingsMap.get("Model hash");
36
+ if (modelName || modelHash) {
37
+ metadata.model = {
38
+ name: modelName,
39
+ hash: modelHash
40
+ };
41
+ }
42
+ const sampler = settingsMap.get("Sampler");
43
+ const scheduler = settingsMap.get("Schedule type");
44
+ const steps = parseNumber(settingsMap.get("Steps"));
45
+ const cfg = parseNumber(
46
+ settingsMap.get("CFG scale") ?? settingsMap.get("CFG Scale")
47
+ );
48
+ const seed = parseNumber(settingsMap.get("Seed"));
49
+ const clipSkip = parseNumber(settingsMap.get("Clip skip"));
50
+ if (sampler !== void 0 || scheduler !== void 0 || steps !== void 0 || cfg !== void 0 || seed !== void 0 || clipSkip !== void 0) {
51
+ metadata.sampling = {
52
+ sampler,
53
+ scheduler,
54
+ steps,
55
+ cfg,
56
+ seed,
57
+ clipSkip
58
+ };
59
+ }
60
+ const hiresScale = parseNumber(settingsMap.get("Hires upscale"));
61
+ const upscaler = settingsMap.get("Hires upscaler");
62
+ const hiresSteps = parseNumber(settingsMap.get("Hires steps"));
63
+ const denoise = parseNumber(settingsMap.get("Denoising strength"));
64
+ const hiresSize = settingsMap.get("Hires size");
65
+ if ([hiresScale, hiresSize, upscaler, hiresSteps, denoise].some(
66
+ (v) => v !== void 0
67
+ )) {
68
+ const [hiresWidth] = parseSize(hiresSize ?? "");
69
+ const scale = hiresScale ?? hiresWidth / width;
70
+ metadata.hires = { scale, upscaler, steps: hiresSteps, denoise };
71
+ }
72
+ return Result.ok(metadata);
59
73
  }
60
- function createEncodedChunk(keyword, text, strategy) {
61
- if (text === void 0) return [];
62
- switch (strategy) {
63
- case "dynamic": {
64
- const chunkType = hasNonLatin1(text) ? "iTXt" : "tEXt";
65
- return chunkType === "iTXt" ? createITxtChunk(keyword, text) : createTextChunk(keyword, text);
66
- }
67
- case "text-unicode-escape": {
68
- const escaped = escapeUnicode(text);
69
- return createTextChunk(keyword, escaped);
70
- }
71
- case "text-utf8-raw": {
72
- return createTextChunk(keyword, text);
73
- }
74
+ function parseParametersText(text) {
75
+ const negativeIndex = text.indexOf("Negative prompt:");
76
+ const stepsIndex = text.indexOf("Steps:");
77
+ if (negativeIndex === -1 && stepsIndex === -1) {
78
+ return { prompt: text.trim(), negativePrompt: "", settings: "" };
79
+ }
80
+ if (negativeIndex === -1) {
81
+ const settingsStart2 = text.lastIndexOf("\n", stepsIndex);
82
+ return {
83
+ prompt: text.slice(0, settingsStart2).trim(),
84
+ negativePrompt: "",
85
+ settings: text.slice(settingsStart2).trim()
86
+ };
87
+ }
88
+ if (stepsIndex === -1) {
89
+ return {
90
+ prompt: text.slice(0, negativeIndex).trim(),
91
+ negativePrompt: text.slice(negativeIndex + 16).trim(),
92
+ settings: ""
93
+ };
74
94
  }
95
+ const settingsStart = text.lastIndexOf("\n", stepsIndex);
96
+ return {
97
+ prompt: text.slice(0, negativeIndex).trim(),
98
+ negativePrompt: text.slice(negativeIndex + 16, settingsStart).trim(),
99
+ settings: text.slice(settingsStart).trim()
100
+ };
75
101
  }
76
-
77
- // src/converters/a1111.ts
78
- function convertA1111PngToSegments(chunks) {
79
- const parameters = chunks.find((c) => c.keyword === "parameters");
80
- if (!parameters) {
81
- return [];
102
+ function parseSettings(settings) {
103
+ const result = /* @__PURE__ */ new Map();
104
+ if (!settings) return result;
105
+ const regex = /([A-Za-z][A-Za-z0-9 ]*?):\s*([^,]+?)(?=,\s*[A-Za-z][A-Za-z0-9 ]*?:|$)/g;
106
+ const matches = Array.from(settings.matchAll(regex));
107
+ for (const match of matches) {
108
+ const key = (match[1] ?? "").trim();
109
+ const value = (match[2] ?? "").trim();
110
+ result.set(key, value);
82
111
  }
112
+ return result;
113
+ }
114
+ function parseSize(size) {
115
+ const match = size.match(/(\d+)x(\d+)/);
116
+ if (!match) return [0, 0];
83
117
  return [
84
- {
85
- source: { type: "exifUserComment" },
86
- data: parameters.text
87
- }
118
+ Number.parseInt(match[1] ?? "0", 10),
119
+ Number.parseInt(match[2] ?? "0", 10)
88
120
  ];
89
121
  }
90
- function convertA1111SegmentsToPng(segments) {
91
- const userComment = segments.find((s) => s.source.type === "exifUserComment");
92
- if (!userComment) {
93
- return [];
94
- }
95
- return createEncodedChunk(
96
- "parameters",
97
- userComment.data,
98
- getEncodingStrategy("a1111")
122
+ function parseNumber(value) {
123
+ if (value === void 0) return void 0;
124
+ const num = Number.parseFloat(value);
125
+ return Number.isNaN(num) ? void 0 : num;
126
+ }
127
+ function detectSoftwareVariant(version, app) {
128
+ if (app === "SD.Next") return "sd-next";
129
+ if (!version) return "sd-webui";
130
+ if (version === "neo") return "forge-neo";
131
+ if (version === "classic") return "forge";
132
+ if (/^f\d+\.\d+/.test(version)) return "forge";
133
+ return "sd-webui";
134
+ }
135
+
136
+ // src/utils/entries.ts
137
+ function buildEntryRecord(entries) {
138
+ return Object.freeze(
139
+ Object.fromEntries(entries.map((e) => [e.keyword, e.text]))
99
140
  );
100
141
  }
101
142
 
@@ -111,1024 +152,438 @@ function parseJson(text) {
111
152
  }
112
153
  }
113
154
 
114
- // src/converters/blind.ts
115
- function blindPngToSegments(chunks) {
116
- if (chunks.length === 0) return [];
117
- const chunkMap = Object.fromEntries(
118
- chunks.map((chunk) => [chunk.keyword, chunk.text])
119
- );
120
- return [
121
- {
122
- source: { type: "exifUserComment" },
123
- data: JSON.stringify(chunkMap)
124
- }
125
- ];
126
- }
127
- function blindSegmentsToPng(segments) {
128
- const userComment = segments.find((s) => s.source.type === "exifUserComment");
129
- if (!userComment) return [];
130
- const parsed = parseJson(userComment.data);
131
- if (parsed.ok) {
132
- return Object.entries(parsed.value).flatMap(([keyword, value]) => {
133
- const text = typeof value === "string" ? value : JSON.stringify(value);
134
- if (!text) return [];
135
- return createEncodedChunk(keyword, text, getEncodingStrategy("blind"));
155
+ // src/parsers/comfyui.ts
156
+ function parseComfyUI(entries) {
157
+ const entryRecord = buildEntryRecord(entries);
158
+ const promptText = findPromptJson(entryRecord);
159
+ if (!promptText) {
160
+ return Result.error({ type: "unsupportedFormat" });
161
+ }
162
+ const parsed = parseJson(promptText);
163
+ if (!parsed.ok) {
164
+ return Result.error({
165
+ type: "parseError",
166
+ message: "Invalid JSON in prompt entry"
136
167
  });
137
168
  }
138
- return createEncodedChunk(
139
- "metadata",
140
- userComment.data,
141
- getEncodingStrategy("blind")
142
- );
143
- }
144
-
145
- // src/converters/comfyui.ts
146
- function convertComfyUIPngToSegments(chunks) {
147
- const data = {};
148
- for (const chunk of chunks) {
149
- const parsed = parseJson(chunk.text);
150
- if (parsed.ok) {
151
- data[chunk.keyword] = parsed.value;
169
+ const prompt = parsed.value;
170
+ const nodes = Object.values(prompt);
171
+ if (!nodes.some((node) => "class_type" in node)) {
172
+ return Result.error({ type: "unsupportedFormat" });
173
+ }
174
+ const ksampler = findNode(prompt, ["Sampler"]);
175
+ const positiveClip = findNode(prompt, ["PositiveCLIP_Base"]);
176
+ const negativeClip = findNode(prompt, ["NegativeCLIP_Base"]);
177
+ const clipPositiveText = extractText(positiveClip);
178
+ const clipNegativeText = extractText(negativeClip);
179
+ const latentImage = findNode(prompt, ["EmptyLatentImage"]);
180
+ const latentWidth = latentImage ? Number(latentImage.inputs.width) || 0 : 0;
181
+ const latentHeight = latentImage ? Number(latentImage.inputs.height) || 0 : 0;
182
+ const extraMeta = extractExtraMetadata(prompt);
183
+ const positiveText = clipPositiveText || extraMeta?.prompt || "";
184
+ const negativeText = clipNegativeText || extraMeta?.negativePrompt || "";
185
+ const width = latentWidth || extraMeta?.width || 0;
186
+ const height = latentHeight || extraMeta?.height || 0;
187
+ const metadata = {
188
+ software: "comfyui",
189
+ prompt: positiveText,
190
+ negativePrompt: negativeText,
191
+ width,
192
+ height,
193
+ nodes: prompt
194
+ // Store the parsed node graph
195
+ };
196
+ const checkpoint = findNode(prompt, ["CheckpointLoader_Base"])?.inputs?.ckpt_name;
197
+ if (checkpoint) {
198
+ metadata.model = { name: String(checkpoint) };
199
+ } else if (extraMeta?.baseModel) {
200
+ metadata.model = { name: extraMeta.baseModel };
201
+ }
202
+ if (ksampler) {
203
+ metadata.sampling = {
204
+ seed: ksampler.inputs.seed,
205
+ steps: ksampler.inputs.steps,
206
+ cfg: ksampler.inputs.cfg,
207
+ sampler: ksampler.inputs.sampler_name,
208
+ scheduler: ksampler.inputs.scheduler
209
+ };
210
+ } else if (extraMeta) {
211
+ metadata.sampling = {
212
+ seed: extraMeta.seed,
213
+ steps: extraMeta.steps,
214
+ cfg: extraMeta.cfgScale,
215
+ sampler: extraMeta.sampler
216
+ };
217
+ }
218
+ const hiresModel = findNode(prompt, [
219
+ "HiresFix_ModelUpscale_UpscaleModelLoader",
220
+ "PostUpscale_ModelUpscale_UpscaleModelLoader"
221
+ ])?.inputs;
222
+ const hiresScale = findNode(prompt, [
223
+ "HiresFix_ImageScale",
224
+ "PostUpscale_ImageScale"
225
+ ])?.inputs;
226
+ const hiresSampler = findNode(prompt, ["HiresFix_Sampler"])?.inputs;
227
+ if (hiresModel && hiresScale) {
228
+ const hiresWidth = hiresScale.width;
229
+ const scale = latentWidth > 0 ? Math.round(hiresWidth / latentWidth * 100) / 100 : void 0;
230
+ if (hiresSampler) {
231
+ metadata.hires = {
232
+ upscaler: hiresModel.model_name,
233
+ scale,
234
+ steps: hiresSampler.steps,
235
+ denoise: hiresSampler.denoise
236
+ };
152
237
  } else {
153
- data[chunk.keyword] = chunk.text;
238
+ metadata.upscale = {
239
+ upscaler: hiresModel.model_name,
240
+ scale
241
+ };
154
242
  }
155
243
  }
156
- return [
157
- {
158
- source: { type: "exifUserComment" },
159
- data: JSON.stringify(data)
244
+ if (extraMeta?.transformations) {
245
+ const upscaleTransform = extraMeta.transformations.find(
246
+ (t) => t.type === "upscale"
247
+ );
248
+ if (upscaleTransform) {
249
+ const originalWidth = extraMeta.width ?? width;
250
+ if (originalWidth > 0 && upscaleTransform.upscaleWidth) {
251
+ const scale = upscaleTransform.upscaleWidth / originalWidth;
252
+ metadata.upscale = {
253
+ scale: Math.round(scale * 100) / 100
254
+ };
255
+ }
160
256
  }
161
- ];
257
+ }
258
+ return Result.ok(metadata);
162
259
  }
163
- var tryParseExtendedFormat = (segments) => {
164
- const imageDescription = findSegment(segments, "exifImageDescription");
165
- const make = findSegment(segments, "exifMake");
166
- if (!imageDescription && !make) {
167
- return null;
260
+ function findPromptJson(entryRecord) {
261
+ if (entryRecord.prompt) {
262
+ return entryRecord.prompt.replace(/:\s*NaN\b/g, ": null");
168
263
  }
169
- return [
170
- ...createEncodedChunk("prompt", make?.data, getEncodingStrategy("comfyui")),
171
- ...createEncodedChunk(
172
- "workflow",
173
- imageDescription?.data,
174
- getEncodingStrategy("comfyui")
175
- )
264
+ const candidates = [
265
+ entryRecord.Comment,
266
+ entryRecord.Description,
267
+ entryRecord.Make,
268
+ entryRecord.Prompt,
269
+ // save-image-extended uses this
270
+ entryRecord.Workflow
271
+ // Not a prompt, but may contain nodes info
176
272
  ];
177
- };
178
- var tryParseSaveImagePlusFormat = (segments) => {
179
- const userComment = findSegment(segments, "exifUserComment");
180
- if (!userComment) {
181
- return null;
182
- }
183
- const parsed = parseJson(userComment.data);
184
- if (!parsed.ok) {
185
- return createEncodedChunk(
186
- "prompt",
187
- userComment.data,
188
- getEncodingStrategy("comfyui")
189
- );
273
+ for (const candidate of candidates) {
274
+ if (!candidate) continue;
275
+ if (candidate.startsWith("{")) {
276
+ const cleaned = candidate.replace(/\0+$/, "").replace(/:\s*NaN\b/g, ": null");
277
+ const parsed = parseJson(cleaned);
278
+ if (!parsed.ok) continue;
279
+ if (parsed.value.prompt && typeof parsed.value.prompt === "object") {
280
+ return JSON.stringify(parsed.value.prompt);
281
+ }
282
+ const values = Object.values(parsed.value);
283
+ if (values.some((v) => v && typeof v === "object" && "class_type" in v)) {
284
+ return cleaned;
285
+ }
286
+ }
190
287
  }
191
- return Object.entries(parsed.value).flatMap(
192
- ([keyword, value]) => createEncodedChunk(
193
- keyword,
194
- stringify(value),
195
- getEncodingStrategy("comfyui")
196
- )
197
- );
198
- };
199
- function convertComfyUISegmentsToPng(segments) {
200
- return tryParseExtendedFormat(segments) ?? tryParseSaveImagePlusFormat(segments) ?? [];
288
+ return void 0;
201
289
  }
202
-
203
- // src/converters/easydiffusion.ts
204
- function convertEasyDiffusionPngToSegments(chunks) {
205
- const json = Object.fromEntries(
206
- chunks.map((chunk) => [chunk.keyword, chunk.text])
207
- );
208
- return [
209
- {
210
- source: { type: "exifUserComment" },
211
- data: JSON.stringify(json)
212
- }
213
- ];
290
+ function findNode(prompt, keys) {
291
+ return Object.entries(prompt).find(([key]) => keys.includes(key))?.[1];
214
292
  }
215
- function convertEasyDiffusionSegmentsToPng(segments) {
216
- const userComment = findSegment(segments, "exifUserComment");
217
- if (!userComment) {
218
- return [];
219
- }
220
- const parsed = parseJson(userComment.data);
221
- if (!parsed.ok) {
222
- return [];
223
- }
224
- return Object.entries(parsed.value).flatMap(([keyword, value]) => {
225
- const text = value != null ? typeof value === "string" ? value : String(value) : void 0;
226
- if (!text) return [];
227
- return createEncodedChunk(
228
- keyword,
229
- text,
230
- getEncodingStrategy("easydiffusion")
231
- );
232
- });
293
+ function extractText(node) {
294
+ return typeof node?.inputs.text === "string" ? node.inputs.text : "";
295
+ }
296
+ function extractExtraMetadata(prompt) {
297
+ const extraMetaField = prompt.extraMetadata;
298
+ if (typeof extraMetaField !== "string") return void 0;
299
+ const parsed = parseJson(extraMetaField);
300
+ return parsed.ok ? parsed.value : void 0;
233
301
  }
234
302
 
235
- // src/converters/invokeai.ts
236
- function convertInvokeAIPngToSegments(chunks) {
237
- const data = {};
238
- for (const chunk of chunks) {
239
- const parsed = parseJson(chunk.text);
240
- if (parsed.ok) {
241
- data[chunk.keyword] = parsed.value;
242
- } else {
243
- data[chunk.keyword] = chunk.text;
244
- }
303
+ // src/parsers/detect.ts
304
+ function detectSoftware(entries) {
305
+ const entryRecord = buildEntryRecord(entries);
306
+ const uniqueResult = detectUniqueKeywords(entryRecord);
307
+ if (uniqueResult) return uniqueResult;
308
+ const comfyResult = detectComfyUIEntries(entryRecord);
309
+ if (comfyResult) return comfyResult;
310
+ const text = entryRecord.parameters ?? entryRecord.Comment ?? "";
311
+ if (text) {
312
+ return detectFromTextContent(text);
245
313
  }
246
- return [
247
- {
248
- source: { type: "exifUserComment" },
249
- data: JSON.stringify(data)
250
- }
251
- ];
314
+ return null;
252
315
  }
253
- function convertInvokeAISegmentsToPng(segments) {
254
- const userComment = findSegment(segments, "exifUserComment");
255
- if (!userComment) {
256
- return [];
316
+ function detectUniqueKeywords(entryRecord) {
317
+ if (entryRecord.Software === "NovelAI") {
318
+ return "novelai";
257
319
  }
258
- const parsed = parseJson(userComment.data);
259
- if (!parsed.ok) {
260
- return createEncodedChunk(
261
- "invokeai_metadata",
262
- userComment.data,
263
- getEncodingStrategy("invokeai")
264
- );
320
+ if ("invokeai_metadata" in entryRecord) {
321
+ return "invokeai";
265
322
  }
266
- const metadataText = stringify(parsed.value.invokeai_metadata);
267
- const graphText = stringify(parsed.value.invokeai_graph);
268
- const chunks = [
269
- ...createEncodedChunk(
270
- "invokeai_metadata",
271
- metadataText,
272
- getEncodingStrategy("invokeai")
273
- ),
274
- ...createEncodedChunk(
275
- "invokeai_graph",
276
- graphText,
277
- getEncodingStrategy("invokeai")
278
- )
279
- ];
280
- if (chunks.length > 0) {
281
- return chunks;
323
+ if ("generation_data" in entryRecord) {
324
+ return "tensorart";
282
325
  }
283
- return createEncodedChunk(
284
- "invokeai_metadata",
285
- userComment.data,
286
- getEncodingStrategy("invokeai")
287
- );
288
- }
289
-
290
- // src/converters/novelai.ts
291
- var NOVELAI_TITLE = "NovelAI generated image";
292
- var NOVELAI_SOFTWARE = "NovelAI";
293
- function convertNovelaiPngToSegments(chunks) {
294
- const comment = chunks.find((c) => c.keyword === "Comment");
295
- if (!comment) {
296
- return [];
297
- }
298
- const description = chunks.find((c) => c.keyword === "Description");
299
- const data = buildUserCommentJson(chunks);
300
- const descriptionSegment = description ? [
301
- {
302
- source: { type: "exifImageDescription" },
303
- data: `\0\0\0\0${description.text}`
304
- }
305
- ] : [];
306
- const userCommentSegment = {
307
- source: { type: "exifUserComment" },
308
- data: JSON.stringify(data)
309
- };
310
- return [...descriptionSegment, userCommentSegment];
311
- }
312
- function buildUserCommentJson(chunks) {
313
- return NOVELAI_KEY_ORDER.map((key) => {
314
- const chunk = chunks.find((c) => c.keyword === key);
315
- return chunk ? { [key]: chunk.text } : null;
316
- }).filter((entry) => entry !== null).reduce(
317
- (acc, entry) => Object.assign(acc, entry),
318
- {}
319
- );
320
- }
321
- var NOVELAI_KEY_ORDER = [
322
- "Comment",
323
- "Description",
324
- "Generation time",
325
- "Software",
326
- "Source",
327
- "Title"
328
- ];
329
- function convertNovelaiSegmentsToPng(segments) {
330
- const userCommentSeg = findSegment(segments, "exifUserComment");
331
- const descriptionSeg = findSegment(segments, "exifImageDescription");
332
- return parseSegments(userCommentSeg, descriptionSeg);
333
- }
334
- function parseSegments(userCommentSeg, descriptionSeg) {
335
- if (!userCommentSeg || !descriptionSeg) {
336
- return [];
326
+ if ("smproj" in entryRecord) {
327
+ return "stability-matrix";
337
328
  }
338
- const parsed = parseJson(userCommentSeg.data);
339
- if (!parsed.ok) {
340
- return createTextChunk("Comment", userCommentSeg.data);
329
+ if ("negative_prompt" in entryRecord || "Negative Prompt" in entryRecord) {
330
+ return "easydiffusion";
341
331
  }
342
- const jsonData = parsed.value;
343
- const descriptionText = extractDescriptionText(
344
- descriptionSeg,
345
- stringify(jsonData.Description)
346
- );
347
- const descriptionChunks = descriptionText ? createEncodedChunk(
348
- "Description",
349
- descriptionText,
350
- getEncodingStrategy("novelai")
351
- ) : [];
352
- return [
353
- // Title (required, use default if missing)
354
- createTextChunk("Title", stringify(jsonData.Title) ?? NOVELAI_TITLE),
355
- // Description (optional, prefer exifImageDescription over JSON)
356
- ...descriptionChunks,
357
- // Software (required, use default if missing)
358
- createTextChunk(
359
- "Software",
360
- stringify(jsonData.Software) ?? NOVELAI_SOFTWARE
361
- ),
362
- // Source (optional)
363
- createTextChunk("Source", stringify(jsonData.Source)),
364
- // Generation time (optional)
365
- createTextChunk("Generation time", stringify(jsonData["Generation time"])),
366
- // Comment (optional)
367
- createTextChunk("Comment", stringify(jsonData.Comment))
368
- ].flat();
369
- }
370
- function extractDescriptionText(descriptionSeg, jsonDescription) {
371
- if (descriptionSeg?.data) {
372
- const data = descriptionSeg.data;
373
- return data.startsWith("\0\0\0\0") ? data.slice(4) : data;
332
+ const parameters = entryRecord.parameters;
333
+ if (parameters?.includes("sui_image_params")) {
334
+ return "swarmui";
374
335
  }
375
- if (jsonDescription) {
376
- return jsonDescription.startsWith("\0\0\0\0") ? jsonDescription.slice(4) : jsonDescription;
336
+ const comment = entryRecord.Comment;
337
+ if (comment?.startsWith("{")) {
338
+ return detectFromCommentJson(comment);
377
339
  }
378
- return void 0;
379
- }
380
-
381
- // src/converters/simple-chunk.ts
382
- function createPngToSegments(keyword) {
383
- return (chunks) => {
384
- const chunk = chunks.find((c) => c.keyword === keyword);
385
- return !chunk ? [] : [{ source: { type: "exifUserComment" }, data: chunk.text }];
386
- };
340
+ return null;
387
341
  }
388
- function createSegmentsToPng(keyword) {
389
- return (segments) => {
390
- const userComment = segments.find(
391
- (s) => s.source.type === "exifUserComment"
392
- );
393
- if (!userComment) return [];
394
- return createEncodedChunk(
395
- keyword,
396
- userComment.data,
397
- getEncodingStrategy(keyword)
398
- );
399
- };
342
+ function detectFromCommentJson(comment) {
343
+ try {
344
+ const parsed = JSON.parse(comment);
345
+ if ("invokeai_metadata" in parsed) {
346
+ return "invokeai";
347
+ }
348
+ if ("prompt" in parsed && "workflow" in parsed) {
349
+ const workflow = parsed.workflow;
350
+ const prompt = parsed.prompt;
351
+ const isObject = typeof workflow === "object" || typeof prompt === "object";
352
+ const isJsonString = typeof workflow === "string" && workflow.startsWith("{") || typeof prompt === "string" && prompt.startsWith("{");
353
+ if (isObject || isJsonString) {
354
+ return "comfyui";
355
+ }
356
+ }
357
+ if ("sui_image_params" in parsed) {
358
+ return "swarmui";
359
+ }
360
+ if ("prompt" in parsed && "parameters" in parsed) {
361
+ const params = String(parsed.parameters || "");
362
+ if (params.includes("sui_image_params") || params.includes("swarm_version")) {
363
+ return "swarmui";
364
+ }
365
+ }
366
+ } catch {
367
+ }
368
+ return null;
400
369
  }
401
-
402
- // src/converters/swarmui.ts
403
- function convertSwarmUIPngToSegments(chunks) {
404
- const parametersChunk = chunks.find((c) => c.keyword === "parameters");
405
- if (!parametersChunk) {
406
- return [];
370
+ function detectComfyUIEntries(entryRecord) {
371
+ if ("prompt" in entryRecord && "workflow" in entryRecord) {
372
+ return "comfyui";
407
373
  }
408
- const parsed = parseJson(parametersChunk.text);
409
- const data = parsed.ok ? parsed.value : parametersChunk.text;
410
- const segments = [
411
- {
412
- source: { type: "exifUserComment" },
413
- data: typeof data === "string" ? data : JSON.stringify(data)
374
+ if ("workflow" in entryRecord) {
375
+ return "comfyui";
376
+ }
377
+ if ("prompt" in entryRecord) {
378
+ const promptText = entryRecord.prompt;
379
+ if (promptText?.startsWith("{")) {
380
+ if (promptText.includes("sui_image_params")) {
381
+ return "swarmui";
382
+ }
383
+ if (promptText.includes("class_type")) {
384
+ return "comfyui";
385
+ }
414
386
  }
415
- ];
416
- const promptChunk = chunks.find((c) => c.keyword === "prompt");
417
- if (promptChunk) {
418
- segments.push({
419
- source: { type: "exifMake" },
420
- data: promptChunk.text
421
- });
422
387
  }
423
- return segments;
388
+ return null;
424
389
  }
425
- function convertSwarmUISegmentsToPng(segments) {
426
- const userComment = findSegment(segments, "exifUserComment");
427
- if (!userComment) {
428
- return [];
429
- }
430
- const chunks = [];
431
- const make = findSegment(segments, "exifMake");
432
- if (make) {
433
- chunks.push(
434
- ...createEncodedChunk(
435
- "prompt",
436
- make.data,
437
- getEncodingStrategy("swarmui")
438
- )
439
- );
390
+ function detectFromTextContent(text) {
391
+ if (text.startsWith("{")) {
392
+ return detectFromJsonFormat(text);
440
393
  }
441
- chunks.push(
442
- ...createEncodedChunk(
443
- "parameters",
444
- userComment.data,
445
- getEncodingStrategy("swarmui")
446
- )
447
- );
448
- return chunks;
394
+ return detectFromA1111Format(text);
449
395
  }
450
-
451
- // src/converters/index.ts
452
- function convertMetadata(parseResult, targetFormat, force = false) {
453
- if (parseResult.status === "empty") {
454
- return Result.error({ type: "missingRawData" });
396
+ function detectFromJsonFormat(json) {
397
+ if (json.includes("sui_image_params")) {
398
+ return "swarmui";
455
399
  }
456
- if (parseResult.status === "invalid") {
457
- return Result.error({
458
- type: "invalidParseResult",
459
- status: parseResult.status
460
- });
400
+ if (json.includes('"software":"RuinedFooocus"') || json.includes('"software": "RuinedFooocus"')) {
401
+ return "ruined-fooocus";
461
402
  }
462
- const raw = parseResult.raw;
463
- if (raw.format === "png" && targetFormat === "png" || raw.format === "jpeg" && targetFormat === "jpeg" || raw.format === "webp" && targetFormat === "webp") {
464
- return Result.ok(raw);
403
+ if (json.includes('"use_stable_diffusion_model"')) {
404
+ return "easydiffusion";
465
405
  }
466
- const software = parseResult.status === "success" ? parseResult.metadata.software : null;
467
- if (!software) {
468
- return force ? convertBlind(raw, targetFormat) : Result.error({
469
- type: "unsupportedSoftware",
470
- software: "unknown"
471
- });
406
+ if (json.includes("civitai:") || json.includes('"resource-stack"')) {
407
+ return "civitai";
472
408
  }
473
- const converter = softwareConverters[software];
474
- if (!converter) {
475
- return Result.error({
476
- type: "unsupportedSoftware",
477
- software
478
- });
409
+ if (json.includes('"v4_prompt"') || json.includes('"noise_schedule"') || json.includes('"uncond_scale"') || json.includes('"Software":"NovelAI"') || json.includes('\\"noise_schedule\\"') || json.includes('\\"v4_prompt\\"')) {
410
+ return "novelai";
479
411
  }
480
- return converter(raw, targetFormat);
481
- }
482
- function createFormatConverter(pngToSegments, segmentsToPng) {
483
- return (raw, targetFormat) => {
484
- if (raw.format === "png") {
485
- if (targetFormat === "png") {
486
- return Result.ok(raw);
487
- }
488
- const segments = pngToSegments(raw.chunks);
489
- return Result.ok({ format: targetFormat, segments });
490
- }
491
- if (targetFormat === "jpeg" || targetFormat === "webp") {
492
- return Result.ok({ format: targetFormat, segments: raw.segments });
493
- }
494
- const chunks = segmentsToPng(raw.segments);
495
- return Result.ok({ format: "png", chunks });
496
- };
497
- }
498
- var convertNovelai = createFormatConverter(
499
- convertNovelaiPngToSegments,
500
- convertNovelaiSegmentsToPng
501
- );
502
- var convertA1111 = createFormatConverter(
503
- convertA1111PngToSegments,
504
- convertA1111SegmentsToPng
505
- );
506
- var convertComfyUI = createFormatConverter(
507
- convertComfyUIPngToSegments,
508
- convertComfyUISegmentsToPng
509
- );
510
- var convertEasyDiffusion = createFormatConverter(
511
- convertEasyDiffusionPngToSegments,
512
- convertEasyDiffusionSegmentsToPng
513
- );
514
- var convertFooocus = createFormatConverter(
515
- createPngToSegments("Comment"),
516
- createSegmentsToPng("Comment")
517
- );
518
- var convertRuinedFooocus = createFormatConverter(
519
- createPngToSegments("parameters"),
520
- createSegmentsToPng("parameters")
521
- );
522
- var convertSwarmUI = createFormatConverter(
523
- convertSwarmUIPngToSegments,
524
- convertSwarmUISegmentsToPng
525
- );
526
- var convertInvokeAI = createFormatConverter(
527
- convertInvokeAIPngToSegments,
528
- convertInvokeAISegmentsToPng
529
- );
530
- var convertHfSpace = createFormatConverter(
531
- createPngToSegments("parameters"),
532
- createSegmentsToPng("parameters")
533
- );
534
- var convertBlind = createFormatConverter(
535
- blindPngToSegments,
536
- blindSegmentsToPng
537
- );
538
- var softwareConverters = {
539
- // NovelAI
540
- novelai: convertNovelai,
541
- // A1111-format (sd-webui, forge, forge-neo, civitai, sd-next)
542
- "sd-webui": convertA1111,
543
- "sd-next": convertA1111,
544
- forge: convertA1111,
545
- "forge-neo": convertA1111,
546
- civitai: convertA1111,
547
- // ComfyUI-format (comfyui, tensorart, stability-matrix)
548
- comfyui: convertComfyUI,
549
- tensorart: convertComfyUI,
550
- "stability-matrix": convertComfyUI,
551
- // Easy Diffusion
552
- easydiffusion: convertEasyDiffusion,
553
- // Fooocus variants
554
- fooocus: convertFooocus,
555
- "ruined-fooocus": convertRuinedFooocus,
556
- // SwarmUI
557
- swarmui: convertSwarmUI,
558
- // InvokeAI
559
- invokeai: convertInvokeAI,
560
- // HuggingFace Space
561
- "hf-space": convertHfSpace
562
- };
563
-
564
- // src/parsers/a1111.ts
565
- function parseA1111(entries) {
566
- const parametersEntry = entries.find(
567
- (e) => e.keyword === "parameters" || e.keyword === "Comment"
568
- );
569
- if (!parametersEntry) {
570
- return Result.error({ type: "unsupportedFormat" });
571
- }
572
- const text = parametersEntry.text;
573
- const hasAIMarkers = text.includes("Steps:") || text.includes("Sampler:") || text.includes("Negative prompt:");
574
- if (!hasAIMarkers) {
575
- return Result.error({ type: "unsupportedFormat" });
576
- }
577
- const { prompt, negativePrompt, settings } = parseParametersText(text);
578
- const settingsMap = parseSettings(settings);
579
- const size = settingsMap.get("Size") ?? "0x0";
580
- const [width, height] = parseSize(size);
581
- const version = settingsMap.get("Version");
582
- const app = settingsMap.get("App");
583
- const software = detectSoftwareVariant(version, app);
584
- const metadata = {
585
- software,
586
- prompt,
587
- negativePrompt,
588
- width,
589
- height
590
- };
591
- const modelName = settingsMap.get("Model");
592
- const modelHash = settingsMap.get("Model hash");
593
- if (modelName || modelHash) {
594
- metadata.model = {
595
- name: modelName,
596
- hash: modelHash
597
- };
412
+ if (json.includes('"Model"') && json.includes('"resolution"')) {
413
+ return "hf-space";
598
414
  }
599
- const sampler = settingsMap.get("Sampler");
600
- const scheduler = settingsMap.get("Schedule type");
601
- const steps = parseNumber(settingsMap.get("Steps"));
602
- const cfg = parseNumber(
603
- settingsMap.get("CFG scale") ?? settingsMap.get("CFG Scale")
604
- );
605
- const seed = parseNumber(settingsMap.get("Seed"));
606
- const clipSkip = parseNumber(settingsMap.get("Clip skip"));
607
- if (sampler !== void 0 || scheduler !== void 0 || steps !== void 0 || cfg !== void 0 || seed !== void 0 || clipSkip !== void 0) {
608
- metadata.sampling = {
609
- sampler,
610
- scheduler,
611
- steps,
612
- cfg,
613
- seed,
614
- clipSkip
615
- };
415
+ if (json.includes('"prompt"') && json.includes('"base_model"')) {
416
+ return "fooocus";
616
417
  }
617
- const hiresScale = parseNumber(settingsMap.get("Hires upscale"));
618
- const upscaler = settingsMap.get("Hires upscaler");
619
- const hiresSteps = parseNumber(settingsMap.get("Hires steps"));
620
- const denoise = parseNumber(settingsMap.get("Denoising strength"));
621
- const hiresSize = settingsMap.get("Hires size");
622
- if ([hiresScale, hiresSize, upscaler, hiresSteps, denoise].some(
623
- (v) => v !== void 0
624
- )) {
625
- const [hiresWidth] = parseSize(hiresSize ?? "");
626
- const scale = hiresScale ?? hiresWidth / width;
627
- metadata.hires = { scale, upscaler, steps: hiresSteps, denoise };
418
+ if (json.includes('"prompt"') || json.includes('"nodes"')) {
419
+ return "comfyui";
628
420
  }
629
- return Result.ok(metadata);
421
+ return null;
630
422
  }
631
- function parseParametersText(text) {
632
- const negativeIndex = text.indexOf("Negative prompt:");
633
- const stepsIndex = text.indexOf("Steps:");
634
- if (negativeIndex === -1 && stepsIndex === -1) {
635
- return { prompt: text.trim(), negativePrompt: "", settings: "" };
423
+ function detectFromA1111Format(text) {
424
+ if (text.includes("sui_image_params") || text.includes("swarm_version")) {
425
+ return "swarmui";
636
426
  }
637
- if (negativeIndex === -1) {
638
- const settingsStart2 = text.lastIndexOf("\n", stepsIndex);
639
- return {
640
- prompt: text.slice(0, settingsStart2).trim(),
641
- negativePrompt: "",
642
- settings: text.slice(settingsStart2).trim()
643
- };
427
+ const versionMatch = text.match(/Version:\s*([^\s,]+)/);
428
+ if (versionMatch) {
429
+ const version = versionMatch[1];
430
+ if (version === "neo" || version?.startsWith("neo")) {
431
+ return "forge-neo";
432
+ }
433
+ if (version?.startsWith("f") && /^f\d/.test(version)) {
434
+ return "forge";
435
+ }
436
+ if (version === "ComfyUI") {
437
+ return "comfyui";
438
+ }
644
439
  }
645
- if (stepsIndex === -1) {
646
- return {
647
- prompt: text.slice(0, negativeIndex).trim(),
648
- negativePrompt: text.slice(negativeIndex + 16).trim(),
649
- settings: ""
650
- };
440
+ if (text.includes("App: SD.Next") || text.includes("App:SD.Next")) {
441
+ return "sd-next";
651
442
  }
652
- const settingsStart = text.lastIndexOf("\n", stepsIndex);
653
- return {
654
- prompt: text.slice(0, negativeIndex).trim(),
655
- negativePrompt: text.slice(negativeIndex + 16, settingsStart).trim(),
656
- settings: text.slice(settingsStart).trim()
657
- };
658
- }
659
- function parseSettings(settings) {
660
- const result = /* @__PURE__ */ new Map();
661
- if (!settings) return result;
662
- const regex = /([A-Za-z][A-Za-z0-9 ]*?):\s*([^,]+?)(?=,\s*[A-Za-z][A-Za-z0-9 ]*?:|$)/g;
663
- const matches = Array.from(settings.matchAll(regex));
664
- for (const match of matches) {
665
- const key = (match[1] ?? "").trim();
666
- const value = (match[2] ?? "").trim();
667
- result.set(key, value);
443
+ if (text.includes("Civitai resources:")) {
444
+ return "civitai";
668
445
  }
669
- return result;
670
- }
671
- function parseSize(size) {
672
- const match = size.match(/(\d+)x(\d+)/);
673
- if (!match) return [0, 0];
674
- return [
675
- Number.parseInt(match[1] ?? "0", 10),
676
- Number.parseInt(match[2] ?? "0", 10)
677
- ];
678
- }
679
- function parseNumber(value) {
680
- if (value === void 0) return void 0;
681
- const num = Number.parseFloat(value);
682
- return Number.isNaN(num) ? void 0 : num;
683
- }
684
- function detectSoftwareVariant(version, app) {
685
- if (app === "SD.Next") return "sd-next";
686
- if (!version) return "sd-webui";
687
- if (version === "neo") return "forge-neo";
688
- if (version === "classic") return "forge";
689
- if (/^f\d+\.\d+/.test(version)) return "forge";
690
- return "sd-webui";
446
+ if (text.includes("Steps:") && text.includes("Sampler:")) {
447
+ return "sd-webui";
448
+ }
449
+ return null;
691
450
  }
692
451
 
693
- // src/utils/entries.ts
694
- function buildEntryRecord(entries) {
695
- return Object.freeze(
696
- Object.fromEntries(entries.map((e) => [e.keyword, e.text]))
697
- );
452
+ // src/parsers/easydiffusion.ts
453
+ function getValue(json, keyA, keyB) {
454
+ return json[keyA] ?? json[keyB];
698
455
  }
699
-
700
- // src/parsers/comfyui.ts
701
- function parseComfyUI(entries) {
456
+ function extractModelName(path) {
457
+ if (!path) return void 0;
458
+ const parts = path.replace(/\\/g, "/").split("/");
459
+ return parts[parts.length - 1];
460
+ }
461
+ function parseEasyDiffusion(entries) {
702
462
  const entryRecord = buildEntryRecord(entries);
703
- const promptText = findPromptJson(entryRecord);
704
- if (!promptText) {
463
+ if (entryRecord.negative_prompt || entryRecord["Negative Prompt"]) {
464
+ return parseFromEntries(entryRecord);
465
+ }
466
+ const jsonText = (entryRecord.parameters?.startsWith("{") ? entryRecord.parameters : void 0) ?? (entryRecord.Comment?.startsWith("{") ? entryRecord.Comment : void 0);
467
+ if (!jsonText) {
705
468
  return Result.error({ type: "unsupportedFormat" });
706
469
  }
707
- const parsed = parseJson(promptText);
470
+ const parsed = parseJson(jsonText);
708
471
  if (!parsed.ok) {
709
472
  return Result.error({
710
473
  type: "parseError",
711
- message: "Invalid JSON in prompt entry"
474
+ message: "Invalid JSON in Easy Diffusion metadata"
712
475
  });
713
476
  }
714
- const prompt = parsed.value;
715
- const nodes = Object.values(prompt);
716
- if (!nodes.some((node) => "class_type" in node)) {
717
- return Result.error({ type: "unsupportedFormat" });
718
- }
719
- const ksampler = findNode(prompt, ["Sampler"]);
720
- const positiveClip = findNode(prompt, ["PositiveCLIP_Base"]);
721
- const negativeClip = findNode(prompt, ["NegativeCLIP_Base"]);
722
- const clipPositiveText = extractText(positiveClip);
723
- const clipNegativeText = extractText(negativeClip);
724
- const latentImage = findNode(prompt, ["EmptyLatentImage"]);
725
- const latentWidth = latentImage ? Number(latentImage.inputs.width) || 0 : 0;
726
- const latentHeight = latentImage ? Number(latentImage.inputs.height) || 0 : 0;
727
- const extraMeta = extractExtraMetadata(prompt);
728
- const positiveText = clipPositiveText || extraMeta?.prompt || "";
729
- const negativeText = clipNegativeText || extraMeta?.negativePrompt || "";
730
- const width = latentWidth || extraMeta?.width || 0;
731
- const height = latentHeight || extraMeta?.height || 0;
477
+ return parseFromJson(parsed.value);
478
+ }
479
+ function parseFromEntries(entryRecord) {
480
+ const prompt = entryRecord.prompt ?? entryRecord.Prompt ?? "";
481
+ const negativePrompt = entryRecord.negative_prompt ?? entryRecord["Negative Prompt"] ?? entryRecord.negative_prompt ?? "";
482
+ const modelPath = entryRecord.use_stable_diffusion_model ?? entryRecord["Stable Diffusion model"];
483
+ const width = Number(entryRecord.width ?? entryRecord.Width) || 0;
484
+ const height = Number(entryRecord.height ?? entryRecord.Height) || 0;
732
485
  const metadata = {
733
- software: "comfyui",
734
- prompt: positiveText,
735
- negativePrompt: negativeText,
486
+ software: "easydiffusion",
487
+ prompt: prompt.trim(),
488
+ negativePrompt: negativePrompt.trim(),
736
489
  width,
737
490
  height,
738
- nodes: prompt
739
- // Store the parsed node graph
740
- };
741
- const checkpoint = findNode(prompt, ["CheckpointLoader_Base"])?.inputs?.ckpt_name;
742
- if (checkpoint) {
743
- metadata.model = { name: String(checkpoint) };
744
- } else if (extraMeta?.baseModel) {
745
- metadata.model = { name: extraMeta.baseModel };
746
- }
747
- if (ksampler) {
748
- metadata.sampling = {
749
- seed: ksampler.inputs.seed,
750
- steps: ksampler.inputs.steps,
751
- cfg: ksampler.inputs.cfg,
752
- sampler: ksampler.inputs.sampler_name,
753
- scheduler: ksampler.inputs.scheduler
754
- };
755
- } else if (extraMeta) {
756
- metadata.sampling = {
757
- seed: extraMeta.seed,
758
- steps: extraMeta.steps,
759
- cfg: extraMeta.cfgScale,
760
- sampler: extraMeta.sampler
761
- };
762
- }
763
- const hiresModel = findNode(prompt, [
764
- "HiresFix_ModelUpscale_UpscaleModelLoader",
765
- "PostUpscale_ModelUpscale_UpscaleModelLoader"
766
- ])?.inputs;
767
- const hiresScale = findNode(prompt, [
768
- "HiresFix_ImageScale",
769
- "PostUpscale_ImageScale"
770
- ])?.inputs;
771
- const hiresSampler = findNode(prompt, ["HiresFix_Sampler"])?.inputs;
772
- if (hiresModel && hiresScale) {
773
- const hiresWidth = hiresScale.width;
774
- const scale = latentWidth > 0 ? Math.round(hiresWidth / latentWidth * 100) / 100 : void 0;
775
- if (hiresSampler) {
776
- metadata.hires = {
777
- upscaler: hiresModel.model_name,
778
- scale,
779
- steps: hiresSampler.steps,
780
- denoise: hiresSampler.denoise
781
- };
782
- } else {
783
- metadata.upscale = {
784
- upscaler: hiresModel.model_name,
785
- scale
786
- };
787
- }
788
- }
789
- if (extraMeta?.transformations) {
790
- const upscaleTransform = extraMeta.transformations.find(
791
- (t) => t.type === "upscale"
792
- );
793
- if (upscaleTransform) {
794
- const originalWidth = extraMeta.width ?? width;
795
- if (originalWidth > 0 && upscaleTransform.upscaleWidth) {
796
- const scale = upscaleTransform.upscaleWidth / originalWidth;
797
- metadata.upscale = {
798
- scale: Math.round(scale * 100) / 100
799
- };
800
- }
491
+ model: {
492
+ name: extractModelName(modelPath),
493
+ vae: entryRecord.use_vae_model ?? entryRecord["VAE model"]
494
+ },
495
+ sampling: {
496
+ sampler: entryRecord.sampler_name ?? entryRecord.Sampler,
497
+ steps: Number(entryRecord.num_inference_steps ?? entryRecord.Steps) || void 0,
498
+ cfg: Number(entryRecord.guidance_scale ?? entryRecord["Guidance Scale"]) || void 0,
499
+ seed: Number(entryRecord.seed ?? entryRecord.Seed) || void 0,
500
+ clipSkip: Number(entryRecord.clip_skip ?? entryRecord["Clip Skip"]) || void 0
801
501
  }
802
- }
502
+ };
803
503
  return Result.ok(metadata);
804
504
  }
805
- function findPromptJson(entryRecord) {
806
- if (entryRecord.prompt) {
807
- return entryRecord.prompt.replace(/:\s*NaN\b/g, ": null");
808
- }
809
- const candidates = [
810
- entryRecord.Comment,
811
- entryRecord.Description,
812
- entryRecord.Make,
813
- entryRecord.Prompt,
814
- // save-image-extended uses this
815
- entryRecord.Workflow
816
- // Not a prompt, but may contain nodes info
817
- ];
818
- for (const candidate of candidates) {
819
- if (!candidate) continue;
820
- if (candidate.startsWith("{")) {
821
- const cleaned = candidate.replace(/\0+$/, "").replace(/:\s*NaN\b/g, ": null");
822
- const parsed = parseJson(cleaned);
823
- if (!parsed.ok) continue;
824
- if (parsed.value.prompt && typeof parsed.value.prompt === "object") {
825
- return JSON.stringify(parsed.value.prompt);
826
- }
827
- const values = Object.values(parsed.value);
828
- if (values.some((v) => v && typeof v === "object" && "class_type" in v)) {
829
- return cleaned;
830
- }
505
+ function parseFromJson(json) {
506
+ const prompt = getValue(json, "prompt", "Prompt") ?? "";
507
+ const negativePrompt = getValue(json, "negative_prompt", "Negative Prompt") ?? "";
508
+ const modelPath = getValue(
509
+ json,
510
+ "use_stable_diffusion_model",
511
+ "Stable Diffusion model"
512
+ );
513
+ const width = getValue(json, "width", "Width") ?? 0;
514
+ const height = getValue(json, "height", "Height") ?? 0;
515
+ const metadata = {
516
+ software: "easydiffusion",
517
+ prompt: prompt.trim(),
518
+ negativePrompt: negativePrompt.trim(),
519
+ width,
520
+ height,
521
+ model: {
522
+ name: extractModelName(modelPath),
523
+ vae: getValue(json, "use_vae_model", "VAE model")
524
+ },
525
+ sampling: {
526
+ sampler: getValue(json, "sampler_name", "Sampler"),
527
+ steps: getValue(json, "num_inference_steps", "Steps"),
528
+ cfg: getValue(json, "guidance_scale", "Guidance Scale"),
529
+ seed: getValue(json, "seed", "Seed"),
530
+ clipSkip: getValue(json, "clip_skip", "Clip Skip")
831
531
  }
832
- }
833
- return void 0;
834
- }
835
- function findNode(prompt, keys) {
836
- return Object.entries(prompt).find(([key]) => keys.includes(key))?.[1];
837
- }
838
- function extractText(node) {
839
- return typeof node?.inputs.text === "string" ? node.inputs.text : "";
840
- }
841
- function extractExtraMetadata(prompt) {
842
- const extraMetaField = prompt.extraMetadata;
843
- if (typeof extraMetaField !== "string") return void 0;
844
- const parsed = parseJson(extraMetaField);
845
- return parsed.ok ? parsed.value : void 0;
532
+ };
533
+ return Result.ok(metadata);
846
534
  }
847
535
 
848
- // src/parsers/detect.ts
849
- function detectSoftware(entries) {
536
+ // src/parsers/fooocus.ts
537
+ function parseFooocus(entries) {
850
538
  const entryRecord = buildEntryRecord(entries);
851
- const uniqueResult = detectUniqueKeywords(entryRecord);
852
- if (uniqueResult) return uniqueResult;
853
- const comfyResult = detectComfyUIEntries(entryRecord);
854
- if (comfyResult) return comfyResult;
855
- const text = entryRecord.parameters ?? entryRecord.Comment ?? "";
856
- if (text) {
857
- return detectFromTextContent(text);
858
- }
859
- return null;
860
- }
861
- function detectUniqueKeywords(entryRecord) {
862
- if (entryRecord.Software === "NovelAI") {
863
- return "novelai";
864
- }
865
- if ("invokeai_metadata" in entryRecord) {
866
- return "invokeai";
867
- }
868
- if ("generation_data" in entryRecord) {
869
- return "tensorart";
539
+ const jsonText = entryRecord.Comment ?? entryRecord.comment;
540
+ if (!jsonText || !jsonText.startsWith("{")) {
541
+ return Result.error({ type: "unsupportedFormat" });
870
542
  }
871
- if ("smproj" in entryRecord) {
872
- return "stability-matrix";
543
+ const parsed = parseJson(jsonText);
544
+ if (!parsed.ok) {
545
+ return Result.error({
546
+ type: "parseError",
547
+ message: "Invalid JSON in Fooocus metadata"
548
+ });
873
549
  }
874
- if ("negative_prompt" in entryRecord || "Negative Prompt" in entryRecord) {
875
- return "easydiffusion";
550
+ const json = parsed.value;
551
+ if (!json.base_model && !json.prompt) {
552
+ return Result.error({ type: "unsupportedFormat" });
876
553
  }
877
- const parameters = entryRecord.parameters;
878
- if (parameters?.includes("sui_image_params")) {
879
- return "swarmui";
554
+ const metadata = {
555
+ software: "fooocus",
556
+ prompt: json.prompt?.trim() ?? "",
557
+ negativePrompt: json.negative_prompt?.trim() ?? "",
558
+ width: json.width ?? 0,
559
+ height: json.height ?? 0,
560
+ model: {
561
+ name: json.base_model
562
+ },
563
+ sampling: {
564
+ sampler: json.sampler,
565
+ scheduler: json.scheduler,
566
+ steps: json.steps,
567
+ cfg: json.cfg,
568
+ seed: json.seed
569
+ }
570
+ };
571
+ return Result.ok(metadata);
572
+ }
573
+
574
+ // src/parsers/hf-space.ts
575
+ function parseHfSpace(entries) {
576
+ const entryRecord = buildEntryRecord(entries);
577
+ const parametersText = entryRecord.parameters;
578
+ if (!parametersText) {
579
+ return Result.error({ type: "unsupportedFormat" });
880
580
  }
881
- const comment = entryRecord.Comment;
882
- if (comment?.startsWith("{")) {
883
- return detectFromCommentJson(comment);
884
- }
885
- return null;
886
- }
887
- function detectFromCommentJson(comment) {
888
- try {
889
- const parsed = JSON.parse(comment);
890
- if ("invokeai_metadata" in parsed) {
891
- return "invokeai";
892
- }
893
- if ("prompt" in parsed && "workflow" in parsed) {
894
- const workflow = parsed.workflow;
895
- const prompt = parsed.prompt;
896
- const isObject = typeof workflow === "object" || typeof prompt === "object";
897
- const isJsonString = typeof workflow === "string" && workflow.startsWith("{") || typeof prompt === "string" && prompt.startsWith("{");
898
- if (isObject || isJsonString) {
899
- return "comfyui";
900
- }
901
- }
902
- if ("sui_image_params" in parsed) {
903
- return "swarmui";
904
- }
905
- if ("prompt" in parsed && "parameters" in parsed) {
906
- const params = String(parsed.parameters || "");
907
- if (params.includes("sui_image_params") || params.includes("swarm_version")) {
908
- return "swarmui";
909
- }
910
- }
911
- } catch {
912
- }
913
- return null;
914
- }
915
- function detectComfyUIEntries(entryRecord) {
916
- if ("prompt" in entryRecord && "workflow" in entryRecord) {
917
- return "comfyui";
918
- }
919
- if ("workflow" in entryRecord) {
920
- return "comfyui";
921
- }
922
- if ("prompt" in entryRecord) {
923
- const promptText = entryRecord.prompt;
924
- if (promptText?.startsWith("{")) {
925
- if (promptText.includes("sui_image_params")) {
926
- return "swarmui";
927
- }
928
- if (promptText.includes("class_type")) {
929
- return "comfyui";
930
- }
931
- }
932
- }
933
- return null;
934
- }
935
- function detectFromTextContent(text) {
936
- if (text.startsWith("{")) {
937
- return detectFromJsonFormat(text);
938
- }
939
- return detectFromA1111Format(text);
940
- }
941
- function detectFromJsonFormat(json) {
942
- if (json.includes("sui_image_params")) {
943
- return "swarmui";
944
- }
945
- if (json.includes('"software":"RuinedFooocus"') || json.includes('"software": "RuinedFooocus"')) {
946
- return "ruined-fooocus";
947
- }
948
- if (json.includes('"use_stable_diffusion_model"')) {
949
- return "easydiffusion";
950
- }
951
- if (json.includes("civitai:") || json.includes('"resource-stack"')) {
952
- return "civitai";
953
- }
954
- if (json.includes('"v4_prompt"') || json.includes('"noise_schedule"') || json.includes('"uncond_scale"') || json.includes('"Software":"NovelAI"') || json.includes('\\"noise_schedule\\"') || json.includes('\\"v4_prompt\\"')) {
955
- return "novelai";
956
- }
957
- if (json.includes('"Model"') && json.includes('"resolution"')) {
958
- return "hf-space";
959
- }
960
- if (json.includes('"prompt"') && json.includes('"base_model"')) {
961
- return "fooocus";
962
- }
963
- if (json.includes('"prompt"') || json.includes('"nodes"')) {
964
- return "comfyui";
965
- }
966
- return null;
967
- }
968
- function detectFromA1111Format(text) {
969
- if (text.includes("sui_image_params") || text.includes("swarm_version")) {
970
- return "swarmui";
971
- }
972
- const versionMatch = text.match(/Version:\s*([^\s,]+)/);
973
- if (versionMatch) {
974
- const version = versionMatch[1];
975
- if (version === "neo" || version?.startsWith("neo")) {
976
- return "forge-neo";
977
- }
978
- if (version?.startsWith("f") && /^f\d/.test(version)) {
979
- return "forge";
980
- }
981
- if (version === "ComfyUI") {
982
- return "comfyui";
983
- }
984
- }
985
- if (text.includes("App: SD.Next") || text.includes("App:SD.Next")) {
986
- return "sd-next";
987
- }
988
- if (text.includes("Civitai resources:")) {
989
- return "civitai";
990
- }
991
- if (text.includes("Steps:") && text.includes("Sampler:")) {
992
- return "sd-webui";
993
- }
994
- return null;
995
- }
996
-
997
- // src/parsers/easydiffusion.ts
998
- function getValue(json, keyA, keyB) {
999
- return json[keyA] ?? json[keyB];
1000
- }
1001
- function extractModelName(path) {
1002
- if (!path) return void 0;
1003
- const parts = path.replace(/\\/g, "/").split("/");
1004
- return parts[parts.length - 1];
1005
- }
1006
- function parseEasyDiffusion(entries) {
1007
- const entryRecord = buildEntryRecord(entries);
1008
- if (entryRecord.negative_prompt || entryRecord["Negative Prompt"]) {
1009
- return parseFromEntries(entryRecord);
1010
- }
1011
- const jsonText = (entryRecord.parameters?.startsWith("{") ? entryRecord.parameters : void 0) ?? (entryRecord.Comment?.startsWith("{") ? entryRecord.Comment : void 0);
1012
- if (!jsonText) {
1013
- return Result.error({ type: "unsupportedFormat" });
1014
- }
1015
- const parsed = parseJson(jsonText);
1016
- if (!parsed.ok) {
1017
- return Result.error({
1018
- type: "parseError",
1019
- message: "Invalid JSON in Easy Diffusion metadata"
1020
- });
1021
- }
1022
- return parseFromJson(parsed.value);
1023
- }
1024
- function parseFromEntries(entryRecord) {
1025
- const prompt = entryRecord.prompt ?? entryRecord.Prompt ?? "";
1026
- const negativePrompt = entryRecord.negative_prompt ?? entryRecord["Negative Prompt"] ?? entryRecord.negative_prompt ?? "";
1027
- const modelPath = entryRecord.use_stable_diffusion_model ?? entryRecord["Stable Diffusion model"];
1028
- const width = Number(entryRecord.width ?? entryRecord.Width) || 0;
1029
- const height = Number(entryRecord.height ?? entryRecord.Height) || 0;
1030
- const metadata = {
1031
- software: "easydiffusion",
1032
- prompt: prompt.trim(),
1033
- negativePrompt: negativePrompt.trim(),
1034
- width,
1035
- height,
1036
- model: {
1037
- name: extractModelName(modelPath),
1038
- vae: entryRecord.use_vae_model ?? entryRecord["VAE model"]
1039
- },
1040
- sampling: {
1041
- sampler: entryRecord.sampler_name ?? entryRecord.Sampler,
1042
- steps: Number(entryRecord.num_inference_steps ?? entryRecord.Steps) || void 0,
1043
- cfg: Number(entryRecord.guidance_scale ?? entryRecord["Guidance Scale"]) || void 0,
1044
- seed: Number(entryRecord.seed ?? entryRecord.Seed) || void 0,
1045
- clipSkip: Number(entryRecord.clip_skip ?? entryRecord["Clip Skip"]) || void 0
1046
- }
1047
- };
1048
- return Result.ok(metadata);
1049
- }
1050
- function parseFromJson(json) {
1051
- const prompt = getValue(json, "prompt", "Prompt") ?? "";
1052
- const negativePrompt = getValue(json, "negative_prompt", "Negative Prompt") ?? "";
1053
- const modelPath = getValue(
1054
- json,
1055
- "use_stable_diffusion_model",
1056
- "Stable Diffusion model"
1057
- );
1058
- const width = getValue(json, "width", "Width") ?? 0;
1059
- const height = getValue(json, "height", "Height") ?? 0;
1060
- const metadata = {
1061
- software: "easydiffusion",
1062
- prompt: prompt.trim(),
1063
- negativePrompt: negativePrompt.trim(),
1064
- width,
1065
- height,
1066
- model: {
1067
- name: extractModelName(modelPath),
1068
- vae: getValue(json, "use_vae_model", "VAE model")
1069
- },
1070
- sampling: {
1071
- sampler: getValue(json, "sampler_name", "Sampler"),
1072
- steps: getValue(json, "num_inference_steps", "Steps"),
1073
- cfg: getValue(json, "guidance_scale", "Guidance Scale"),
1074
- seed: getValue(json, "seed", "Seed"),
1075
- clipSkip: getValue(json, "clip_skip", "Clip Skip")
1076
- }
1077
- };
1078
- return Result.ok(metadata);
1079
- }
1080
-
1081
- // src/parsers/fooocus.ts
1082
- function parseFooocus(entries) {
1083
- const entryRecord = buildEntryRecord(entries);
1084
- const jsonText = entryRecord.Comment ?? entryRecord.comment;
1085
- if (!jsonText || !jsonText.startsWith("{")) {
1086
- return Result.error({ type: "unsupportedFormat" });
1087
- }
1088
- const parsed = parseJson(jsonText);
1089
- if (!parsed.ok) {
1090
- return Result.error({
1091
- type: "parseError",
1092
- message: "Invalid JSON in Fooocus metadata"
1093
- });
1094
- }
1095
- const json = parsed.value;
1096
- if (!json.base_model && !json.prompt) {
1097
- return Result.error({ type: "unsupportedFormat" });
1098
- }
1099
- const metadata = {
1100
- software: "fooocus",
1101
- prompt: json.prompt?.trim() ?? "",
1102
- negativePrompt: json.negative_prompt?.trim() ?? "",
1103
- width: json.width ?? 0,
1104
- height: json.height ?? 0,
1105
- model: {
1106
- name: json.base_model
1107
- },
1108
- sampling: {
1109
- sampler: json.sampler,
1110
- scheduler: json.scheduler,
1111
- steps: json.steps,
1112
- cfg: json.cfg,
1113
- seed: json.seed
1114
- }
1115
- };
1116
- return Result.ok(metadata);
1117
- }
1118
-
1119
- // src/parsers/hf-space.ts
1120
- function parseHfSpace(entries) {
1121
- const entryRecord = buildEntryRecord(entries);
1122
- const parametersText = entryRecord.parameters;
1123
- if (!parametersText) {
1124
- return Result.error({ type: "unsupportedFormat" });
1125
- }
1126
- const parsed = parseJson(parametersText);
1127
- if (!parsed.ok) {
1128
- return Result.error({
1129
- type: "parseError",
1130
- message: "Invalid JSON in parameters entry"
1131
- });
581
+ const parsed = parseJson(parametersText);
582
+ if (!parsed.ok) {
583
+ return Result.error({
584
+ type: "parseError",
585
+ message: "Invalid JSON in parameters entry"
586
+ });
1132
587
  }
1133
588
  const json = parsed.value;
1134
589
  const parseResolution = (res) => {
@@ -1565,588 +1020,1268 @@ function readUint16(data, offset, isLittleEndian) {
1565
1020
  if (isLittleEndian) {
1566
1021
  return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8;
1567
1022
  }
1568
- return (data[offset] ?? 0) << 8 | (data[offset + 1] ?? 0);
1569
- }
1570
- function readUint32(data, offset, isLittleEndian) {
1571
- if (isLittleEndian) {
1572
- return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8 | (data[offset + 2] ?? 0) << 16 | (data[offset + 3] ?? 0) << 24;
1023
+ return (data[offset] ?? 0) << 8 | (data[offset + 1] ?? 0);
1024
+ }
1025
+ function readUint32(data, offset, isLittleEndian) {
1026
+ if (isLittleEndian) {
1027
+ return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8 | (data[offset + 2] ?? 0) << 16 | (data[offset + 3] ?? 0) << 24;
1028
+ }
1029
+ return (data[offset] ?? 0) << 24 | (data[offset + 1] ?? 0) << 16 | (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
1030
+ }
1031
+ function arraysEqual(a, b) {
1032
+ if (a.length !== b.length) return false;
1033
+ for (let i = 0; i < a.length; i++) {
1034
+ if (a[i] !== b[i]) return false;
1035
+ }
1036
+ return true;
1037
+ }
1038
+ function writeUint16(data, offset, value, isLittleEndian) {
1039
+ if (isLittleEndian) {
1040
+ data[offset] = value & 255;
1041
+ data[offset + 1] = value >>> 8 & 255;
1042
+ } else {
1043
+ data[offset] = value >>> 8 & 255;
1044
+ data[offset + 1] = value & 255;
1045
+ }
1046
+ }
1047
+ function writeUint32(data, offset, value, isLittleEndian) {
1048
+ if (isLittleEndian) {
1049
+ data[offset] = value & 255;
1050
+ data[offset + 1] = value >>> 8 & 255;
1051
+ data[offset + 2] = value >>> 16 & 255;
1052
+ data[offset + 3] = value >>> 24 & 255;
1053
+ } else {
1054
+ data[offset] = value >>> 24 & 255;
1055
+ data[offset + 1] = value >>> 16 & 255;
1056
+ data[offset + 2] = value >>> 8 & 255;
1057
+ data[offset + 3] = value & 255;
1058
+ }
1059
+ }
1060
+ function writeUint32LE(data, offset, value) {
1061
+ data[offset] = value & 255;
1062
+ data[offset + 1] = value >>> 8 & 255;
1063
+ data[offset + 2] = value >>> 16 & 255;
1064
+ data[offset + 3] = value >>> 24 & 255;
1065
+ }
1066
+ function isPng(data) {
1067
+ if (data.length < 8) return false;
1068
+ return data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10;
1069
+ }
1070
+ function isJpeg(data) {
1071
+ if (data.length < 2) return false;
1072
+ return data[0] === 255 && data[1] === 216;
1073
+ }
1074
+ function isWebp(data) {
1075
+ if (data.length < 12) return false;
1076
+ return data[0] === 82 && // R
1077
+ data[1] === 73 && // I
1078
+ data[2] === 70 && // F
1079
+ data[3] === 70 && // F
1080
+ data[8] === 87 && // W
1081
+ data[9] === 69 && // E
1082
+ data[10] === 66 && // B
1083
+ data[11] === 80;
1084
+ }
1085
+ function detectFormat(data) {
1086
+ if (isPng(data)) return "png";
1087
+ if (isJpeg(data)) return "jpeg";
1088
+ if (isWebp(data)) return "webp";
1089
+ return null;
1090
+ }
1091
+
1092
+ // src/utils/exif-constants.ts
1093
+ var USER_COMMENT_TAG = 37510;
1094
+ var IMAGE_DESCRIPTION_TAG = 270;
1095
+ var MAKE_TAG = 271;
1096
+ var EXIF_IFD_POINTER_TAG = 34665;
1097
+
1098
+ // src/readers/exif.ts
1099
+ function parseExifMetadataSegments(exifData) {
1100
+ if (exifData.length < 8) return [];
1101
+ const isLittleEndian = exifData[0] === 73 && exifData[1] === 73;
1102
+ const isBigEndian = exifData[0] === 77 && exifData[1] === 77;
1103
+ if (!isLittleEndian && !isBigEndian) return [];
1104
+ const magic = readUint16(exifData, 2, isLittleEndian);
1105
+ if (magic !== 42) return [];
1106
+ const ifd0Offset = readUint32(exifData, 4, isLittleEndian);
1107
+ const ifd0Segments = extractTagsFromIfd(exifData, ifd0Offset, isLittleEndian);
1108
+ const exifIfdOffset = findExifIfdOffset(exifData, ifd0Offset, isLittleEndian);
1109
+ const exifIfdSegments = exifIfdOffset !== null ? extractTagsFromIfd(exifData, exifIfdOffset, isLittleEndian) : [];
1110
+ return [...ifd0Segments, ...exifIfdSegments];
1111
+ }
1112
+ function extractTagsFromIfd(data, ifdOffset, isLittleEndian) {
1113
+ const segments = [];
1114
+ if (ifdOffset + 2 > data.length) return segments;
1115
+ const entryCount = readUint16(data, ifdOffset, isLittleEndian);
1116
+ let offset = ifdOffset + 2;
1117
+ for (let i = 0; i < entryCount; i++) {
1118
+ if (offset + 12 > data.length) return segments;
1119
+ const tag = readUint16(data, offset, isLittleEndian);
1120
+ const type = readUint16(data, offset + 2, isLittleEndian);
1121
+ const count = readUint32(data, offset + 4, isLittleEndian);
1122
+ const typeSize = getTypeSize(type);
1123
+ const dataSize = count * typeSize;
1124
+ let valueOffset;
1125
+ if (dataSize <= 4) {
1126
+ valueOffset = offset + 8;
1127
+ } else {
1128
+ valueOffset = readUint32(data, offset + 8, isLittleEndian);
1129
+ }
1130
+ if (valueOffset + dataSize > data.length) {
1131
+ offset += 12;
1132
+ continue;
1133
+ }
1134
+ const tagData = data.slice(valueOffset, valueOffset + dataSize);
1135
+ if (tag === IMAGE_DESCRIPTION_TAG) {
1136
+ const text = decodeAsciiString(tagData);
1137
+ if (text) {
1138
+ const prefix = extractPrefix(text);
1139
+ segments.push({
1140
+ source: { type: "exifImageDescription", prefix: prefix ?? void 0 },
1141
+ data: prefix ? text.slice(prefix.length + 2) : text
1142
+ });
1143
+ }
1144
+ } else if (tag === MAKE_TAG) {
1145
+ const text = decodeAsciiString(tagData);
1146
+ if (text) {
1147
+ const prefix = extractPrefix(text);
1148
+ segments.push({
1149
+ source: { type: "exifMake", prefix: prefix ?? void 0 },
1150
+ data: prefix ? text.slice(prefix.length + 2) : text
1151
+ });
1152
+ }
1153
+ } else if (tag === USER_COMMENT_TAG) {
1154
+ const text = decodeUserComment(tagData);
1155
+ if (text) {
1156
+ segments.push({
1157
+ source: { type: "exifUserComment" },
1158
+ data: text
1159
+ });
1160
+ }
1161
+ }
1162
+ offset += 12;
1163
+ }
1164
+ return segments;
1165
+ }
1166
+ function extractPrefix(text) {
1167
+ const match = text.match(/^([A-Za-z]+):\s/);
1168
+ return match?.[1] ?? null;
1169
+ }
1170
+ function getTypeSize(type) {
1171
+ switch (type) {
1172
+ case 1:
1173
+ return 1;
1174
+ // BYTE
1175
+ case 2:
1176
+ return 1;
1177
+ // ASCII
1178
+ case 3:
1179
+ return 2;
1180
+ // SHORT
1181
+ case 4:
1182
+ return 4;
1183
+ // LONG
1184
+ case 5:
1185
+ return 8;
1186
+ // RATIONAL
1187
+ case 7:
1188
+ return 1;
1189
+ // UNDEFINED
1190
+ default:
1191
+ return 1;
1192
+ }
1193
+ }
1194
+ function decodeAsciiString(data) {
1195
+ try {
1196
+ const decoder = new TextDecoder("utf-8", { fatal: false });
1197
+ let text = decoder.decode(data);
1198
+ if (text.endsWith("\0")) {
1199
+ text = text.slice(0, -1);
1200
+ }
1201
+ return text.trim() || null;
1202
+ } catch {
1203
+ return null;
1204
+ }
1205
+ }
1206
+ function findExifIfdOffset(data, ifdOffset, isLittleEndian) {
1207
+ if (ifdOffset + 2 > data.length) return null;
1208
+ const entryCount = readUint16(data, ifdOffset, isLittleEndian);
1209
+ let offset = ifdOffset + 2;
1210
+ for (let i = 0; i < entryCount; i++) {
1211
+ if (offset + 12 > data.length) return null;
1212
+ const tag = readUint16(data, offset, isLittleEndian);
1213
+ if (tag === EXIF_IFD_POINTER_TAG) {
1214
+ return readUint32(data, offset + 8, isLittleEndian);
1215
+ }
1216
+ offset += 12;
1217
+ }
1218
+ return null;
1219
+ }
1220
+ function decodeUserComment(data) {
1221
+ if (data.length < 8) return null;
1222
+ if (data[0] === 85 && // U
1223
+ data[1] === 78 && // N
1224
+ data[2] === 73 && // I
1225
+ data[3] === 67 && // C
1226
+ data[4] === 79 && // O
1227
+ data[5] === 68 && // D
1228
+ data[6] === 69 && // E
1229
+ data[7] === 0) {
1230
+ const textData = data.slice(8);
1231
+ if (textData.length >= 2) {
1232
+ const isLikelyLE = textData[0] !== 0 && textData[1] === 0;
1233
+ return isLikelyLE ? decodeUtf16LE(textData) : decodeUtf16BE(textData);
1234
+ }
1235
+ return decodeUtf16BE(textData);
1236
+ }
1237
+ if (data[0] === 65 && // A
1238
+ data[1] === 83 && // S
1239
+ data[2] === 67 && // C
1240
+ data[3] === 73 && // I
1241
+ data[4] === 73 && // I
1242
+ data[5] === 0 && // NULL
1243
+ data[6] === 0 && // NULL
1244
+ data[7] === 0) {
1245
+ return decodeAscii(data.slice(8));
1246
+ }
1247
+ try {
1248
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1249
+ let result = decoder.decode(data);
1250
+ if (result.endsWith("\0")) {
1251
+ result = result.slice(0, -1);
1252
+ }
1253
+ return result;
1254
+ } catch {
1255
+ return null;
1256
+ }
1257
+ }
1258
+ function decodeUtf16BE(data) {
1259
+ const chars = [];
1260
+ for (let i = 0; i < data.length - 1; i += 2) {
1261
+ const code = (data[i] ?? 0) << 8 | (data[i + 1] ?? 0);
1262
+ if (code === 0) break;
1263
+ chars.push(String.fromCharCode(code));
1264
+ }
1265
+ return chars.join("");
1266
+ }
1267
+ function decodeUtf16LE(data) {
1268
+ const chars = [];
1269
+ for (let i = 0; i < data.length - 1; i += 2) {
1270
+ const code = (data[i] ?? 0) | (data[i + 1] ?? 0) << 8;
1271
+ if (code === 0) break;
1272
+ chars.push(String.fromCharCode(code));
1273
+ }
1274
+ return chars.join("");
1275
+ }
1276
+ function decodeAscii(data) {
1277
+ const chars = [];
1278
+ for (let i = 0; i < data.length; i++) {
1279
+ if (data[i] === 0) break;
1280
+ chars.push(String.fromCharCode(data[i] ?? 0));
1281
+ }
1282
+ return chars.join("");
1283
+ }
1284
+
1285
+ // src/readers/jpeg.ts
1286
+ var APP1_MARKER = 225;
1287
+ var COM_MARKER = 254;
1288
+ var EXIF_HEADER = new Uint8Array([69, 120, 105, 102, 0, 0]);
1289
+ function readJpegMetadata(data) {
1290
+ if (!isJpeg(data)) {
1291
+ return Result.error({ type: "invalidSignature" });
1292
+ }
1293
+ const segments = [];
1294
+ const app1 = findApp1Segment(data);
1295
+ if (app1) {
1296
+ const exifData = data.slice(app1.offset, app1.offset + app1.length);
1297
+ const exifSegments = parseExifMetadataSegments(exifData);
1298
+ segments.push(...exifSegments);
1299
+ }
1300
+ const comSegment = findComSegment(data);
1301
+ if (comSegment) {
1302
+ const comData = data.slice(
1303
+ comSegment.offset,
1304
+ comSegment.offset + comSegment.length
1305
+ );
1306
+ const comText = decodeComSegment(comData);
1307
+ if (comText !== null) {
1308
+ segments.push({
1309
+ source: { type: "jpegCom" },
1310
+ data: comText
1311
+ });
1312
+ }
1573
1313
  }
1574
- return (data[offset] ?? 0) << 24 | (data[offset + 1] ?? 0) << 16 | (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
1314
+ return Result.ok(segments);
1575
1315
  }
1576
- function arraysEqual(a, b) {
1577
- if (a.length !== b.length) return false;
1578
- for (let i = 0; i < a.length; i++) {
1579
- if (a[i] !== b[i]) return false;
1316
+ function findApp1Segment(data) {
1317
+ let offset = 2;
1318
+ while (offset < data.length - 4) {
1319
+ if (data[offset] !== 255) {
1320
+ offset++;
1321
+ continue;
1322
+ }
1323
+ const marker = data[offset + 1];
1324
+ if (marker === 255) {
1325
+ offset++;
1326
+ continue;
1327
+ }
1328
+ const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
1329
+ if (marker === APP1_MARKER) {
1330
+ const headerStart = offset + 4;
1331
+ if (headerStart + 6 <= data.length) {
1332
+ const header = data.slice(headerStart, headerStart + 6);
1333
+ if (arraysEqual(header, EXIF_HEADER)) {
1334
+ return {
1335
+ offset: headerStart + 6,
1336
+ length: length - 8
1337
+ // Subtract length bytes and Exif header
1338
+ };
1339
+ }
1340
+ }
1341
+ }
1342
+ offset += 2 + length;
1343
+ if (marker === 218 || marker === 217) {
1344
+ break;
1345
+ }
1580
1346
  }
1581
- return true;
1347
+ return null;
1582
1348
  }
1583
- function writeUint16(data, offset, value, isLittleEndian) {
1584
- if (isLittleEndian) {
1585
- data[offset] = value & 255;
1586
- data[offset + 1] = value >>> 8 & 255;
1587
- } else {
1588
- data[offset] = value >>> 8 & 255;
1589
- data[offset + 1] = value & 255;
1349
+ function findComSegment(data) {
1350
+ let offset = 2;
1351
+ while (offset < data.length - 4) {
1352
+ if (data[offset] !== 255) {
1353
+ offset++;
1354
+ continue;
1355
+ }
1356
+ const marker = data[offset + 1];
1357
+ if (marker === 255) {
1358
+ offset++;
1359
+ continue;
1360
+ }
1361
+ const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
1362
+ if (marker === COM_MARKER) {
1363
+ return {
1364
+ offset: offset + 4,
1365
+ length: length - 2
1366
+ // Subtract length bytes only
1367
+ };
1368
+ }
1369
+ offset += 2 + length;
1370
+ if (marker === 218 || marker === 217) {
1371
+ break;
1372
+ }
1590
1373
  }
1374
+ return null;
1591
1375
  }
1592
- function writeUint32(data, offset, value, isLittleEndian) {
1593
- if (isLittleEndian) {
1594
- data[offset] = value & 255;
1595
- data[offset + 1] = value >>> 8 & 255;
1596
- data[offset + 2] = value >>> 16 & 255;
1597
- data[offset + 3] = value >>> 24 & 255;
1598
- } else {
1599
- data[offset] = value >>> 24 & 255;
1600
- data[offset + 1] = value >>> 16 & 255;
1601
- data[offset + 2] = value >>> 8 & 255;
1602
- data[offset + 3] = value & 255;
1376
+ function decodeComSegment(data) {
1377
+ try {
1378
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1379
+ return decoder.decode(data);
1380
+ } catch {
1381
+ return null;
1603
1382
  }
1604
1383
  }
1605
- function writeUint32LE(data, offset, value) {
1606
- data[offset] = value & 255;
1607
- data[offset + 1] = value >>> 8 & 255;
1608
- data[offset + 2] = value >>> 16 & 255;
1609
- data[offset + 3] = value >>> 24 & 255;
1610
- }
1611
- function isPng(data) {
1612
- if (data.length < 8) return false;
1613
- return data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10;
1614
- }
1615
- function isJpeg(data) {
1616
- if (data.length < 2) return false;
1617
- return data[0] === 255 && data[1] === 216;
1618
- }
1619
- function isWebp(data) {
1620
- if (data.length < 12) return false;
1621
- return data[0] === 82 && // R
1622
- data[1] === 73 && // I
1623
- data[2] === 70 && // F
1624
- data[3] === 70 && // F
1625
- data[8] === 87 && // W
1626
- data[9] === 69 && // E
1627
- data[10] === 66 && // B
1628
- data[11] === 80;
1629
- }
1630
- function detectFormat(data) {
1631
- if (isPng(data)) return "png";
1632
- if (isJpeg(data)) return "jpeg";
1633
- if (isWebp(data)) return "webp";
1634
- return null;
1635
- }
1636
-
1637
- // src/utils/exif-constants.ts
1638
- var USER_COMMENT_TAG = 37510;
1639
- var IMAGE_DESCRIPTION_TAG = 270;
1640
- var MAKE_TAG = 271;
1641
- var EXIF_IFD_POINTER_TAG = 34665;
1642
1384
 
1643
- // src/readers/exif.ts
1644
- function parseExifMetadataSegments(exifData) {
1645
- if (exifData.length < 8) return [];
1646
- const isLittleEndian = exifData[0] === 73 && exifData[1] === 73;
1647
- const isBigEndian = exifData[0] === 77 && exifData[1] === 77;
1648
- if (!isLittleEndian && !isBigEndian) return [];
1649
- const magic = readUint16(exifData, 2, isLittleEndian);
1650
- if (magic !== 42) return [];
1651
- const ifd0Offset = readUint32(exifData, 4, isLittleEndian);
1652
- const ifd0Segments = extractTagsFromIfd(exifData, ifd0Offset, isLittleEndian);
1653
- const exifIfdOffset = findExifIfdOffset(exifData, ifd0Offset, isLittleEndian);
1654
- const exifIfdSegments = exifIfdOffset !== null ? extractTagsFromIfd(exifData, exifIfdOffset, isLittleEndian) : [];
1655
- return [...ifd0Segments, ...exifIfdSegments];
1385
+ // src/readers/png.ts
1386
+ function readPngMetadata(data) {
1387
+ if (!isPng(data)) {
1388
+ return Result.error({ type: "invalidSignature" });
1389
+ }
1390
+ const chunksResult = extractTextChunks(data);
1391
+ if (!chunksResult.ok) {
1392
+ return chunksResult;
1393
+ }
1394
+ return Result.ok(chunksResult.value);
1656
1395
  }
1657
- function extractTagsFromIfd(data, ifdOffset, isLittleEndian) {
1658
- const segments = [];
1659
- if (ifdOffset + 2 > data.length) return segments;
1660
- const entryCount = readUint16(data, ifdOffset, isLittleEndian);
1661
- let offset = ifdOffset + 2;
1662
- for (let i = 0; i < entryCount; i++) {
1663
- if (offset + 12 > data.length) return segments;
1664
- const tag = readUint16(data, offset, isLittleEndian);
1665
- const type = readUint16(data, offset + 2, isLittleEndian);
1666
- const count = readUint32(data, offset + 4, isLittleEndian);
1667
- const typeSize = getTypeSize(type);
1668
- const dataSize = count * typeSize;
1669
- let valueOffset;
1670
- if (dataSize <= 4) {
1671
- valueOffset = offset + 8;
1672
- } else {
1673
- valueOffset = readUint32(data, offset + 8, isLittleEndian);
1396
+ var PNG_SIGNATURE_LENGTH = 8;
1397
+ function extractTextChunks(data) {
1398
+ const chunks = [];
1399
+ let offset = PNG_SIGNATURE_LENGTH;
1400
+ while (offset < data.length) {
1401
+ if (offset + 4 > data.length) {
1402
+ return Result.error({
1403
+ type: "corruptedChunk",
1404
+ message: "Unexpected end of file while reading chunk length"
1405
+ });
1674
1406
  }
1675
- if (valueOffset + dataSize > data.length) {
1676
- offset += 12;
1677
- continue;
1407
+ const length = readUint32BE(data, offset);
1408
+ offset += 4;
1409
+ if (offset + 4 > data.length) {
1410
+ return Result.error({
1411
+ type: "corruptedChunk",
1412
+ message: "Unexpected end of file while reading chunk type"
1413
+ });
1678
1414
  }
1679
- const tagData = data.slice(valueOffset, valueOffset + dataSize);
1680
- if (tag === IMAGE_DESCRIPTION_TAG) {
1681
- const text = decodeAsciiString(tagData);
1682
- if (text) {
1683
- const prefix = extractPrefix(text);
1684
- segments.push({
1685
- source: { type: "exifImageDescription", prefix: prefix ?? void 0 },
1686
- data: prefix ? text.slice(prefix.length + 2) : text
1687
- });
1688
- }
1689
- } else if (tag === MAKE_TAG) {
1690
- const text = decodeAsciiString(tagData);
1691
- if (text) {
1692
- const prefix = extractPrefix(text);
1693
- segments.push({
1694
- source: { type: "exifMake", prefix: prefix ?? void 0 },
1695
- data: prefix ? text.slice(prefix.length + 2) : text
1696
- });
1415
+ const chunkType = readChunkType(data, offset);
1416
+ offset += 4;
1417
+ if (offset + length > data.length) {
1418
+ return Result.error({
1419
+ type: "corruptedChunk",
1420
+ message: `Unexpected end of file while reading chunk data (${chunkType})`
1421
+ });
1422
+ }
1423
+ const chunkData = data.slice(offset, offset + length);
1424
+ offset += length;
1425
+ offset += 4;
1426
+ if (chunkType === "tEXt") {
1427
+ const parsed = parseTExtChunk(chunkData);
1428
+ if (parsed) {
1429
+ chunks.push(parsed);
1697
1430
  }
1698
- } else if (tag === USER_COMMENT_TAG) {
1699
- const text = decodeUserComment(tagData);
1700
- if (text) {
1701
- segments.push({
1702
- source: { type: "exifUserComment" },
1703
- data: text
1704
- });
1431
+ } else if (chunkType === "iTXt") {
1432
+ const parsed = parseITXtChunk(chunkData);
1433
+ if (parsed) {
1434
+ chunks.push(parsed);
1705
1435
  }
1706
1436
  }
1707
- offset += 12;
1708
- }
1709
- return segments;
1710
- }
1711
- function extractPrefix(text) {
1712
- const match = text.match(/^([A-Za-z]+):\s/);
1713
- return match?.[1] ?? null;
1437
+ if (chunkType === "IEND") {
1438
+ break;
1439
+ }
1440
+ }
1441
+ return Result.ok(chunks);
1714
1442
  }
1715
- function getTypeSize(type) {
1716
- switch (type) {
1717
- case 1:
1718
- return 1;
1719
- // BYTE
1720
- case 2:
1721
- return 1;
1722
- // ASCII
1723
- case 3:
1724
- return 2;
1725
- // SHORT
1726
- case 4:
1727
- return 4;
1728
- // LONG
1729
- case 5:
1730
- return 8;
1731
- // RATIONAL
1732
- case 7:
1733
- return 1;
1734
- // UNDEFINED
1735
- default:
1736
- return 1;
1443
+ function parseTExtChunk(data) {
1444
+ const nullIndex = data.indexOf(0);
1445
+ if (nullIndex === -1) {
1446
+ return null;
1737
1447
  }
1448
+ const keyword = latin1Decode(data.slice(0, nullIndex));
1449
+ const textData = data.slice(nullIndex + 1);
1450
+ const text = tryUtf8Decode(textData) ?? latin1Decode(textData);
1451
+ return { type: "tEXt", keyword, text };
1738
1452
  }
1739
- function decodeAsciiString(data) {
1453
+ function tryUtf8Decode(data) {
1740
1454
  try {
1741
- const decoder = new TextDecoder("utf-8", { fatal: false });
1742
- let text = decoder.decode(data);
1743
- if (text.endsWith("\0")) {
1744
- text = text.slice(0, -1);
1745
- }
1746
- return text.trim() || null;
1455
+ return new TextDecoder("utf-8", { fatal: true }).decode(data);
1747
1456
  } catch {
1748
1457
  return null;
1749
1458
  }
1750
1459
  }
1751
- function findExifIfdOffset(data, ifdOffset, isLittleEndian) {
1752
- if (ifdOffset + 2 > data.length) return null;
1753
- const entryCount = readUint16(data, ifdOffset, isLittleEndian);
1754
- let offset = ifdOffset + 2;
1755
- for (let i = 0; i < entryCount; i++) {
1756
- if (offset + 12 > data.length) return null;
1757
- const tag = readUint16(data, offset, isLittleEndian);
1758
- if (tag === EXIF_IFD_POINTER_TAG) {
1759
- return readUint32(data, offset + 8, isLittleEndian);
1460
+ function parseITXtChunk(data) {
1461
+ let offset = 0;
1462
+ const keywordEnd = findNull(data, offset);
1463
+ if (keywordEnd === -1) return null;
1464
+ const keyword = utf8Decode(data.slice(offset, keywordEnd));
1465
+ offset = keywordEnd + 1;
1466
+ if (offset >= data.length) return null;
1467
+ const compressionFlag = data[offset] ?? 0;
1468
+ offset += 1;
1469
+ if (offset >= data.length) return null;
1470
+ const compressionMethod = data[offset] ?? 0;
1471
+ offset += 1;
1472
+ const langEnd = findNull(data, offset);
1473
+ if (langEnd === -1) return null;
1474
+ const languageTag = utf8Decode(data.slice(offset, langEnd));
1475
+ offset = langEnd + 1;
1476
+ const transEnd = findNull(data, offset);
1477
+ if (transEnd === -1) return null;
1478
+ const translatedKeyword = utf8Decode(data.slice(offset, transEnd));
1479
+ offset = transEnd + 1;
1480
+ let text;
1481
+ if (compressionFlag === 1) {
1482
+ const decompressed = decompressZlib(data.slice(offset));
1483
+ if (!decompressed) return null;
1484
+ text = utf8Decode(decompressed);
1485
+ } else {
1486
+ text = utf8Decode(data.slice(offset));
1487
+ }
1488
+ return {
1489
+ type: "iTXt",
1490
+ keyword,
1491
+ compressionFlag,
1492
+ compressionMethod,
1493
+ languageTag,
1494
+ translatedKeyword,
1495
+ text
1496
+ };
1497
+ }
1498
+ function findNull(data, offset) {
1499
+ for (let i = offset; i < data.length; i++) {
1500
+ if (data[i] === 0) {
1501
+ return i;
1760
1502
  }
1761
- offset += 12;
1762
1503
  }
1504
+ return -1;
1505
+ }
1506
+ function latin1Decode(data) {
1507
+ let result = "";
1508
+ for (let i = 0; i < data.length; i++) {
1509
+ result += String.fromCharCode(data[i] ?? 0);
1510
+ }
1511
+ return result;
1512
+ }
1513
+ function utf8Decode(data) {
1514
+ return new TextDecoder("utf-8").decode(data);
1515
+ }
1516
+ function decompressZlib(_data) {
1763
1517
  return null;
1764
1518
  }
1765
- function decodeUserComment(data) {
1766
- if (data.length < 8) return null;
1767
- if (data[0] === 85 && // U
1768
- data[1] === 78 && // N
1769
- data[2] === 73 && // I
1770
- data[3] === 67 && // C
1771
- data[4] === 79 && // O
1772
- data[5] === 68 && // D
1773
- data[6] === 69 && // E
1774
- data[7] === 0) {
1775
- const textData = data.slice(8);
1776
- if (textData.length >= 2) {
1777
- const isLikelyLE = textData[0] !== 0 && textData[1] === 0;
1778
- return isLikelyLE ? decodeUtf16LE(textData) : decodeUtf16BE(textData);
1779
- }
1780
- return decodeUtf16BE(textData);
1519
+
1520
+ // src/readers/webp.ts
1521
+ var EXIF_CHUNK_TYPE = new Uint8Array([69, 88, 73, 70]);
1522
+ function readWebpMetadata(data) {
1523
+ if (!isWebp(data)) {
1524
+ return Result.error({ type: "invalidSignature" });
1781
1525
  }
1782
- if (data[0] === 65 && // A
1783
- data[1] === 83 && // S
1784
- data[2] === 67 && // C
1785
- data[3] === 73 && // I
1786
- data[4] === 73 && // I
1787
- data[5] === 0 && // NULL
1788
- data[6] === 0 && // NULL
1789
- data[7] === 0) {
1790
- return decodeAscii(data.slice(8));
1526
+ const exifChunk = findExifChunk(data);
1527
+ if (!exifChunk) {
1528
+ return Result.ok([]);
1791
1529
  }
1792
- try {
1793
- const decoder = new TextDecoder("utf-8", { fatal: true });
1794
- let result = decoder.decode(data);
1795
- if (result.endsWith("\0")) {
1796
- result = result.slice(0, -1);
1530
+ const exifData = data.slice(
1531
+ exifChunk.offset,
1532
+ exifChunk.offset + exifChunk.length
1533
+ );
1534
+ const segments = parseExifMetadataSegments(exifData);
1535
+ return Result.ok(segments);
1536
+ }
1537
+ function findExifChunk(data) {
1538
+ let offset = 12;
1539
+ while (offset < data.length - 8) {
1540
+ const chunkType = data.slice(offset, offset + 4);
1541
+ const chunkSize = readUint32LE(data, offset + 4);
1542
+ if (arraysEqual(chunkType, EXIF_CHUNK_TYPE)) {
1543
+ return {
1544
+ offset: offset + 8,
1545
+ length: chunkSize
1546
+ };
1797
1547
  }
1798
- return result;
1799
- } catch {
1800
- return null;
1548
+ const paddedSize = chunkSize + chunkSize % 2;
1549
+ offset += 8 + paddedSize;
1801
1550
  }
1551
+ return null;
1802
1552
  }
1803
- function decodeUtf16BE(data) {
1804
- const chars = [];
1805
- for (let i = 0; i < data.length - 1; i += 2) {
1806
- const code = (data[i] ?? 0) << 8 | (data[i + 1] ?? 0);
1807
- if (code === 0) break;
1808
- chars.push(String.fromCharCode(code));
1553
+
1554
+ // src/utils/convert.ts
1555
+ function pngChunksToEntries(chunks) {
1556
+ return chunks.map((chunk) => ({
1557
+ keyword: chunk.keyword,
1558
+ text: chunk.text
1559
+ }));
1560
+ }
1561
+ function segmentsToEntries(segments) {
1562
+ const entries = [];
1563
+ for (const segment of segments) {
1564
+ const keyword = sourceToKeyword(segment.source);
1565
+ const text = segment.data;
1566
+ if (segment.source.type === "exifUserComment" && text.startsWith("{")) {
1567
+ const expanded = tryExpandNovelAIWebpFormat(text);
1568
+ if (expanded) {
1569
+ entries.push(...expanded);
1570
+ continue;
1571
+ }
1572
+ }
1573
+ entries.push({ keyword, text });
1809
1574
  }
1810
- return chars.join("");
1575
+ return entries;
1811
1576
  }
1812
- function decodeUtf16LE(data) {
1813
- const chars = [];
1814
- for (let i = 0; i < data.length - 1; i += 2) {
1815
- const code = (data[i] ?? 0) | (data[i + 1] ?? 0) << 8;
1816
- if (code === 0) break;
1817
- chars.push(String.fromCharCode(code));
1577
+ function tryExpandNovelAIWebpFormat(text) {
1578
+ const outerParsed = parseJson(text);
1579
+ if (!outerParsed.ok) {
1580
+ return null;
1818
1581
  }
1819
- return chars.join("");
1582
+ const outer = outerParsed.value;
1583
+ if (typeof outer !== "object" || outer === null || outer.Software !== "NovelAI" || typeof outer.Comment !== "string") {
1584
+ return null;
1585
+ }
1586
+ const entries = [{ keyword: "Software", text: "NovelAI" }];
1587
+ const innerParsed = parseJson(outer.Comment);
1588
+ return [
1589
+ ...entries,
1590
+ innerParsed.ok ? { keyword: "Comment", text: JSON.stringify(innerParsed.value) } : { keyword: "Comment", text: outer.Comment }
1591
+ ];
1820
1592
  }
1821
- function decodeAscii(data) {
1822
- const chars = [];
1823
- for (let i = 0; i < data.length; i++) {
1824
- if (data[i] === 0) break;
1825
- chars.push(String.fromCharCode(data[i] ?? 0));
1593
+ function sourceToKeyword(source) {
1594
+ switch (source.type) {
1595
+ case "jpegCom":
1596
+ return "Comment";
1597
+ case "exifUserComment":
1598
+ return "Comment";
1599
+ case "exifImageDescription":
1600
+ return source.prefix ?? "Description";
1601
+ case "exifMake":
1602
+ return source.prefix ?? "Make";
1603
+ }
1604
+ }
1605
+
1606
+ // src/api/read.ts
1607
+ function read(data) {
1608
+ const format = detectFormat(data);
1609
+ if (!format) {
1610
+ return { status: "invalid", message: "Unknown image format" };
1611
+ }
1612
+ const rawResult = readRawMetadata(data, format);
1613
+ if (rawResult.status !== "success") {
1614
+ return rawResult;
1615
+ }
1616
+ const raw = rawResult.raw;
1617
+ const entries = raw.format === "png" ? pngChunksToEntries(raw.chunks) : segmentsToEntries(raw.segments);
1618
+ const parseResult = parseMetadata(entries);
1619
+ if (!parseResult.ok) {
1620
+ return { status: "unrecognized", raw };
1621
+ }
1622
+ const metadata = parseResult.value;
1623
+ if (metadata.width === 0 || metadata.height === 0) {
1624
+ const dims = HELPERS[format].readDimensions(data);
1625
+ if (dims) {
1626
+ metadata.width = metadata.width || dims.width;
1627
+ metadata.height = metadata.height || dims.height;
1628
+ }
1826
1629
  }
1827
- return chars.join("");
1630
+ return { status: "success", metadata, raw };
1828
1631
  }
1829
-
1830
- // src/readers/jpeg.ts
1831
- var APP1_MARKER = 225;
1832
- var COM_MARKER = 254;
1833
- var EXIF_HEADER = new Uint8Array([69, 120, 105, 102, 0, 0]);
1834
- function readJpegMetadata(data) {
1835
- if (!isJpeg(data)) {
1836
- return Result.error({ type: "invalidSignature" });
1632
+ var HELPERS = {
1633
+ png: {
1634
+ readMetadata: readPngMetadata,
1635
+ readDimensions: readPngDimensions,
1636
+ createRaw: (chunks) => ({ format: "png", chunks })
1637
+ },
1638
+ jpeg: {
1639
+ readMetadata: readJpegMetadata,
1640
+ readDimensions: readJpegDimensions,
1641
+ createRaw: (segments) => ({
1642
+ format: "jpeg",
1643
+ segments
1644
+ })
1645
+ },
1646
+ webp: {
1647
+ readMetadata: readWebpMetadata,
1648
+ readDimensions: readWebpDimensions,
1649
+ createRaw: (segments) => ({
1650
+ format: "webp",
1651
+ segments
1652
+ })
1837
1653
  }
1838
- const segments = [];
1839
- const app1 = findApp1Segment(data);
1840
- if (app1) {
1841
- const exifData = data.slice(app1.offset, app1.offset + app1.length);
1842
- const exifSegments = parseExifMetadataSegments(exifData);
1843
- segments.push(...exifSegments);
1654
+ };
1655
+ function readRawMetadata(data, format) {
1656
+ const result = HELPERS[format].readMetadata(data);
1657
+ if (!result.ok) {
1658
+ const message = result.error.type === "invalidSignature" ? `Invalid ${format.toUpperCase()} signature` : result.error.message;
1659
+ return { status: "invalid", message };
1844
1660
  }
1845
- const comSegment = findComSegment(data);
1846
- if (comSegment) {
1847
- const comData = data.slice(
1848
- comSegment.offset,
1849
- comSegment.offset + comSegment.length
1850
- );
1851
- const comText = decodeComSegment(comData);
1852
- if (comText !== null) {
1853
- segments.push({
1854
- source: { type: "jpegCom" },
1855
- data: comText
1856
- });
1857
- }
1661
+ if (result.value.length === 0) return { status: "empty" };
1662
+ if (format === "png") {
1663
+ return {
1664
+ status: "success",
1665
+ raw: HELPERS.png.createRaw(result.value)
1666
+ };
1858
1667
  }
1859
- return Result.ok(segments);
1668
+ return {
1669
+ status: "success",
1670
+ raw: HELPERS[format].createRaw(result.value)
1671
+ };
1860
1672
  }
1861
- function findApp1Segment(data) {
1673
+ function readPngDimensions(data) {
1674
+ const PNG_SIGNATURE_LENGTH2 = 8;
1675
+ if (data.length < 24) return null;
1676
+ return {
1677
+ width: readUint32BE(data, PNG_SIGNATURE_LENGTH2 + 8),
1678
+ height: readUint32BE(data, PNG_SIGNATURE_LENGTH2 + 12)
1679
+ };
1680
+ }
1681
+ function readJpegDimensions(data) {
1862
1682
  let offset = 2;
1863
1683
  while (offset < data.length - 4) {
1864
1684
  if (data[offset] !== 255) {
1865
1685
  offset++;
1866
1686
  continue;
1867
1687
  }
1868
- const marker = data[offset + 1];
1688
+ const marker = data[offset + 1] ?? 0;
1869
1689
  if (marker === 255) {
1870
1690
  offset++;
1871
1691
  continue;
1872
1692
  }
1873
1693
  const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
1874
- if (marker === APP1_MARKER) {
1875
- const headerStart = offset + 4;
1876
- if (headerStart + 6 <= data.length) {
1877
- const header = data.slice(headerStart, headerStart + 6);
1878
- if (arraysEqual(header, EXIF_HEADER)) {
1879
- return {
1880
- offset: headerStart + 6,
1881
- length: length - 8
1882
- // Subtract length bytes and Exif header
1883
- };
1694
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
1695
+ const height = (data[offset + 5] ?? 0) << 8 | (data[offset + 6] ?? 0);
1696
+ const width = (data[offset + 7] ?? 0) << 8 | (data[offset + 8] ?? 0);
1697
+ return { width, height };
1698
+ }
1699
+ offset += 2 + length;
1700
+ if (marker === 218) break;
1701
+ }
1702
+ return null;
1703
+ }
1704
+ function readWebpDimensions(data) {
1705
+ let offset = 12;
1706
+ while (offset < data.length) {
1707
+ if (offset + 8 > data.length) break;
1708
+ const chunkType = readChunkType(data, offset);
1709
+ const chunkSize = readUint32LE(data, offset + 4);
1710
+ const paddedSize = chunkSize + chunkSize % 2;
1711
+ if (chunkType === "VP8X") {
1712
+ const wMinus1 = readUint24LE(data, offset + 12);
1713
+ const hMinus1 = readUint24LE(data, offset + 15);
1714
+ return { width: wMinus1 + 1, height: hMinus1 + 1 };
1715
+ }
1716
+ if (chunkType === "VP8 ") {
1717
+ const start = offset + 8;
1718
+ const tag = (data[start] ?? 0) | (data[start + 1] ?? 0) << 8 | (data[start + 2] ?? 0) << 16;
1719
+ const keyFrame = !(tag & 1);
1720
+ if (keyFrame) {
1721
+ if (data[start + 3] === 157 && data[start + 4] === 1 && data[start + 5] === 42) {
1722
+ const wRaw = (data[start + 6] ?? 0) | (data[start + 7] ?? 0) << 8;
1723
+ const hRaw = (data[start + 8] ?? 0) | (data[start + 9] ?? 0) << 8;
1724
+ return { width: wRaw & 16383, height: hRaw & 16383 };
1884
1725
  }
1885
1726
  }
1886
1727
  }
1887
- offset += 2 + length;
1888
- if (marker === 218 || marker === 217) {
1889
- break;
1728
+ if (chunkType === "VP8L") {
1729
+ if (data[offset + 8] === 47) {
1730
+ const bits = readUint32LE(data, offset + 9);
1731
+ const width = (bits & 16383) + 1;
1732
+ const height = (bits >> 14 & 16383) + 1;
1733
+ return { width, height };
1734
+ }
1890
1735
  }
1736
+ offset += 8 + paddedSize;
1891
1737
  }
1892
1738
  return null;
1893
1739
  }
1894
- function findComSegment(data) {
1895
- let offset = 2;
1896
- while (offset < data.length - 4) {
1897
- if (data[offset] !== 255) {
1898
- offset++;
1899
- continue;
1740
+
1741
+ // src/converters/utils.ts
1742
+ var createTextChunk = (keyword, text) => text !== void 0 ? [{ type: "tEXt", keyword, text }] : [];
1743
+ var createITxtChunk = (keyword, text) => text !== void 0 ? [
1744
+ {
1745
+ type: "iTXt",
1746
+ keyword,
1747
+ compressionFlag: 0,
1748
+ compressionMethod: 0,
1749
+ languageTag: "",
1750
+ translatedKeyword: "",
1751
+ text
1752
+ }
1753
+ ] : [];
1754
+ var findSegment = (segments, type) => segments.find((s) => s.source.type === type);
1755
+ var stringify = (value) => {
1756
+ if (value === void 0) return void 0;
1757
+ return typeof value === "string" ? value : JSON.stringify(value);
1758
+ };
1759
+
1760
+ // src/converters/chunk-encoding.ts
1761
+ var CHUNK_ENCODING_STRATEGIES = {
1762
+ // Dynamic selection tools
1763
+ a1111: "dynamic",
1764
+ forge: "dynamic",
1765
+ "forge-neo": "dynamic",
1766
+ "sd-webui": "dynamic",
1767
+ invokeai: "dynamic",
1768
+ novelai: "dynamic",
1769
+ "sd-next": "dynamic",
1770
+ easydiffusion: "dynamic",
1771
+ blind: "dynamic",
1772
+ // Unicode escape tools (spec-compliant)
1773
+ comfyui: "text-unicode-escape",
1774
+ swarmui: "text-unicode-escape",
1775
+ fooocus: "text-unicode-escape",
1776
+ "ruined-fooocus": "text-unicode-escape",
1777
+ "hf-space": "text-unicode-escape",
1778
+ // Raw UTF-8 tools (non-compliant but compatible)
1779
+ "stability-matrix": "text-utf8-raw",
1780
+ tensorart: "text-utf8-raw"
1781
+ };
1782
+ function getEncodingStrategy(tool) {
1783
+ return CHUNK_ENCODING_STRATEGIES[tool] ?? "text-unicode-escape";
1784
+ }
1785
+ function escapeUnicode(text) {
1786
+ return text.replace(/[\u0100-\uffff]/g, (char) => {
1787
+ const code = char.charCodeAt(0).toString(16).padStart(4, "0");
1788
+ return `\\u${code}`;
1789
+ });
1790
+ }
1791
+ function hasNonLatin1(text) {
1792
+ return /[^\x00-\xFF]/.test(text);
1793
+ }
1794
+ function createEncodedChunk(keyword, text, strategy) {
1795
+ if (text === void 0) return [];
1796
+ switch (strategy) {
1797
+ case "dynamic": {
1798
+ const chunkType = hasNonLatin1(text) ? "iTXt" : "tEXt";
1799
+ return chunkType === "iTXt" ? createITxtChunk(keyword, text) : createTextChunk(keyword, text);
1900
1800
  }
1901
- const marker = data[offset + 1];
1902
- if (marker === 255) {
1903
- offset++;
1904
- continue;
1801
+ case "text-unicode-escape": {
1802
+ const escaped = escapeUnicode(text);
1803
+ return createTextChunk(keyword, escaped);
1905
1804
  }
1906
- const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
1907
- if (marker === COM_MARKER) {
1908
- return {
1909
- offset: offset + 4,
1910
- length: length - 2
1911
- // Subtract length bytes only
1912
- };
1805
+ case "text-utf8-raw": {
1806
+ return createTextChunk(keyword, text);
1913
1807
  }
1914
- offset += 2 + length;
1915
- if (marker === 218 || marker === 217) {
1916
- break;
1808
+ }
1809
+ }
1810
+
1811
+ // src/converters/a1111.ts
1812
+ function convertA1111PngToSegments(chunks) {
1813
+ const parameters = chunks.find((c) => c.keyword === "parameters");
1814
+ if (!parameters) {
1815
+ return [];
1816
+ }
1817
+ return [
1818
+ {
1819
+ source: { type: "exifUserComment" },
1820
+ data: parameters.text
1821
+ }
1822
+ ];
1823
+ }
1824
+ function convertA1111SegmentsToPng(segments) {
1825
+ const userComment = segments.find((s) => s.source.type === "exifUserComment");
1826
+ if (!userComment) {
1827
+ return [];
1828
+ }
1829
+ return createEncodedChunk(
1830
+ "parameters",
1831
+ userComment.data,
1832
+ getEncodingStrategy("a1111")
1833
+ );
1834
+ }
1835
+
1836
+ // src/converters/blind.ts
1837
+ function blindPngToSegments(chunks) {
1838
+ if (chunks.length === 0) return [];
1839
+ const chunkMap = Object.fromEntries(
1840
+ chunks.map((chunk) => [chunk.keyword, chunk.text])
1841
+ );
1842
+ return [
1843
+ {
1844
+ source: { type: "exifUserComment" },
1845
+ data: JSON.stringify(chunkMap)
1917
1846
  }
1918
- }
1919
- return null;
1847
+ ];
1920
1848
  }
1921
- function decodeComSegment(data) {
1922
- try {
1923
- const decoder = new TextDecoder("utf-8", { fatal: true });
1924
- return decoder.decode(data);
1925
- } catch {
1926
- return null;
1849
+ function blindSegmentsToPng(segments) {
1850
+ const userComment = segments.find((s) => s.source.type === "exifUserComment");
1851
+ if (!userComment) return [];
1852
+ const parsed = parseJson(userComment.data);
1853
+ if (parsed.ok) {
1854
+ return Object.entries(parsed.value).flatMap(([keyword, value]) => {
1855
+ const text = typeof value === "string" ? value : JSON.stringify(value);
1856
+ if (!text) return [];
1857
+ return createEncodedChunk(keyword, text, getEncodingStrategy("blind"));
1858
+ });
1927
1859
  }
1860
+ return createEncodedChunk(
1861
+ "metadata",
1862
+ userComment.data,
1863
+ getEncodingStrategy("blind")
1864
+ );
1928
1865
  }
1929
1866
 
1930
- // src/readers/png.ts
1931
- function readPngMetadata(data) {
1932
- if (!isPng(data)) {
1933
- return Result.error({ type: "invalidSignature" });
1934
- }
1935
- const chunksResult = extractTextChunks(data);
1936
- if (!chunksResult.ok) {
1937
- return chunksResult;
1938
- }
1939
- return Result.ok(chunksResult.value);
1940
- }
1941
- var PNG_SIGNATURE_LENGTH = 8;
1942
- function extractTextChunks(data) {
1943
- const chunks = [];
1944
- let offset = PNG_SIGNATURE_LENGTH;
1945
- while (offset < data.length) {
1946
- if (offset + 4 > data.length) {
1947
- return Result.error({
1948
- type: "corruptedChunk",
1949
- message: "Unexpected end of file while reading chunk length"
1950
- });
1951
- }
1952
- const length = readUint32BE(data, offset);
1953
- offset += 4;
1954
- if (offset + 4 > data.length) {
1955
- return Result.error({
1956
- type: "corruptedChunk",
1957
- message: "Unexpected end of file while reading chunk type"
1958
- });
1959
- }
1960
- const chunkType = readChunkType(data, offset);
1961
- offset += 4;
1962
- if (offset + length > data.length) {
1963
- return Result.error({
1964
- type: "corruptedChunk",
1965
- message: `Unexpected end of file while reading chunk data (${chunkType})`
1966
- });
1967
- }
1968
- const chunkData = data.slice(offset, offset + length);
1969
- offset += length;
1970
- offset += 4;
1971
- if (chunkType === "tEXt") {
1972
- const parsed = parseTExtChunk(chunkData);
1973
- if (parsed) {
1974
- chunks.push(parsed);
1975
- }
1976
- } else if (chunkType === "iTXt") {
1977
- const parsed = parseITXtChunk(chunkData);
1978
- if (parsed) {
1979
- chunks.push(parsed);
1980
- }
1981
- }
1982
- if (chunkType === "IEND") {
1983
- break;
1867
+ // src/converters/comfyui.ts
1868
+ function convertComfyUIPngToSegments(chunks) {
1869
+ const data = {};
1870
+ for (const chunk of chunks) {
1871
+ const parsed = parseJson(chunk.text);
1872
+ if (parsed.ok) {
1873
+ data[chunk.keyword] = parsed.value;
1874
+ } else {
1875
+ data[chunk.keyword] = chunk.text;
1984
1876
  }
1985
1877
  }
1986
- return Result.ok(chunks);
1878
+ return [
1879
+ {
1880
+ source: { type: "exifUserComment" },
1881
+ data: JSON.stringify(data)
1882
+ }
1883
+ ];
1987
1884
  }
1988
- function parseTExtChunk(data) {
1989
- const nullIndex = data.indexOf(0);
1990
- if (nullIndex === -1) {
1885
+ var tryParseExtendedFormat = (segments) => {
1886
+ const imageDescription = findSegment(segments, "exifImageDescription");
1887
+ const make = findSegment(segments, "exifMake");
1888
+ if (!imageDescription && !make) {
1991
1889
  return null;
1992
1890
  }
1993
- const keyword = latin1Decode(data.slice(0, nullIndex));
1994
- const textData = data.slice(nullIndex + 1);
1995
- const text = tryUtf8Decode(textData) ?? latin1Decode(textData);
1996
- return { type: "tEXt", keyword, text };
1997
- }
1998
- function tryUtf8Decode(data) {
1999
- try {
2000
- return new TextDecoder("utf-8", { fatal: true }).decode(data);
2001
- } catch {
1891
+ return [
1892
+ ...createEncodedChunk("prompt", make?.data, getEncodingStrategy("comfyui")),
1893
+ ...createEncodedChunk(
1894
+ "workflow",
1895
+ imageDescription?.data,
1896
+ getEncodingStrategy("comfyui")
1897
+ )
1898
+ ];
1899
+ };
1900
+ var tryParseSaveImagePlusFormat = (segments) => {
1901
+ const userComment = findSegment(segments, "exifUserComment");
1902
+ if (!userComment) {
2002
1903
  return null;
2003
1904
  }
1905
+ const parsed = parseJson(userComment.data);
1906
+ if (!parsed.ok) {
1907
+ return createEncodedChunk(
1908
+ "prompt",
1909
+ userComment.data,
1910
+ getEncodingStrategy("comfyui")
1911
+ );
1912
+ }
1913
+ return Object.entries(parsed.value).flatMap(
1914
+ ([keyword, value]) => createEncodedChunk(
1915
+ keyword,
1916
+ stringify(value),
1917
+ getEncodingStrategy("comfyui")
1918
+ )
1919
+ );
1920
+ };
1921
+ function convertComfyUISegmentsToPng(segments) {
1922
+ return tryParseExtendedFormat(segments) ?? tryParseSaveImagePlusFormat(segments) ?? [];
2004
1923
  }
2005
- function parseITXtChunk(data) {
2006
- let offset = 0;
2007
- const keywordEnd = findNull(data, offset);
2008
- if (keywordEnd === -1) return null;
2009
- const keyword = utf8Decode(data.slice(offset, keywordEnd));
2010
- offset = keywordEnd + 1;
2011
- if (offset >= data.length) return null;
2012
- const compressionFlag = data[offset] ?? 0;
2013
- offset += 1;
2014
- if (offset >= data.length) return null;
2015
- const compressionMethod = data[offset] ?? 0;
2016
- offset += 1;
2017
- const langEnd = findNull(data, offset);
2018
- if (langEnd === -1) return null;
2019
- const languageTag = utf8Decode(data.slice(offset, langEnd));
2020
- offset = langEnd + 1;
2021
- const transEnd = findNull(data, offset);
2022
- if (transEnd === -1) return null;
2023
- const translatedKeyword = utf8Decode(data.slice(offset, transEnd));
2024
- offset = transEnd + 1;
2025
- let text;
2026
- if (compressionFlag === 1) {
2027
- const decompressed = decompressZlib(data.slice(offset));
2028
- if (!decompressed) return null;
2029
- text = utf8Decode(decompressed);
2030
- } else {
2031
- text = utf8Decode(data.slice(offset));
1924
+
1925
+ // src/converters/easydiffusion.ts
1926
+ function convertEasyDiffusionPngToSegments(chunks) {
1927
+ const json = Object.fromEntries(
1928
+ chunks.map((chunk) => [chunk.keyword, chunk.text])
1929
+ );
1930
+ return [
1931
+ {
1932
+ source: { type: "exifUserComment" },
1933
+ data: JSON.stringify(json)
1934
+ }
1935
+ ];
1936
+ }
1937
+ function convertEasyDiffusionSegmentsToPng(segments) {
1938
+ const userComment = findSegment(segments, "exifUserComment");
1939
+ if (!userComment) {
1940
+ return [];
2032
1941
  }
2033
- return {
2034
- type: "iTXt",
2035
- keyword,
2036
- compressionFlag,
2037
- compressionMethod,
2038
- languageTag,
2039
- translatedKeyword,
2040
- text
2041
- };
1942
+ const parsed = parseJson(userComment.data);
1943
+ if (!parsed.ok) {
1944
+ return [];
1945
+ }
1946
+ return Object.entries(parsed.value).flatMap(([keyword, value]) => {
1947
+ const text = value != null ? typeof value === "string" ? value : String(value) : void 0;
1948
+ if (!text) return [];
1949
+ return createEncodedChunk(
1950
+ keyword,
1951
+ text,
1952
+ getEncodingStrategy("easydiffusion")
1953
+ );
1954
+ });
2042
1955
  }
2043
- function findNull(data, offset) {
2044
- for (let i = offset; i < data.length; i++) {
2045
- if (data[i] === 0) {
2046
- return i;
1956
+
1957
+ // src/converters/invokeai.ts
1958
+ function convertInvokeAIPngToSegments(chunks) {
1959
+ const data = {};
1960
+ for (const chunk of chunks) {
1961
+ const parsed = parseJson(chunk.text);
1962
+ if (parsed.ok) {
1963
+ data[chunk.keyword] = parsed.value;
1964
+ } else {
1965
+ data[chunk.keyword] = chunk.text;
1966
+ }
1967
+ }
1968
+ return [
1969
+ {
1970
+ source: { type: "exifUserComment" },
1971
+ data: JSON.stringify(data)
2047
1972
  }
1973
+ ];
1974
+ }
1975
+ function convertInvokeAISegmentsToPng(segments) {
1976
+ const userComment = findSegment(segments, "exifUserComment");
1977
+ if (!userComment) {
1978
+ return [];
1979
+ }
1980
+ const parsed = parseJson(userComment.data);
1981
+ if (!parsed.ok) {
1982
+ return createEncodedChunk(
1983
+ "invokeai_metadata",
1984
+ userComment.data,
1985
+ getEncodingStrategy("invokeai")
1986
+ );
1987
+ }
1988
+ const metadataText = stringify(parsed.value.invokeai_metadata);
1989
+ const graphText = stringify(parsed.value.invokeai_graph);
1990
+ const chunks = [
1991
+ ...createEncodedChunk(
1992
+ "invokeai_metadata",
1993
+ metadataText,
1994
+ getEncodingStrategy("invokeai")
1995
+ ),
1996
+ ...createEncodedChunk(
1997
+ "invokeai_graph",
1998
+ graphText,
1999
+ getEncodingStrategy("invokeai")
2000
+ )
2001
+ ];
2002
+ if (chunks.length > 0) {
2003
+ return chunks;
2048
2004
  }
2049
- return -1;
2005
+ return createEncodedChunk(
2006
+ "invokeai_metadata",
2007
+ userComment.data,
2008
+ getEncodingStrategy("invokeai")
2009
+ );
2050
2010
  }
2051
- function latin1Decode(data) {
2052
- let result = "";
2053
- for (let i = 0; i < data.length; i++) {
2054
- result += String.fromCharCode(data[i] ?? 0);
2011
+
2012
+ // src/converters/novelai.ts
2013
+ var NOVELAI_TITLE = "NovelAI generated image";
2014
+ var NOVELAI_SOFTWARE = "NovelAI";
2015
+ function convertNovelaiPngToSegments(chunks) {
2016
+ const comment = chunks.find((c) => c.keyword === "Comment");
2017
+ if (!comment) {
2018
+ return [];
2055
2019
  }
2056
- return result;
2020
+ const description = chunks.find((c) => c.keyword === "Description");
2021
+ const data = buildUserCommentJson(chunks);
2022
+ const descriptionSegment = description ? [
2023
+ {
2024
+ source: { type: "exifImageDescription" },
2025
+ data: `\0\0\0\0${description.text}`
2026
+ }
2027
+ ] : [];
2028
+ const userCommentSegment = {
2029
+ source: { type: "exifUserComment" },
2030
+ data: JSON.stringify(data)
2031
+ };
2032
+ return [...descriptionSegment, userCommentSegment];
2057
2033
  }
2058
- function utf8Decode(data) {
2059
- return new TextDecoder("utf-8").decode(data);
2034
+ function buildUserCommentJson(chunks) {
2035
+ return NOVELAI_KEY_ORDER.map((key) => {
2036
+ const chunk = chunks.find((c) => c.keyword === key);
2037
+ return chunk ? { [key]: chunk.text } : null;
2038
+ }).filter((entry) => entry !== null).reduce(
2039
+ (acc, entry) => Object.assign(acc, entry),
2040
+ {}
2041
+ );
2060
2042
  }
2061
- function decompressZlib(_data) {
2062
- return null;
2043
+ var NOVELAI_KEY_ORDER = [
2044
+ "Comment",
2045
+ "Description",
2046
+ "Generation time",
2047
+ "Software",
2048
+ "Source",
2049
+ "Title"
2050
+ ];
2051
+ function convertNovelaiSegmentsToPng(segments) {
2052
+ const userCommentSeg = findSegment(segments, "exifUserComment");
2053
+ const descriptionSeg = findSegment(segments, "exifImageDescription");
2054
+ return parseSegments(userCommentSeg, descriptionSeg);
2063
2055
  }
2064
-
2065
- // src/readers/webp.ts
2066
- var EXIF_CHUNK_TYPE = new Uint8Array([69, 88, 73, 70]);
2067
- function readWebpMetadata(data) {
2068
- if (!isWebp(data)) {
2069
- return Result.error({ type: "invalidSignature" });
2056
+ function parseSegments(userCommentSeg, descriptionSeg) {
2057
+ if (!userCommentSeg || !descriptionSeg) {
2058
+ return [];
2070
2059
  }
2071
- const exifChunk = findExifChunk(data);
2072
- if (!exifChunk) {
2073
- return Result.ok([]);
2060
+ const parsed = parseJson(userCommentSeg.data);
2061
+ if (!parsed.ok) {
2062
+ return createTextChunk("Comment", userCommentSeg.data);
2074
2063
  }
2075
- const exifData = data.slice(
2076
- exifChunk.offset,
2077
- exifChunk.offset + exifChunk.length
2064
+ const jsonData = parsed.value;
2065
+ const descriptionText = extractDescriptionText(
2066
+ descriptionSeg,
2067
+ stringify(jsonData.Description)
2078
2068
  );
2079
- const segments = parseExifMetadataSegments(exifData);
2080
- return Result.ok(segments);
2069
+ const descriptionChunks = descriptionText ? createEncodedChunk(
2070
+ "Description",
2071
+ descriptionText,
2072
+ getEncodingStrategy("novelai")
2073
+ ) : [];
2074
+ return [
2075
+ // Title (required, use default if missing)
2076
+ createTextChunk("Title", stringify(jsonData.Title) ?? NOVELAI_TITLE),
2077
+ // Description (optional, prefer exifImageDescription over JSON)
2078
+ ...descriptionChunks,
2079
+ // Software (required, use default if missing)
2080
+ createTextChunk(
2081
+ "Software",
2082
+ stringify(jsonData.Software) ?? NOVELAI_SOFTWARE
2083
+ ),
2084
+ // Source (optional)
2085
+ createTextChunk("Source", stringify(jsonData.Source)),
2086
+ // Generation time (optional)
2087
+ createTextChunk("Generation time", stringify(jsonData["Generation time"])),
2088
+ // Comment (optional)
2089
+ createTextChunk("Comment", stringify(jsonData.Comment))
2090
+ ].flat();
2081
2091
  }
2082
- function findExifChunk(data) {
2083
- let offset = 12;
2084
- while (offset < data.length - 8) {
2085
- const chunkType = data.slice(offset, offset + 4);
2086
- const chunkSize = readUint32LE(data, offset + 4);
2087
- if (arraysEqual(chunkType, EXIF_CHUNK_TYPE)) {
2088
- return {
2089
- offset: offset + 8,
2090
- length: chunkSize
2091
- };
2092
- }
2093
- const paddedSize = chunkSize + chunkSize % 2;
2094
- offset += 8 + paddedSize;
2092
+ function extractDescriptionText(descriptionSeg, jsonDescription) {
2093
+ if (descriptionSeg?.data) {
2094
+ const data = descriptionSeg.data;
2095
+ return data.startsWith("\0\0\0\0") ? data.slice(4) : data;
2095
2096
  }
2096
- return null;
2097
+ if (jsonDescription) {
2098
+ return jsonDescription.startsWith("\0\0\0\0") ? jsonDescription.slice(4) : jsonDescription;
2099
+ }
2100
+ return void 0;
2097
2101
  }
2098
2102
 
2099
- // src/utils/convert.ts
2100
- function pngChunksToEntries(chunks) {
2101
- return chunks.map((chunk) => ({
2102
- keyword: chunk.keyword,
2103
- text: chunk.text
2104
- }));
2103
+ // src/converters/simple-chunk.ts
2104
+ function createPngToSegments(keyword) {
2105
+ return (chunks) => {
2106
+ const chunk = chunks.find((c) => c.keyword === keyword);
2107
+ return !chunk ? [] : [{ source: { type: "exifUserComment" }, data: chunk.text }];
2108
+ };
2105
2109
  }
2106
- function segmentsToEntries(segments) {
2107
- const entries = [];
2108
- for (const segment of segments) {
2109
- const keyword = sourceToKeyword(segment.source);
2110
- const text = segment.data;
2111
- if (segment.source.type === "exifUserComment" && text.startsWith("{")) {
2112
- const expanded = tryExpandNovelAIWebpFormat(text);
2113
- if (expanded) {
2114
- entries.push(...expanded);
2115
- continue;
2116
- }
2110
+ function createSegmentsToPng(keyword) {
2111
+ return (segments) => {
2112
+ const userComment = segments.find(
2113
+ (s) => s.source.type === "exifUserComment"
2114
+ );
2115
+ if (!userComment) return [];
2116
+ return createEncodedChunk(
2117
+ keyword,
2118
+ userComment.data,
2119
+ getEncodingStrategy(keyword)
2120
+ );
2121
+ };
2122
+ }
2123
+
2124
+ // src/converters/swarmui.ts
2125
+ function convertSwarmUIPngToSegments(chunks) {
2126
+ const parametersChunk = chunks.find((c) => c.keyword === "parameters");
2127
+ if (!parametersChunk) {
2128
+ return [];
2129
+ }
2130
+ const parsed = parseJson(parametersChunk.text);
2131
+ const data = parsed.ok ? parsed.value : parametersChunk.text;
2132
+ const segments = [
2133
+ {
2134
+ source: { type: "exifUserComment" },
2135
+ data: typeof data === "string" ? data : JSON.stringify(data)
2117
2136
  }
2118
- entries.push({ keyword, text });
2137
+ ];
2138
+ const promptChunk = chunks.find((c) => c.keyword === "prompt");
2139
+ if (promptChunk) {
2140
+ segments.push({
2141
+ source: { type: "exifMake" },
2142
+ data: promptChunk.text
2143
+ });
2119
2144
  }
2120
- return entries;
2145
+ return segments;
2121
2146
  }
2122
- function tryExpandNovelAIWebpFormat(text) {
2123
- const outerParsed = parseJson(text);
2124
- if (!outerParsed.ok) {
2125
- return null;
2147
+ function convertSwarmUISegmentsToPng(segments) {
2148
+ const userComment = findSegment(segments, "exifUserComment");
2149
+ if (!userComment) {
2150
+ return [];
2126
2151
  }
2127
- const outer = outerParsed.value;
2128
- if (typeof outer !== "object" || outer === null || outer.Software !== "NovelAI" || typeof outer.Comment !== "string") {
2129
- return null;
2152
+ const chunks = [];
2153
+ const make = findSegment(segments, "exifMake");
2154
+ if (make) {
2155
+ chunks.push(
2156
+ ...createEncodedChunk(
2157
+ "prompt",
2158
+ make.data,
2159
+ getEncodingStrategy("swarmui")
2160
+ )
2161
+ );
2130
2162
  }
2131
- const entries = [{ keyword: "Software", text: "NovelAI" }];
2132
- const innerParsed = parseJson(outer.Comment);
2133
- return [
2134
- ...entries,
2135
- innerParsed.ok ? { keyword: "Comment", text: JSON.stringify(innerParsed.value) } : { keyword: "Comment", text: outer.Comment }
2136
- ];
2163
+ chunks.push(
2164
+ ...createEncodedChunk(
2165
+ "parameters",
2166
+ userComment.data,
2167
+ getEncodingStrategy("swarmui")
2168
+ )
2169
+ );
2170
+ return chunks;
2137
2171
  }
2138
- function sourceToKeyword(source) {
2139
- switch (source.type) {
2140
- case "jpegCom":
2141
- return "Comment";
2142
- case "exifUserComment":
2143
- return "Comment";
2144
- case "exifImageDescription":
2145
- return source.prefix ?? "Description";
2146
- case "exifMake":
2147
- return source.prefix ?? "Make";
2172
+
2173
+ // src/converters/index.ts
2174
+ function convertMetadata(parseResult, targetFormat, force = false) {
2175
+ if (parseResult.status === "empty") {
2176
+ return Result.error({ type: "missingRawData" });
2177
+ }
2178
+ if (parseResult.status === "invalid") {
2179
+ return Result.error({
2180
+ type: "invalidParseResult",
2181
+ status: parseResult.status
2182
+ });
2183
+ }
2184
+ const raw = parseResult.raw;
2185
+ if (raw.format === "png" && targetFormat === "png" || raw.format === "jpeg" && targetFormat === "jpeg" || raw.format === "webp" && targetFormat === "webp") {
2186
+ return Result.ok(raw);
2187
+ }
2188
+ const software = parseResult.status === "success" ? parseResult.metadata.software : null;
2189
+ if (!software) {
2190
+ return force ? convertBlind(raw, targetFormat) : Result.error({
2191
+ type: "unsupportedSoftware",
2192
+ software: "unknown"
2193
+ });
2194
+ }
2195
+ const converter = softwareConverters[software];
2196
+ if (!converter) {
2197
+ return Result.error({
2198
+ type: "unsupportedSoftware",
2199
+ software
2200
+ });
2148
2201
  }
2202
+ return converter(raw, targetFormat);
2203
+ }
2204
+ function createFormatConverter(pngToSegments, segmentsToPng) {
2205
+ return (raw, targetFormat) => {
2206
+ if (raw.format === "png") {
2207
+ if (targetFormat === "png") {
2208
+ return Result.ok(raw);
2209
+ }
2210
+ const segments = pngToSegments(raw.chunks);
2211
+ return Result.ok({ format: targetFormat, segments });
2212
+ }
2213
+ if (targetFormat === "jpeg" || targetFormat === "webp") {
2214
+ return Result.ok({ format: targetFormat, segments: raw.segments });
2215
+ }
2216
+ const chunks = segmentsToPng(raw.segments);
2217
+ return Result.ok({ format: "png", chunks });
2218
+ };
2149
2219
  }
2220
+ var convertNovelai = createFormatConverter(
2221
+ convertNovelaiPngToSegments,
2222
+ convertNovelaiSegmentsToPng
2223
+ );
2224
+ var convertA1111 = createFormatConverter(
2225
+ convertA1111PngToSegments,
2226
+ convertA1111SegmentsToPng
2227
+ );
2228
+ var convertComfyUI = createFormatConverter(
2229
+ convertComfyUIPngToSegments,
2230
+ convertComfyUISegmentsToPng
2231
+ );
2232
+ var convertEasyDiffusion = createFormatConverter(
2233
+ convertEasyDiffusionPngToSegments,
2234
+ convertEasyDiffusionSegmentsToPng
2235
+ );
2236
+ var convertFooocus = createFormatConverter(
2237
+ createPngToSegments("Comment"),
2238
+ createSegmentsToPng("Comment")
2239
+ );
2240
+ var convertRuinedFooocus = createFormatConverter(
2241
+ createPngToSegments("parameters"),
2242
+ createSegmentsToPng("parameters")
2243
+ );
2244
+ var convertSwarmUI = createFormatConverter(
2245
+ convertSwarmUIPngToSegments,
2246
+ convertSwarmUISegmentsToPng
2247
+ );
2248
+ var convertInvokeAI = createFormatConverter(
2249
+ convertInvokeAIPngToSegments,
2250
+ convertInvokeAISegmentsToPng
2251
+ );
2252
+ var convertHfSpace = createFormatConverter(
2253
+ createPngToSegments("parameters"),
2254
+ createSegmentsToPng("parameters")
2255
+ );
2256
+ var convertBlind = createFormatConverter(
2257
+ blindPngToSegments,
2258
+ blindSegmentsToPng
2259
+ );
2260
+ var softwareConverters = {
2261
+ // NovelAI
2262
+ novelai: convertNovelai,
2263
+ // A1111-format (sd-webui, forge, forge-neo, civitai, sd-next)
2264
+ "sd-webui": convertA1111,
2265
+ "sd-next": convertA1111,
2266
+ forge: convertA1111,
2267
+ "forge-neo": convertA1111,
2268
+ civitai: convertA1111,
2269
+ // ComfyUI-format (comfyui, tensorart, stability-matrix)
2270
+ comfyui: convertComfyUI,
2271
+ tensorart: convertComfyUI,
2272
+ "stability-matrix": convertComfyUI,
2273
+ // Easy Diffusion
2274
+ easydiffusion: convertEasyDiffusion,
2275
+ // Fooocus variants
2276
+ fooocus: convertFooocus,
2277
+ "ruined-fooocus": convertRuinedFooocus,
2278
+ // SwarmUI
2279
+ swarmui: convertSwarmUI,
2280
+ // InvokeAI
2281
+ invokeai: convertInvokeAI,
2282
+ // HuggingFace Space
2283
+ "hf-space": convertHfSpace
2284
+ };
2150
2285
 
2151
2286
  // src/writers/exif.ts
2152
2287
  function buildExifTiffData(segments) {
@@ -2674,65 +2809,14 @@ function buildExifChunk(segments) {
2674
2809
  return chunk;
2675
2810
  }
2676
2811
 
2677
- // src/index.ts
2678
- var HELPERS = {
2679
- png: {
2680
- readMetadata: readPngMetadata,
2681
- readDimensions: readPngDimensions,
2682
- writeEmpty: writePngMetadata,
2683
- createRaw: (chunks) => ({ format: "png", chunks })
2684
- },
2685
- jpeg: {
2686
- readMetadata: readJpegMetadata,
2687
- readDimensions: readJpegDimensions,
2688
- writeEmpty: writeJpegMetadata,
2689
- createRaw: (segments) => ({
2690
- format: "jpeg",
2691
- segments
2692
- })
2693
- },
2694
- webp: {
2695
- readMetadata: readWebpMetadata,
2696
- readDimensions: readWebpDimensions,
2697
- writeEmpty: writeWebpMetadata,
2698
- createRaw: (segments) => ({
2699
- format: "webp",
2700
- segments
2701
- })
2702
- }
2703
- };
2704
- function read(data) {
2705
- const format = detectFormat(data);
2706
- if (!format) {
2707
- return { status: "invalid", message: "Unknown image format" };
2708
- }
2709
- const rawResult = readRawMetadata(data, format);
2710
- if (rawResult.status !== "success") {
2711
- return rawResult;
2712
- }
2713
- const raw = rawResult.raw;
2714
- const entries = raw.format === "png" ? pngChunksToEntries(raw.chunks) : segmentsToEntries(raw.segments);
2715
- const parseResult = parseMetadata(entries);
2716
- if (!parseResult.ok) {
2717
- return { status: "unrecognized", raw };
2718
- }
2719
- const metadata = parseResult.value;
2720
- if (metadata.width === 0 || metadata.height === 0) {
2721
- const dims = HELPERS[format].readDimensions(data);
2722
- if (dims) {
2723
- metadata.width = metadata.width || dims.width;
2724
- metadata.height = metadata.height || dims.height;
2725
- }
2726
- }
2727
- return { status: "success", metadata, raw };
2728
- }
2812
+ // src/api/write.ts
2729
2813
  function write(data, metadata, options) {
2730
2814
  const targetFormat = detectFormat(data);
2731
2815
  if (!targetFormat) {
2732
2816
  return Result.error({ type: "unsupportedFormat" });
2733
2817
  }
2734
2818
  if (metadata.status === "empty") {
2735
- const result = HELPERS[targetFormat].writeEmpty(data, []);
2819
+ const result = HELPERS2[targetFormat].writeEmpty(data, []);
2736
2820
  if (!result.ok) {
2737
2821
  return Result.error({ type: "writeFailed", message: result.error.type });
2738
2822
  }
@@ -2779,93 +2863,171 @@ function write(data, metadata, options) {
2779
2863
  message: "Internal error: format mismatch after conversion"
2780
2864
  });
2781
2865
  }
2782
- function readRawMetadata(data, format) {
2783
- const result = HELPERS[format].readMetadata(data);
2784
- if (!result.ok) {
2785
- const message = result.error.type === "invalidSignature" ? `Invalid ${format.toUpperCase()} signature` : result.error.message;
2786
- return { status: "invalid", message };
2866
+ var HELPERS2 = {
2867
+ png: {
2868
+ writeEmpty: writePngMetadata
2869
+ },
2870
+ jpeg: {
2871
+ writeEmpty: writeJpegMetadata
2872
+ },
2873
+ webp: {
2874
+ writeEmpty: writeWebpMetadata
2787
2875
  }
2788
- if (result.value.length === 0) return { status: "empty" };
2789
- if (format === "png") {
2876
+ };
2877
+
2878
+ // src/serializers/a1111.ts
2879
+ function normalizeLineEndings(text) {
2880
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2881
+ }
2882
+ function mergeUpscaleHires(hires, upscale) {
2883
+ if (hires) {
2884
+ return hires;
2885
+ }
2886
+ if (upscale) {
2790
2887
  return {
2791
- status: "success",
2792
- raw: HELPERS.png.createRaw(result.value)
2888
+ scale: upscale.scale,
2889
+ upscaler: upscale.upscaler
2890
+ // steps and denoise are not available from upscale
2793
2891
  };
2794
2892
  }
2795
- return {
2796
- status: "success",
2797
- raw: HELPERS[format].createRaw(result.value)
2798
- };
2799
- }
2800
- function readPngDimensions(data) {
2801
- const PNG_SIGNATURE_LENGTH2 = 8;
2802
- if (data.length < 24) return null;
2803
- return {
2804
- width: readUint32BE(data, PNG_SIGNATURE_LENGTH2 + 8),
2805
- height: readUint32BE(data, PNG_SIGNATURE_LENGTH2 + 12)
2806
- };
2893
+ return void 0;
2807
2894
  }
2808
- function readJpegDimensions(data) {
2809
- let offset = 2;
2810
- while (offset < data.length - 4) {
2811
- if (data[offset] !== 255) {
2812
- offset++;
2813
- continue;
2895
+ function buildSettingsLine(metadata) {
2896
+ const parts = [];
2897
+ if (metadata.sampling?.steps !== void 0) {
2898
+ parts.push(`Steps: ${metadata.sampling.steps}`);
2899
+ }
2900
+ if (metadata.sampling?.sampler) {
2901
+ parts.push(`Sampler: ${metadata.sampling.sampler}`);
2902
+ }
2903
+ if (metadata.sampling?.scheduler) {
2904
+ parts.push(`Schedule type: ${metadata.sampling.scheduler}`);
2905
+ }
2906
+ if (metadata.sampling?.cfg !== void 0) {
2907
+ parts.push(`CFG scale: ${metadata.sampling.cfg}`);
2908
+ }
2909
+ if (metadata.sampling?.seed !== void 0) {
2910
+ parts.push(`Seed: ${metadata.sampling.seed}`);
2911
+ }
2912
+ if (metadata.width > 0 && metadata.height > 0) {
2913
+ parts.push(`Size: ${metadata.width}x${metadata.height}`);
2914
+ }
2915
+ if (metadata.model?.hash) {
2916
+ parts.push(`Model hash: ${metadata.model.hash}`);
2917
+ }
2918
+ if (metadata.model?.name) {
2919
+ parts.push(`Model: ${metadata.model.name}`);
2920
+ }
2921
+ if (metadata.sampling?.clipSkip !== void 0) {
2922
+ parts.push(`Clip skip: ${metadata.sampling.clipSkip}`);
2923
+ }
2924
+ const mergedHires = mergeUpscaleHires(metadata.hires, metadata.upscale);
2925
+ if (mergedHires) {
2926
+ if (mergedHires.denoise !== void 0) {
2927
+ parts.push(`Denoising strength: ${mergedHires.denoise}`);
2814
2928
  }
2815
- const marker = data[offset + 1] ?? 0;
2816
- if (marker === 255) {
2817
- offset++;
2818
- continue;
2929
+ if (mergedHires.scale !== void 0) {
2930
+ parts.push(`Hires upscale: ${mergedHires.scale}`);
2819
2931
  }
2820
- const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
2821
- if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
2822
- const height = (data[offset + 5] ?? 0) << 8 | (data[offset + 6] ?? 0);
2823
- const width = (data[offset + 7] ?? 0) << 8 | (data[offset + 8] ?? 0);
2824
- return { width, height };
2932
+ if (mergedHires.steps !== void 0) {
2933
+ parts.push(`Hires steps: ${mergedHires.steps}`);
2934
+ }
2935
+ if (mergedHires.upscaler) {
2936
+ parts.push(`Hires upscaler: ${mergedHires.upscaler}`);
2825
2937
  }
2826
- offset += 2 + length;
2827
- if (marker === 218) break;
2828
2938
  }
2829
- return null;
2939
+ return parts.join(", ");
2830
2940
  }
2831
- function readWebpDimensions(data) {
2832
- let offset = 12;
2833
- while (offset < data.length) {
2834
- if (offset + 8 > data.length) break;
2835
- const chunkType = readChunkType(data, offset);
2836
- const chunkSize = readUint32LE(data, offset + 4);
2837
- const paddedSize = chunkSize + chunkSize % 2;
2838
- if (chunkType === "VP8X") {
2839
- const wMinus1 = readUint24LE(data, offset + 12);
2840
- const hMinus1 = readUint24LE(data, offset + 15);
2841
- return { width: wMinus1 + 1, height: hMinus1 + 1 };
2842
- }
2843
- if (chunkType === "VP8 ") {
2844
- const start = offset + 8;
2845
- const tag = (data[start] ?? 0) | (data[start + 1] ?? 0) << 8 | (data[start + 2] ?? 0) << 16;
2846
- const keyFrame = !(tag & 1);
2847
- if (keyFrame) {
2848
- if (data[start + 3] === 157 && data[start + 4] === 1 && data[start + 5] === 42) {
2849
- const wRaw = (data[start + 6] ?? 0) | (data[start + 7] ?? 0) << 8;
2850
- const hRaw = (data[start + 8] ?? 0) | (data[start + 9] ?? 0) << 8;
2851
- return { width: wRaw & 16383, height: hRaw & 16383 };
2852
- }
2853
- }
2941
+ function buildCharacterPromptsSection(metadata) {
2942
+ if (!metadata.characterPrompts || metadata.characterPrompts.length === 0) {
2943
+ return [];
2944
+ }
2945
+ const lines = [];
2946
+ for (const [index, cp] of metadata.characterPrompts.entries()) {
2947
+ const characterNum = index + 1;
2948
+ const coords = cp.center ? ` [${cp.center.x}, ${cp.center.y}]` : "";
2949
+ lines.push(`# Character ${characterNum}${coords}:`);
2950
+ lines.push(normalizeLineEndings(cp.prompt));
2951
+ }
2952
+ return lines;
2953
+ }
2954
+ function formatAsWebUI(metadata) {
2955
+ const sections = [];
2956
+ sections.push(normalizeLineEndings(metadata.prompt));
2957
+ if (metadata.software === "novelai") {
2958
+ const characterLines = buildCharacterPromptsSection(metadata);
2959
+ if (characterLines.length > 0) {
2960
+ sections.push(characterLines.join("\n"));
2854
2961
  }
2855
- if (chunkType === "VP8L") {
2856
- if (data[offset + 8] === 47) {
2857
- const bits = readUint32LE(data, offset + 9);
2858
- const width = (bits & 16383) + 1;
2859
- const height = (bits >> 14 & 16383) + 1;
2860
- return { width, height };
2861
- }
2962
+ }
2963
+ if (metadata.negativePrompt) {
2964
+ sections.push(
2965
+ `Negative prompt: ${normalizeLineEndings(metadata.negativePrompt)}`
2966
+ );
2967
+ }
2968
+ const settingsLine = buildSettingsLine(metadata);
2969
+ if (settingsLine) {
2970
+ sections.push(settingsLine);
2971
+ }
2972
+ return sections.join("\n");
2973
+ }
2974
+
2975
+ // src/api/write-webui.ts
2976
+ function writeAsWebUI(data, metadata) {
2977
+ const format = detectFormat(data);
2978
+ if (!format) {
2979
+ return Result.error({ type: "unsupportedFormat" });
2980
+ }
2981
+ const text = formatAsWebUI(metadata);
2982
+ let writeResult;
2983
+ if (format === "png") {
2984
+ const chunks = createPngChunks(text);
2985
+ writeResult = writePngMetadata(data, chunks);
2986
+ } else if (format === "jpeg") {
2987
+ const segments = createExifSegments(text);
2988
+ writeResult = writeJpegMetadata(data, segments);
2989
+ } else if (format === "webp") {
2990
+ const segments = createExifSegments(text);
2991
+ writeResult = writeWebpMetadata(data, segments);
2992
+ } else {
2993
+ return Result.error({ type: "unsupportedFormat" });
2994
+ }
2995
+ if (!writeResult.ok) {
2996
+ return Result.error({
2997
+ type: "writeFailed",
2998
+ message: writeResult.error.type
2999
+ });
3000
+ }
3001
+ return Result.ok(writeResult.value);
3002
+ }
3003
+ function createPngChunks(text) {
3004
+ const strategy = getEncodingStrategy("a1111");
3005
+ return createEncodedChunk("parameters", text, strategy);
3006
+ }
3007
+ function createExifSegments(text) {
3008
+ return [
3009
+ {
3010
+ source: { type: "exifUserComment" },
3011
+ data: text
2862
3012
  }
2863
- offset += 8 + paddedSize;
3013
+ ];
3014
+ }
3015
+
3016
+ // src/serializers/raw.ts
3017
+ function formatRaw(raw) {
3018
+ switch (raw.format) {
3019
+ case "png":
3020
+ return raw.chunks.map((chunk) => chunk.text).join("\n\n");
3021
+ case "jpeg":
3022
+ case "webp":
3023
+ return raw.segments.map((segment) => segment.data).join("\n\n");
2864
3024
  }
2865
- return null;
2866
3025
  }
2867
3026
  export {
3027
+ formatAsWebUI,
3028
+ formatRaw,
2868
3029
  read,
2869
- write
3030
+ write,
3031
+ writeAsWebUI
2870
3032
  };
2871
3033
  //# sourceMappingURL=index.js.map