@enslo/sd-metadata 1.1.0 → 1.2.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/README.md +111 -1
- package/dist/index.d.ts +112 -12
- package/dist/index.js +1789 -1639
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,98 +4,139 @@ var Result = {
|
|
|
4
4
|
error: (error) => ({ ok: false, error })
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
// src/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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/
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
238
|
+
metadata.upscale = {
|
|
239
|
+
upscaler: hiresModel.model_name,
|
|
240
|
+
scale
|
|
241
|
+
};
|
|
154
242
|
}
|
|
155
243
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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/
|
|
236
|
-
function
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return [];
|
|
316
|
+
function detectUniqueKeywords(entryRecord) {
|
|
317
|
+
if (entryRecord.Software === "NovelAI") {
|
|
318
|
+
return "novelai";
|
|
257
319
|
}
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
284
|
-
"
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
return createTextChunk("Comment", userCommentSeg.data);
|
|
329
|
+
if ("negative_prompt" in entryRecord || "Negative Prompt" in entryRecord) {
|
|
330
|
+
return "easydiffusion";
|
|
341
331
|
}
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
376
|
-
|
|
336
|
+
const comment = entryRecord.Comment;
|
|
337
|
+
if (comment?.startsWith("{")) {
|
|
338
|
+
return detectFromCommentJson(comment);
|
|
377
339
|
}
|
|
378
|
-
return
|
|
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
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
388
|
+
return null;
|
|
424
389
|
}
|
|
425
|
-
function
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
442
|
-
...createEncodedChunk(
|
|
443
|
-
"parameters",
|
|
444
|
-
userComment.data,
|
|
445
|
-
getEncodingStrategy("swarmui")
|
|
446
|
-
)
|
|
447
|
-
);
|
|
448
|
-
return chunks;
|
|
394
|
+
return detectFromA1111Format(text);
|
|
449
395
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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 (
|
|
457
|
-
return
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
return Result.ok(raw);
|
|
403
|
+
if (json.includes('"use_stable_diffusion_model"')) {
|
|
404
|
+
return "easydiffusion";
|
|
465
405
|
}
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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
|
|
421
|
+
return null;
|
|
630
422
|
}
|
|
631
|
-
function
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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 (
|
|
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
|
-
|
|
653
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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/
|
|
694
|
-
function
|
|
695
|
-
return
|
|
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
|
-
|
|
701
|
-
|
|
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
|
-
|
|
704
|
-
|
|
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(
|
|
470
|
+
const parsed = parseJson(jsonText);
|
|
708
471
|
if (!parsed.ok) {
|
|
709
472
|
return Result.error({
|
|
710
473
|
type: "parseError",
|
|
711
|
-
message: "Invalid JSON in
|
|
474
|
+
message: "Invalid JSON in Easy Diffusion metadata"
|
|
712
475
|
});
|
|
713
476
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
const
|
|
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: "
|
|
734
|
-
prompt:
|
|
735
|
-
negativePrompt:
|
|
486
|
+
software: "easydiffusion",
|
|
487
|
+
prompt: prompt.trim(),
|
|
488
|
+
negativePrompt: negativePrompt.trim(),
|
|
736
489
|
width,
|
|
737
490
|
height,
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
|
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/
|
|
849
|
-
function
|
|
536
|
+
// src/parsers/fooocus.ts
|
|
537
|
+
function parseFooocus(entries) {
|
|
850
538
|
const entryRecord = buildEntryRecord(entries);
|
|
851
|
-
const
|
|
852
|
-
if (
|
|
853
|
-
|
|
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
|
-
|
|
872
|
-
|
|
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
|
-
|
|
875
|
-
|
|
550
|
+
const json = parsed.value;
|
|
551
|
+
if (!json.base_model && !json.prompt) {
|
|
552
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
876
553
|
}
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
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
|
|
882
|
-
if (
|
|
883
|
-
return
|
|
884
|
-
|
|
885
|
-
|
|
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 (
|
|
1314
|
+
return Result.ok(segments);
|
|
1575
1315
|
}
|
|
1576
|
-
function
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
if (
|
|
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
|
|
1347
|
+
return null;
|
|
1582
1348
|
}
|
|
1583
|
-
function
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
data[offset
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
data
|
|
1596
|
-
|
|
1597
|
-
|
|
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/
|
|
1644
|
-
function
|
|
1645
|
-
if (
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
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
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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 (
|
|
1699
|
-
const
|
|
1700
|
-
if (
|
|
1701
|
-
|
|
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
|
-
|
|
1437
|
+
if (chunkType === "IEND") {
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1708
1440
|
}
|
|
1709
|
-
return
|
|
1710
|
-
}
|
|
1711
|
-
function extractPrefix(text) {
|
|
1712
|
-
const match = text.match(/^([A-Za-z]+):\s/);
|
|
1713
|
-
return match?.[1] ?? null;
|
|
1441
|
+
return Result.ok(chunks);
|
|
1714
1442
|
}
|
|
1715
|
-
function
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|
|
1453
|
+
function tryUtf8Decode(data) {
|
|
1740
1454
|
try {
|
|
1741
|
-
|
|
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
|
|
1752
|
-
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
data
|
|
1770
|
-
|
|
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
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
1799
|
-
|
|
1800
|
-
return null;
|
|
1548
|
+
const paddedSize = chunkSize + chunkSize % 2;
|
|
1549
|
+
offset += 8 + paddedSize;
|
|
1801
1550
|
}
|
|
1551
|
+
return null;
|
|
1802
1552
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
|
1575
|
+
return entries;
|
|
1811
1576
|
}
|
|
1812
|
-
function
|
|
1813
|
-
const
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
-
|
|
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
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
|
1630
|
+
return { status: "success", metadata, raw };
|
|
1828
1631
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const
|
|
1843
|
-
|
|
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
|
-
|
|
1846
|
-
if (
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
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
|
|
1668
|
+
return {
|
|
1669
|
+
status: "success",
|
|
1670
|
+
raw: HELPERS[format].createRaw(result.value)
|
|
1671
|
+
};
|
|
1860
1672
|
}
|
|
1861
|
-
function
|
|
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
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
continue;
|
|
1801
|
+
case "text-unicode-escape": {
|
|
1802
|
+
const escaped = escapeUnicode(text);
|
|
1803
|
+
return createTextChunk(keyword, escaped);
|
|
1905
1804
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
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
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
return
|
|
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/
|
|
1931
|
-
function
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
|
1878
|
+
return [
|
|
1879
|
+
{
|
|
1880
|
+
source: { type: "exifUserComment" },
|
|
1881
|
+
data: JSON.stringify(data)
|
|
1882
|
+
}
|
|
1883
|
+
];
|
|
1987
1884
|
}
|
|
1988
|
-
|
|
1989
|
-
const
|
|
1990
|
-
|
|
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
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
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
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
const
|
|
2020
|
-
|
|
2021
|
-
|
|
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
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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;
|
|
2047
1966
|
}
|
|
2048
1967
|
}
|
|
2049
|
-
return
|
|
1968
|
+
return [
|
|
1969
|
+
{
|
|
1970
|
+
source: { type: "exifUserComment" },
|
|
1971
|
+
data: JSON.stringify(data)
|
|
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;
|
|
2004
|
+
}
|
|
2005
|
+
return createEncodedChunk(
|
|
2006
|
+
"invokeai_metadata",
|
|
2007
|
+
userComment.data,
|
|
2008
|
+
getEncodingStrategy("invokeai")
|
|
2009
|
+
);
|
|
2050
2010
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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
|
-
|
|
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
|
|
2059
|
-
return
|
|
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
|
-
|
|
2062
|
-
|
|
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
|
-
|
|
2066
|
-
|
|
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
|
|
2072
|
-
if (!
|
|
2073
|
-
return
|
|
2060
|
+
const parsed = parseJson(userCommentSeg.data);
|
|
2061
|
+
if (!parsed.ok) {
|
|
2062
|
+
return createTextChunk("Comment", userCommentSeg.data);
|
|
2074
2063
|
}
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2077
|
-
|
|
2064
|
+
const jsonData = parsed.value;
|
|
2065
|
+
const descriptionText = extractDescriptionText(
|
|
2066
|
+
descriptionSeg,
|
|
2067
|
+
stringify(jsonData.Description)
|
|
2078
2068
|
);
|
|
2079
|
-
const
|
|
2080
|
-
|
|
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
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
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
|
-
|
|
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/
|
|
2100
|
-
function
|
|
2101
|
-
return chunks
|
|
2102
|
-
|
|
2103
|
-
|
|
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
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
if (
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
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
|
-
|
|
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
|
|
2145
|
+
return segments;
|
|
2121
2146
|
}
|
|
2122
|
-
function
|
|
2123
|
-
const
|
|
2124
|
-
if (!
|
|
2125
|
-
return
|
|
2147
|
+
function convertSwarmUISegmentsToPng(segments) {
|
|
2148
|
+
const userComment = findSegment(segments, "exifUserComment");
|
|
2149
|
+
if (!userComment) {
|
|
2150
|
+
return [];
|
|
2126
2151
|
}
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
|
|
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
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2163
|
+
chunks.push(
|
|
2164
|
+
...createEncodedChunk(
|
|
2165
|
+
"parameters",
|
|
2166
|
+
userComment.data,
|
|
2167
|
+
getEncodingStrategy("swarmui")
|
|
2168
|
+
)
|
|
2169
|
+
);
|
|
2170
|
+
return chunks;
|
|
2137
2171
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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/
|
|
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 =
|
|
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,159 @@ function write(data, metadata, options) {
|
|
|
2779
2863
|
message: "Internal error: format mismatch after conversion"
|
|
2780
2864
|
});
|
|
2781
2865
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2866
|
+
var HELPERS2 = {
|
|
2867
|
+
png: {
|
|
2868
|
+
writeEmpty: writePngMetadata
|
|
2869
|
+
},
|
|
2870
|
+
jpeg: {
|
|
2871
|
+
writeEmpty: writeJpegMetadata
|
|
2872
|
+
},
|
|
2873
|
+
webp: {
|
|
2874
|
+
writeEmpty: writeWebpMetadata
|
|
2787
2875
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
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
|
-
|
|
2792
|
-
|
|
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
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
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
|
-
|
|
2816
|
-
|
|
2817
|
-
offset++;
|
|
2818
|
-
continue;
|
|
2929
|
+
if (mergedHires.scale !== void 0) {
|
|
2930
|
+
parts.push(`Hires upscale: ${mergedHires.scale}`);
|
|
2819
2931
|
}
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
|
2939
|
+
return parts.join(", ");
|
|
2830
2940
|
}
|
|
2831
|
-
function
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
const
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
return { width: wRaw & 16383, height: hRaw & 16383 };
|
|
2852
|
-
}
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
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
|
-
}
|
|
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"));
|
|
2862
2961
|
}
|
|
2863
|
-
offset += 8 + paddedSize;
|
|
2864
2962
|
}
|
|
2865
|
-
|
|
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
|
|
3012
|
+
}
|
|
3013
|
+
];
|
|
2866
3014
|
}
|
|
2867
3015
|
export {
|
|
3016
|
+
formatAsWebUI,
|
|
2868
3017
|
read,
|
|
2869
|
-
write
|
|
3018
|
+
write,
|
|
3019
|
+
writeAsWebUI
|
|
2870
3020
|
};
|
|
2871
3021
|
//# sourceMappingURL=index.js.map
|