@enslo/sd-metadata 1.0.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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/index.d.ts +320 -0
- package/dist/index.js +2805 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2805 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var Result = {
|
|
3
|
+
ok: (value) => ({ ok: true, value }),
|
|
4
|
+
error: (error) => ({ ok: false, error })
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/converters/utils.ts
|
|
8
|
+
var createTextChunk = (keyword, text) => text !== void 0 ? [{ type: "tEXt", keyword, text }] : [];
|
|
9
|
+
var createITxtChunk = (keyword, text) => text !== void 0 ? [
|
|
10
|
+
{
|
|
11
|
+
type: "iTXt",
|
|
12
|
+
keyword,
|
|
13
|
+
compressionFlag: 0,
|
|
14
|
+
compressionMethod: 0,
|
|
15
|
+
languageTag: "",
|
|
16
|
+
translatedKeyword: "",
|
|
17
|
+
text
|
|
18
|
+
}
|
|
19
|
+
] : [];
|
|
20
|
+
var findSegment = (segments, type) => segments.find((s) => s.source.type === type);
|
|
21
|
+
var stringify = (value) => {
|
|
22
|
+
if (value === void 0) return void 0;
|
|
23
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/converters/chunk-encoding.ts
|
|
27
|
+
var CHUNK_ENCODING_STRATEGIES = {
|
|
28
|
+
// Dynamic selection tools
|
|
29
|
+
a1111: "dynamic",
|
|
30
|
+
forge: "dynamic",
|
|
31
|
+
"forge-neo": "dynamic",
|
|
32
|
+
"sd-webui": "dynamic",
|
|
33
|
+
invokeai: "dynamic",
|
|
34
|
+
novelai: "dynamic",
|
|
35
|
+
"sd-next": "dynamic",
|
|
36
|
+
easydiffusion: "dynamic",
|
|
37
|
+
blind: "dynamic",
|
|
38
|
+
// Unicode escape tools (spec-compliant)
|
|
39
|
+
comfyui: "text-unicode-escape",
|
|
40
|
+
swarmui: "text-unicode-escape",
|
|
41
|
+
fooocus: "text-unicode-escape",
|
|
42
|
+
"ruined-fooocus": "text-unicode-escape",
|
|
43
|
+
"hf-space": "text-unicode-escape",
|
|
44
|
+
// Raw UTF-8 tools (non-compliant but compatible)
|
|
45
|
+
"stability-matrix": "text-utf8-raw",
|
|
46
|
+
tensorart: "text-utf8-raw"
|
|
47
|
+
};
|
|
48
|
+
function getEncodingStrategy(tool) {
|
|
49
|
+
return CHUNK_ENCODING_STRATEGIES[tool] ?? "text-unicode-escape";
|
|
50
|
+
}
|
|
51
|
+
function escapeUnicode(text) {
|
|
52
|
+
return text.replace(/[\u0100-\uffff]/g, (char) => {
|
|
53
|
+
const code = char.charCodeAt(0).toString(16).padStart(4, "0");
|
|
54
|
+
return `\\u${code}`;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function hasNonLatin1(text) {
|
|
58
|
+
return /[^\x00-\xFF]/.test(text);
|
|
59
|
+
}
|
|
60
|
+
function createEncodedChunk(keyword, text, strategy) {
|
|
61
|
+
if (text === void 0) return [];
|
|
62
|
+
switch (strategy) {
|
|
63
|
+
case "dynamic": {
|
|
64
|
+
const chunkType = hasNonLatin1(text) ? "iTXt" : "tEXt";
|
|
65
|
+
return chunkType === "iTXt" ? createITxtChunk(keyword, text) : createTextChunk(keyword, text);
|
|
66
|
+
}
|
|
67
|
+
case "text-unicode-escape": {
|
|
68
|
+
const escaped = escapeUnicode(text);
|
|
69
|
+
return createTextChunk(keyword, escaped);
|
|
70
|
+
}
|
|
71
|
+
case "text-utf8-raw": {
|
|
72
|
+
return createTextChunk(keyword, text);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/converters/a1111.ts
|
|
78
|
+
function convertA1111PngToSegments(chunks) {
|
|
79
|
+
const parameters = chunks.find((c) => c.keyword === "parameters");
|
|
80
|
+
if (!parameters) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
source: { type: "exifUserComment" },
|
|
86
|
+
data: parameters.text
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
function convertA1111SegmentsToPng(segments) {
|
|
91
|
+
const userComment = segments.find((s) => s.source.type === "exifUserComment");
|
|
92
|
+
if (!userComment) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
return createEncodedChunk(
|
|
96
|
+
"parameters",
|
|
97
|
+
userComment.data,
|
|
98
|
+
getEncodingStrategy("a1111")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/utils/json.ts
|
|
103
|
+
function parseJson(text) {
|
|
104
|
+
try {
|
|
105
|
+
return Result.ok(JSON.parse(text));
|
|
106
|
+
} catch {
|
|
107
|
+
return Result.error({
|
|
108
|
+
type: "parseError",
|
|
109
|
+
message: "Invalid JSON"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/converters/blind.ts
|
|
115
|
+
function blindPngToSegments(chunks) {
|
|
116
|
+
if (chunks.length === 0) return [];
|
|
117
|
+
const chunkMap = Object.fromEntries(
|
|
118
|
+
chunks.map((chunk) => [chunk.keyword, chunk.text])
|
|
119
|
+
);
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
source: { type: "exifUserComment" },
|
|
123
|
+
data: JSON.stringify(chunkMap)
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
function blindSegmentsToPng(segments) {
|
|
128
|
+
const userComment = segments.find((s) => s.source.type === "exifUserComment");
|
|
129
|
+
if (!userComment) return [];
|
|
130
|
+
const parsed = parseJson(userComment.data);
|
|
131
|
+
if (parsed.ok) {
|
|
132
|
+
return Object.entries(parsed.value).flatMap(([keyword, value]) => {
|
|
133
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
134
|
+
if (!text) return [];
|
|
135
|
+
return createEncodedChunk(keyword, text, getEncodingStrategy("blind"));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return createEncodedChunk(
|
|
139
|
+
"metadata",
|
|
140
|
+
userComment.data,
|
|
141
|
+
getEncodingStrategy("blind")
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/converters/comfyui.ts
|
|
146
|
+
function convertComfyUIPngToSegments(chunks) {
|
|
147
|
+
const data = {};
|
|
148
|
+
for (const chunk of chunks) {
|
|
149
|
+
const parsed = parseJson(chunk.text);
|
|
150
|
+
if (parsed.ok) {
|
|
151
|
+
data[chunk.keyword] = parsed.value;
|
|
152
|
+
} else {
|
|
153
|
+
data[chunk.keyword] = chunk.text;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
source: { type: "exifUserComment" },
|
|
159
|
+
data: JSON.stringify(data)
|
|
160
|
+
}
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
var tryParseExtendedFormat = (segments) => {
|
|
164
|
+
const imageDescription = findSegment(segments, "exifImageDescription");
|
|
165
|
+
const make = findSegment(segments, "exifMake");
|
|
166
|
+
if (!imageDescription && !make) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return [
|
|
170
|
+
...createEncodedChunk("prompt", make?.data, getEncodingStrategy("comfyui")),
|
|
171
|
+
...createEncodedChunk(
|
|
172
|
+
"workflow",
|
|
173
|
+
imageDescription?.data,
|
|
174
|
+
getEncodingStrategy("comfyui")
|
|
175
|
+
)
|
|
176
|
+
];
|
|
177
|
+
};
|
|
178
|
+
var tryParseSaveImagePlusFormat = (segments) => {
|
|
179
|
+
const userComment = findSegment(segments, "exifUserComment");
|
|
180
|
+
if (!userComment) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const parsed = parseJson(userComment.data);
|
|
184
|
+
if (!parsed.ok) {
|
|
185
|
+
return createEncodedChunk(
|
|
186
|
+
"prompt",
|
|
187
|
+
userComment.data,
|
|
188
|
+
getEncodingStrategy("comfyui")
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return Object.entries(parsed.value).flatMap(
|
|
192
|
+
([keyword, value]) => createEncodedChunk(
|
|
193
|
+
keyword,
|
|
194
|
+
stringify(value),
|
|
195
|
+
getEncodingStrategy("comfyui")
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
function convertComfyUISegmentsToPng(segments) {
|
|
200
|
+
return tryParseExtendedFormat(segments) ?? tryParseSaveImagePlusFormat(segments) ?? [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/converters/easydiffusion.ts
|
|
204
|
+
function convertEasyDiffusionPngToSegments(chunks) {
|
|
205
|
+
const json = Object.fromEntries(
|
|
206
|
+
chunks.map((chunk) => [chunk.keyword, chunk.text])
|
|
207
|
+
);
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
source: { type: "exifUserComment" },
|
|
211
|
+
data: JSON.stringify(json)
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
function convertEasyDiffusionSegmentsToPng(segments) {
|
|
216
|
+
const userComment = findSegment(segments, "exifUserComment");
|
|
217
|
+
if (!userComment) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const parsed = parseJson(userComment.data);
|
|
221
|
+
if (!parsed.ok) {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
return Object.entries(parsed.value).flatMap(([keyword, value]) => {
|
|
225
|
+
const text = value != null ? typeof value === "string" ? value : String(value) : void 0;
|
|
226
|
+
if (!text) return [];
|
|
227
|
+
return createEncodedChunk(
|
|
228
|
+
keyword,
|
|
229
|
+
text,
|
|
230
|
+
getEncodingStrategy("easydiffusion")
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/converters/invokeai.ts
|
|
236
|
+
function convertInvokeAIPngToSegments(chunks) {
|
|
237
|
+
const data = {};
|
|
238
|
+
for (const chunk of chunks) {
|
|
239
|
+
const parsed = parseJson(chunk.text);
|
|
240
|
+
if (parsed.ok) {
|
|
241
|
+
data[chunk.keyword] = parsed.value;
|
|
242
|
+
} else {
|
|
243
|
+
data[chunk.keyword] = chunk.text;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
source: { type: "exifUserComment" },
|
|
249
|
+
data: JSON.stringify(data)
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
function convertInvokeAISegmentsToPng(segments) {
|
|
254
|
+
const userComment = findSegment(segments, "exifUserComment");
|
|
255
|
+
if (!userComment) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const parsed = parseJson(userComment.data);
|
|
259
|
+
if (!parsed.ok) {
|
|
260
|
+
return createEncodedChunk(
|
|
261
|
+
"invokeai_metadata",
|
|
262
|
+
userComment.data,
|
|
263
|
+
getEncodingStrategy("invokeai")
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const metadataText = stringify(parsed.value.invokeai_metadata);
|
|
267
|
+
const graphText = stringify(parsed.value.invokeai_graph);
|
|
268
|
+
const chunks = [
|
|
269
|
+
...createEncodedChunk(
|
|
270
|
+
"invokeai_metadata",
|
|
271
|
+
metadataText,
|
|
272
|
+
getEncodingStrategy("invokeai")
|
|
273
|
+
),
|
|
274
|
+
...createEncodedChunk(
|
|
275
|
+
"invokeai_graph",
|
|
276
|
+
graphText,
|
|
277
|
+
getEncodingStrategy("invokeai")
|
|
278
|
+
)
|
|
279
|
+
];
|
|
280
|
+
if (chunks.length > 0) {
|
|
281
|
+
return chunks;
|
|
282
|
+
}
|
|
283
|
+
return createEncodedChunk(
|
|
284
|
+
"invokeai_metadata",
|
|
285
|
+
userComment.data,
|
|
286
|
+
getEncodingStrategy("invokeai")
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/converters/novelai.ts
|
|
291
|
+
var NOVELAI_TITLE = "NovelAI generated image";
|
|
292
|
+
var NOVELAI_SOFTWARE = "NovelAI";
|
|
293
|
+
function convertNovelaiPngToSegments(chunks) {
|
|
294
|
+
const comment = chunks.find((c) => c.keyword === "Comment");
|
|
295
|
+
if (!comment) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
const description = chunks.find((c) => c.keyword === "Description");
|
|
299
|
+
const data = buildUserCommentJson(chunks);
|
|
300
|
+
const descriptionSegment = description ? [
|
|
301
|
+
{
|
|
302
|
+
source: { type: "exifImageDescription" },
|
|
303
|
+
data: `\0\0\0\0${description.text}`
|
|
304
|
+
}
|
|
305
|
+
] : [];
|
|
306
|
+
const userCommentSegment = {
|
|
307
|
+
source: { type: "exifUserComment" },
|
|
308
|
+
data: JSON.stringify(data)
|
|
309
|
+
};
|
|
310
|
+
return [...descriptionSegment, userCommentSegment];
|
|
311
|
+
}
|
|
312
|
+
function buildUserCommentJson(chunks) {
|
|
313
|
+
return NOVELAI_KEY_ORDER.map((key) => {
|
|
314
|
+
const chunk = chunks.find((c) => c.keyword === key);
|
|
315
|
+
return chunk ? { [key]: chunk.text } : null;
|
|
316
|
+
}).filter((entry) => entry !== null).reduce(
|
|
317
|
+
(acc, entry) => Object.assign(acc, entry),
|
|
318
|
+
{}
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
var NOVELAI_KEY_ORDER = [
|
|
322
|
+
"Comment",
|
|
323
|
+
"Description",
|
|
324
|
+
"Generation time",
|
|
325
|
+
"Software",
|
|
326
|
+
"Source",
|
|
327
|
+
"Title"
|
|
328
|
+
];
|
|
329
|
+
function convertNovelaiSegmentsToPng(segments) {
|
|
330
|
+
const userCommentSeg = findSegment(segments, "exifUserComment");
|
|
331
|
+
const descriptionSeg = findSegment(segments, "exifImageDescription");
|
|
332
|
+
return parseSegments(userCommentSeg, descriptionSeg);
|
|
333
|
+
}
|
|
334
|
+
function parseSegments(userCommentSeg, descriptionSeg) {
|
|
335
|
+
if (!userCommentSeg || !descriptionSeg) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
const parsed = parseJson(userCommentSeg.data);
|
|
339
|
+
if (!parsed.ok) {
|
|
340
|
+
return createTextChunk("Comment", userCommentSeg.data);
|
|
341
|
+
}
|
|
342
|
+
const jsonData = parsed.value;
|
|
343
|
+
const descriptionText = extractDescriptionText(
|
|
344
|
+
descriptionSeg,
|
|
345
|
+
stringify(jsonData.Description)
|
|
346
|
+
);
|
|
347
|
+
const descriptionChunks = descriptionText ? createEncodedChunk(
|
|
348
|
+
"Description",
|
|
349
|
+
descriptionText,
|
|
350
|
+
getEncodingStrategy("novelai")
|
|
351
|
+
) : [];
|
|
352
|
+
return [
|
|
353
|
+
// Title (required, use default if missing)
|
|
354
|
+
createTextChunk("Title", stringify(jsonData.Title) ?? NOVELAI_TITLE),
|
|
355
|
+
// Description (optional, prefer exifImageDescription over JSON)
|
|
356
|
+
...descriptionChunks,
|
|
357
|
+
// Software (required, use default if missing)
|
|
358
|
+
createTextChunk(
|
|
359
|
+
"Software",
|
|
360
|
+
stringify(jsonData.Software) ?? NOVELAI_SOFTWARE
|
|
361
|
+
),
|
|
362
|
+
// Source (optional)
|
|
363
|
+
createTextChunk("Source", stringify(jsonData.Source)),
|
|
364
|
+
// Generation time (optional)
|
|
365
|
+
createTextChunk("Generation time", stringify(jsonData["Generation time"])),
|
|
366
|
+
// Comment (optional)
|
|
367
|
+
createTextChunk("Comment", stringify(jsonData.Comment))
|
|
368
|
+
].flat();
|
|
369
|
+
}
|
|
370
|
+
function extractDescriptionText(descriptionSeg, jsonDescription) {
|
|
371
|
+
if (descriptionSeg?.data) {
|
|
372
|
+
const data = descriptionSeg.data;
|
|
373
|
+
return data.startsWith("\0\0\0\0") ? data.slice(4) : data;
|
|
374
|
+
}
|
|
375
|
+
if (jsonDescription) {
|
|
376
|
+
return jsonDescription.startsWith("\0\0\0\0") ? jsonDescription.slice(4) : jsonDescription;
|
|
377
|
+
}
|
|
378
|
+
return void 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/converters/simple-chunk.ts
|
|
382
|
+
function createPngToSegments(keyword) {
|
|
383
|
+
return (chunks) => {
|
|
384
|
+
const chunk = chunks.find((c) => c.keyword === keyword);
|
|
385
|
+
return !chunk ? [] : [{ source: { type: "exifUserComment" }, data: chunk.text }];
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function createSegmentsToPng(keyword) {
|
|
389
|
+
return (segments) => {
|
|
390
|
+
const userComment = segments.find(
|
|
391
|
+
(s) => s.source.type === "exifUserComment"
|
|
392
|
+
);
|
|
393
|
+
if (!userComment) return [];
|
|
394
|
+
return createEncodedChunk(
|
|
395
|
+
keyword,
|
|
396
|
+
userComment.data,
|
|
397
|
+
getEncodingStrategy(keyword)
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/converters/swarmui.ts
|
|
403
|
+
function convertSwarmUIPngToSegments(chunks) {
|
|
404
|
+
const parametersChunk = chunks.find((c) => c.keyword === "parameters");
|
|
405
|
+
if (!parametersChunk) {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
const parsed = parseJson(parametersChunk.text);
|
|
409
|
+
const data = parsed.ok ? parsed.value : parametersChunk.text;
|
|
410
|
+
return [
|
|
411
|
+
{
|
|
412
|
+
source: { type: "exifUserComment" },
|
|
413
|
+
data: typeof data === "string" ? data : JSON.stringify(data)
|
|
414
|
+
}
|
|
415
|
+
];
|
|
416
|
+
}
|
|
417
|
+
function convertSwarmUISegmentsToPng(segments) {
|
|
418
|
+
const userComment = findSegment(segments, "exifUserComment");
|
|
419
|
+
if (!userComment) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
return createEncodedChunk(
|
|
423
|
+
"parameters",
|
|
424
|
+
userComment.data,
|
|
425
|
+
getEncodingStrategy("swarmui")
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/converters/index.ts
|
|
430
|
+
function convertMetadata(parseResult, targetFormat, force = false) {
|
|
431
|
+
if (parseResult.status === "empty") {
|
|
432
|
+
return Result.error({ type: "missingRawData" });
|
|
433
|
+
}
|
|
434
|
+
if (parseResult.status === "invalid") {
|
|
435
|
+
return Result.error({
|
|
436
|
+
type: "invalidParseResult",
|
|
437
|
+
status: parseResult.status
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const raw = parseResult.raw;
|
|
441
|
+
if (raw.format === "png" && targetFormat === "png" || raw.format === "jpeg" && targetFormat === "jpeg" || raw.format === "webp" && targetFormat === "webp") {
|
|
442
|
+
return Result.ok(raw);
|
|
443
|
+
}
|
|
444
|
+
const software = parseResult.status === "success" ? parseResult.metadata.software : null;
|
|
445
|
+
if (!software) {
|
|
446
|
+
return force ? convertBlind(raw, targetFormat) : Result.error({
|
|
447
|
+
type: "unsupportedSoftware",
|
|
448
|
+
software: "unknown"
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const converter = softwareConverters[software];
|
|
452
|
+
if (!converter) {
|
|
453
|
+
return Result.error({
|
|
454
|
+
type: "unsupportedSoftware",
|
|
455
|
+
software
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
return converter(raw, targetFormat);
|
|
459
|
+
}
|
|
460
|
+
function createFormatConverter(pngToSegments, segmentsToPng) {
|
|
461
|
+
return (raw, targetFormat) => {
|
|
462
|
+
if (raw.format === "png") {
|
|
463
|
+
if (targetFormat === "png") {
|
|
464
|
+
return Result.ok(raw);
|
|
465
|
+
}
|
|
466
|
+
const segments = pngToSegments(raw.chunks);
|
|
467
|
+
return Result.ok({ format: targetFormat, segments });
|
|
468
|
+
}
|
|
469
|
+
if (targetFormat === "jpeg" || targetFormat === "webp") {
|
|
470
|
+
return Result.ok({ format: targetFormat, segments: raw.segments });
|
|
471
|
+
}
|
|
472
|
+
const chunks = segmentsToPng(raw.segments);
|
|
473
|
+
return Result.ok({ format: "png", chunks });
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
var convertNovelai = createFormatConverter(
|
|
477
|
+
convertNovelaiPngToSegments,
|
|
478
|
+
convertNovelaiSegmentsToPng
|
|
479
|
+
);
|
|
480
|
+
var convertA1111 = createFormatConverter(
|
|
481
|
+
convertA1111PngToSegments,
|
|
482
|
+
convertA1111SegmentsToPng
|
|
483
|
+
);
|
|
484
|
+
var convertComfyUI = createFormatConverter(
|
|
485
|
+
convertComfyUIPngToSegments,
|
|
486
|
+
convertComfyUISegmentsToPng
|
|
487
|
+
);
|
|
488
|
+
var convertEasyDiffusion = createFormatConverter(
|
|
489
|
+
convertEasyDiffusionPngToSegments,
|
|
490
|
+
convertEasyDiffusionSegmentsToPng
|
|
491
|
+
);
|
|
492
|
+
var convertFooocus = createFormatConverter(
|
|
493
|
+
createPngToSegments("Comment"),
|
|
494
|
+
createSegmentsToPng("Comment")
|
|
495
|
+
);
|
|
496
|
+
var convertRuinedFooocus = createFormatConverter(
|
|
497
|
+
createPngToSegments("parameters"),
|
|
498
|
+
createSegmentsToPng("parameters")
|
|
499
|
+
);
|
|
500
|
+
var convertSwarmUI = createFormatConverter(
|
|
501
|
+
convertSwarmUIPngToSegments,
|
|
502
|
+
convertSwarmUISegmentsToPng
|
|
503
|
+
);
|
|
504
|
+
var convertInvokeAI = createFormatConverter(
|
|
505
|
+
convertInvokeAIPngToSegments,
|
|
506
|
+
convertInvokeAISegmentsToPng
|
|
507
|
+
);
|
|
508
|
+
var convertHfSpace = createFormatConverter(
|
|
509
|
+
createPngToSegments("parameters"),
|
|
510
|
+
createSegmentsToPng("parameters")
|
|
511
|
+
);
|
|
512
|
+
var convertBlind = createFormatConverter(
|
|
513
|
+
blindPngToSegments,
|
|
514
|
+
blindSegmentsToPng
|
|
515
|
+
);
|
|
516
|
+
var softwareConverters = {
|
|
517
|
+
// NovelAI
|
|
518
|
+
novelai: convertNovelai,
|
|
519
|
+
// A1111-format (sd-webui, forge, forge-neo, civitai, sd-next)
|
|
520
|
+
"sd-webui": convertA1111,
|
|
521
|
+
"sd-next": convertA1111,
|
|
522
|
+
forge: convertA1111,
|
|
523
|
+
"forge-neo": convertA1111,
|
|
524
|
+
civitai: convertA1111,
|
|
525
|
+
// ComfyUI-format (comfyui, tensorart, stability-matrix)
|
|
526
|
+
comfyui: convertComfyUI,
|
|
527
|
+
tensorart: convertComfyUI,
|
|
528
|
+
"stability-matrix": convertComfyUI,
|
|
529
|
+
// Easy Diffusion
|
|
530
|
+
easydiffusion: convertEasyDiffusion,
|
|
531
|
+
// Fooocus variants
|
|
532
|
+
fooocus: convertFooocus,
|
|
533
|
+
"ruined-fooocus": convertRuinedFooocus,
|
|
534
|
+
// SwarmUI
|
|
535
|
+
swarmui: convertSwarmUI,
|
|
536
|
+
// InvokeAI
|
|
537
|
+
invokeai: convertInvokeAI,
|
|
538
|
+
// HuggingFace Space
|
|
539
|
+
"hf-space": convertHfSpace
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/parsers/a1111.ts
|
|
543
|
+
function parseA1111(entries) {
|
|
544
|
+
const parametersEntry = entries.find(
|
|
545
|
+
(e) => e.keyword === "parameters" || e.keyword === "Comment"
|
|
546
|
+
);
|
|
547
|
+
if (!parametersEntry) {
|
|
548
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
549
|
+
}
|
|
550
|
+
const text = parametersEntry.text;
|
|
551
|
+
const { prompt, negativePrompt, settings } = parseParametersText(text);
|
|
552
|
+
const settingsMap = parseSettings(settings);
|
|
553
|
+
const size = settingsMap.get("Size") ?? "0x0";
|
|
554
|
+
const [width, height] = parseSize(size);
|
|
555
|
+
const version = settingsMap.get("Version");
|
|
556
|
+
const app = settingsMap.get("App");
|
|
557
|
+
const software = detectSoftwareVariant(version, app);
|
|
558
|
+
const metadata = {
|
|
559
|
+
type: "a1111",
|
|
560
|
+
software,
|
|
561
|
+
prompt,
|
|
562
|
+
negativePrompt,
|
|
563
|
+
width,
|
|
564
|
+
height
|
|
565
|
+
};
|
|
566
|
+
const modelName = settingsMap.get("Model");
|
|
567
|
+
const modelHash = settingsMap.get("Model hash");
|
|
568
|
+
if (modelName || modelHash) {
|
|
569
|
+
metadata.model = {
|
|
570
|
+
name: modelName,
|
|
571
|
+
hash: modelHash
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const sampler = settingsMap.get("Sampler");
|
|
575
|
+
const scheduler = settingsMap.get("Schedule type");
|
|
576
|
+
const steps = parseNumber(settingsMap.get("Steps"));
|
|
577
|
+
const cfg = parseNumber(
|
|
578
|
+
settingsMap.get("CFG scale") ?? settingsMap.get("CFG Scale")
|
|
579
|
+
);
|
|
580
|
+
const seed = parseNumber(settingsMap.get("Seed"));
|
|
581
|
+
const clipSkip = parseNumber(settingsMap.get("Clip skip"));
|
|
582
|
+
if (sampler !== void 0 || scheduler !== void 0 || steps !== void 0 || cfg !== void 0 || seed !== void 0 || clipSkip !== void 0) {
|
|
583
|
+
metadata.sampling = {
|
|
584
|
+
sampler,
|
|
585
|
+
scheduler,
|
|
586
|
+
steps,
|
|
587
|
+
cfg,
|
|
588
|
+
seed,
|
|
589
|
+
clipSkip
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
const hiresScale = parseNumber(settingsMap.get("Hires upscale"));
|
|
593
|
+
const upscaler = settingsMap.get("Hires upscaler");
|
|
594
|
+
const hiresSteps = parseNumber(settingsMap.get("Hires steps"));
|
|
595
|
+
const denoise = parseNumber(settingsMap.get("Denoising strength"));
|
|
596
|
+
const hiresSize = settingsMap.get("Hires size");
|
|
597
|
+
if ([hiresScale, hiresSize, upscaler, hiresSteps, denoise].some(
|
|
598
|
+
(v) => v !== void 0
|
|
599
|
+
)) {
|
|
600
|
+
const [hiresWidth] = parseSize(hiresSize ?? "");
|
|
601
|
+
const scale = hiresScale ?? hiresWidth / width;
|
|
602
|
+
metadata.hires = { scale, upscaler, steps: hiresSteps, denoise };
|
|
603
|
+
}
|
|
604
|
+
return Result.ok(metadata);
|
|
605
|
+
}
|
|
606
|
+
function parseParametersText(text) {
|
|
607
|
+
const negativeIndex = text.indexOf("Negative prompt:");
|
|
608
|
+
const stepsIndex = text.indexOf("Steps:");
|
|
609
|
+
if (negativeIndex === -1 && stepsIndex === -1) {
|
|
610
|
+
return { prompt: text.trim(), negativePrompt: "", settings: "" };
|
|
611
|
+
}
|
|
612
|
+
if (negativeIndex === -1) {
|
|
613
|
+
const settingsStart2 = text.lastIndexOf("\n", stepsIndex);
|
|
614
|
+
return {
|
|
615
|
+
prompt: text.slice(0, settingsStart2).trim(),
|
|
616
|
+
negativePrompt: "",
|
|
617
|
+
settings: text.slice(settingsStart2).trim()
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (stepsIndex === -1) {
|
|
621
|
+
return {
|
|
622
|
+
prompt: text.slice(0, negativeIndex).trim(),
|
|
623
|
+
negativePrompt: text.slice(negativeIndex + 16).trim(),
|
|
624
|
+
settings: ""
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const settingsStart = text.lastIndexOf("\n", stepsIndex);
|
|
628
|
+
return {
|
|
629
|
+
prompt: text.slice(0, negativeIndex).trim(),
|
|
630
|
+
negativePrompt: text.slice(negativeIndex + 16, settingsStart).trim(),
|
|
631
|
+
settings: text.slice(settingsStart).trim()
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function parseSettings(settings) {
|
|
635
|
+
const result = /* @__PURE__ */ new Map();
|
|
636
|
+
if (!settings) return result;
|
|
637
|
+
const regex = /([A-Za-z][A-Za-z0-9 ]*?):\s*([^,]+?)(?=,\s*[A-Za-z][A-Za-z0-9 ]*?:|$)/g;
|
|
638
|
+
const matches = Array.from(settings.matchAll(regex));
|
|
639
|
+
for (const match of matches) {
|
|
640
|
+
const key = (match[1] ?? "").trim();
|
|
641
|
+
const value = (match[2] ?? "").trim();
|
|
642
|
+
result.set(key, value);
|
|
643
|
+
}
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
function parseSize(size) {
|
|
647
|
+
const match = size.match(/(\d+)x(\d+)/);
|
|
648
|
+
if (!match) return [0, 0];
|
|
649
|
+
return [
|
|
650
|
+
Number.parseInt(match[1] ?? "0", 10),
|
|
651
|
+
Number.parseInt(match[2] ?? "0", 10)
|
|
652
|
+
];
|
|
653
|
+
}
|
|
654
|
+
function parseNumber(value) {
|
|
655
|
+
if (value === void 0) return void 0;
|
|
656
|
+
const num = Number.parseFloat(value);
|
|
657
|
+
return Number.isNaN(num) ? void 0 : num;
|
|
658
|
+
}
|
|
659
|
+
function detectSoftwareVariant(version, app) {
|
|
660
|
+
if (app === "SD.Next") return "sd-next";
|
|
661
|
+
if (!version) return "sd-webui";
|
|
662
|
+
if (version === "neo") return "forge-neo";
|
|
663
|
+
if (version === "classic") return "forge";
|
|
664
|
+
if (/^f\d+\.\d+/.test(version)) return "forge";
|
|
665
|
+
return "sd-webui";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/utils/entries.ts
|
|
669
|
+
function buildEntryRecord(entries) {
|
|
670
|
+
return Object.freeze(
|
|
671
|
+
Object.fromEntries(entries.map((e) => [e.keyword, e.text]))
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/parsers/comfyui.ts
|
|
676
|
+
function parseComfyUI(entries) {
|
|
677
|
+
const entryRecord = buildEntryRecord(entries);
|
|
678
|
+
const promptText = findPromptJson(entryRecord);
|
|
679
|
+
if (!promptText) {
|
|
680
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
681
|
+
}
|
|
682
|
+
const parsed = parseJson(promptText);
|
|
683
|
+
if (!parsed.ok) {
|
|
684
|
+
return Result.error({
|
|
685
|
+
type: "parseError",
|
|
686
|
+
message: "Invalid JSON in prompt entry"
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
const prompt = parsed.value;
|
|
690
|
+
const nodes = Object.values(prompt);
|
|
691
|
+
if (!nodes.some((node) => "class_type" in node)) {
|
|
692
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
693
|
+
}
|
|
694
|
+
const ksampler = findNode(prompt, ["Sampler"]);
|
|
695
|
+
const positiveClip = findNode(prompt, ["PositiveCLIP_Base"]);
|
|
696
|
+
const negativeClip = findNode(prompt, ["NegativeCLIP_Base"]);
|
|
697
|
+
const clipPositiveText = extractText(positiveClip);
|
|
698
|
+
const clipNegativeText = extractText(negativeClip);
|
|
699
|
+
const latentImage = findNode(prompt, ["EmptyLatentImage"]);
|
|
700
|
+
const latentWidth = latentImage ? Number(latentImage.inputs.width) || 0 : 0;
|
|
701
|
+
const latentHeight = latentImage ? Number(latentImage.inputs.height) || 0 : 0;
|
|
702
|
+
const extraMeta = extractExtraMetadata(prompt);
|
|
703
|
+
const positiveText = clipPositiveText || extraMeta?.prompt || "";
|
|
704
|
+
const negativeText = clipNegativeText || extraMeta?.negativePrompt || "";
|
|
705
|
+
const width = latentWidth || extraMeta?.width || 0;
|
|
706
|
+
const height = latentHeight || extraMeta?.height || 0;
|
|
707
|
+
const metadata = {
|
|
708
|
+
type: "comfyui",
|
|
709
|
+
software: "comfyui",
|
|
710
|
+
prompt: positiveText,
|
|
711
|
+
negativePrompt: negativeText,
|
|
712
|
+
width,
|
|
713
|
+
height
|
|
714
|
+
};
|
|
715
|
+
const checkpoint = findNode(prompt, ["CheckpointLoader_Base"])?.inputs?.ckpt_name;
|
|
716
|
+
if (checkpoint) {
|
|
717
|
+
metadata.model = { name: String(checkpoint) };
|
|
718
|
+
} else if (extraMeta?.baseModel) {
|
|
719
|
+
metadata.model = { name: extraMeta.baseModel };
|
|
720
|
+
}
|
|
721
|
+
if (ksampler) {
|
|
722
|
+
metadata.sampling = {
|
|
723
|
+
seed: ksampler.inputs.seed,
|
|
724
|
+
steps: ksampler.inputs.steps,
|
|
725
|
+
cfg: ksampler.inputs.cfg,
|
|
726
|
+
sampler: ksampler.inputs.sampler_name,
|
|
727
|
+
scheduler: ksampler.inputs.scheduler
|
|
728
|
+
};
|
|
729
|
+
} else if (extraMeta) {
|
|
730
|
+
metadata.sampling = {
|
|
731
|
+
seed: extraMeta.seed,
|
|
732
|
+
steps: extraMeta.steps,
|
|
733
|
+
cfg: extraMeta.cfgScale,
|
|
734
|
+
sampler: extraMeta.sampler
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
const hiresModel = findNode(prompt, [
|
|
738
|
+
"HiresFix_ModelUpscale_UpscaleModelLoader",
|
|
739
|
+
"PostUpscale_ModelUpscale_UpscaleModelLoader"
|
|
740
|
+
])?.inputs;
|
|
741
|
+
const hiresScale = findNode(prompt, [
|
|
742
|
+
"HiresFix_ImageScale",
|
|
743
|
+
"PostUpscale_ImageScale"
|
|
744
|
+
])?.inputs;
|
|
745
|
+
const hiresSampler = findNode(prompt, ["HiresFix_Sampler"])?.inputs;
|
|
746
|
+
if (hiresModel && hiresScale) {
|
|
747
|
+
const hiresWidth = hiresScale.width;
|
|
748
|
+
const scale = latentWidth > 0 ? Math.round(hiresWidth / latentWidth * 100) / 100 : void 0;
|
|
749
|
+
if (hiresSampler) {
|
|
750
|
+
metadata.hires = {
|
|
751
|
+
upscaler: hiresModel.model_name,
|
|
752
|
+
scale,
|
|
753
|
+
steps: hiresSampler.steps,
|
|
754
|
+
denoise: hiresSampler.denoise
|
|
755
|
+
};
|
|
756
|
+
} else {
|
|
757
|
+
metadata.upscale = {
|
|
758
|
+
upscaler: hiresModel.model_name,
|
|
759
|
+
scale
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (extraMeta?.transformations) {
|
|
764
|
+
const upscaleTransform = extraMeta.transformations.find(
|
|
765
|
+
(t) => t.type === "upscale"
|
|
766
|
+
);
|
|
767
|
+
if (upscaleTransform) {
|
|
768
|
+
const originalWidth = extraMeta.width ?? width;
|
|
769
|
+
if (originalWidth > 0 && upscaleTransform.upscaleWidth) {
|
|
770
|
+
const scale = upscaleTransform.upscaleWidth / originalWidth;
|
|
771
|
+
metadata.upscale = {
|
|
772
|
+
scale: Math.round(scale * 100) / 100
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return Result.ok(metadata);
|
|
778
|
+
}
|
|
779
|
+
function findPromptJson(entryRecord) {
|
|
780
|
+
if (entryRecord.prompt) {
|
|
781
|
+
return entryRecord.prompt;
|
|
782
|
+
}
|
|
783
|
+
const candidates = [
|
|
784
|
+
entryRecord.Comment,
|
|
785
|
+
entryRecord.Description,
|
|
786
|
+
entryRecord.Make,
|
|
787
|
+
entryRecord.Prompt,
|
|
788
|
+
// save-image-extended uses this
|
|
789
|
+
entryRecord.Workflow
|
|
790
|
+
// Not a prompt, but may contain nodes info
|
|
791
|
+
];
|
|
792
|
+
for (const candidate of candidates) {
|
|
793
|
+
if (!candidate) continue;
|
|
794
|
+
if (candidate.startsWith("{")) {
|
|
795
|
+
const cleaned = candidate.replace(/\0+$/, "");
|
|
796
|
+
const parsed = parseJson(cleaned);
|
|
797
|
+
if (!parsed.ok) continue;
|
|
798
|
+
if (parsed.value.prompt && typeof parsed.value.prompt === "object") {
|
|
799
|
+
return JSON.stringify(parsed.value.prompt);
|
|
800
|
+
}
|
|
801
|
+
const values = Object.values(parsed.value);
|
|
802
|
+
if (values.some((v) => v && typeof v === "object" && "class_type" in v)) {
|
|
803
|
+
return candidate;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return void 0;
|
|
808
|
+
}
|
|
809
|
+
function findNode(prompt, keys) {
|
|
810
|
+
return Object.entries(prompt).find(([key]) => keys.includes(key))?.[1];
|
|
811
|
+
}
|
|
812
|
+
function extractText(node) {
|
|
813
|
+
return typeof node?.inputs.text === "string" ? node.inputs.text : "";
|
|
814
|
+
}
|
|
815
|
+
function extractExtraMetadata(prompt) {
|
|
816
|
+
const extraMetaField = prompt.extraMetadata;
|
|
817
|
+
if (typeof extraMetaField !== "string") return void 0;
|
|
818
|
+
const parsed = parseJson(extraMetaField);
|
|
819
|
+
return parsed.ok ? parsed.value : void 0;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/parsers/detect.ts
|
|
823
|
+
function detectSoftware(entries) {
|
|
824
|
+
const entryRecord = buildEntryRecord(entries);
|
|
825
|
+
const keywordResult = detectFromKeywords(entryRecord);
|
|
826
|
+
if (keywordResult) return keywordResult;
|
|
827
|
+
return detectFromContent(entryRecord);
|
|
828
|
+
}
|
|
829
|
+
function detectFromKeywords(entryRecord) {
|
|
830
|
+
if (entryRecord.Software === "NovelAI") {
|
|
831
|
+
return "novelai";
|
|
832
|
+
}
|
|
833
|
+
if ("invokeai_metadata" in entryRecord) {
|
|
834
|
+
return "invokeai";
|
|
835
|
+
}
|
|
836
|
+
if ("generation_data" in entryRecord) {
|
|
837
|
+
return "tensorart";
|
|
838
|
+
}
|
|
839
|
+
if ("smproj" in entryRecord) {
|
|
840
|
+
return "stability-matrix";
|
|
841
|
+
}
|
|
842
|
+
if ("negative_prompt" in entryRecord || "Negative Prompt" in entryRecord) {
|
|
843
|
+
return "easydiffusion";
|
|
844
|
+
}
|
|
845
|
+
const comment = entryRecord.Comment;
|
|
846
|
+
if (comment?.startsWith("{")) {
|
|
847
|
+
try {
|
|
848
|
+
const parsed = JSON.parse(comment);
|
|
849
|
+
if ("invokeai_metadata" in parsed) {
|
|
850
|
+
return "invokeai";
|
|
851
|
+
}
|
|
852
|
+
if ("prompt" in parsed && "workflow" in parsed) {
|
|
853
|
+
const workflow = parsed.workflow;
|
|
854
|
+
const prompt = parsed.prompt;
|
|
855
|
+
const isObject = typeof workflow === "object" || typeof prompt === "object";
|
|
856
|
+
const isJsonString = typeof workflow === "string" && workflow.startsWith("{") || typeof prompt === "string" && prompt.startsWith("{");
|
|
857
|
+
if (isObject || isJsonString) {
|
|
858
|
+
return "comfyui";
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if ("sui_image_params" in parsed) {
|
|
862
|
+
return "swarmui";
|
|
863
|
+
}
|
|
864
|
+
if ("prompt" in parsed && "parameters" in parsed) {
|
|
865
|
+
const params = String(parsed.parameters || "");
|
|
866
|
+
if (params.includes("sui_image_params") || params.includes("swarm_version")) {
|
|
867
|
+
return "swarmui";
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
function detectFromContent(entryRecord) {
|
|
876
|
+
if ("prompt" in entryRecord && "workflow" in entryRecord) {
|
|
877
|
+
return "comfyui";
|
|
878
|
+
}
|
|
879
|
+
const text = entryRecord.parameters ?? entryRecord.Comment ?? "";
|
|
880
|
+
if (!text) {
|
|
881
|
+
if ("workflow" in entryRecord) {
|
|
882
|
+
return "comfyui";
|
|
883
|
+
}
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
if (text.startsWith("{")) {
|
|
887
|
+
return detectFromJson(text);
|
|
888
|
+
}
|
|
889
|
+
return detectFromA1111Text(text);
|
|
890
|
+
}
|
|
891
|
+
function detectFromJson(json) {
|
|
892
|
+
if (json.includes("sui_image_params")) {
|
|
893
|
+
return "swarmui";
|
|
894
|
+
}
|
|
895
|
+
if (json.includes("civitai:") || json.includes('"resource-stack"')) {
|
|
896
|
+
return "civitai";
|
|
897
|
+
}
|
|
898
|
+
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\\"')) {
|
|
899
|
+
return "novelai";
|
|
900
|
+
}
|
|
901
|
+
if (json.includes('"Model"') && json.includes('"resolution"')) {
|
|
902
|
+
return "hf-space";
|
|
903
|
+
}
|
|
904
|
+
if (json.includes('"use_stable_diffusion_model"')) {
|
|
905
|
+
return "easydiffusion";
|
|
906
|
+
}
|
|
907
|
+
if (json.includes('"software":"RuinedFooocus"') || json.includes('"software": "RuinedFooocus"')) {
|
|
908
|
+
return "ruined-fooocus";
|
|
909
|
+
}
|
|
910
|
+
if (json.includes('"prompt"') && json.includes('"base_model"')) {
|
|
911
|
+
return "fooocus";
|
|
912
|
+
}
|
|
913
|
+
if (json.includes('"prompt"') || json.includes('"nodes"')) {
|
|
914
|
+
return "comfyui";
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
function detectFromA1111Text(text) {
|
|
919
|
+
if (text.includes("sui_image_params")) {
|
|
920
|
+
return "swarmui";
|
|
921
|
+
}
|
|
922
|
+
if (text.includes("swarm_version")) {
|
|
923
|
+
return "swarmui";
|
|
924
|
+
}
|
|
925
|
+
const versionMatch = text.match(/Version:\s*([^\s,]+)/);
|
|
926
|
+
if (versionMatch) {
|
|
927
|
+
const version = versionMatch[1];
|
|
928
|
+
if (version === "neo" || version?.startsWith("neo")) {
|
|
929
|
+
return "forge-neo";
|
|
930
|
+
}
|
|
931
|
+
if (version?.startsWith("f") && /^f\d/.test(version)) {
|
|
932
|
+
return "forge";
|
|
933
|
+
}
|
|
934
|
+
if (version === "ComfyUI") {
|
|
935
|
+
return "comfyui";
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (text.includes("App: SD.Next") || text.includes("App:SD.Next")) {
|
|
939
|
+
return "sd-next";
|
|
940
|
+
}
|
|
941
|
+
if (text.includes("Civitai resources:")) {
|
|
942
|
+
return "civitai";
|
|
943
|
+
}
|
|
944
|
+
if (text.includes("Steps:") && text.includes("Sampler:")) {
|
|
945
|
+
return "sd-webui";
|
|
946
|
+
}
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/parsers/easydiffusion.ts
|
|
951
|
+
function getValue(json, keyA, keyB) {
|
|
952
|
+
return json[keyA] ?? json[keyB];
|
|
953
|
+
}
|
|
954
|
+
function extractModelName(path) {
|
|
955
|
+
if (!path) return void 0;
|
|
956
|
+
const parts = path.replace(/\\/g, "/").split("/");
|
|
957
|
+
return parts[parts.length - 1];
|
|
958
|
+
}
|
|
959
|
+
function parseEasyDiffusion(entries) {
|
|
960
|
+
const entryRecord = buildEntryRecord(entries);
|
|
961
|
+
if (entryRecord.negative_prompt || entryRecord["Negative Prompt"]) {
|
|
962
|
+
return parseFromEntries(entryRecord);
|
|
963
|
+
}
|
|
964
|
+
const jsonText = (entryRecord.parameters?.startsWith("{") ? entryRecord.parameters : void 0) ?? (entryRecord.Comment?.startsWith("{") ? entryRecord.Comment : void 0);
|
|
965
|
+
if (!jsonText) {
|
|
966
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
967
|
+
}
|
|
968
|
+
const parsed = parseJson(jsonText);
|
|
969
|
+
if (!parsed.ok) {
|
|
970
|
+
return Result.error({
|
|
971
|
+
type: "parseError",
|
|
972
|
+
message: "Invalid JSON in Easy Diffusion metadata"
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
return parseFromJson(parsed.value);
|
|
976
|
+
}
|
|
977
|
+
function parseFromEntries(entryRecord) {
|
|
978
|
+
const prompt = entryRecord.prompt ?? entryRecord.Prompt ?? "";
|
|
979
|
+
const negativePrompt = entryRecord.negative_prompt ?? entryRecord["Negative Prompt"] ?? entryRecord.negative_prompt ?? "";
|
|
980
|
+
const modelPath = entryRecord.use_stable_diffusion_model ?? entryRecord["Stable Diffusion model"];
|
|
981
|
+
const width = Number(entryRecord.width ?? entryRecord.Width) || 0;
|
|
982
|
+
const height = Number(entryRecord.height ?? entryRecord.Height) || 0;
|
|
983
|
+
const metadata = {
|
|
984
|
+
type: "a1111",
|
|
985
|
+
software: "easydiffusion",
|
|
986
|
+
prompt: prompt.trim(),
|
|
987
|
+
negativePrompt: negativePrompt.trim(),
|
|
988
|
+
width,
|
|
989
|
+
height,
|
|
990
|
+
model: {
|
|
991
|
+
name: extractModelName(modelPath),
|
|
992
|
+
vae: entryRecord.use_vae_model ?? entryRecord["VAE model"]
|
|
993
|
+
},
|
|
994
|
+
sampling: {
|
|
995
|
+
sampler: entryRecord.sampler_name ?? entryRecord.Sampler,
|
|
996
|
+
steps: Number(entryRecord.num_inference_steps ?? entryRecord.Steps) || void 0,
|
|
997
|
+
cfg: Number(entryRecord.guidance_scale ?? entryRecord["Guidance Scale"]) || void 0,
|
|
998
|
+
seed: Number(entryRecord.seed ?? entryRecord.Seed) || void 0,
|
|
999
|
+
clipSkip: Number(entryRecord.clip_skip ?? entryRecord["Clip Skip"]) || void 0
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
return Result.ok(metadata);
|
|
1003
|
+
}
|
|
1004
|
+
function parseFromJson(json) {
|
|
1005
|
+
const prompt = getValue(json, "prompt", "Prompt") ?? "";
|
|
1006
|
+
const negativePrompt = getValue(json, "negative_prompt", "Negative Prompt") ?? "";
|
|
1007
|
+
const modelPath = getValue(
|
|
1008
|
+
json,
|
|
1009
|
+
"use_stable_diffusion_model",
|
|
1010
|
+
"Stable Diffusion model"
|
|
1011
|
+
);
|
|
1012
|
+
const width = getValue(json, "width", "Width") ?? 0;
|
|
1013
|
+
const height = getValue(json, "height", "Height") ?? 0;
|
|
1014
|
+
const metadata = {
|
|
1015
|
+
type: "a1111",
|
|
1016
|
+
software: "easydiffusion",
|
|
1017
|
+
prompt: prompt.trim(),
|
|
1018
|
+
negativePrompt: negativePrompt.trim(),
|
|
1019
|
+
width,
|
|
1020
|
+
height,
|
|
1021
|
+
model: {
|
|
1022
|
+
name: extractModelName(modelPath),
|
|
1023
|
+
vae: getValue(json, "use_vae_model", "VAE model")
|
|
1024
|
+
},
|
|
1025
|
+
sampling: {
|
|
1026
|
+
sampler: getValue(json, "sampler_name", "Sampler"),
|
|
1027
|
+
steps: getValue(json, "num_inference_steps", "Steps"),
|
|
1028
|
+
cfg: getValue(json, "guidance_scale", "Guidance Scale"),
|
|
1029
|
+
seed: getValue(json, "seed", "Seed"),
|
|
1030
|
+
clipSkip: getValue(json, "clip_skip", "Clip Skip")
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
return Result.ok(metadata);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// src/parsers/fooocus.ts
|
|
1037
|
+
function parseFooocus(entries) {
|
|
1038
|
+
const entryRecord = buildEntryRecord(entries);
|
|
1039
|
+
const jsonText = entryRecord.Comment ?? entryRecord.comment;
|
|
1040
|
+
if (!jsonText || !jsonText.startsWith("{")) {
|
|
1041
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1042
|
+
}
|
|
1043
|
+
const parsed = parseJson(jsonText);
|
|
1044
|
+
if (!parsed.ok) {
|
|
1045
|
+
return Result.error({
|
|
1046
|
+
type: "parseError",
|
|
1047
|
+
message: "Invalid JSON in Fooocus metadata"
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
const json = parsed.value;
|
|
1051
|
+
if (!json.base_model && !json.prompt) {
|
|
1052
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1053
|
+
}
|
|
1054
|
+
const metadata = {
|
|
1055
|
+
type: "a1111",
|
|
1056
|
+
software: "fooocus",
|
|
1057
|
+
prompt: json.prompt?.trim() ?? "",
|
|
1058
|
+
negativePrompt: json.negative_prompt?.trim() ?? "",
|
|
1059
|
+
width: json.width ?? 0,
|
|
1060
|
+
height: json.height ?? 0,
|
|
1061
|
+
model: {
|
|
1062
|
+
name: json.base_model
|
|
1063
|
+
},
|
|
1064
|
+
sampling: {
|
|
1065
|
+
sampler: json.sampler,
|
|
1066
|
+
scheduler: json.scheduler,
|
|
1067
|
+
steps: json.steps,
|
|
1068
|
+
cfg: json.cfg,
|
|
1069
|
+
seed: json.seed
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
return Result.ok(metadata);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/parsers/hf-space.ts
|
|
1076
|
+
function parseHfSpace(entries) {
|
|
1077
|
+
const entryRecord = buildEntryRecord(entries);
|
|
1078
|
+
const parametersText = entryRecord.parameters;
|
|
1079
|
+
if (!parametersText) {
|
|
1080
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1081
|
+
}
|
|
1082
|
+
const parsed = parseJson(parametersText);
|
|
1083
|
+
if (!parsed.ok) {
|
|
1084
|
+
return Result.error({
|
|
1085
|
+
type: "parseError",
|
|
1086
|
+
message: "Invalid JSON in parameters entry"
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
const json = parsed.value;
|
|
1090
|
+
const parseResolution = (res) => {
|
|
1091
|
+
const match = res?.match(/(\d+)\s*x\s*(\d+)/);
|
|
1092
|
+
return match?.[1] && match?.[2] ? {
|
|
1093
|
+
width: Number.parseInt(match[1], 10),
|
|
1094
|
+
height: Number.parseInt(match[2], 10)
|
|
1095
|
+
} : { width: 0, height: 0 };
|
|
1096
|
+
};
|
|
1097
|
+
const { width, height } = parseResolution(json.resolution);
|
|
1098
|
+
const metadata = {
|
|
1099
|
+
type: "a1111",
|
|
1100
|
+
software: "hf-space",
|
|
1101
|
+
prompt: json.prompt ?? "",
|
|
1102
|
+
negativePrompt: json.negative_prompt ?? "",
|
|
1103
|
+
width,
|
|
1104
|
+
height,
|
|
1105
|
+
model: {
|
|
1106
|
+
name: json.Model,
|
|
1107
|
+
hash: json["Model hash"]
|
|
1108
|
+
},
|
|
1109
|
+
sampling: {
|
|
1110
|
+
sampler: json.sampler,
|
|
1111
|
+
steps: json.num_inference_steps,
|
|
1112
|
+
cfg: json.guidance_scale,
|
|
1113
|
+
seed: json.seed
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
return Result.ok(metadata);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/parsers/invokeai.ts
|
|
1120
|
+
function extractInvokeAIMetadata(entryRecord) {
|
|
1121
|
+
if (entryRecord.invokeai_metadata) {
|
|
1122
|
+
return entryRecord.invokeai_metadata;
|
|
1123
|
+
}
|
|
1124
|
+
if (!entryRecord.Comment) {
|
|
1125
|
+
return void 0;
|
|
1126
|
+
}
|
|
1127
|
+
const commentParsed = parseJson(entryRecord.Comment);
|
|
1128
|
+
if (!commentParsed.ok || !("invokeai_metadata" in commentParsed.value)) {
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1131
|
+
return JSON.stringify(commentParsed.value.invokeai_metadata);
|
|
1132
|
+
}
|
|
1133
|
+
function parseInvokeAI(entries) {
|
|
1134
|
+
const entryRecord = buildEntryRecord(entries);
|
|
1135
|
+
const metadataText = extractInvokeAIMetadata(entryRecord);
|
|
1136
|
+
if (!metadataText) {
|
|
1137
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1138
|
+
}
|
|
1139
|
+
const parsed = parseJson(metadataText);
|
|
1140
|
+
if (!parsed.ok) {
|
|
1141
|
+
return Result.error({
|
|
1142
|
+
type: "parseError",
|
|
1143
|
+
message: "Invalid JSON in invokeai_metadata entry"
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
const data = parsed.value;
|
|
1147
|
+
const width = data.width ?? 0;
|
|
1148
|
+
const height = data.height ?? 0;
|
|
1149
|
+
const metadata = {
|
|
1150
|
+
type: "invokeai",
|
|
1151
|
+
software: "invokeai",
|
|
1152
|
+
prompt: data.positive_prompt ?? "",
|
|
1153
|
+
negativePrompt: data.negative_prompt ?? "",
|
|
1154
|
+
width,
|
|
1155
|
+
height
|
|
1156
|
+
};
|
|
1157
|
+
if (data.model?.name || data.model?.hash) {
|
|
1158
|
+
metadata.model = {
|
|
1159
|
+
name: data.model.name,
|
|
1160
|
+
hash: data.model.hash
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (data.seed !== void 0 || data.steps !== void 0 || data.cfg_scale !== void 0 || data.scheduler !== void 0) {
|
|
1164
|
+
metadata.sampling = {
|
|
1165
|
+
seed: data.seed,
|
|
1166
|
+
steps: data.steps,
|
|
1167
|
+
cfg: data.cfg_scale,
|
|
1168
|
+
sampler: data.scheduler
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
return Result.ok(metadata);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/parsers/novelai.ts
|
|
1175
|
+
function parseNovelAI(entries) {
|
|
1176
|
+
const entryRecord = buildEntryRecord(entries);
|
|
1177
|
+
if (entryRecord.Software !== "NovelAI") {
|
|
1178
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1179
|
+
}
|
|
1180
|
+
const commentText = entryRecord.Comment;
|
|
1181
|
+
if (!commentText) {
|
|
1182
|
+
return Result.error({
|
|
1183
|
+
type: "parseError",
|
|
1184
|
+
message: "Missing Comment entry"
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
const parsed = parseJson(commentText);
|
|
1188
|
+
if (!parsed.ok) {
|
|
1189
|
+
return Result.error({
|
|
1190
|
+
type: "parseError",
|
|
1191
|
+
message: "Invalid JSON in Comment entry"
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
const comment = parsed.value;
|
|
1195
|
+
const width = comment.width ?? 0;
|
|
1196
|
+
const height = comment.height ?? 0;
|
|
1197
|
+
const prompt = comment.v4_prompt?.caption?.base_caption ?? comment.prompt ?? "";
|
|
1198
|
+
const negativePrompt = comment.v4_negative_prompt?.caption?.base_caption ?? comment.uc ?? "";
|
|
1199
|
+
const metadata = {
|
|
1200
|
+
type: "novelai",
|
|
1201
|
+
software: "novelai",
|
|
1202
|
+
prompt,
|
|
1203
|
+
negativePrompt,
|
|
1204
|
+
width,
|
|
1205
|
+
height
|
|
1206
|
+
};
|
|
1207
|
+
if (comment.steps !== void 0 || comment.scale !== void 0 || comment.seed !== void 0 || comment.noise_schedule !== void 0 || comment.sampler !== void 0) {
|
|
1208
|
+
metadata.sampling = {
|
|
1209
|
+
steps: comment.steps,
|
|
1210
|
+
cfg: comment.scale,
|
|
1211
|
+
seed: comment.seed,
|
|
1212
|
+
sampler: comment.sampler,
|
|
1213
|
+
scheduler: comment.noise_schedule
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
const charCaptions = comment.v4_prompt?.caption?.char_captions;
|
|
1217
|
+
if (charCaptions && charCaptions.length > 0) {
|
|
1218
|
+
metadata.characterPrompts = charCaptions.map((cc) => {
|
|
1219
|
+
if (!cc.char_caption) return null;
|
|
1220
|
+
return {
|
|
1221
|
+
prompt: cc.char_caption,
|
|
1222
|
+
center: cc.centers?.[0]
|
|
1223
|
+
};
|
|
1224
|
+
}).filter((cp) => cp !== null);
|
|
1225
|
+
metadata.useCoords = comment.v4_prompt?.use_coords;
|
|
1226
|
+
metadata.useOrder = comment.v4_prompt?.use_order;
|
|
1227
|
+
}
|
|
1228
|
+
return Result.ok(metadata);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/parsers/ruined-fooocus.ts
|
|
1232
|
+
function parseRuinedFooocus(entries) {
|
|
1233
|
+
const entryRecord = buildEntryRecord(entries);
|
|
1234
|
+
const jsonText = entryRecord.parameters;
|
|
1235
|
+
if (!jsonText || !jsonText.startsWith("{")) {
|
|
1236
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1237
|
+
}
|
|
1238
|
+
const parsed = parseJson(jsonText);
|
|
1239
|
+
if (!parsed.ok) {
|
|
1240
|
+
return Result.error({
|
|
1241
|
+
type: "parseError",
|
|
1242
|
+
message: "Invalid JSON in Ruined Fooocus metadata"
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const json = parsed.value;
|
|
1246
|
+
if (json.software !== "RuinedFooocus") {
|
|
1247
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1248
|
+
}
|
|
1249
|
+
const metadata = {
|
|
1250
|
+
type: "a1111",
|
|
1251
|
+
software: "ruined-fooocus",
|
|
1252
|
+
prompt: json.Prompt?.trim() ?? "",
|
|
1253
|
+
negativePrompt: json.Negative?.trim() ?? "",
|
|
1254
|
+
width: json.width ?? 0,
|
|
1255
|
+
height: json.height ?? 0,
|
|
1256
|
+
model: {
|
|
1257
|
+
name: json.base_model_name,
|
|
1258
|
+
hash: json.base_model_hash
|
|
1259
|
+
},
|
|
1260
|
+
sampling: {
|
|
1261
|
+
sampler: json.sampler_name,
|
|
1262
|
+
scheduler: json.scheduler,
|
|
1263
|
+
steps: json.steps,
|
|
1264
|
+
cfg: json.cfg,
|
|
1265
|
+
seed: json.seed,
|
|
1266
|
+
clipSkip: json.clip_skip
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
return Result.ok(metadata);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/parsers/stability-matrix.ts
|
|
1273
|
+
function parseStabilityMatrix(entries) {
|
|
1274
|
+
const entryRecord = buildEntryRecord(entries);
|
|
1275
|
+
const comfyResult = parseComfyUI(entries);
|
|
1276
|
+
if (!comfyResult.ok) {
|
|
1277
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1278
|
+
}
|
|
1279
|
+
const metadata = {
|
|
1280
|
+
...comfyResult.value,
|
|
1281
|
+
type: "comfyui",
|
|
1282
|
+
software: "stability-matrix"
|
|
1283
|
+
};
|
|
1284
|
+
const jsonText = entryRecord["parameters-json"];
|
|
1285
|
+
if (jsonText) {
|
|
1286
|
+
const parsed = parseJson(jsonText);
|
|
1287
|
+
if (parsed.ok) {
|
|
1288
|
+
const data = parsed.value;
|
|
1289
|
+
if (data.PositivePrompt !== void 0) {
|
|
1290
|
+
metadata.prompt = data.PositivePrompt;
|
|
1291
|
+
}
|
|
1292
|
+
if (data.NegativePrompt !== void 0) {
|
|
1293
|
+
metadata.negativePrompt = data.NegativePrompt;
|
|
1294
|
+
}
|
|
1295
|
+
if (data.ModelName !== void 0 || data.ModelHash !== void 0) {
|
|
1296
|
+
metadata.model = {
|
|
1297
|
+
name: data.ModelName,
|
|
1298
|
+
hash: data.ModelHash
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return Result.ok(metadata);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/parsers/swarmui.ts
|
|
1307
|
+
function extractSwarmUIParameters(entryRecord) {
|
|
1308
|
+
if (entryRecord.parameters) {
|
|
1309
|
+
return entryRecord.parameters;
|
|
1310
|
+
}
|
|
1311
|
+
if (!entryRecord.Comment) {
|
|
1312
|
+
return void 0;
|
|
1313
|
+
}
|
|
1314
|
+
const commentParsed = parseJson(entryRecord.Comment);
|
|
1315
|
+
if (!commentParsed.ok) {
|
|
1316
|
+
return void 0;
|
|
1317
|
+
}
|
|
1318
|
+
if ("sui_image_params" in commentParsed.value) {
|
|
1319
|
+
return entryRecord.Comment;
|
|
1320
|
+
}
|
|
1321
|
+
return void 0;
|
|
1322
|
+
}
|
|
1323
|
+
function parseSwarmUI(entries) {
|
|
1324
|
+
const entryRecord = buildEntryRecord(entries);
|
|
1325
|
+
const parametersText = extractSwarmUIParameters(entryRecord);
|
|
1326
|
+
if (!parametersText) {
|
|
1327
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1328
|
+
}
|
|
1329
|
+
const parsed = parseJson(parametersText);
|
|
1330
|
+
if (!parsed.ok) {
|
|
1331
|
+
return Result.error({
|
|
1332
|
+
type: "parseError",
|
|
1333
|
+
message: "Invalid JSON in parameters entry"
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
const params = parsed.value.sui_image_params;
|
|
1337
|
+
if (!params) {
|
|
1338
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1339
|
+
}
|
|
1340
|
+
const width = params.width ?? 0;
|
|
1341
|
+
const height = params.height ?? 0;
|
|
1342
|
+
const metadata = {
|
|
1343
|
+
type: "swarmui",
|
|
1344
|
+
software: "swarmui",
|
|
1345
|
+
prompt: params.prompt ?? "",
|
|
1346
|
+
negativePrompt: params.negativeprompt ?? "",
|
|
1347
|
+
width,
|
|
1348
|
+
height
|
|
1349
|
+
};
|
|
1350
|
+
if (params.model) {
|
|
1351
|
+
metadata.model = {
|
|
1352
|
+
name: params.model
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
if (params.seed !== void 0 || params.steps !== void 0 || params.cfgscale !== void 0 || params.sampler !== void 0 || params.scheduler !== void 0) {
|
|
1356
|
+
metadata.sampling = {
|
|
1357
|
+
seed: params.seed,
|
|
1358
|
+
steps: params.steps,
|
|
1359
|
+
cfg: params.cfgscale,
|
|
1360
|
+
sampler: params.sampler,
|
|
1361
|
+
scheduler: params.scheduler
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
if (params.refinerupscale !== void 0 || params.refinerupscalemethod !== void 0 || params.refinercontrolpercentage !== void 0) {
|
|
1365
|
+
metadata.hires = {
|
|
1366
|
+
scale: params.refinerupscale,
|
|
1367
|
+
upscaler: params.refinerupscalemethod,
|
|
1368
|
+
denoise: params.refinercontrolpercentage
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
return Result.ok(metadata);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/parsers/tensorart.ts
|
|
1375
|
+
function parseTensorArt(entries) {
|
|
1376
|
+
const entryRecord = buildEntryRecord(entries);
|
|
1377
|
+
const dataText = entryRecord.generation_data;
|
|
1378
|
+
if (!dataText) {
|
|
1379
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1380
|
+
}
|
|
1381
|
+
const cleanedText = dataText.replace(/\0+$/, "");
|
|
1382
|
+
const parsed = parseJson(cleanedText);
|
|
1383
|
+
if (!parsed.ok) {
|
|
1384
|
+
return Result.error({
|
|
1385
|
+
type: "parseError",
|
|
1386
|
+
message: "Invalid JSON in generation_data entry"
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
const data = parsed.value;
|
|
1390
|
+
const width = data.width ?? 0;
|
|
1391
|
+
const height = data.height ?? 0;
|
|
1392
|
+
const metadata = {
|
|
1393
|
+
type: "comfyui",
|
|
1394
|
+
software: "tensorart",
|
|
1395
|
+
prompt: data.prompt ?? "",
|
|
1396
|
+
negativePrompt: data.negativePrompt ?? "",
|
|
1397
|
+
width,
|
|
1398
|
+
height
|
|
1399
|
+
};
|
|
1400
|
+
if (data.baseModel?.modelFileName || data.baseModel?.hash) {
|
|
1401
|
+
metadata.model = {
|
|
1402
|
+
name: data.baseModel.modelFileName,
|
|
1403
|
+
hash: data.baseModel.hash
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
if (data.seed !== void 0 || data.steps !== void 0 || data.cfgScale !== void 0 || data.clipSkip !== void 0) {
|
|
1407
|
+
metadata.sampling = {
|
|
1408
|
+
seed: data.seed ? Number(data.seed) : void 0,
|
|
1409
|
+
steps: data.steps,
|
|
1410
|
+
cfg: data.cfgScale,
|
|
1411
|
+
clipSkip: data.clipSkip
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
return Result.ok(metadata);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// src/parsers/index.ts
|
|
1418
|
+
function parseMetadata(entries) {
|
|
1419
|
+
const software = detectSoftware(entries);
|
|
1420
|
+
switch (software) {
|
|
1421
|
+
case "novelai":
|
|
1422
|
+
return parseNovelAI(entries);
|
|
1423
|
+
case "sd-webui":
|
|
1424
|
+
case "sd-next":
|
|
1425
|
+
case "forge":
|
|
1426
|
+
case "forge-neo":
|
|
1427
|
+
return parseA1111(entries);
|
|
1428
|
+
case "hf-space":
|
|
1429
|
+
return parseHfSpace(entries);
|
|
1430
|
+
case "civitai": {
|
|
1431
|
+
const comfyResult = parseComfyUI(entries);
|
|
1432
|
+
if (comfyResult.ok) return comfyResult;
|
|
1433
|
+
return parseA1111(entries);
|
|
1434
|
+
}
|
|
1435
|
+
case "comfyui": {
|
|
1436
|
+
const comfyResult = parseComfyUI(entries);
|
|
1437
|
+
if (comfyResult.ok) return comfyResult;
|
|
1438
|
+
return parseA1111(entries);
|
|
1439
|
+
}
|
|
1440
|
+
case "invokeai":
|
|
1441
|
+
return parseInvokeAI(entries);
|
|
1442
|
+
case "swarmui":
|
|
1443
|
+
return parseSwarmUI(entries);
|
|
1444
|
+
case "tensorart":
|
|
1445
|
+
return parseTensorArt(entries);
|
|
1446
|
+
case "stability-matrix":
|
|
1447
|
+
return parseStabilityMatrix(entries);
|
|
1448
|
+
case "easydiffusion":
|
|
1449
|
+
return parseEasyDiffusion(entries);
|
|
1450
|
+
case "fooocus":
|
|
1451
|
+
return parseFooocus(entries);
|
|
1452
|
+
case "ruined-fooocus":
|
|
1453
|
+
return parseRuinedFooocus(entries);
|
|
1454
|
+
default: {
|
|
1455
|
+
const a1111Result = parseA1111(entries);
|
|
1456
|
+
if (a1111Result.ok) return a1111Result;
|
|
1457
|
+
const comfyResult = parseComfyUI(entries);
|
|
1458
|
+
if (comfyResult.ok) return comfyResult;
|
|
1459
|
+
const invokeResult = parseInvokeAI(entries);
|
|
1460
|
+
if (invokeResult.ok) return invokeResult;
|
|
1461
|
+
const swarmResult = parseSwarmUI(entries);
|
|
1462
|
+
if (swarmResult.ok) return swarmResult;
|
|
1463
|
+
const tensorResult = parseTensorArt(entries);
|
|
1464
|
+
if (tensorResult.ok) return tensorResult;
|
|
1465
|
+
const stabilityResult = parseStabilityMatrix(entries);
|
|
1466
|
+
if (stabilityResult.ok) return stabilityResult;
|
|
1467
|
+
const novelaiResult = parseNovelAI(entries);
|
|
1468
|
+
if (novelaiResult.ok) return novelaiResult;
|
|
1469
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/utils/binary.ts
|
|
1475
|
+
function readUint24LE(data, offset) {
|
|
1476
|
+
return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8 | (data[offset + 2] ?? 0) << 16;
|
|
1477
|
+
}
|
|
1478
|
+
function readUint32BE(data, offset) {
|
|
1479
|
+
return (data[offset] ?? 0) << 24 | (data[offset + 1] ?? 0) << 16 | (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
|
|
1480
|
+
}
|
|
1481
|
+
function readUint32LE(data, offset) {
|
|
1482
|
+
return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8 | (data[offset + 2] ?? 0) << 16 | (data[offset + 3] ?? 0) << 24;
|
|
1483
|
+
}
|
|
1484
|
+
function writeUint32BE(data, offset, value) {
|
|
1485
|
+
data[offset] = value >>> 24 & 255;
|
|
1486
|
+
data[offset + 1] = value >>> 16 & 255;
|
|
1487
|
+
data[offset + 2] = value >>> 8 & 255;
|
|
1488
|
+
data[offset + 3] = value & 255;
|
|
1489
|
+
}
|
|
1490
|
+
function readChunkType(data, offset) {
|
|
1491
|
+
return String.fromCharCode(
|
|
1492
|
+
data[offset] ?? 0,
|
|
1493
|
+
data[offset + 1] ?? 0,
|
|
1494
|
+
data[offset + 2] ?? 0,
|
|
1495
|
+
data[offset + 3] ?? 0
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
function readUint16(data, offset, isLittleEndian) {
|
|
1499
|
+
if (isLittleEndian) {
|
|
1500
|
+
return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8;
|
|
1501
|
+
}
|
|
1502
|
+
return (data[offset] ?? 0) << 8 | (data[offset + 1] ?? 0);
|
|
1503
|
+
}
|
|
1504
|
+
function readUint32(data, offset, isLittleEndian) {
|
|
1505
|
+
if (isLittleEndian) {
|
|
1506
|
+
return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8 | (data[offset + 2] ?? 0) << 16 | (data[offset + 3] ?? 0) << 24;
|
|
1507
|
+
}
|
|
1508
|
+
return (data[offset] ?? 0) << 24 | (data[offset + 1] ?? 0) << 16 | (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
|
|
1509
|
+
}
|
|
1510
|
+
function arraysEqual(a, b) {
|
|
1511
|
+
if (a.length !== b.length) return false;
|
|
1512
|
+
for (let i = 0; i < a.length; i++) {
|
|
1513
|
+
if (a[i] !== b[i]) return false;
|
|
1514
|
+
}
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
function writeUint16(data, offset, value, isLittleEndian) {
|
|
1518
|
+
if (isLittleEndian) {
|
|
1519
|
+
data[offset] = value & 255;
|
|
1520
|
+
data[offset + 1] = value >>> 8 & 255;
|
|
1521
|
+
} else {
|
|
1522
|
+
data[offset] = value >>> 8 & 255;
|
|
1523
|
+
data[offset + 1] = value & 255;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function writeUint32(data, offset, value, isLittleEndian) {
|
|
1527
|
+
if (isLittleEndian) {
|
|
1528
|
+
data[offset] = value & 255;
|
|
1529
|
+
data[offset + 1] = value >>> 8 & 255;
|
|
1530
|
+
data[offset + 2] = value >>> 16 & 255;
|
|
1531
|
+
data[offset + 3] = value >>> 24 & 255;
|
|
1532
|
+
} else {
|
|
1533
|
+
data[offset] = value >>> 24 & 255;
|
|
1534
|
+
data[offset + 1] = value >>> 16 & 255;
|
|
1535
|
+
data[offset + 2] = value >>> 8 & 255;
|
|
1536
|
+
data[offset + 3] = value & 255;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
function writeUint32LE(data, offset, value) {
|
|
1540
|
+
data[offset] = value & 255;
|
|
1541
|
+
data[offset + 1] = value >>> 8 & 255;
|
|
1542
|
+
data[offset + 2] = value >>> 16 & 255;
|
|
1543
|
+
data[offset + 3] = value >>> 24 & 255;
|
|
1544
|
+
}
|
|
1545
|
+
function isPng(data) {
|
|
1546
|
+
if (data.length < 8) return false;
|
|
1547
|
+
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;
|
|
1548
|
+
}
|
|
1549
|
+
function isJpeg(data) {
|
|
1550
|
+
if (data.length < 2) return false;
|
|
1551
|
+
return data[0] === 255 && data[1] === 216;
|
|
1552
|
+
}
|
|
1553
|
+
function isWebp(data) {
|
|
1554
|
+
if (data.length < 12) return false;
|
|
1555
|
+
return data[0] === 82 && // R
|
|
1556
|
+
data[1] === 73 && // I
|
|
1557
|
+
data[2] === 70 && // F
|
|
1558
|
+
data[3] === 70 && // F
|
|
1559
|
+
data[8] === 87 && // W
|
|
1560
|
+
data[9] === 69 && // E
|
|
1561
|
+
data[10] === 66 && // B
|
|
1562
|
+
data[11] === 80;
|
|
1563
|
+
}
|
|
1564
|
+
function detectFormat(data) {
|
|
1565
|
+
if (isPng(data)) return "png";
|
|
1566
|
+
if (isJpeg(data)) return "jpeg";
|
|
1567
|
+
if (isWebp(data)) return "webp";
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/utils/exif-constants.ts
|
|
1572
|
+
var USER_COMMENT_TAG = 37510;
|
|
1573
|
+
var IMAGE_DESCRIPTION_TAG = 270;
|
|
1574
|
+
var MAKE_TAG = 271;
|
|
1575
|
+
var EXIF_IFD_POINTER_TAG = 34665;
|
|
1576
|
+
|
|
1577
|
+
// src/readers/exif.ts
|
|
1578
|
+
function parseExifMetadataSegments(exifData) {
|
|
1579
|
+
if (exifData.length < 8) return [];
|
|
1580
|
+
const isLittleEndian = exifData[0] === 73 && exifData[1] === 73;
|
|
1581
|
+
const isBigEndian = exifData[0] === 77 && exifData[1] === 77;
|
|
1582
|
+
if (!isLittleEndian && !isBigEndian) return [];
|
|
1583
|
+
const magic = readUint16(exifData, 2, isLittleEndian);
|
|
1584
|
+
if (magic !== 42) return [];
|
|
1585
|
+
const ifd0Offset = readUint32(exifData, 4, isLittleEndian);
|
|
1586
|
+
const ifd0Segments = extractTagsFromIfd(exifData, ifd0Offset, isLittleEndian);
|
|
1587
|
+
const exifIfdOffset = findExifIfdOffset(exifData, ifd0Offset, isLittleEndian);
|
|
1588
|
+
const exifIfdSegments = exifIfdOffset !== null ? extractTagsFromIfd(exifData, exifIfdOffset, isLittleEndian) : [];
|
|
1589
|
+
return [...ifd0Segments, ...exifIfdSegments];
|
|
1590
|
+
}
|
|
1591
|
+
function extractTagsFromIfd(data, ifdOffset, isLittleEndian) {
|
|
1592
|
+
const segments = [];
|
|
1593
|
+
if (ifdOffset + 2 > data.length) return segments;
|
|
1594
|
+
const entryCount = readUint16(data, ifdOffset, isLittleEndian);
|
|
1595
|
+
let offset = ifdOffset + 2;
|
|
1596
|
+
for (let i = 0; i < entryCount; i++) {
|
|
1597
|
+
if (offset + 12 > data.length) return segments;
|
|
1598
|
+
const tag = readUint16(data, offset, isLittleEndian);
|
|
1599
|
+
const type = readUint16(data, offset + 2, isLittleEndian);
|
|
1600
|
+
const count = readUint32(data, offset + 4, isLittleEndian);
|
|
1601
|
+
const typeSize = getTypeSize(type);
|
|
1602
|
+
const dataSize = count * typeSize;
|
|
1603
|
+
let valueOffset;
|
|
1604
|
+
if (dataSize <= 4) {
|
|
1605
|
+
valueOffset = offset + 8;
|
|
1606
|
+
} else {
|
|
1607
|
+
valueOffset = readUint32(data, offset + 8, isLittleEndian);
|
|
1608
|
+
}
|
|
1609
|
+
if (valueOffset + dataSize > data.length) {
|
|
1610
|
+
offset += 12;
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
const tagData = data.slice(valueOffset, valueOffset + dataSize);
|
|
1614
|
+
if (tag === IMAGE_DESCRIPTION_TAG) {
|
|
1615
|
+
const text = decodeAsciiString(tagData);
|
|
1616
|
+
if (text) {
|
|
1617
|
+
const prefix = extractPrefix(text);
|
|
1618
|
+
segments.push({
|
|
1619
|
+
source: { type: "exifImageDescription", prefix: prefix ?? void 0 },
|
|
1620
|
+
data: prefix ? text.slice(prefix.length + 2) : text
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
} else if (tag === MAKE_TAG) {
|
|
1624
|
+
const text = decodeAsciiString(tagData);
|
|
1625
|
+
if (text) {
|
|
1626
|
+
const prefix = extractPrefix(text);
|
|
1627
|
+
segments.push({
|
|
1628
|
+
source: { type: "exifMake", prefix: prefix ?? void 0 },
|
|
1629
|
+
data: prefix ? text.slice(prefix.length + 2) : text
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
} else if (tag === USER_COMMENT_TAG) {
|
|
1633
|
+
const text = decodeUserComment(tagData);
|
|
1634
|
+
if (text) {
|
|
1635
|
+
segments.push({
|
|
1636
|
+
source: { type: "exifUserComment" },
|
|
1637
|
+
data: text
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
offset += 12;
|
|
1642
|
+
}
|
|
1643
|
+
return segments;
|
|
1644
|
+
}
|
|
1645
|
+
function extractPrefix(text) {
|
|
1646
|
+
const match = text.match(/^([A-Za-z]+):\s/);
|
|
1647
|
+
return match?.[1] ?? null;
|
|
1648
|
+
}
|
|
1649
|
+
function getTypeSize(type) {
|
|
1650
|
+
switch (type) {
|
|
1651
|
+
case 1:
|
|
1652
|
+
return 1;
|
|
1653
|
+
// BYTE
|
|
1654
|
+
case 2:
|
|
1655
|
+
return 1;
|
|
1656
|
+
// ASCII
|
|
1657
|
+
case 3:
|
|
1658
|
+
return 2;
|
|
1659
|
+
// SHORT
|
|
1660
|
+
case 4:
|
|
1661
|
+
return 4;
|
|
1662
|
+
// LONG
|
|
1663
|
+
case 5:
|
|
1664
|
+
return 8;
|
|
1665
|
+
// RATIONAL
|
|
1666
|
+
case 7:
|
|
1667
|
+
return 1;
|
|
1668
|
+
// UNDEFINED
|
|
1669
|
+
default:
|
|
1670
|
+
return 1;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function decodeAsciiString(data) {
|
|
1674
|
+
try {
|
|
1675
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
1676
|
+
let text = decoder.decode(data);
|
|
1677
|
+
if (text.endsWith("\0")) {
|
|
1678
|
+
text = text.slice(0, -1);
|
|
1679
|
+
}
|
|
1680
|
+
return text.trim() || null;
|
|
1681
|
+
} catch {
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function findExifIfdOffset(data, ifdOffset, isLittleEndian) {
|
|
1686
|
+
if (ifdOffset + 2 > data.length) return null;
|
|
1687
|
+
const entryCount = readUint16(data, ifdOffset, isLittleEndian);
|
|
1688
|
+
let offset = ifdOffset + 2;
|
|
1689
|
+
for (let i = 0; i < entryCount; i++) {
|
|
1690
|
+
if (offset + 12 > data.length) return null;
|
|
1691
|
+
const tag = readUint16(data, offset, isLittleEndian);
|
|
1692
|
+
if (tag === EXIF_IFD_POINTER_TAG) {
|
|
1693
|
+
return readUint32(data, offset + 8, isLittleEndian);
|
|
1694
|
+
}
|
|
1695
|
+
offset += 12;
|
|
1696
|
+
}
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
function decodeUserComment(data) {
|
|
1700
|
+
if (data.length < 8) return null;
|
|
1701
|
+
if (data[0] === 85 && // U
|
|
1702
|
+
data[1] === 78 && // N
|
|
1703
|
+
data[2] === 73 && // I
|
|
1704
|
+
data[3] === 67 && // C
|
|
1705
|
+
data[4] === 79 && // O
|
|
1706
|
+
data[5] === 68 && // D
|
|
1707
|
+
data[6] === 69 && // E
|
|
1708
|
+
data[7] === 0) {
|
|
1709
|
+
const textData = data.slice(8);
|
|
1710
|
+
if (textData.length >= 2) {
|
|
1711
|
+
const isLikelyLE = textData[0] !== 0 && textData[1] === 0;
|
|
1712
|
+
return isLikelyLE ? decodeUtf16LE(textData) : decodeUtf16BE(textData);
|
|
1713
|
+
}
|
|
1714
|
+
return decodeUtf16BE(textData);
|
|
1715
|
+
}
|
|
1716
|
+
if (data[0] === 65 && // A
|
|
1717
|
+
data[1] === 83 && // S
|
|
1718
|
+
data[2] === 67 && // C
|
|
1719
|
+
data[3] === 73 && // I
|
|
1720
|
+
data[4] === 73 && // I
|
|
1721
|
+
data[5] === 0 && // NULL
|
|
1722
|
+
data[6] === 0 && // NULL
|
|
1723
|
+
data[7] === 0) {
|
|
1724
|
+
return decodeAscii(data.slice(8));
|
|
1725
|
+
}
|
|
1726
|
+
try {
|
|
1727
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1728
|
+
let result = decoder.decode(data);
|
|
1729
|
+
if (result.endsWith("\0")) {
|
|
1730
|
+
result = result.slice(0, -1);
|
|
1731
|
+
}
|
|
1732
|
+
return result;
|
|
1733
|
+
} catch {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function decodeUtf16BE(data) {
|
|
1738
|
+
const chars = [];
|
|
1739
|
+
for (let i = 0; i < data.length - 1; i += 2) {
|
|
1740
|
+
const code = (data[i] ?? 0) << 8 | (data[i + 1] ?? 0);
|
|
1741
|
+
if (code === 0) break;
|
|
1742
|
+
chars.push(String.fromCharCode(code));
|
|
1743
|
+
}
|
|
1744
|
+
return chars.join("");
|
|
1745
|
+
}
|
|
1746
|
+
function decodeUtf16LE(data) {
|
|
1747
|
+
const chars = [];
|
|
1748
|
+
for (let i = 0; i < data.length - 1; i += 2) {
|
|
1749
|
+
const code = (data[i] ?? 0) | (data[i + 1] ?? 0) << 8;
|
|
1750
|
+
if (code === 0) break;
|
|
1751
|
+
chars.push(String.fromCharCode(code));
|
|
1752
|
+
}
|
|
1753
|
+
return chars.join("");
|
|
1754
|
+
}
|
|
1755
|
+
function decodeAscii(data) {
|
|
1756
|
+
const chars = [];
|
|
1757
|
+
for (let i = 0; i < data.length; i++) {
|
|
1758
|
+
if (data[i] === 0) break;
|
|
1759
|
+
chars.push(String.fromCharCode(data[i] ?? 0));
|
|
1760
|
+
}
|
|
1761
|
+
return chars.join("");
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// src/readers/jpeg.ts
|
|
1765
|
+
var APP1_MARKER = 225;
|
|
1766
|
+
var COM_MARKER = 254;
|
|
1767
|
+
var EXIF_HEADER = new Uint8Array([69, 120, 105, 102, 0, 0]);
|
|
1768
|
+
function readJpegMetadata(data) {
|
|
1769
|
+
if (!isJpeg(data)) {
|
|
1770
|
+
return Result.error({ type: "invalidSignature" });
|
|
1771
|
+
}
|
|
1772
|
+
const segments = [];
|
|
1773
|
+
const app1 = findApp1Segment(data);
|
|
1774
|
+
if (app1) {
|
|
1775
|
+
const exifData = data.slice(app1.offset, app1.offset + app1.length);
|
|
1776
|
+
const exifSegments = parseExifMetadataSegments(exifData);
|
|
1777
|
+
segments.push(...exifSegments);
|
|
1778
|
+
}
|
|
1779
|
+
const comSegment = findComSegment(data);
|
|
1780
|
+
if (comSegment) {
|
|
1781
|
+
const comData = data.slice(
|
|
1782
|
+
comSegment.offset,
|
|
1783
|
+
comSegment.offset + comSegment.length
|
|
1784
|
+
);
|
|
1785
|
+
const comText = decodeComSegment(comData);
|
|
1786
|
+
if (comText !== null) {
|
|
1787
|
+
segments.push({
|
|
1788
|
+
source: { type: "jpegCom" },
|
|
1789
|
+
data: comText
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
return Result.ok(segments);
|
|
1794
|
+
}
|
|
1795
|
+
function findApp1Segment(data) {
|
|
1796
|
+
let offset = 2;
|
|
1797
|
+
while (offset < data.length - 4) {
|
|
1798
|
+
if (data[offset] !== 255) {
|
|
1799
|
+
offset++;
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
const marker = data[offset + 1];
|
|
1803
|
+
if (marker === 255) {
|
|
1804
|
+
offset++;
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
|
|
1808
|
+
if (marker === APP1_MARKER) {
|
|
1809
|
+
const headerStart = offset + 4;
|
|
1810
|
+
if (headerStart + 6 <= data.length) {
|
|
1811
|
+
const header = data.slice(headerStart, headerStart + 6);
|
|
1812
|
+
if (arraysEqual(header, EXIF_HEADER)) {
|
|
1813
|
+
return {
|
|
1814
|
+
offset: headerStart + 6,
|
|
1815
|
+
length: length - 8
|
|
1816
|
+
// Subtract length bytes and Exif header
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
offset += 2 + length;
|
|
1822
|
+
if (marker === 218 || marker === 217) {
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
function findComSegment(data) {
|
|
1829
|
+
let offset = 2;
|
|
1830
|
+
while (offset < data.length - 4) {
|
|
1831
|
+
if (data[offset] !== 255) {
|
|
1832
|
+
offset++;
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
const marker = data[offset + 1];
|
|
1836
|
+
if (marker === 255) {
|
|
1837
|
+
offset++;
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
|
|
1841
|
+
if (marker === COM_MARKER) {
|
|
1842
|
+
return {
|
|
1843
|
+
offset: offset + 4,
|
|
1844
|
+
length: length - 2
|
|
1845
|
+
// Subtract length bytes only
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
offset += 2 + length;
|
|
1849
|
+
if (marker === 218 || marker === 217) {
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return null;
|
|
1854
|
+
}
|
|
1855
|
+
function decodeComSegment(data) {
|
|
1856
|
+
try {
|
|
1857
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1858
|
+
return decoder.decode(data);
|
|
1859
|
+
} catch {
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/readers/png.ts
|
|
1865
|
+
function readPngMetadata(data) {
|
|
1866
|
+
if (!isPng(data)) {
|
|
1867
|
+
return Result.error({ type: "invalidSignature" });
|
|
1868
|
+
}
|
|
1869
|
+
const chunksResult = extractTextChunks(data);
|
|
1870
|
+
if (!chunksResult.ok) {
|
|
1871
|
+
return chunksResult;
|
|
1872
|
+
}
|
|
1873
|
+
return Result.ok(chunksResult.value);
|
|
1874
|
+
}
|
|
1875
|
+
var PNG_SIGNATURE_LENGTH = 8;
|
|
1876
|
+
function extractTextChunks(data) {
|
|
1877
|
+
const chunks = [];
|
|
1878
|
+
let offset = PNG_SIGNATURE_LENGTH;
|
|
1879
|
+
while (offset < data.length) {
|
|
1880
|
+
if (offset + 4 > data.length) {
|
|
1881
|
+
return Result.error({
|
|
1882
|
+
type: "corruptedChunk",
|
|
1883
|
+
message: "Unexpected end of file while reading chunk length"
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
const length = readUint32BE(data, offset);
|
|
1887
|
+
offset += 4;
|
|
1888
|
+
if (offset + 4 > data.length) {
|
|
1889
|
+
return Result.error({
|
|
1890
|
+
type: "corruptedChunk",
|
|
1891
|
+
message: "Unexpected end of file while reading chunk type"
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const chunkType = readChunkType(data, offset);
|
|
1895
|
+
offset += 4;
|
|
1896
|
+
if (offset + length > data.length) {
|
|
1897
|
+
return Result.error({
|
|
1898
|
+
type: "corruptedChunk",
|
|
1899
|
+
message: `Unexpected end of file while reading chunk data (${chunkType})`
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
const chunkData = data.slice(offset, offset + length);
|
|
1903
|
+
offset += length;
|
|
1904
|
+
offset += 4;
|
|
1905
|
+
if (chunkType === "tEXt") {
|
|
1906
|
+
const parsed = parseTExtChunk(chunkData);
|
|
1907
|
+
if (parsed) {
|
|
1908
|
+
chunks.push(parsed);
|
|
1909
|
+
}
|
|
1910
|
+
} else if (chunkType === "iTXt") {
|
|
1911
|
+
const parsed = parseITXtChunk(chunkData);
|
|
1912
|
+
if (parsed) {
|
|
1913
|
+
chunks.push(parsed);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (chunkType === "IEND") {
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return Result.ok(chunks);
|
|
1921
|
+
}
|
|
1922
|
+
function parseTExtChunk(data) {
|
|
1923
|
+
const nullIndex = data.indexOf(0);
|
|
1924
|
+
if (nullIndex === -1) {
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
const keyword = latin1Decode(data.slice(0, nullIndex));
|
|
1928
|
+
const textData = data.slice(nullIndex + 1);
|
|
1929
|
+
const text = tryUtf8Decode(textData) ?? latin1Decode(textData);
|
|
1930
|
+
return { type: "tEXt", keyword, text };
|
|
1931
|
+
}
|
|
1932
|
+
function tryUtf8Decode(data) {
|
|
1933
|
+
try {
|
|
1934
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
1935
|
+
} catch {
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function parseITXtChunk(data) {
|
|
1940
|
+
let offset = 0;
|
|
1941
|
+
const keywordEnd = findNull(data, offset);
|
|
1942
|
+
if (keywordEnd === -1) return null;
|
|
1943
|
+
const keyword = utf8Decode(data.slice(offset, keywordEnd));
|
|
1944
|
+
offset = keywordEnd + 1;
|
|
1945
|
+
if (offset >= data.length) return null;
|
|
1946
|
+
const compressionFlag = data[offset] ?? 0;
|
|
1947
|
+
offset += 1;
|
|
1948
|
+
if (offset >= data.length) return null;
|
|
1949
|
+
const compressionMethod = data[offset] ?? 0;
|
|
1950
|
+
offset += 1;
|
|
1951
|
+
const langEnd = findNull(data, offset);
|
|
1952
|
+
if (langEnd === -1) return null;
|
|
1953
|
+
const languageTag = utf8Decode(data.slice(offset, langEnd));
|
|
1954
|
+
offset = langEnd + 1;
|
|
1955
|
+
const transEnd = findNull(data, offset);
|
|
1956
|
+
if (transEnd === -1) return null;
|
|
1957
|
+
const translatedKeyword = utf8Decode(data.slice(offset, transEnd));
|
|
1958
|
+
offset = transEnd + 1;
|
|
1959
|
+
let text;
|
|
1960
|
+
if (compressionFlag === 1) {
|
|
1961
|
+
const decompressed = decompressZlib(data.slice(offset));
|
|
1962
|
+
if (!decompressed) return null;
|
|
1963
|
+
text = utf8Decode(decompressed);
|
|
1964
|
+
} else {
|
|
1965
|
+
text = utf8Decode(data.slice(offset));
|
|
1966
|
+
}
|
|
1967
|
+
return {
|
|
1968
|
+
type: "iTXt",
|
|
1969
|
+
keyword,
|
|
1970
|
+
compressionFlag,
|
|
1971
|
+
compressionMethod,
|
|
1972
|
+
languageTag,
|
|
1973
|
+
translatedKeyword,
|
|
1974
|
+
text
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
function findNull(data, offset) {
|
|
1978
|
+
for (let i = offset; i < data.length; i++) {
|
|
1979
|
+
if (data[i] === 0) {
|
|
1980
|
+
return i;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return -1;
|
|
1984
|
+
}
|
|
1985
|
+
function latin1Decode(data) {
|
|
1986
|
+
let result = "";
|
|
1987
|
+
for (let i = 0; i < data.length; i++) {
|
|
1988
|
+
result += String.fromCharCode(data[i] ?? 0);
|
|
1989
|
+
}
|
|
1990
|
+
return result;
|
|
1991
|
+
}
|
|
1992
|
+
function utf8Decode(data) {
|
|
1993
|
+
return new TextDecoder("utf-8").decode(data);
|
|
1994
|
+
}
|
|
1995
|
+
function decompressZlib(_data) {
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/readers/webp.ts
|
|
2000
|
+
var EXIF_CHUNK_TYPE = new Uint8Array([69, 88, 73, 70]);
|
|
2001
|
+
function readWebpMetadata(data) {
|
|
2002
|
+
if (!isWebp(data)) {
|
|
2003
|
+
return Result.error({ type: "invalidSignature" });
|
|
2004
|
+
}
|
|
2005
|
+
const exifChunk = findExifChunk(data);
|
|
2006
|
+
if (!exifChunk) {
|
|
2007
|
+
return Result.ok([]);
|
|
2008
|
+
}
|
|
2009
|
+
const exifData = data.slice(
|
|
2010
|
+
exifChunk.offset,
|
|
2011
|
+
exifChunk.offset + exifChunk.length
|
|
2012
|
+
);
|
|
2013
|
+
const segments = parseExifMetadataSegments(exifData);
|
|
2014
|
+
return Result.ok(segments);
|
|
2015
|
+
}
|
|
2016
|
+
function findExifChunk(data) {
|
|
2017
|
+
let offset = 12;
|
|
2018
|
+
while (offset < data.length - 8) {
|
|
2019
|
+
const chunkType = data.slice(offset, offset + 4);
|
|
2020
|
+
const chunkSize = readUint32LE(data, offset + 4);
|
|
2021
|
+
if (arraysEqual(chunkType, EXIF_CHUNK_TYPE)) {
|
|
2022
|
+
return {
|
|
2023
|
+
offset: offset + 8,
|
|
2024
|
+
length: chunkSize
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
const paddedSize = chunkSize + chunkSize % 2;
|
|
2028
|
+
offset += 8 + paddedSize;
|
|
2029
|
+
}
|
|
2030
|
+
return null;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/utils/convert.ts
|
|
2034
|
+
function pngChunksToEntries(chunks) {
|
|
2035
|
+
return chunks.map((chunk) => ({
|
|
2036
|
+
keyword: chunk.keyword,
|
|
2037
|
+
text: chunk.text
|
|
2038
|
+
}));
|
|
2039
|
+
}
|
|
2040
|
+
function segmentsToEntries(segments) {
|
|
2041
|
+
const entries = [];
|
|
2042
|
+
for (const segment of segments) {
|
|
2043
|
+
const keyword = sourceToKeyword(segment.source);
|
|
2044
|
+
const text = segment.data;
|
|
2045
|
+
if (segment.source.type === "exifUserComment" && text.startsWith("{")) {
|
|
2046
|
+
const expanded = tryExpandNovelAIWebpFormat(text);
|
|
2047
|
+
if (expanded) {
|
|
2048
|
+
entries.push(...expanded);
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
entries.push({ keyword, text });
|
|
2053
|
+
}
|
|
2054
|
+
return entries;
|
|
2055
|
+
}
|
|
2056
|
+
function tryExpandNovelAIWebpFormat(text) {
|
|
2057
|
+
const outerParsed = parseJson(text);
|
|
2058
|
+
if (!outerParsed.ok) {
|
|
2059
|
+
return null;
|
|
2060
|
+
}
|
|
2061
|
+
const outer = outerParsed.value;
|
|
2062
|
+
if (typeof outer !== "object" || outer === null || outer.Software !== "NovelAI" || typeof outer.Comment !== "string") {
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
const entries = [{ keyword: "Software", text: "NovelAI" }];
|
|
2066
|
+
const innerParsed = parseJson(outer.Comment);
|
|
2067
|
+
return [
|
|
2068
|
+
...entries,
|
|
2069
|
+
innerParsed.ok ? { keyword: "Comment", text: JSON.stringify(innerParsed.value) } : { keyword: "Comment", text: outer.Comment }
|
|
2070
|
+
];
|
|
2071
|
+
}
|
|
2072
|
+
function sourceToKeyword(source) {
|
|
2073
|
+
switch (source.type) {
|
|
2074
|
+
case "jpegCom":
|
|
2075
|
+
return "Comment";
|
|
2076
|
+
case "exifUserComment":
|
|
2077
|
+
return "Comment";
|
|
2078
|
+
case "exifImageDescription":
|
|
2079
|
+
return source.prefix ?? "Description";
|
|
2080
|
+
case "exifMake":
|
|
2081
|
+
return source.prefix ?? "Make";
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/writers/exif.ts
|
|
2086
|
+
function buildExifTiffData(segments) {
|
|
2087
|
+
const ifd0Segments = segments.filter(
|
|
2088
|
+
(s) => s.source.type === "exifImageDescription" || s.source.type === "exifMake"
|
|
2089
|
+
);
|
|
2090
|
+
const exifIfdSegments = segments.filter(
|
|
2091
|
+
(s) => s.source.type === "exifUserComment"
|
|
2092
|
+
);
|
|
2093
|
+
if (ifd0Segments.length === 0 && exifIfdSegments.length === 0) {
|
|
2094
|
+
return new Uint8Array(0);
|
|
2095
|
+
}
|
|
2096
|
+
const isLittleEndian = true;
|
|
2097
|
+
const ifd0Tags = [];
|
|
2098
|
+
const exifTags = [];
|
|
2099
|
+
for (const seg of ifd0Segments) {
|
|
2100
|
+
if (seg.source.type === "exifImageDescription") {
|
|
2101
|
+
const data = encodeAsciiTag(seg.data, seg.source.prefix);
|
|
2102
|
+
ifd0Tags.push({ tag: IMAGE_DESCRIPTION_TAG, type: 2, data });
|
|
2103
|
+
} else if (seg.source.type === "exifMake") {
|
|
2104
|
+
const data = encodeAsciiTag(seg.data, seg.source.prefix);
|
|
2105
|
+
ifd0Tags.push({ tag: MAKE_TAG, type: 2, data });
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
for (const seg of exifIfdSegments) {
|
|
2109
|
+
if (seg.source.type === "exifUserComment") {
|
|
2110
|
+
const data = encodeUserComment(seg.data);
|
|
2111
|
+
exifTags.push({ tag: USER_COMMENT_TAG, type: 7, data });
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
const hasExifIfd = exifTags.length > 0;
|
|
2115
|
+
if (hasExifIfd) {
|
|
2116
|
+
ifd0Tags.push({
|
|
2117
|
+
tag: EXIF_IFD_POINTER_TAG,
|
|
2118
|
+
type: 4,
|
|
2119
|
+
data: new Uint8Array(4)
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
ifd0Tags.sort((a, b) => a.tag - b.tag);
|
|
2123
|
+
exifTags.sort((a, b) => a.tag - b.tag);
|
|
2124
|
+
const headerSize = 8;
|
|
2125
|
+
const ifd0EntryCount = ifd0Tags.length;
|
|
2126
|
+
const ifd0Size = 2 + 12 * ifd0EntryCount + 4;
|
|
2127
|
+
const exifEntryCount = exifTags.length;
|
|
2128
|
+
const exifIfdSize = hasExifIfd ? 2 + 12 * exifEntryCount + 4 : 0;
|
|
2129
|
+
const ifd0Offset = headerSize;
|
|
2130
|
+
const exifIfdOffset = ifd0Offset + ifd0Size;
|
|
2131
|
+
let dataOffset = exifIfdOffset + exifIfdSize;
|
|
2132
|
+
if (hasExifIfd) {
|
|
2133
|
+
const exifPtrTag = ifd0Tags.find((t) => t.tag === EXIF_IFD_POINTER_TAG);
|
|
2134
|
+
if (exifPtrTag) {
|
|
2135
|
+
writeUint32(exifPtrTag.data, 0, exifIfdOffset, isLittleEndian);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const tagDataOffsets = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const tag of [...ifd0Tags, ...exifTags]) {
|
|
2140
|
+
if (tag.data.length > 4) {
|
|
2141
|
+
tagDataOffsets.set(tag, dataOffset);
|
|
2142
|
+
dataOffset += tag.data.length;
|
|
2143
|
+
if (tag.data.length % 2 !== 0) {
|
|
2144
|
+
dataOffset += 1;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const totalSize = dataOffset;
|
|
2149
|
+
const result = new Uint8Array(totalSize);
|
|
2150
|
+
result[0] = 73;
|
|
2151
|
+
result[1] = 73;
|
|
2152
|
+
writeUint16(result, 2, 42, isLittleEndian);
|
|
2153
|
+
writeUint32(result, 4, ifd0Offset, isLittleEndian);
|
|
2154
|
+
let offset = ifd0Offset;
|
|
2155
|
+
writeUint16(result, offset, ifd0EntryCount, isLittleEndian);
|
|
2156
|
+
offset += 2;
|
|
2157
|
+
for (const tag of ifd0Tags) {
|
|
2158
|
+
writeIfdEntry(result, offset, tag, tagDataOffsets.get(tag), isLittleEndian);
|
|
2159
|
+
offset += 12;
|
|
2160
|
+
}
|
|
2161
|
+
writeUint32(result, offset, 0, isLittleEndian);
|
|
2162
|
+
offset += 4;
|
|
2163
|
+
if (hasExifIfd) {
|
|
2164
|
+
writeUint16(result, offset, exifEntryCount, isLittleEndian);
|
|
2165
|
+
offset += 2;
|
|
2166
|
+
for (const tag of exifTags) {
|
|
2167
|
+
writeIfdEntry(
|
|
2168
|
+
result,
|
|
2169
|
+
offset,
|
|
2170
|
+
tag,
|
|
2171
|
+
tagDataOffsets.get(tag),
|
|
2172
|
+
isLittleEndian
|
|
2173
|
+
);
|
|
2174
|
+
offset += 12;
|
|
2175
|
+
}
|
|
2176
|
+
writeUint32(result, offset, 0, isLittleEndian);
|
|
2177
|
+
}
|
|
2178
|
+
for (const [tag, dataOff] of tagDataOffsets) {
|
|
2179
|
+
result.set(tag.data, dataOff);
|
|
2180
|
+
}
|
|
2181
|
+
return result;
|
|
2182
|
+
}
|
|
2183
|
+
function writeIfdEntry(data, offset, tag, dataOffset, isLittleEndian) {
|
|
2184
|
+
writeUint16(data, offset, tag.tag, isLittleEndian);
|
|
2185
|
+
writeUint16(data, offset + 2, tag.type, isLittleEndian);
|
|
2186
|
+
writeUint32(data, offset + 4, tag.data.length, isLittleEndian);
|
|
2187
|
+
if (tag.data.length <= 4) {
|
|
2188
|
+
data.set(tag.data, offset + 8);
|
|
2189
|
+
} else {
|
|
2190
|
+
writeUint32(data, offset + 8, dataOffset ?? 0, isLittleEndian);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
function encodeUserComment(text) {
|
|
2194
|
+
const utf16Data = [];
|
|
2195
|
+
for (let i = 0; i < text.length; i++) {
|
|
2196
|
+
const code = text.charCodeAt(i);
|
|
2197
|
+
utf16Data.push(code & 255);
|
|
2198
|
+
utf16Data.push(code >> 8 & 255);
|
|
2199
|
+
}
|
|
2200
|
+
const result = new Uint8Array(8 + utf16Data.length);
|
|
2201
|
+
result[0] = 85;
|
|
2202
|
+
result[1] = 78;
|
|
2203
|
+
result[2] = 73;
|
|
2204
|
+
result[3] = 67;
|
|
2205
|
+
result[4] = 79;
|
|
2206
|
+
result[5] = 68;
|
|
2207
|
+
result[6] = 69;
|
|
2208
|
+
result[7] = 0;
|
|
2209
|
+
result.set(new Uint8Array(utf16Data), 8);
|
|
2210
|
+
return result;
|
|
2211
|
+
}
|
|
2212
|
+
function encodeAsciiTag(text, prefix) {
|
|
2213
|
+
const fullText = prefix ? `${prefix}: ${text}` : text;
|
|
2214
|
+
const textBytes = new TextEncoder().encode(fullText);
|
|
2215
|
+
const result = new Uint8Array(textBytes.length + 1);
|
|
2216
|
+
result.set(textBytes, 0);
|
|
2217
|
+
result[textBytes.length] = 0;
|
|
2218
|
+
return result;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// src/writers/jpeg.ts
|
|
2222
|
+
var APP1_MARKER2 = 225;
|
|
2223
|
+
var COM_MARKER2 = 254;
|
|
2224
|
+
var SOS_MARKER = 218;
|
|
2225
|
+
var EOI_MARKER = 217;
|
|
2226
|
+
var EXIF_HEADER2 = new Uint8Array([69, 120, 105, 102, 0, 0]);
|
|
2227
|
+
function writeJpegMetadata(data, segments) {
|
|
2228
|
+
if (!isJpeg(data)) {
|
|
2229
|
+
return Result.error({ type: "invalidSignature" });
|
|
2230
|
+
}
|
|
2231
|
+
const comSegments = segments.filter((s) => s.source.type === "jpegCom");
|
|
2232
|
+
const exifSegments = segments.filter(
|
|
2233
|
+
(s) => s.source.type === "exifUserComment" || s.source.type === "exifImageDescription" || s.source.type === "exifMake"
|
|
2234
|
+
);
|
|
2235
|
+
const collectResult = collectNonMetadataSegments(data);
|
|
2236
|
+
if (!collectResult.ok) {
|
|
2237
|
+
return collectResult;
|
|
2238
|
+
}
|
|
2239
|
+
const { beforeSos, scanData } = collectResult.value;
|
|
2240
|
+
const app1Segment = exifSegments.length > 0 ? buildApp1Segment(exifSegments) : null;
|
|
2241
|
+
const comSegmentData = comSegments.map((s) => buildComSegment(s.data));
|
|
2242
|
+
let totalSize = 2;
|
|
2243
|
+
if (app1Segment) {
|
|
2244
|
+
totalSize += app1Segment.length;
|
|
2245
|
+
}
|
|
2246
|
+
for (const seg of beforeSos) {
|
|
2247
|
+
totalSize += seg.length;
|
|
2248
|
+
}
|
|
2249
|
+
for (const com of comSegmentData) {
|
|
2250
|
+
totalSize += com.length;
|
|
2251
|
+
}
|
|
2252
|
+
totalSize += scanData.length;
|
|
2253
|
+
const output = new Uint8Array(totalSize);
|
|
2254
|
+
let offset = 0;
|
|
2255
|
+
output[offset++] = 255;
|
|
2256
|
+
output[offset++] = 216;
|
|
2257
|
+
if (app1Segment) {
|
|
2258
|
+
output.set(app1Segment, offset);
|
|
2259
|
+
offset += app1Segment.length;
|
|
2260
|
+
}
|
|
2261
|
+
for (const seg of beforeSos) {
|
|
2262
|
+
output.set(seg, offset);
|
|
2263
|
+
offset += seg.length;
|
|
2264
|
+
}
|
|
2265
|
+
for (const com of comSegmentData) {
|
|
2266
|
+
output.set(com, offset);
|
|
2267
|
+
offset += com.length;
|
|
2268
|
+
}
|
|
2269
|
+
output.set(scanData, offset);
|
|
2270
|
+
return Result.ok(output);
|
|
2271
|
+
}
|
|
2272
|
+
function collectNonMetadataSegments(data) {
|
|
2273
|
+
const beforeSos = [];
|
|
2274
|
+
let offset = 2;
|
|
2275
|
+
while (offset < data.length - 1) {
|
|
2276
|
+
if (data[offset] !== 255) {
|
|
2277
|
+
return Result.error({
|
|
2278
|
+
type: "corruptedStructure",
|
|
2279
|
+
message: `Expected marker at offset ${offset}`
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
while (data[offset] === 255 && offset < data.length - 1) {
|
|
2283
|
+
offset++;
|
|
2284
|
+
}
|
|
2285
|
+
const marker = data[offset];
|
|
2286
|
+
offset++;
|
|
2287
|
+
if (marker === SOS_MARKER) {
|
|
2288
|
+
const scanData = data.slice(offset - 2);
|
|
2289
|
+
return Result.ok({ beforeSos, scanData });
|
|
2290
|
+
}
|
|
2291
|
+
if (marker === EOI_MARKER) {
|
|
2292
|
+
return Result.ok({ beforeSos, scanData: new Uint8Array([255, 217]) });
|
|
2293
|
+
}
|
|
2294
|
+
if (offset + 2 > data.length) {
|
|
2295
|
+
return Result.error({
|
|
2296
|
+
type: "corruptedStructure",
|
|
2297
|
+
message: "Unexpected end of file"
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
const length = (data[offset] ?? 0) << 8 | (data[offset + 1] ?? 0);
|
|
2301
|
+
const segmentStart = offset - 2;
|
|
2302
|
+
const segmentEnd = offset + length;
|
|
2303
|
+
if (segmentEnd > data.length) {
|
|
2304
|
+
return Result.error({
|
|
2305
|
+
type: "corruptedStructure",
|
|
2306
|
+
message: "Segment extends beyond file"
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
const isExifApp1 = marker === APP1_MARKER2 && offset + 2 + 6 <= data.length && data[offset + 2] === 69 && // E
|
|
2310
|
+
data[offset + 3] === 120 && // x
|
|
2311
|
+
data[offset + 4] === 105 && // i
|
|
2312
|
+
data[offset + 5] === 102 && // f
|
|
2313
|
+
data[offset + 6] === 0 && // NULL
|
|
2314
|
+
data[offset + 7] === 0;
|
|
2315
|
+
const isCom = marker === COM_MARKER2;
|
|
2316
|
+
if (!isExifApp1 && !isCom) {
|
|
2317
|
+
beforeSos.push(data.slice(segmentStart, segmentEnd));
|
|
2318
|
+
}
|
|
2319
|
+
offset = segmentEnd;
|
|
2320
|
+
}
|
|
2321
|
+
return Result.error({
|
|
2322
|
+
type: "corruptedStructure",
|
|
2323
|
+
message: "No SOS marker found"
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
function buildApp1Segment(segments) {
|
|
2327
|
+
const tiffData = buildExifTiffData(segments);
|
|
2328
|
+
if (tiffData.length === 0) {
|
|
2329
|
+
return new Uint8Array(0);
|
|
2330
|
+
}
|
|
2331
|
+
const segmentLength = 2 + EXIF_HEADER2.length + tiffData.length;
|
|
2332
|
+
const segment = new Uint8Array(2 + segmentLength);
|
|
2333
|
+
segment[0] = 255;
|
|
2334
|
+
segment[1] = APP1_MARKER2;
|
|
2335
|
+
segment[2] = segmentLength >> 8 & 255;
|
|
2336
|
+
segment[3] = segmentLength & 255;
|
|
2337
|
+
segment.set(EXIF_HEADER2, 4);
|
|
2338
|
+
segment.set(tiffData, 4 + EXIF_HEADER2.length);
|
|
2339
|
+
return segment;
|
|
2340
|
+
}
|
|
2341
|
+
function buildComSegment(text) {
|
|
2342
|
+
const textBytes = new TextEncoder().encode(text);
|
|
2343
|
+
const segmentLength = 2 + textBytes.length;
|
|
2344
|
+
const segment = new Uint8Array(2 + segmentLength);
|
|
2345
|
+
segment[0] = 255;
|
|
2346
|
+
segment[1] = COM_MARKER2;
|
|
2347
|
+
segment[2] = segmentLength >> 8 & 255;
|
|
2348
|
+
segment[3] = segmentLength & 255;
|
|
2349
|
+
segment.set(textBytes, 4);
|
|
2350
|
+
return segment;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// src/writers/png.ts
|
|
2354
|
+
var PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2355
|
+
function writePngMetadata(data, chunks) {
|
|
2356
|
+
if (!isPng(data)) {
|
|
2357
|
+
return Result.error({ type: "invalidSignature" });
|
|
2358
|
+
}
|
|
2359
|
+
const ihdrEnd = findIhdrChunkEnd(data);
|
|
2360
|
+
if (ihdrEnd === -1) {
|
|
2361
|
+
return Result.error({ type: "noIhdrChunk" });
|
|
2362
|
+
}
|
|
2363
|
+
const originalChunks = collectNonTextChunks(data);
|
|
2364
|
+
const serializedTextChunks = chunks.map(
|
|
2365
|
+
(chunk) => chunk.type === "tEXt" ? serializeTExtChunk(chunk) : serializeITXtChunk(chunk)
|
|
2366
|
+
);
|
|
2367
|
+
const totalSize = PNG_SIGNATURE.length + originalChunks.ihdr.length + serializedTextChunks.reduce((sum, chunk) => sum + chunk.length, 0) + originalChunks.others.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
2368
|
+
const output = new Uint8Array(totalSize);
|
|
2369
|
+
let offset = 0;
|
|
2370
|
+
output.set(PNG_SIGNATURE, offset);
|
|
2371
|
+
offset += PNG_SIGNATURE.length;
|
|
2372
|
+
output.set(originalChunks.ihdr, offset);
|
|
2373
|
+
offset += originalChunks.ihdr.length;
|
|
2374
|
+
for (const chunk of serializedTextChunks) {
|
|
2375
|
+
output.set(chunk, offset);
|
|
2376
|
+
offset += chunk.length;
|
|
2377
|
+
}
|
|
2378
|
+
for (const chunk of originalChunks.others) {
|
|
2379
|
+
output.set(chunk, offset);
|
|
2380
|
+
offset += chunk.length;
|
|
2381
|
+
}
|
|
2382
|
+
return Result.ok(output);
|
|
2383
|
+
}
|
|
2384
|
+
function findIhdrChunkEnd(data) {
|
|
2385
|
+
const offset = PNG_SIGNATURE.length;
|
|
2386
|
+
if (offset + 8 > data.length) {
|
|
2387
|
+
return -1;
|
|
2388
|
+
}
|
|
2389
|
+
const length = readUint32BE(data, offset);
|
|
2390
|
+
const chunkType = readChunkType(data, offset + 4);
|
|
2391
|
+
if (chunkType !== "IHDR") {
|
|
2392
|
+
return -1;
|
|
2393
|
+
}
|
|
2394
|
+
return offset + 4 + 4 + length + 4;
|
|
2395
|
+
}
|
|
2396
|
+
function collectNonTextChunks(data) {
|
|
2397
|
+
const others = [];
|
|
2398
|
+
let offset = PNG_SIGNATURE.length;
|
|
2399
|
+
let ihdr = new Uint8Array(0);
|
|
2400
|
+
while (offset < data.length) {
|
|
2401
|
+
const chunkStart = offset;
|
|
2402
|
+
if (offset + 4 > data.length) break;
|
|
2403
|
+
const length = readUint32BE(data, offset);
|
|
2404
|
+
offset += 4;
|
|
2405
|
+
if (offset + 4 > data.length) break;
|
|
2406
|
+
const chunkType = readChunkType(data, offset);
|
|
2407
|
+
offset += 4;
|
|
2408
|
+
offset += length;
|
|
2409
|
+
offset += 4;
|
|
2410
|
+
const chunkEnd = offset;
|
|
2411
|
+
const chunkData = data.slice(chunkStart, chunkEnd);
|
|
2412
|
+
if (chunkType === "IHDR") {
|
|
2413
|
+
ihdr = chunkData;
|
|
2414
|
+
} else if (chunkType !== "tEXt" && chunkType !== "iTXt") {
|
|
2415
|
+
others.push(chunkData);
|
|
2416
|
+
}
|
|
2417
|
+
if (chunkType === "IEND") {
|
|
2418
|
+
break;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return { ihdr, others };
|
|
2422
|
+
}
|
|
2423
|
+
function serializeTExtChunk(chunk) {
|
|
2424
|
+
const keyword = latin1Encode(chunk.keyword);
|
|
2425
|
+
const text = utf8Encode(chunk.text);
|
|
2426
|
+
const chunkData = new Uint8Array(keyword.length + 1 + text.length);
|
|
2427
|
+
chunkData.set(keyword, 0);
|
|
2428
|
+
chunkData[keyword.length] = 0;
|
|
2429
|
+
chunkData.set(text, keyword.length + 1);
|
|
2430
|
+
return buildChunk("tEXt", chunkData);
|
|
2431
|
+
}
|
|
2432
|
+
function serializeITXtChunk(chunk) {
|
|
2433
|
+
const keyword = utf8Encode(chunk.keyword);
|
|
2434
|
+
const languageTag = utf8Encode(chunk.languageTag);
|
|
2435
|
+
const translatedKeyword = utf8Encode(chunk.translatedKeyword);
|
|
2436
|
+
const text = utf8Encode(chunk.text);
|
|
2437
|
+
const dataSize = keyword.length + 1 + // null
|
|
2438
|
+
1 + // compression flag
|
|
2439
|
+
1 + // compression method
|
|
2440
|
+
languageTag.length + 1 + // null
|
|
2441
|
+
translatedKeyword.length + 1 + // null
|
|
2442
|
+
text.length;
|
|
2443
|
+
const chunkData = new Uint8Array(dataSize);
|
|
2444
|
+
let offset = 0;
|
|
2445
|
+
chunkData.set(keyword, offset);
|
|
2446
|
+
offset += keyword.length;
|
|
2447
|
+
chunkData[offset++] = 0;
|
|
2448
|
+
chunkData[offset++] = chunk.compressionFlag;
|
|
2449
|
+
chunkData[offset++] = chunk.compressionMethod;
|
|
2450
|
+
chunkData.set(languageTag, offset);
|
|
2451
|
+
offset += languageTag.length;
|
|
2452
|
+
chunkData[offset++] = 0;
|
|
2453
|
+
chunkData.set(translatedKeyword, offset);
|
|
2454
|
+
offset += translatedKeyword.length;
|
|
2455
|
+
chunkData[offset++] = 0;
|
|
2456
|
+
chunkData.set(text, offset);
|
|
2457
|
+
return buildChunk("iTXt", chunkData);
|
|
2458
|
+
}
|
|
2459
|
+
function buildChunk(type, data) {
|
|
2460
|
+
const chunk = new Uint8Array(4 + 4 + data.length + 4);
|
|
2461
|
+
writeUint32BE(chunk, 0, data.length);
|
|
2462
|
+
for (let i = 0; i < 4; i++) {
|
|
2463
|
+
chunk[4 + i] = type.charCodeAt(i);
|
|
2464
|
+
}
|
|
2465
|
+
chunk.set(data, 8);
|
|
2466
|
+
const crcData = chunk.slice(4, 8 + data.length);
|
|
2467
|
+
const crc = calculateCrc32(crcData);
|
|
2468
|
+
writeUint32BE(chunk, 8 + data.length, crc);
|
|
2469
|
+
return chunk;
|
|
2470
|
+
}
|
|
2471
|
+
function latin1Encode(str) {
|
|
2472
|
+
const bytes = new Uint8Array(str.length);
|
|
2473
|
+
for (let i = 0; i < str.length; i++) {
|
|
2474
|
+
bytes[i] = str.charCodeAt(i) & 255;
|
|
2475
|
+
}
|
|
2476
|
+
return bytes;
|
|
2477
|
+
}
|
|
2478
|
+
function utf8Encode(str) {
|
|
2479
|
+
return new TextEncoder().encode(str);
|
|
2480
|
+
}
|
|
2481
|
+
var CRC_TABLE = makeCrcTable();
|
|
2482
|
+
function makeCrcTable() {
|
|
2483
|
+
const table = new Uint32Array(256);
|
|
2484
|
+
for (let n = 0; n < 256; n++) {
|
|
2485
|
+
let c = n;
|
|
2486
|
+
for (let k = 0; k < 8; k++) {
|
|
2487
|
+
if (c & 1) {
|
|
2488
|
+
c = 3988292384 ^ c >>> 1;
|
|
2489
|
+
} else {
|
|
2490
|
+
c = c >>> 1;
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
table[n] = c >>> 0;
|
|
2494
|
+
}
|
|
2495
|
+
return table;
|
|
2496
|
+
}
|
|
2497
|
+
function calculateCrc32(data) {
|
|
2498
|
+
let crc = 4294967295;
|
|
2499
|
+
for (let i = 0; i < data.length; i++) {
|
|
2500
|
+
crc = (CRC_TABLE[(crc ^ (data[i] ?? 0)) & 255] ?? 0) ^ crc >>> 8;
|
|
2501
|
+
}
|
|
2502
|
+
return (crc ^ 4294967295) >>> 0;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// src/writers/webp.ts
|
|
2506
|
+
var RIFF_SIGNATURE = new Uint8Array([82, 73, 70, 70]);
|
|
2507
|
+
var WEBP_MARKER = new Uint8Array([87, 69, 66, 80]);
|
|
2508
|
+
var EXIF_CHUNK_TYPE2 = new Uint8Array([69, 88, 73, 70]);
|
|
2509
|
+
function writeWebpMetadata(data, segments) {
|
|
2510
|
+
if (!isWebp(data)) {
|
|
2511
|
+
return Result.error({ type: "invalidSignature" });
|
|
2512
|
+
}
|
|
2513
|
+
const collectResult = collectNonExifChunks(data);
|
|
2514
|
+
if (!collectResult.ok) {
|
|
2515
|
+
return collectResult;
|
|
2516
|
+
}
|
|
2517
|
+
const { chunks } = collectResult.value;
|
|
2518
|
+
const exifChunk = buildExifChunk(segments);
|
|
2519
|
+
let newFileSize = 4;
|
|
2520
|
+
for (const chunk of chunks) {
|
|
2521
|
+
newFileSize += chunk.length;
|
|
2522
|
+
}
|
|
2523
|
+
if (exifChunk) {
|
|
2524
|
+
newFileSize += exifChunk.length;
|
|
2525
|
+
}
|
|
2526
|
+
const output = new Uint8Array(8 + newFileSize);
|
|
2527
|
+
let offset = 0;
|
|
2528
|
+
output.set(RIFF_SIGNATURE, offset);
|
|
2529
|
+
offset += 4;
|
|
2530
|
+
writeUint32LE(output, offset, newFileSize);
|
|
2531
|
+
offset += 4;
|
|
2532
|
+
output.set(WEBP_MARKER, offset);
|
|
2533
|
+
offset += 4;
|
|
2534
|
+
let exifWritten = false;
|
|
2535
|
+
for (const chunk of chunks) {
|
|
2536
|
+
output.set(chunk, offset);
|
|
2537
|
+
offset += chunk.length;
|
|
2538
|
+
if (!exifWritten && exifChunk && isImageChunk(chunk)) {
|
|
2539
|
+
output.set(exifChunk, offset);
|
|
2540
|
+
offset += exifChunk.length;
|
|
2541
|
+
exifWritten = true;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
if (!exifWritten && exifChunk) {
|
|
2545
|
+
output.set(exifChunk, offset);
|
|
2546
|
+
}
|
|
2547
|
+
return Result.ok(output);
|
|
2548
|
+
}
|
|
2549
|
+
function isImageChunk(chunk) {
|
|
2550
|
+
if (chunk.length < 4) return false;
|
|
2551
|
+
const type = String.fromCharCode(
|
|
2552
|
+
chunk[0] ?? 0,
|
|
2553
|
+
chunk[1] ?? 0,
|
|
2554
|
+
chunk[2] ?? 0,
|
|
2555
|
+
chunk[3] ?? 0
|
|
2556
|
+
);
|
|
2557
|
+
return type === "VP8 " || type === "VP8L" || type === "VP8X";
|
|
2558
|
+
}
|
|
2559
|
+
function collectNonExifChunks(data) {
|
|
2560
|
+
const chunks = [];
|
|
2561
|
+
let firstChunkType = "";
|
|
2562
|
+
let offset = 12;
|
|
2563
|
+
while (offset < data.length - 8) {
|
|
2564
|
+
const chunkType = data.slice(offset, offset + 4);
|
|
2565
|
+
const typeStr = String.fromCharCode(
|
|
2566
|
+
chunkType[0] ?? 0,
|
|
2567
|
+
chunkType[1] ?? 0,
|
|
2568
|
+
chunkType[2] ?? 0,
|
|
2569
|
+
chunkType[3] ?? 0
|
|
2570
|
+
);
|
|
2571
|
+
if (!firstChunkType) {
|
|
2572
|
+
firstChunkType = typeStr;
|
|
2573
|
+
}
|
|
2574
|
+
const chunkSize = (data[offset + 4] ?? 0) | (data[offset + 5] ?? 0) << 8 | (data[offset + 6] ?? 0) << 16 | (data[offset + 7] ?? 0) << 24;
|
|
2575
|
+
if (offset + 8 + chunkSize > data.length) {
|
|
2576
|
+
return Result.error({
|
|
2577
|
+
type: "invalidRiffStructure",
|
|
2578
|
+
message: `Chunk extends beyond file at offset ${offset}`
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
if (!arraysEqual(chunkType, EXIF_CHUNK_TYPE2)) {
|
|
2582
|
+
const paddedSize2 = chunkSize + chunkSize % 2;
|
|
2583
|
+
const chunkData = data.slice(offset, offset + 8 + paddedSize2);
|
|
2584
|
+
chunks.push(chunkData);
|
|
2585
|
+
}
|
|
2586
|
+
const paddedSize = chunkSize + chunkSize % 2;
|
|
2587
|
+
offset += 8 + paddedSize;
|
|
2588
|
+
}
|
|
2589
|
+
return Result.ok({ chunks, firstChunkType });
|
|
2590
|
+
}
|
|
2591
|
+
function buildExifChunk(segments) {
|
|
2592
|
+
const exifSegments = segments.filter(
|
|
2593
|
+
(s) => s.source.type === "exifUserComment" || s.source.type === "exifImageDescription" || s.source.type === "exifMake"
|
|
2594
|
+
);
|
|
2595
|
+
if (exifSegments.length === 0) {
|
|
2596
|
+
return null;
|
|
2597
|
+
}
|
|
2598
|
+
const tiffData = buildExifTiffData(exifSegments);
|
|
2599
|
+
if (tiffData.length === 0) {
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
const chunkSize = tiffData.length;
|
|
2603
|
+
const paddedSize = chunkSize + chunkSize % 2;
|
|
2604
|
+
const chunk = new Uint8Array(8 + paddedSize);
|
|
2605
|
+
chunk.set(EXIF_CHUNK_TYPE2, 0);
|
|
2606
|
+
writeUint32LE(chunk, 4, chunkSize);
|
|
2607
|
+
chunk.set(tiffData, 8);
|
|
2608
|
+
return chunk;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// src/index.ts
|
|
2612
|
+
var HELPERS = {
|
|
2613
|
+
png: {
|
|
2614
|
+
readMetadata: readPngMetadata,
|
|
2615
|
+
readDimensions: readPngDimensions,
|
|
2616
|
+
writeEmpty: writePngMetadata,
|
|
2617
|
+
createRaw: (chunks) => ({ format: "png", chunks })
|
|
2618
|
+
},
|
|
2619
|
+
jpeg: {
|
|
2620
|
+
readMetadata: readJpegMetadata,
|
|
2621
|
+
readDimensions: readJpegDimensions,
|
|
2622
|
+
writeEmpty: writeJpegMetadata,
|
|
2623
|
+
createRaw: (segments) => ({
|
|
2624
|
+
format: "jpeg",
|
|
2625
|
+
segments
|
|
2626
|
+
})
|
|
2627
|
+
},
|
|
2628
|
+
webp: {
|
|
2629
|
+
readMetadata: readWebpMetadata,
|
|
2630
|
+
readDimensions: readWebpDimensions,
|
|
2631
|
+
writeEmpty: writeWebpMetadata,
|
|
2632
|
+
createRaw: (segments) => ({
|
|
2633
|
+
format: "webp",
|
|
2634
|
+
segments
|
|
2635
|
+
})
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
function read(data) {
|
|
2639
|
+
const format = detectFormat(data);
|
|
2640
|
+
if (!format) {
|
|
2641
|
+
return { status: "invalid", message: "Unknown image format" };
|
|
2642
|
+
}
|
|
2643
|
+
const rawResult = readRawMetadata(data, format);
|
|
2644
|
+
if (rawResult.status !== "success") {
|
|
2645
|
+
return rawResult;
|
|
2646
|
+
}
|
|
2647
|
+
const raw = rawResult.raw;
|
|
2648
|
+
const entries = raw.format === "png" ? pngChunksToEntries(raw.chunks) : segmentsToEntries(raw.segments);
|
|
2649
|
+
const parseResult = parseMetadata(entries);
|
|
2650
|
+
if (!parseResult.ok) {
|
|
2651
|
+
return { status: "unrecognized", raw };
|
|
2652
|
+
}
|
|
2653
|
+
const metadata = parseResult.value;
|
|
2654
|
+
if (metadata.width === 0 || metadata.height === 0) {
|
|
2655
|
+
const dims = HELPERS[format].readDimensions(data);
|
|
2656
|
+
if (dims) {
|
|
2657
|
+
metadata.width = metadata.width || dims.width;
|
|
2658
|
+
metadata.height = metadata.height || dims.height;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return { status: "success", metadata, raw };
|
|
2662
|
+
}
|
|
2663
|
+
function write(data, metadata, options) {
|
|
2664
|
+
const targetFormat = detectFormat(data);
|
|
2665
|
+
if (!targetFormat) {
|
|
2666
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
2667
|
+
}
|
|
2668
|
+
if (metadata.status === "empty") {
|
|
2669
|
+
const result = HELPERS[targetFormat].writeEmpty(data, []);
|
|
2670
|
+
if (!result.ok) {
|
|
2671
|
+
return Result.error({ type: "writeFailed", message: result.error.type });
|
|
2672
|
+
}
|
|
2673
|
+
return Result.ok(result.value);
|
|
2674
|
+
}
|
|
2675
|
+
if (metadata.status === "invalid") {
|
|
2676
|
+
return Result.error({
|
|
2677
|
+
type: "writeFailed",
|
|
2678
|
+
message: "Cannot write invalid metadata"
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
const conversionResult = convertMetadata(
|
|
2682
|
+
metadata,
|
|
2683
|
+
targetFormat,
|
|
2684
|
+
options?.force ?? false
|
|
2685
|
+
);
|
|
2686
|
+
if (!conversionResult.ok) {
|
|
2687
|
+
return Result.error({
|
|
2688
|
+
type: "conversionFailed",
|
|
2689
|
+
message: `Failed to convert metadata: ${conversionResult.error.type}`
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
const newRaw = conversionResult.value;
|
|
2693
|
+
if (targetFormat === "png" && newRaw.format === "png") {
|
|
2694
|
+
const result = writePngMetadata(data, newRaw.chunks);
|
|
2695
|
+
if (!result.ok)
|
|
2696
|
+
return Result.error({ type: "writeFailed", message: result.error.type });
|
|
2697
|
+
return Result.ok(result.value);
|
|
2698
|
+
}
|
|
2699
|
+
if (targetFormat === "jpeg" && newRaw.format === "jpeg") {
|
|
2700
|
+
const result = writeJpegMetadata(data, newRaw.segments);
|
|
2701
|
+
if (!result.ok)
|
|
2702
|
+
return Result.error({ type: "writeFailed", message: result.error.type });
|
|
2703
|
+
return Result.ok(result.value);
|
|
2704
|
+
}
|
|
2705
|
+
if (targetFormat === "webp" && newRaw.format === "webp") {
|
|
2706
|
+
const result = writeWebpMetadata(data, newRaw.segments);
|
|
2707
|
+
if (!result.ok)
|
|
2708
|
+
return Result.error({ type: "writeFailed", message: result.error.type });
|
|
2709
|
+
return Result.ok(result.value);
|
|
2710
|
+
}
|
|
2711
|
+
return Result.error({
|
|
2712
|
+
type: "writeFailed",
|
|
2713
|
+
message: "Internal error: format mismatch after conversion"
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
function readRawMetadata(data, format) {
|
|
2717
|
+
const result = HELPERS[format].readMetadata(data);
|
|
2718
|
+
if (!result.ok) {
|
|
2719
|
+
const message = result.error.type === "invalidSignature" ? `Invalid ${format.toUpperCase()} signature` : result.error.message;
|
|
2720
|
+
return { status: "invalid", message };
|
|
2721
|
+
}
|
|
2722
|
+
if (result.value.length === 0) return { status: "empty" };
|
|
2723
|
+
if (format === "png") {
|
|
2724
|
+
return {
|
|
2725
|
+
status: "success",
|
|
2726
|
+
raw: HELPERS.png.createRaw(result.value)
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
return {
|
|
2730
|
+
status: "success",
|
|
2731
|
+
raw: HELPERS[format].createRaw(result.value)
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
function readPngDimensions(data) {
|
|
2735
|
+
const PNG_SIGNATURE_LENGTH2 = 8;
|
|
2736
|
+
if (data.length < 24) return null;
|
|
2737
|
+
return {
|
|
2738
|
+
width: readUint32BE(data, PNG_SIGNATURE_LENGTH2 + 8),
|
|
2739
|
+
height: readUint32BE(data, PNG_SIGNATURE_LENGTH2 + 12)
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function readJpegDimensions(data) {
|
|
2743
|
+
let offset = 2;
|
|
2744
|
+
while (offset < data.length - 4) {
|
|
2745
|
+
if (data[offset] !== 255) {
|
|
2746
|
+
offset++;
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
const marker = data[offset + 1] ?? 0;
|
|
2750
|
+
if (marker === 255) {
|
|
2751
|
+
offset++;
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
|
|
2755
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
2756
|
+
const height = (data[offset + 5] ?? 0) << 8 | (data[offset + 6] ?? 0);
|
|
2757
|
+
const width = (data[offset + 7] ?? 0) << 8 | (data[offset + 8] ?? 0);
|
|
2758
|
+
return { width, height };
|
|
2759
|
+
}
|
|
2760
|
+
offset += 2 + length;
|
|
2761
|
+
if (marker === 218) break;
|
|
2762
|
+
}
|
|
2763
|
+
return null;
|
|
2764
|
+
}
|
|
2765
|
+
function readWebpDimensions(data) {
|
|
2766
|
+
let offset = 12;
|
|
2767
|
+
while (offset < data.length) {
|
|
2768
|
+
if (offset + 8 > data.length) break;
|
|
2769
|
+
const chunkType = readChunkType(data, offset);
|
|
2770
|
+
const chunkSize = readUint32LE(data, offset + 4);
|
|
2771
|
+
const paddedSize = chunkSize + chunkSize % 2;
|
|
2772
|
+
if (chunkType === "VP8X") {
|
|
2773
|
+
const wMinus1 = readUint24LE(data, offset + 12);
|
|
2774
|
+
const hMinus1 = readUint24LE(data, offset + 15);
|
|
2775
|
+
return { width: wMinus1 + 1, height: hMinus1 + 1 };
|
|
2776
|
+
}
|
|
2777
|
+
if (chunkType === "VP8 ") {
|
|
2778
|
+
const start = offset + 8;
|
|
2779
|
+
const tag = (data[start] ?? 0) | (data[start + 1] ?? 0) << 8 | (data[start + 2] ?? 0) << 16;
|
|
2780
|
+
const keyFrame = !(tag & 1);
|
|
2781
|
+
if (keyFrame) {
|
|
2782
|
+
if (data[start + 3] === 157 && data[start + 4] === 1 && data[start + 5] === 42) {
|
|
2783
|
+
const wRaw = (data[start + 6] ?? 0) | (data[start + 7] ?? 0) << 8;
|
|
2784
|
+
const hRaw = (data[start + 8] ?? 0) | (data[start + 9] ?? 0) << 8;
|
|
2785
|
+
return { width: wRaw & 16383, height: hRaw & 16383 };
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
if (chunkType === "VP8L") {
|
|
2790
|
+
if (data[offset + 8] === 47) {
|
|
2791
|
+
const bits = readUint32LE(data, offset + 9);
|
|
2792
|
+
const width = (bits & 16383) + 1;
|
|
2793
|
+
const height = (bits >> 14 & 16383) + 1;
|
|
2794
|
+
return { width, height };
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
offset += 8 + paddedSize;
|
|
2798
|
+
}
|
|
2799
|
+
return null;
|
|
2800
|
+
}
|
|
2801
|
+
export {
|
|
2802
|
+
read,
|
|
2803
|
+
write
|
|
2804
|
+
};
|
|
2805
|
+
//# sourceMappingURL=index.js.map
|