@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/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