@enslo/sd-metadata 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.ja.md +7 -3
- package/README.md +8 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.global.js +6 -6
- package/dist/index.js +308 -35
- package/dist/index.js.map +1 -1
- package/docs/types.ja.md +7 -4
- package/docs/types.md +8 -5
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -164,6 +164,62 @@ function writeUint32(data, offset, value, isLittleEndian) {
|
|
|
164
164
|
);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// src/utils/xmp.ts
|
|
168
|
+
var XMP_KEYWORD = "XML:com.adobe.xmp";
|
|
169
|
+
var XMP_APP1_PREFIX = /* @__PURE__ */ new TextEncoder().encode(
|
|
170
|
+
"http://ns.adobe.com/xap/1.0/\0"
|
|
171
|
+
);
|
|
172
|
+
function matchesXmpPrefix(data, offset) {
|
|
173
|
+
if (offset + XMP_APP1_PREFIX.length > data.length) return false;
|
|
174
|
+
for (let i = 0; i < XMP_APP1_PREFIX.length; i++) {
|
|
175
|
+
if (data[offset + i] !== XMP_APP1_PREFIX[i]) return false;
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
function isXmpKeyword(keyword) {
|
|
180
|
+
return keyword === XMP_KEYWORD;
|
|
181
|
+
}
|
|
182
|
+
var MAX_XMP_TEXT_LENGTH = 65535;
|
|
183
|
+
function extractXmpEntries(xmpText) {
|
|
184
|
+
if (xmpText.length > MAX_XMP_TEXT_LENGTH) return null;
|
|
185
|
+
const candidates = [
|
|
186
|
+
["CreatorTool", extractSimpleElement(xmpText, "xmp", "CreatorTool")],
|
|
187
|
+
["UserComment", extractAltElement(xmpText, "exif", "UserComment")],
|
|
188
|
+
["parameters", extractAltElement(xmpText, "dc", "description")]
|
|
189
|
+
];
|
|
190
|
+
const entries = candidates.filter(
|
|
191
|
+
(entry) => entry[1] !== void 0
|
|
192
|
+
);
|
|
193
|
+
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
194
|
+
}
|
|
195
|
+
function extractSimpleElement(xmp, ns, field) {
|
|
196
|
+
const pattern = new RegExp(`<${ns}:${field}>([^<]*)</${ns}:${field}>`);
|
|
197
|
+
const match = xmp.match(pattern);
|
|
198
|
+
return match?.[1] ? decodeXmlEntities(match[1]) : void 0;
|
|
199
|
+
}
|
|
200
|
+
function extractAltElement(xmp, ns, field) {
|
|
201
|
+
const pattern = new RegExp(
|
|
202
|
+
`<${ns}:${field}>[\\s\\S]*?<rdf:li[^>]*(?<!/)>([\\s\\S]*?)</rdf:li>[\\s\\S]*?</${ns}:${field}>`
|
|
203
|
+
);
|
|
204
|
+
const match = xmp.match(pattern);
|
|
205
|
+
return match?.[1] ? decodeXmlEntities(match[1]) : void 0;
|
|
206
|
+
}
|
|
207
|
+
function decodeXmlEntities(text) {
|
|
208
|
+
return text.replace(/&#x([0-9a-fA-F]+);/g, (match, hex) => {
|
|
209
|
+
try {
|
|
210
|
+
return String.fromCodePoint(parseInt(hex, 16));
|
|
211
|
+
} catch {
|
|
212
|
+
return match;
|
|
213
|
+
}
|
|
214
|
+
}).replace(/&#(\d+);/g, (match, dec) => {
|
|
215
|
+
try {
|
|
216
|
+
return String.fromCodePoint(parseInt(dec, 10));
|
|
217
|
+
} catch {
|
|
218
|
+
return match;
|
|
219
|
+
}
|
|
220
|
+
}).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
221
|
+
}
|
|
222
|
+
|
|
167
223
|
// src/utils/exif-constants.ts
|
|
168
224
|
var USER_COMMENT_TAG = 37510;
|
|
169
225
|
var IMAGE_DESCRIPTION_TAG = 270;
|
|
@@ -317,6 +373,7 @@ function writeJpegMetadata(data, segments) {
|
|
|
317
373
|
return Result.error({ type: "invalidSignature" });
|
|
318
374
|
}
|
|
319
375
|
const comSegments = segments.filter((s) => s.source.type === "jpegCom");
|
|
376
|
+
const xmpSegments = segments.filter((s) => s.source.type === "xmpPacket");
|
|
320
377
|
const exifSegments = segments.filter(
|
|
321
378
|
(s) => s.source.type === "exifUserComment" || s.source.type === "exifImageDescription" || s.source.type === "exifMake"
|
|
322
379
|
);
|
|
@@ -326,11 +383,16 @@ function writeJpegMetadata(data, segments) {
|
|
|
326
383
|
}
|
|
327
384
|
const { beforeSos, scanData } = collectResult.value;
|
|
328
385
|
const app1Segment = exifSegments.length > 0 ? buildApp1Segment(exifSegments) : null;
|
|
329
|
-
const
|
|
386
|
+
const firstXmp = xmpSegments[0];
|
|
387
|
+
const xmpApp1Segment = firstXmp ? buildXmpApp1Segment(firstXmp.data) : null;
|
|
388
|
+
const comSegmentData = comSegments.map((s) => buildComSegment(s.data)).filter((s) => s !== null);
|
|
330
389
|
let totalSize = 2;
|
|
331
390
|
if (app1Segment) {
|
|
332
391
|
totalSize += app1Segment.length;
|
|
333
392
|
}
|
|
393
|
+
if (xmpApp1Segment) {
|
|
394
|
+
totalSize += xmpApp1Segment.length;
|
|
395
|
+
}
|
|
334
396
|
for (const seg of beforeSos) {
|
|
335
397
|
totalSize += seg.length;
|
|
336
398
|
}
|
|
@@ -346,6 +408,10 @@ function writeJpegMetadata(data, segments) {
|
|
|
346
408
|
output.set(app1Segment, offset);
|
|
347
409
|
offset += app1Segment.length;
|
|
348
410
|
}
|
|
411
|
+
if (xmpApp1Segment) {
|
|
412
|
+
output.set(xmpApp1Segment, offset);
|
|
413
|
+
offset += xmpApp1Segment.length;
|
|
414
|
+
}
|
|
349
415
|
for (const seg of beforeSos) {
|
|
350
416
|
output.set(seg, offset);
|
|
351
417
|
offset += seg.length;
|
|
@@ -400,8 +466,9 @@ function collectNonMetadataSegments(data) {
|
|
|
400
466
|
data[offset + 5] === 102 && // f
|
|
401
467
|
data[offset + 6] === 0 && // NULL
|
|
402
468
|
data[offset + 7] === 0;
|
|
469
|
+
const isXmpApp1 = marker === APP1_MARKER && !isExifApp1 && matchesXmpPrefix(data, offset + 2);
|
|
403
470
|
const isCom = marker === COM_MARKER;
|
|
404
|
-
if (!isExifApp1 && !isCom) {
|
|
471
|
+
if (!isExifApp1 && !isXmpApp1 && !isCom) {
|
|
405
472
|
beforeSos.push(data.slice(segmentStart, segmentEnd));
|
|
406
473
|
}
|
|
407
474
|
offset = segmentEnd;
|
|
@@ -425,9 +492,26 @@ function buildApp1Segment(segments) {
|
|
|
425
492
|
segment.set(tiffData, 4 + EXIF_HEADER.length);
|
|
426
493
|
return segment;
|
|
427
494
|
}
|
|
495
|
+
function buildXmpApp1Segment(xmpText) {
|
|
496
|
+
const textBytes = new TextEncoder().encode(xmpText);
|
|
497
|
+
const segmentLength = 2 + XMP_APP1_PREFIX.length + textBytes.length;
|
|
498
|
+
if (segmentLength > 65535) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
const segment = new Uint8Array(2 + segmentLength);
|
|
502
|
+
segment[0] = 255;
|
|
503
|
+
segment[1] = APP1_MARKER;
|
|
504
|
+
writeUint16BE(segment, 2, segmentLength);
|
|
505
|
+
segment.set(XMP_APP1_PREFIX, 4);
|
|
506
|
+
segment.set(textBytes, 4 + XMP_APP1_PREFIX.length);
|
|
507
|
+
return segment;
|
|
508
|
+
}
|
|
428
509
|
function buildComSegment(text) {
|
|
429
510
|
const textBytes = new TextEncoder().encode(text);
|
|
430
511
|
const segmentLength = 2 + textBytes.length;
|
|
512
|
+
if (segmentLength > 65535) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
431
515
|
const segment = new Uint8Array(2 + segmentLength);
|
|
432
516
|
segment[0] = 255;
|
|
433
517
|
segment[1] = COM_MARKER;
|
|
@@ -594,18 +678,25 @@ function writeWebpMetadata(data, segments) {
|
|
|
594
678
|
if (!isWebp(data)) {
|
|
595
679
|
return Result.error({ type: "invalidSignature" });
|
|
596
680
|
}
|
|
597
|
-
const
|
|
681
|
+
const xmpSegments = segments.filter((s) => s.source.type === "xmpPacket");
|
|
682
|
+
const exifSegments = segments.filter((s) => s.source.type !== "xmpPacket");
|
|
683
|
+
const collectResult = collectNonMetadataChunks(data);
|
|
598
684
|
if (!collectResult.ok) {
|
|
599
685
|
return collectResult;
|
|
600
686
|
}
|
|
601
687
|
const { chunks } = collectResult.value;
|
|
602
|
-
const exifChunk = buildExifChunk(
|
|
688
|
+
const exifChunk = buildExifChunk(exifSegments);
|
|
689
|
+
const firstXmp = xmpSegments[0];
|
|
690
|
+
const xmpChunk = firstXmp ? buildXmpChunk(firstXmp.data) : null;
|
|
691
|
+
const metadataChunks = [];
|
|
692
|
+
if (exifChunk) metadataChunks.push(exifChunk);
|
|
693
|
+
if (xmpChunk) metadataChunks.push(xmpChunk);
|
|
603
694
|
let newFileSize = 4;
|
|
604
695
|
for (const chunk of chunks) {
|
|
605
696
|
newFileSize += chunk.length;
|
|
606
697
|
}
|
|
607
|
-
|
|
608
|
-
newFileSize +=
|
|
698
|
+
for (const meta of metadataChunks) {
|
|
699
|
+
newFileSize += meta.length;
|
|
609
700
|
}
|
|
610
701
|
const output = new Uint8Array(8 + newFileSize);
|
|
611
702
|
let offset = 0;
|
|
@@ -615,18 +706,23 @@ function writeWebpMetadata(data, segments) {
|
|
|
615
706
|
offset += 4;
|
|
616
707
|
output.set(WEBP_MARKER, offset);
|
|
617
708
|
offset += 4;
|
|
618
|
-
let
|
|
709
|
+
let metadataWritten = false;
|
|
619
710
|
for (const chunk of chunks) {
|
|
620
711
|
output.set(chunk, offset);
|
|
621
712
|
offset += chunk.length;
|
|
622
|
-
if (!
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
713
|
+
if (!metadataWritten && metadataChunks.length > 0 && isImageChunk(chunk)) {
|
|
714
|
+
for (const meta of metadataChunks) {
|
|
715
|
+
output.set(meta, offset);
|
|
716
|
+
offset += meta.length;
|
|
717
|
+
}
|
|
718
|
+
metadataWritten = true;
|
|
626
719
|
}
|
|
627
720
|
}
|
|
628
|
-
if (!
|
|
629
|
-
|
|
721
|
+
if (!metadataWritten) {
|
|
722
|
+
for (const meta of metadataChunks) {
|
|
723
|
+
output.set(meta, offset);
|
|
724
|
+
offset += meta.length;
|
|
725
|
+
}
|
|
630
726
|
}
|
|
631
727
|
return Result.ok(output);
|
|
632
728
|
}
|
|
@@ -635,7 +731,7 @@ function isImageChunk(chunk) {
|
|
|
635
731
|
const type = readChunkType(chunk, 0);
|
|
636
732
|
return type === "VP8 " || type === "VP8L" || type === "VP8X";
|
|
637
733
|
}
|
|
638
|
-
function
|
|
734
|
+
function collectNonMetadataChunks(data) {
|
|
639
735
|
const chunks = [];
|
|
640
736
|
let firstChunkType = "";
|
|
641
737
|
let offset = 12;
|
|
@@ -651,7 +747,7 @@ function collectNonExifChunks(data) {
|
|
|
651
747
|
message: `Chunk extends beyond file at offset ${offset}`
|
|
652
748
|
});
|
|
653
749
|
}
|
|
654
|
-
if (typeStr !== "EXIF") {
|
|
750
|
+
if (typeStr !== "EXIF" && typeStr !== "XMP ") {
|
|
655
751
|
const paddedSize2 = chunkSize + chunkSize % 2;
|
|
656
752
|
const chunkData = data.slice(offset, offset + 8 + paddedSize2);
|
|
657
753
|
chunks.push(chunkData);
|
|
@@ -662,13 +758,10 @@ function collectNonExifChunks(data) {
|
|
|
662
758
|
return Result.ok({ chunks, firstChunkType });
|
|
663
759
|
}
|
|
664
760
|
function buildExifChunk(segments) {
|
|
665
|
-
|
|
666
|
-
(s) => s.source.type === "exifUserComment" || s.source.type === "exifImageDescription" || s.source.type === "exifMake"
|
|
667
|
-
);
|
|
668
|
-
if (exifSegments.length === 0) {
|
|
761
|
+
if (segments.length === 0) {
|
|
669
762
|
return null;
|
|
670
763
|
}
|
|
671
|
-
const tiffData = buildExifTiffData(
|
|
764
|
+
const tiffData = buildExifTiffData(segments);
|
|
672
765
|
if (tiffData.length === 0) {
|
|
673
766
|
return null;
|
|
674
767
|
}
|
|
@@ -680,6 +773,16 @@ function buildExifChunk(segments) {
|
|
|
680
773
|
chunk.set(tiffData, 8);
|
|
681
774
|
return chunk;
|
|
682
775
|
}
|
|
776
|
+
function buildXmpChunk(xmpText) {
|
|
777
|
+
const textBytes = new TextEncoder().encode(xmpText);
|
|
778
|
+
const chunkSize = textBytes.length;
|
|
779
|
+
const paddedSize = chunkSize + chunkSize % 2;
|
|
780
|
+
const chunk = new Uint8Array(8 + paddedSize);
|
|
781
|
+
chunk.set(new TextEncoder().encode("XMP "));
|
|
782
|
+
writeUint32LE(chunk, 4, chunkSize);
|
|
783
|
+
chunk.set(textBytes, 8);
|
|
784
|
+
return chunk;
|
|
785
|
+
}
|
|
683
786
|
|
|
684
787
|
// src/api/stringify.ts
|
|
685
788
|
function normalizeLineEndings(text) {
|
|
@@ -1393,6 +1496,9 @@ function detectUniqueKeywords(entryRecord) {
|
|
|
1393
1496
|
if (entryRecord.Software?.startsWith("NovelAI")) {
|
|
1394
1497
|
return "novelai";
|
|
1395
1498
|
}
|
|
1499
|
+
if (entryRecord.CreatorTool?.startsWith("Draw Things")) {
|
|
1500
|
+
return "draw-things";
|
|
1501
|
+
}
|
|
1396
1502
|
const keyResult = detectByUniqueKey(entryRecord);
|
|
1397
1503
|
if (keyResult) return keyResult;
|
|
1398
1504
|
if ("fooocus_scheme" in entryRecord) {
|
|
@@ -1531,6 +1637,60 @@ function detectFromA1111Format(text) {
|
|
|
1531
1637
|
return null;
|
|
1532
1638
|
}
|
|
1533
1639
|
|
|
1640
|
+
// src/parsers/draw-things.ts
|
|
1641
|
+
function parseDrawThings(entries) {
|
|
1642
|
+
const jsonText = entries.UserComment?.startsWith("{") ? entries.UserComment : entries.Comment?.startsWith("{") ? entries.Comment : void 0;
|
|
1643
|
+
if (!jsonText) {
|
|
1644
|
+
return Result.error({ type: "unsupportedFormat" });
|
|
1645
|
+
}
|
|
1646
|
+
const parsed = parseJson(jsonText);
|
|
1647
|
+
if (!parsed.ok || parsed.type !== "object") {
|
|
1648
|
+
return Result.error({
|
|
1649
|
+
type: "parseError",
|
|
1650
|
+
message: "Invalid JSON in Draw Things metadata"
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
return buildMetadata(parsed.value);
|
|
1654
|
+
}
|
|
1655
|
+
function parseSize2(size) {
|
|
1656
|
+
if (typeof size !== "string") return { width: 0, height: 0 };
|
|
1657
|
+
const match = size.match(/^(\d+)x(\d+)$/);
|
|
1658
|
+
if (!match) return { width: 0, height: 0 };
|
|
1659
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
1660
|
+
}
|
|
1661
|
+
function buildMetadata(data) {
|
|
1662
|
+
const str = (key) => {
|
|
1663
|
+
const v = data[key];
|
|
1664
|
+
return typeof v === "string" ? v : void 0;
|
|
1665
|
+
};
|
|
1666
|
+
const num = (key) => {
|
|
1667
|
+
const v = data[key];
|
|
1668
|
+
if (typeof v === "number" && v !== 0) return v;
|
|
1669
|
+
return void 0;
|
|
1670
|
+
};
|
|
1671
|
+
const prompt = (str("c") ?? "").trim();
|
|
1672
|
+
const negativePrompt = (str("uc") ?? "").trim();
|
|
1673
|
+
const { width, height } = parseSize2(data.size);
|
|
1674
|
+
const metadata = {
|
|
1675
|
+
software: "draw-things",
|
|
1676
|
+
prompt,
|
|
1677
|
+
negativePrompt,
|
|
1678
|
+
width,
|
|
1679
|
+
height,
|
|
1680
|
+
model: trimObject({
|
|
1681
|
+
name: str("model")
|
|
1682
|
+
}),
|
|
1683
|
+
sampling: trimObject({
|
|
1684
|
+
sampler: str("sampler"),
|
|
1685
|
+
steps: num("steps"),
|
|
1686
|
+
cfg: num("scale"),
|
|
1687
|
+
seed: num("seed"),
|
|
1688
|
+
denoise: num("strength")
|
|
1689
|
+
})
|
|
1690
|
+
};
|
|
1691
|
+
return Result.ok(metadata);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1534
1694
|
// src/parsers/easydiffusion.ts
|
|
1535
1695
|
function extractModelName(path) {
|
|
1536
1696
|
if (!path) return void 0;
|
|
@@ -1539,7 +1699,7 @@ function extractModelName(path) {
|
|
|
1539
1699
|
}
|
|
1540
1700
|
function parseEasyDiffusion(entries) {
|
|
1541
1701
|
if ("use_stable_diffusion_model" in entries) {
|
|
1542
|
-
return
|
|
1702
|
+
return buildMetadata2(entries);
|
|
1543
1703
|
}
|
|
1544
1704
|
const jsonText = entries.UserComment?.startsWith("{") ? entries.UserComment : entries.parameters?.startsWith("{") ? entries.parameters : void 0;
|
|
1545
1705
|
if (!jsonText) {
|
|
@@ -1552,9 +1712,9 @@ function parseEasyDiffusion(entries) {
|
|
|
1552
1712
|
message: "Invalid JSON in Easy Diffusion metadata"
|
|
1553
1713
|
});
|
|
1554
1714
|
}
|
|
1555
|
-
return
|
|
1715
|
+
return buildMetadata2(parsed.value);
|
|
1556
1716
|
}
|
|
1557
|
-
function
|
|
1717
|
+
function buildMetadata2(data) {
|
|
1558
1718
|
const str = (key) => {
|
|
1559
1719
|
const v = data[key];
|
|
1560
1720
|
return typeof v === "string" ? v : void 0;
|
|
@@ -2015,6 +2175,8 @@ function parseMetadata(entries) {
|
|
|
2015
2175
|
}
|
|
2016
2176
|
case "ruined-fooocus":
|
|
2017
2177
|
return parseRuinedFooocus(entries);
|
|
2178
|
+
case "draw-things":
|
|
2179
|
+
return parseDrawThings(entries);
|
|
2018
2180
|
default:
|
|
2019
2181
|
return Result.error({ type: "unsupportedFormat" });
|
|
2020
2182
|
}
|
|
@@ -2295,6 +2457,15 @@ function readJpegMetadata(data) {
|
|
|
2295
2457
|
const exifSegments = parseExifMetadataSegments(exifData);
|
|
2296
2458
|
segments.push(...exifSegments);
|
|
2297
2459
|
}
|
|
2460
|
+
const xmpApp1 = findXmpApp1Segment(data);
|
|
2461
|
+
if (xmpApp1) {
|
|
2462
|
+
const xmpData = data.slice(xmpApp1.offset, xmpApp1.offset + xmpApp1.length);
|
|
2463
|
+
const xmpText = new TextDecoder("utf-8").decode(xmpData);
|
|
2464
|
+
segments.push({
|
|
2465
|
+
source: { type: "xmpPacket" },
|
|
2466
|
+
data: xmpText
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2298
2469
|
const comSegment = findComSegment(data);
|
|
2299
2470
|
if (comSegment) {
|
|
2300
2471
|
const comData = data.slice(
|
|
@@ -2343,6 +2514,35 @@ function findApp1Segment(data) {
|
|
|
2343
2514
|
}
|
|
2344
2515
|
return null;
|
|
2345
2516
|
}
|
|
2517
|
+
function findXmpApp1Segment(data) {
|
|
2518
|
+
let offset = 2;
|
|
2519
|
+
while (offset < data.length - 4) {
|
|
2520
|
+
if (data[offset] !== 255) {
|
|
2521
|
+
offset++;
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
const marker = data[offset + 1];
|
|
2525
|
+
if (marker === 255) {
|
|
2526
|
+
offset++;
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
const length = readUint16BE(data, offset + 2);
|
|
2530
|
+
if (marker === APP1_MARKER2 && length >= XMP_APP1_PREFIX.length + 2) {
|
|
2531
|
+
const headerStart = offset + 4;
|
|
2532
|
+
if (matchesXmpPrefix(data, headerStart)) {
|
|
2533
|
+
return {
|
|
2534
|
+
offset: headerStart + XMP_APP1_PREFIX.length,
|
|
2535
|
+
length: length - 2 - XMP_APP1_PREFIX.length
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
offset += 2 + length;
|
|
2540
|
+
if (marker === 218 || marker === 217) {
|
|
2541
|
+
break;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return null;
|
|
2545
|
+
}
|
|
2346
2546
|
function findComSegment(data) {
|
|
2347
2547
|
let offset = 2;
|
|
2348
2548
|
while (offset < data.length - 4) {
|
|
@@ -2507,15 +2707,27 @@ function readWebpMetadata(data) {
|
|
|
2507
2707
|
if (!isWebp(data)) {
|
|
2508
2708
|
return Result.error({ type: "invalidSignature" });
|
|
2509
2709
|
}
|
|
2710
|
+
const segments = [];
|
|
2510
2711
|
const exifChunk = findExifChunk(data);
|
|
2511
|
-
if (
|
|
2512
|
-
|
|
2712
|
+
if (exifChunk) {
|
|
2713
|
+
const exifData = data.slice(
|
|
2714
|
+
exifChunk.offset,
|
|
2715
|
+
exifChunk.offset + exifChunk.length
|
|
2716
|
+
);
|
|
2717
|
+
segments.push(...parseExifMetadataSegments(exifData));
|
|
2718
|
+
}
|
|
2719
|
+
const xmpChunk = findXmpChunk(data);
|
|
2720
|
+
if (xmpChunk) {
|
|
2721
|
+
const xmpData = data.slice(
|
|
2722
|
+
xmpChunk.offset,
|
|
2723
|
+
xmpChunk.offset + xmpChunk.length
|
|
2724
|
+
);
|
|
2725
|
+
const xmpText = new TextDecoder("utf-8").decode(xmpData);
|
|
2726
|
+
segments.push({
|
|
2727
|
+
source: { type: "xmpPacket" },
|
|
2728
|
+
data: xmpText
|
|
2729
|
+
});
|
|
2513
2730
|
}
|
|
2514
|
-
const exifData = data.slice(
|
|
2515
|
-
exifChunk.offset,
|
|
2516
|
-
exifChunk.offset + exifChunk.length
|
|
2517
|
-
);
|
|
2518
|
-
const segments = parseExifMetadataSegments(exifData);
|
|
2519
2731
|
return Result.ok(segments);
|
|
2520
2732
|
}
|
|
2521
2733
|
function findExifChunk(data) {
|
|
@@ -2523,6 +2735,23 @@ function findExifChunk(data) {
|
|
|
2523
2735
|
while (offset + 8 <= data.length) {
|
|
2524
2736
|
const chunkSize = readUint32LE(data, offset + 4);
|
|
2525
2737
|
if (readChunkType(data, offset) === "EXIF") {
|
|
2738
|
+
if (offset + 8 + chunkSize > data.length) return null;
|
|
2739
|
+
return {
|
|
2740
|
+
offset: offset + 8,
|
|
2741
|
+
length: chunkSize
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
const paddedSize = chunkSize + chunkSize % 2;
|
|
2745
|
+
offset += 8 + paddedSize;
|
|
2746
|
+
}
|
|
2747
|
+
return null;
|
|
2748
|
+
}
|
|
2749
|
+
function findXmpChunk(data) {
|
|
2750
|
+
let offset = 12;
|
|
2751
|
+
while (offset + 8 <= data.length) {
|
|
2752
|
+
const chunkSize = readUint32LE(data, offset + 4);
|
|
2753
|
+
if (readChunkType(data, offset) === "XMP ") {
|
|
2754
|
+
if (offset + 8 + chunkSize > data.length) return null;
|
|
2526
2755
|
return {
|
|
2527
2756
|
offset: offset + 8,
|
|
2528
2757
|
length: chunkSize
|
|
@@ -2536,20 +2765,37 @@ function findExifChunk(data) {
|
|
|
2536
2765
|
|
|
2537
2766
|
// src/utils/convert.ts
|
|
2538
2767
|
function pngChunksToRecord(chunks) {
|
|
2539
|
-
return Object.fromEntries(
|
|
2768
|
+
return Object.fromEntries(
|
|
2769
|
+
chunks.flatMap((chunk) => {
|
|
2770
|
+
if (isXmpKeyword(chunk.keyword)) {
|
|
2771
|
+
const expanded = extractXmpEntries(chunk.text);
|
|
2772
|
+
if (expanded) return Object.entries(expanded);
|
|
2773
|
+
}
|
|
2774
|
+
return [[chunk.keyword, chunk.text]];
|
|
2775
|
+
})
|
|
2776
|
+
);
|
|
2540
2777
|
}
|
|
2541
2778
|
function segmentsToRecord(segments) {
|
|
2542
2779
|
const record = {};
|
|
2543
2780
|
for (const segment of segments) {
|
|
2544
2781
|
const keyword = sourceToKeyword(segment.source);
|
|
2545
2782
|
const text = segment.data;
|
|
2546
|
-
if (segment.source.type === "
|
|
2547
|
-
const expanded =
|
|
2783
|
+
if (segment.source.type === "xmpPacket") {
|
|
2784
|
+
const expanded = extractXmpEntries(text);
|
|
2548
2785
|
if (expanded) {
|
|
2549
2786
|
Object.assign(record, expanded);
|
|
2550
2787
|
continue;
|
|
2551
2788
|
}
|
|
2552
2789
|
}
|
|
2790
|
+
if (segment.source.type === "exifUserComment") {
|
|
2791
|
+
if (text.startsWith("{")) {
|
|
2792
|
+
const expanded = tryExpandNovelAIWebpFormat(text);
|
|
2793
|
+
if (expanded) {
|
|
2794
|
+
Object.assign(record, expanded);
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2553
2799
|
record[keyword] = text;
|
|
2554
2800
|
}
|
|
2555
2801
|
return record;
|
|
@@ -2579,6 +2825,8 @@ function sourceToKeyword(source) {
|
|
|
2579
2825
|
return source.prefix ?? "ImageDescription";
|
|
2580
2826
|
case "exifMake":
|
|
2581
2827
|
return source.prefix ?? "Make";
|
|
2828
|
+
case "xmpPacket":
|
|
2829
|
+
return "XML:com.adobe.xmp";
|
|
2582
2830
|
}
|
|
2583
2831
|
}
|
|
2584
2832
|
|
|
@@ -2750,6 +2998,24 @@ function convertComfyUISegmentsToPng(segments) {
|
|
|
2750
2998
|
return tryParseExtendedFormat(segments) ?? tryParseSaveImagePlusFormat(segments) ?? [];
|
|
2751
2999
|
}
|
|
2752
3000
|
|
|
3001
|
+
// src/converters/draw-things.ts
|
|
3002
|
+
var XMP_KEYWORD2 = "XML:com.adobe.xmp";
|
|
3003
|
+
function convertDrawThingsPngToSegments(chunks) {
|
|
3004
|
+
const xmpChunk = chunks.find((c) => isXmpKeyword(c.keyword));
|
|
3005
|
+
if (!xmpChunk) return [];
|
|
3006
|
+
return [
|
|
3007
|
+
{
|
|
3008
|
+
source: { type: "xmpPacket" },
|
|
3009
|
+
data: xmpChunk.text
|
|
3010
|
+
}
|
|
3011
|
+
];
|
|
3012
|
+
}
|
|
3013
|
+
function convertDrawThingsSegmentsToPng(segments) {
|
|
3014
|
+
const xmpSegment = findSegment(segments, "xmpPacket");
|
|
3015
|
+
if (!xmpSegment) return [];
|
|
3016
|
+
return createITxtChunk(XMP_KEYWORD2, xmpSegment.data);
|
|
3017
|
+
}
|
|
3018
|
+
|
|
2753
3019
|
// src/converters/easydiffusion.ts
|
|
2754
3020
|
function convertEasyDiffusionPngToSegments(chunks) {
|
|
2755
3021
|
const json = Object.fromEntries(
|
|
@@ -3065,6 +3331,10 @@ var convertHfSpace = createFormatConverter(
|
|
|
3065
3331
|
createPngToSegments("parameters"),
|
|
3066
3332
|
createSegmentsToPng("parameters", "text-unicode-escape")
|
|
3067
3333
|
);
|
|
3334
|
+
var convertDrawThings = createFormatConverter(
|
|
3335
|
+
convertDrawThingsPngToSegments,
|
|
3336
|
+
convertDrawThingsSegmentsToPng
|
|
3337
|
+
);
|
|
3068
3338
|
var convertCivitai = createFormatConverter(
|
|
3069
3339
|
convertCivitaiPngToSegments,
|
|
3070
3340
|
convertCivitaiSegmentsToPng
|
|
@@ -3106,7 +3376,9 @@ var softwareConverters = {
|
|
|
3106
3376
|
// InvokeAI
|
|
3107
3377
|
invokeai: convertInvokeAI,
|
|
3108
3378
|
// HuggingFace Space
|
|
3109
|
-
"hf-space": convertHfSpace
|
|
3379
|
+
"hf-space": convertHfSpace,
|
|
3380
|
+
// Draw Things (XMP format)
|
|
3381
|
+
"draw-things": convertDrawThings
|
|
3110
3382
|
};
|
|
3111
3383
|
|
|
3112
3384
|
// src/api/write.ts
|
|
@@ -3232,7 +3504,8 @@ var softwareLabels = Object.freeze({
|
|
|
3232
3504
|
"hf-space": "Hugging Face Space",
|
|
3233
3505
|
easydiffusion: "Easy Diffusion",
|
|
3234
3506
|
fooocus: "Fooocus",
|
|
3235
|
-
"ruined-fooocus": "Ruined Fooocus"
|
|
3507
|
+
"ruined-fooocus": "Ruined Fooocus",
|
|
3508
|
+
"draw-things": "Draw Things"
|
|
3236
3509
|
});
|
|
3237
3510
|
export {
|
|
3238
3511
|
embed,
|