@enslo/sd-metadata 2.1.0 → 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/dist/index.js CHANGED
@@ -66,20 +66,61 @@ var Result = {
66
66
  function toUint8Array(input) {
67
67
  return input instanceof ArrayBuffer ? new Uint8Array(input) : input;
68
68
  }
69
+ function isPng(data) {
70
+ if (data.length < 8) return false;
71
+ 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;
72
+ }
73
+ function isJpeg(data) {
74
+ if (data.length < 2) return false;
75
+ return data[0] === 255 && data[1] === 216;
76
+ }
77
+ function isWebp(data) {
78
+ if (data.length < 12) return false;
79
+ return data[0] === 82 && // R
80
+ data[1] === 73 && // I
81
+ data[2] === 70 && // F
82
+ data[3] === 70 && // F
83
+ data[8] === 87 && // W
84
+ data[9] === 69 && // E
85
+ data[10] === 66 && // B
86
+ data[11] === 80;
87
+ }
88
+ function detectFormat(data) {
89
+ if (isPng(data)) return "png";
90
+ if (isJpeg(data)) return "jpeg";
91
+ if (isWebp(data)) return "webp";
92
+ return null;
93
+ }
94
+ function readUint16BE(data, offset) {
95
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint16(
96
+ offset
97
+ );
98
+ }
99
+ function readUint16(data, offset, isLittleEndian) {
100
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint16(
101
+ offset,
102
+ isLittleEndian
103
+ );
104
+ }
69
105
  function readUint24LE(data, offset) {
70
106
  return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8 | (data[offset + 2] ?? 0) << 16;
71
107
  }
72
108
  function readUint32BE(data, offset) {
73
- return (data[offset] ?? 0) << 24 | (data[offset + 1] ?? 0) << 16 | (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
109
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(
110
+ offset
111
+ );
74
112
  }
75
113
  function readUint32LE(data, offset) {
76
- return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8 | (data[offset + 2] ?? 0) << 16 | (data[offset + 3] ?? 0) << 24;
114
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(
115
+ offset,
116
+ true
117
+ );
77
118
  }
78
- function writeUint32BE(data, offset, value) {
79
- data[offset] = value >>> 24 & 255;
80
- data[offset + 1] = value >>> 16 & 255;
81
- data[offset + 2] = value >>> 8 & 255;
82
- data[offset + 3] = value & 255;
119
+ function readUint32(data, offset, isLittleEndian) {
120
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(
121
+ offset,
122
+ isLittleEndian
123
+ );
83
124
  }
84
125
  function readChunkType(data, offset) {
85
126
  return String.fromCharCode(
@@ -89,77 +130,94 @@ function readChunkType(data, offset) {
89
130
  data[offset + 3] ?? 0
90
131
  );
91
132
  }
92
- function readUint16(data, offset, isLittleEndian) {
93
- if (isLittleEndian) {
94
- return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8;
95
- }
96
- return (data[offset] ?? 0) << 8 | (data[offset + 1] ?? 0);
133
+ function writeUint16BE(data, offset, value) {
134
+ new DataView(data.buffer, data.byteOffset, data.byteLength).setUint16(
135
+ offset,
136
+ value
137
+ );
97
138
  }
98
- function readUint32(data, offset, isLittleEndian) {
99
- if (isLittleEndian) {
100
- return (data[offset] ?? 0) | (data[offset + 1] ?? 0) << 8 | (data[offset + 2] ?? 0) << 16 | (data[offset + 3] ?? 0) << 24;
101
- }
102
- return (data[offset] ?? 0) << 24 | (data[offset + 1] ?? 0) << 16 | (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
139
+ function writeUint16(data, offset, value, isLittleEndian) {
140
+ new DataView(data.buffer, data.byteOffset, data.byteLength).setUint16(
141
+ offset,
142
+ value,
143
+ isLittleEndian
144
+ );
103
145
  }
104
- function arraysEqual(a, b) {
105
- if (a.length !== b.length) return false;
106
- for (let i = 0; i < a.length; i++) {
107
- if (a[i] !== b[i]) return false;
108
- }
109
- return true;
146
+ function writeUint32BE(data, offset, value) {
147
+ new DataView(data.buffer, data.byteOffset, data.byteLength).setUint32(
148
+ offset,
149
+ value
150
+ );
110
151
  }
111
- function writeUint16(data, offset, value, isLittleEndian) {
112
- if (isLittleEndian) {
113
- data[offset] = value & 255;
114
- data[offset + 1] = value >>> 8 & 255;
115
- } else {
116
- data[offset] = value >>> 8 & 255;
117
- data[offset + 1] = value & 255;
118
- }
152
+ function writeUint32LE(data, offset, value) {
153
+ new DataView(data.buffer, data.byteOffset, data.byteLength).setUint32(
154
+ offset,
155
+ value,
156
+ true
157
+ );
119
158
  }
120
159
  function writeUint32(data, offset, value, isLittleEndian) {
121
- if (isLittleEndian) {
122
- data[offset] = value & 255;
123
- data[offset + 1] = value >>> 8 & 255;
124
- data[offset + 2] = value >>> 16 & 255;
125
- data[offset + 3] = value >>> 24 & 255;
126
- } else {
127
- data[offset] = value >>> 24 & 255;
128
- data[offset + 1] = value >>> 16 & 255;
129
- data[offset + 2] = value >>> 8 & 255;
130
- data[offset + 3] = value & 255;
160
+ new DataView(data.buffer, data.byteOffset, data.byteLength).setUint32(
161
+ offset,
162
+ value,
163
+ isLittleEndian
164
+ );
165
+ }
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;
131
176
  }
177
+ return true;
132
178
  }
133
- function writeUint32LE(data, offset, value) {
134
- data[offset] = value & 255;
135
- data[offset + 1] = value >>> 8 & 255;
136
- data[offset + 2] = value >>> 16 & 255;
137
- data[offset + 3] = value >>> 24 & 255;
179
+ function isXmpKeyword(keyword) {
180
+ return keyword === XMP_KEYWORD;
138
181
  }
139
- function isPng(data) {
140
- if (data.length < 8) return false;
141
- 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;
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;
142
194
  }
143
- function isJpeg(data) {
144
- if (data.length < 2) return false;
145
- return data[0] === 255 && data[1] === 216;
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;
146
199
  }
147
- function isWebp(data) {
148
- if (data.length < 12) return false;
149
- return data[0] === 82 && // R
150
- data[1] === 73 && // I
151
- data[2] === 70 && // F
152
- data[3] === 70 && // F
153
- data[8] === 87 && // W
154
- data[9] === 69 && // E
155
- data[10] === 66 && // B
156
- data[11] === 80;
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;
157
206
  }
158
- function detectFormat(data) {
159
- if (isPng(data)) return "png";
160
- if (isJpeg(data)) return "jpeg";
161
- if (isWebp(data)) return "webp";
162
- return null;
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
163
221
  }
164
222
 
165
223
  // src/utils/exif-constants.ts
@@ -276,23 +334,23 @@ function writeIfdEntry(data, offset, tag, dataOffset, isLittleEndian) {
276
334
  writeUint32(data, offset + 8, dataOffset ?? 0, isLittleEndian);
277
335
  }
278
336
  }
337
+ var UNICODE_PREFIX = new Uint8Array([
338
+ 85,
339
+ 78,
340
+ 73,
341
+ 67,
342
+ 79,
343
+ 68,
344
+ 69,
345
+ 0
346
+ ]);
279
347
  function encodeUserComment(text) {
280
- const utf16Data = [];
348
+ const result = new Uint8Array(8 + text.length * 2);
349
+ const dataView = new DataView(result.buffer);
350
+ result.set(UNICODE_PREFIX);
281
351
  for (let i = 0; i < text.length; i++) {
282
- const code = text.charCodeAt(i);
283
- utf16Data.push(code & 255);
284
- utf16Data.push(code >> 8 & 255);
285
- }
286
- const result = new Uint8Array(8 + utf16Data.length);
287
- result[0] = 85;
288
- result[1] = 78;
289
- result[2] = 73;
290
- result[3] = 67;
291
- result[4] = 79;
292
- result[5] = 68;
293
- result[6] = 69;
294
- result[7] = 0;
295
- result.set(new Uint8Array(utf16Data), 8);
352
+ dataView.setUint16(8 + i * 2, text.charCodeAt(i), true);
353
+ }
296
354
  return result;
297
355
  }
298
356
  function encodeAsciiTag(text, prefix) {
@@ -315,6 +373,7 @@ function writeJpegMetadata(data, segments) {
315
373
  return Result.error({ type: "invalidSignature" });
316
374
  }
317
375
  const comSegments = segments.filter((s) => s.source.type === "jpegCom");
376
+ const xmpSegments = segments.filter((s) => s.source.type === "xmpPacket");
318
377
  const exifSegments = segments.filter(
319
378
  (s) => s.source.type === "exifUserComment" || s.source.type === "exifImageDescription" || s.source.type === "exifMake"
320
379
  );
@@ -324,11 +383,16 @@ function writeJpegMetadata(data, segments) {
324
383
  }
325
384
  const { beforeSos, scanData } = collectResult.value;
326
385
  const app1Segment = exifSegments.length > 0 ? buildApp1Segment(exifSegments) : null;
327
- const comSegmentData = comSegments.map((s) => buildComSegment(s.data));
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);
328
389
  let totalSize = 2;
329
390
  if (app1Segment) {
330
391
  totalSize += app1Segment.length;
331
392
  }
393
+ if (xmpApp1Segment) {
394
+ totalSize += xmpApp1Segment.length;
395
+ }
332
396
  for (const seg of beforeSos) {
333
397
  totalSize += seg.length;
334
398
  }
@@ -344,6 +408,10 @@ function writeJpegMetadata(data, segments) {
344
408
  output.set(app1Segment, offset);
345
409
  offset += app1Segment.length;
346
410
  }
411
+ if (xmpApp1Segment) {
412
+ output.set(xmpApp1Segment, offset);
413
+ offset += xmpApp1Segment.length;
414
+ }
347
415
  for (const seg of beforeSos) {
348
416
  output.set(seg, offset);
349
417
  offset += seg.length;
@@ -383,7 +451,7 @@ function collectNonMetadataSegments(data) {
383
451
  message: "Unexpected end of file"
384
452
  });
385
453
  }
386
- const length = (data[offset] ?? 0) << 8 | (data[offset + 1] ?? 0);
454
+ const length = readUint16BE(data, offset);
387
455
  const segmentStart = offset - 2;
388
456
  const segmentEnd = offset + length;
389
457
  if (segmentEnd > data.length) {
@@ -398,8 +466,9 @@ function collectNonMetadataSegments(data) {
398
466
  data[offset + 5] === 102 && // f
399
467
  data[offset + 6] === 0 && // NULL
400
468
  data[offset + 7] === 0;
469
+ const isXmpApp1 = marker === APP1_MARKER && !isExifApp1 && matchesXmpPrefix(data, offset + 2);
401
470
  const isCom = marker === COM_MARKER;
402
- if (!isExifApp1 && !isCom) {
471
+ if (!isExifApp1 && !isXmpApp1 && !isCom) {
403
472
  beforeSos.push(data.slice(segmentStart, segmentEnd));
404
473
  }
405
474
  offset = segmentEnd;
@@ -418,20 +487,35 @@ function buildApp1Segment(segments) {
418
487
  const segment = new Uint8Array(2 + segmentLength);
419
488
  segment[0] = 255;
420
489
  segment[1] = APP1_MARKER;
421
- segment[2] = segmentLength >> 8 & 255;
422
- segment[3] = segmentLength & 255;
490
+ writeUint16BE(segment, 2, segmentLength);
423
491
  segment.set(EXIF_HEADER, 4);
424
492
  segment.set(tiffData, 4 + EXIF_HEADER.length);
425
493
  return segment;
426
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
+ }
427
509
  function buildComSegment(text) {
428
510
  const textBytes = new TextEncoder().encode(text);
429
511
  const segmentLength = 2 + textBytes.length;
512
+ if (segmentLength > 65535) {
513
+ return null;
514
+ }
430
515
  const segment = new Uint8Array(2 + segmentLength);
431
516
  segment[0] = 255;
432
517
  segment[1] = COM_MARKER;
433
- segment[2] = segmentLength >> 8 & 255;
434
- segment[3] = segmentLength & 255;
518
+ writeUint16BE(segment, 2, segmentLength);
435
519
  segment.set(textBytes, 4);
436
520
  return segment;
437
521
  }
@@ -491,6 +575,7 @@ function collectNonTextChunks(data) {
491
575
  if (offset + 4 > data.length) break;
492
576
  const chunkType = readChunkType(data, offset);
493
577
  offset += 4;
578
+ if (offset + length + 4 > data.length) break;
494
579
  offset += length;
495
580
  offset += 4;
496
581
  const chunkEnd = offset;
@@ -545,9 +630,7 @@ function serializeITXtChunk(chunk) {
545
630
  function buildChunk(type, data) {
546
631
  const chunk = new Uint8Array(4 + 4 + data.length + 4);
547
632
  writeUint32BE(chunk, 0, data.length);
548
- for (let i = 0; i < 4; i++) {
549
- chunk[4 + i] = type.charCodeAt(i);
550
- }
633
+ chunk.set(new TextEncoder().encode(type), 4);
551
634
  chunk.set(data, 8);
552
635
  const crcData = chunk.slice(4, 8 + data.length);
553
636
  const crc = calculateCrc32(crcData);
@@ -591,23 +674,29 @@ function calculateCrc32(data) {
591
674
  // src/writers/webp.ts
592
675
  var RIFF_SIGNATURE = new Uint8Array([82, 73, 70, 70]);
593
676
  var WEBP_MARKER = new Uint8Array([87, 69, 66, 80]);
594
- var EXIF_CHUNK_TYPE = new Uint8Array([69, 88, 73, 70]);
595
677
  function writeWebpMetadata(data, segments) {
596
678
  if (!isWebp(data)) {
597
679
  return Result.error({ type: "invalidSignature" });
598
680
  }
599
- const collectResult = collectNonExifChunks(data);
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);
600
684
  if (!collectResult.ok) {
601
685
  return collectResult;
602
686
  }
603
687
  const { chunks } = collectResult.value;
604
- const exifChunk = buildExifChunk(segments);
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);
605
694
  let newFileSize = 4;
606
695
  for (const chunk of chunks) {
607
696
  newFileSize += chunk.length;
608
697
  }
609
- if (exifChunk) {
610
- newFileSize += exifChunk.length;
698
+ for (const meta of metadataChunks) {
699
+ newFileSize += meta.length;
611
700
  }
612
701
  const output = new Uint8Array(8 + newFileSize);
613
702
  let offset = 0;
@@ -617,54 +706,48 @@ function writeWebpMetadata(data, segments) {
617
706
  offset += 4;
618
707
  output.set(WEBP_MARKER, offset);
619
708
  offset += 4;
620
- let exifWritten = false;
709
+ let metadataWritten = false;
621
710
  for (const chunk of chunks) {
622
711
  output.set(chunk, offset);
623
712
  offset += chunk.length;
624
- if (!exifWritten && exifChunk && isImageChunk(chunk)) {
625
- output.set(exifChunk, offset);
626
- offset += exifChunk.length;
627
- exifWritten = true;
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;
628
719
  }
629
720
  }
630
- if (!exifWritten && exifChunk) {
631
- output.set(exifChunk, offset);
721
+ if (!metadataWritten) {
722
+ for (const meta of metadataChunks) {
723
+ output.set(meta, offset);
724
+ offset += meta.length;
725
+ }
632
726
  }
633
727
  return Result.ok(output);
634
728
  }
635
729
  function isImageChunk(chunk) {
636
730
  if (chunk.length < 4) return false;
637
- const type = String.fromCharCode(
638
- chunk[0] ?? 0,
639
- chunk[1] ?? 0,
640
- chunk[2] ?? 0,
641
- chunk[3] ?? 0
642
- );
731
+ const type = readChunkType(chunk, 0);
643
732
  return type === "VP8 " || type === "VP8L" || type === "VP8X";
644
733
  }
645
- function collectNonExifChunks(data) {
734
+ function collectNonMetadataChunks(data) {
646
735
  const chunks = [];
647
736
  let firstChunkType = "";
648
737
  let offset = 12;
649
738
  while (offset < data.length - 8) {
650
- const chunkType = data.slice(offset, offset + 4);
651
- const typeStr = String.fromCharCode(
652
- chunkType[0] ?? 0,
653
- chunkType[1] ?? 0,
654
- chunkType[2] ?? 0,
655
- chunkType[3] ?? 0
656
- );
739
+ const typeStr = readChunkType(data, offset);
740
+ const chunkSize = readUint32LE(data, offset + 4);
657
741
  if (!firstChunkType) {
658
742
  firstChunkType = typeStr;
659
743
  }
660
- const chunkSize = (data[offset + 4] ?? 0) | (data[offset + 5] ?? 0) << 8 | (data[offset + 6] ?? 0) << 16 | (data[offset + 7] ?? 0) << 24;
661
744
  if (offset + 8 + chunkSize > data.length) {
662
745
  return Result.error({
663
746
  type: "invalidRiffStructure",
664
747
  message: `Chunk extends beyond file at offset ${offset}`
665
748
  });
666
749
  }
667
- if (!arraysEqual(chunkType, EXIF_CHUNK_TYPE)) {
750
+ if (typeStr !== "EXIF" && typeStr !== "XMP ") {
668
751
  const paddedSize2 = chunkSize + chunkSize % 2;
669
752
  const chunkData = data.slice(offset, offset + 8 + paddedSize2);
670
753
  chunks.push(chunkData);
@@ -675,24 +758,31 @@ function collectNonExifChunks(data) {
675
758
  return Result.ok({ chunks, firstChunkType });
676
759
  }
677
760
  function buildExifChunk(segments) {
678
- const exifSegments = segments.filter(
679
- (s) => s.source.type === "exifUserComment" || s.source.type === "exifImageDescription" || s.source.type === "exifMake"
680
- );
681
- if (exifSegments.length === 0) {
761
+ if (segments.length === 0) {
682
762
  return null;
683
763
  }
684
- const tiffData = buildExifTiffData(exifSegments);
764
+ const tiffData = buildExifTiffData(segments);
685
765
  if (tiffData.length === 0) {
686
766
  return null;
687
767
  }
688
768
  const chunkSize = tiffData.length;
689
769
  const paddedSize = chunkSize + chunkSize % 2;
690
770
  const chunk = new Uint8Array(8 + paddedSize);
691
- chunk.set(EXIF_CHUNK_TYPE, 0);
771
+ chunk.set(new TextEncoder().encode("EXIF"));
692
772
  writeUint32LE(chunk, 4, chunkSize);
693
773
  chunk.set(tiffData, 8);
694
774
  return chunk;
695
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
+ }
696
786
 
697
787
  // src/api/stringify.ts
698
788
  function normalizeLineEndings(text) {
@@ -877,6 +967,7 @@ function parseParametersText(text) {
877
967
  function parseSettings(settings) {
878
968
  const result = /* @__PURE__ */ new Map();
879
969
  if (!settings) return result;
970
+ if (settings.length > 1e4) return result;
880
971
  const regex = /([A-Za-z][A-Za-z0-9 ]*?):\s*([^,]+?)(?=,\s*[A-Za-z][A-Za-z0-9 ]*?:|$)/g;
881
972
  for (const match of settings.matchAll(regex)) {
882
973
  const key = (match[1] ?? "").trim();
@@ -1405,6 +1496,9 @@ function detectUniqueKeywords(entryRecord) {
1405
1496
  if (entryRecord.Software?.startsWith("NovelAI")) {
1406
1497
  return "novelai";
1407
1498
  }
1499
+ if (entryRecord.CreatorTool?.startsWith("Draw Things")) {
1500
+ return "draw-things";
1501
+ }
1408
1502
  const keyResult = detectByUniqueKey(entryRecord);
1409
1503
  if (keyResult) return keyResult;
1410
1504
  if ("fooocus_scheme" in entryRecord) {
@@ -1424,29 +1518,28 @@ function detectUniqueKeywords(entryRecord) {
1424
1518
  return null;
1425
1519
  }
1426
1520
  function detectFromCommentJson(comment) {
1427
- try {
1428
- const parsed = JSON.parse(comment);
1429
- const keyResult = detectByUniqueKey(parsed);
1430
- if (keyResult) return keyResult;
1431
- if ("prompt" in parsed && "workflow" in parsed) {
1432
- const workflow = parsed.workflow;
1433
- const prompt = parsed.prompt;
1434
- const isObject = typeof workflow === "object" || typeof prompt === "object";
1435
- const isJsonString = typeof workflow === "string" && workflow.startsWith("{") || typeof prompt === "string" && prompt.startsWith("{");
1436
- if (isObject || isJsonString) {
1437
- return "comfyui";
1438
- }
1521
+ const result = parseJson(comment);
1522
+ if (!result.ok || result.type !== "object") return null;
1523
+ const parsed = result.value;
1524
+ const keyResult = detectByUniqueKey(parsed);
1525
+ if (keyResult) return keyResult;
1526
+ if ("prompt" in parsed && "workflow" in parsed) {
1527
+ const workflow = parsed.workflow;
1528
+ const prompt = parsed.prompt;
1529
+ const isObject = typeof workflow === "object" || typeof prompt === "object";
1530
+ const isJsonString = typeof workflow === "string" && workflow.startsWith("{") || typeof prompt === "string" && prompt.startsWith("{");
1531
+ if (isObject || isJsonString) {
1532
+ return "comfyui";
1439
1533
  }
1440
- if (M_SWARMUI in parsed) {
1534
+ }
1535
+ if (M_SWARMUI in parsed) {
1536
+ return "swarmui";
1537
+ }
1538
+ if ("prompt" in parsed && "parameters" in parsed) {
1539
+ const params = String(parsed.parameters || "");
1540
+ if (params.includes(M_SWARMUI) || params.includes(M_SWARM_VERSION)) {
1441
1541
  return "swarmui";
1442
1542
  }
1443
- if ("prompt" in parsed && "parameters" in parsed) {
1444
- const params = String(parsed.parameters || "");
1445
- if (params.includes(M_SWARMUI) || params.includes(M_SWARM_VERSION)) {
1446
- return "swarmui";
1447
- }
1448
- }
1449
- } catch {
1450
1543
  }
1451
1544
  return null;
1452
1545
  }
@@ -1544,6 +1637,60 @@ function detectFromA1111Format(text) {
1544
1637
  return null;
1545
1638
  }
1546
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
+
1547
1694
  // src/parsers/easydiffusion.ts
1548
1695
  function extractModelName(path) {
1549
1696
  if (!path) return void 0;
@@ -1552,7 +1699,7 @@ function extractModelName(path) {
1552
1699
  }
1553
1700
  function parseEasyDiffusion(entries) {
1554
1701
  if ("use_stable_diffusion_model" in entries) {
1555
- return buildMetadata(entries);
1702
+ return buildMetadata2(entries);
1556
1703
  }
1557
1704
  const jsonText = entries.UserComment?.startsWith("{") ? entries.UserComment : entries.parameters?.startsWith("{") ? entries.parameters : void 0;
1558
1705
  if (!jsonText) {
@@ -1565,9 +1712,9 @@ function parseEasyDiffusion(entries) {
1565
1712
  message: "Invalid JSON in Easy Diffusion metadata"
1566
1713
  });
1567
1714
  }
1568
- return buildMetadata(parsed.value);
1715
+ return buildMetadata2(parsed.value);
1569
1716
  }
1570
- function buildMetadata(data) {
1717
+ function buildMetadata2(data) {
1571
1718
  const str = (key) => {
1572
1719
  const v = data[key];
1573
1720
  return typeof v === "string" ? v : void 0;
@@ -1996,8 +2143,10 @@ function parseMetadata(entries) {
1996
2143
  case "civitai": {
1997
2144
  const comfyResult = parseComfyUI(entries);
1998
2145
  if (comfyResult.ok) {
1999
- comfyResult.value.software = "civitai";
2000
- return comfyResult;
2146
+ return Result.ok({
2147
+ ...comfyResult.value,
2148
+ software: "civitai"
2149
+ });
2001
2150
  }
2002
2151
  return parseA1111(entries, "civitai");
2003
2152
  }
@@ -2026,6 +2175,8 @@ function parseMetadata(entries) {
2026
2175
  }
2027
2176
  case "ruined-fooocus":
2028
2177
  return parseRuinedFooocus(entries);
2178
+ case "draw-things":
2179
+ return parseDrawThings(entries);
2029
2180
  default:
2030
2181
  return Result.error({ type: "unsupportedFormat" });
2031
2182
  }
@@ -2057,10 +2208,10 @@ function readJpegDimensions(data) {
2057
2208
  offset++;
2058
2209
  continue;
2059
2210
  }
2060
- const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
2211
+ const length = readUint16BE(data, offset + 2);
2061
2212
  if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
2062
- const height = (data[offset + 5] ?? 0) << 8 | (data[offset + 6] ?? 0);
2063
- const width = (data[offset + 7] ?? 0) << 8 | (data[offset + 8] ?? 0);
2213
+ const height = readUint16BE(data, offset + 5);
2214
+ const width = readUint16BE(data, offset + 7);
2064
2215
  return { width, height };
2065
2216
  }
2066
2217
  offset += 2 + length;
@@ -2082,12 +2233,12 @@ function readWebpDimensions(data) {
2082
2233
  }
2083
2234
  if (chunkType === "VP8 ") {
2084
2235
  const start = offset + 8;
2085
- const tag = (data[start] ?? 0) | (data[start + 1] ?? 0) << 8 | (data[start + 2] ?? 0) << 16;
2236
+ const tag = readUint24LE(data, start);
2086
2237
  const keyFrame = !(tag & 1);
2087
2238
  if (keyFrame) {
2088
2239
  if (data[start + 3] === 157 && data[start + 4] === 1 && data[start + 5] === 42) {
2089
- const wRaw = (data[start + 6] ?? 0) | (data[start + 7] ?? 0) << 8;
2090
- const hRaw = (data[start + 8] ?? 0) | (data[start + 9] ?? 0) << 8;
2240
+ const wRaw = readUint16(data, start + 6, true);
2241
+ const hRaw = readUint16(data, start + 8, true);
2091
2242
  return { width: wRaw & 16383, height: hRaw & 16383 };
2092
2243
  }
2093
2244
  }
@@ -2131,6 +2282,10 @@ function extractTagsFromIfd(data, ifdOffset, isLittleEndian) {
2131
2282
  const count = readUint32(data, offset + 4, isLittleEndian);
2132
2283
  const typeSize = getTypeSize(type);
2133
2284
  const dataSize = count * typeSize;
2285
+ if (dataSize > data.length) {
2286
+ offset += 12;
2287
+ continue;
2288
+ }
2134
2289
  let valueOffset;
2135
2290
  if (dataSize <= 4) {
2136
2291
  valueOffset = offset + 8;
@@ -2266,36 +2421,31 @@ function decodeUserComment(data) {
2266
2421
  }
2267
2422
  }
2268
2423
  function decodeUtf16BE(data) {
2269
- const chars = [];
2270
- for (let i = 0; i < data.length - 1; i += 2) {
2271
- const code = (data[i] ?? 0) << 8 | (data[i + 1] ?? 0);
2272
- if (code === 0) break;
2273
- chars.push(String.fromCharCode(code));
2274
- }
2275
- return chars.join("");
2424
+ const decoded = new TextDecoder("utf-16be").decode(data);
2425
+ const nullIndex = decoded.indexOf("\0");
2426
+ return nullIndex >= 0 ? decoded.slice(0, nullIndex) : decoded;
2276
2427
  }
2277
2428
  function decodeUtf16LE(data) {
2278
- const chars = [];
2279
- for (let i = 0; i < data.length - 1; i += 2) {
2280
- const code = (data[i] ?? 0) | (data[i + 1] ?? 0) << 8;
2281
- if (code === 0) break;
2282
- chars.push(String.fromCharCode(code));
2283
- }
2284
- return chars.join("");
2429
+ const decoded = new TextDecoder("utf-16le").decode(data);
2430
+ const nullIndex = decoded.indexOf("\0");
2431
+ return nullIndex >= 0 ? decoded.slice(0, nullIndex) : decoded;
2285
2432
  }
2286
2433
  function decodeAscii(data) {
2287
- const chars = [];
2288
- for (let i = 0; i < data.length; i++) {
2289
- if (data[i] === 0) break;
2290
- chars.push(String.fromCharCode(data[i] ?? 0));
2291
- }
2292
- return chars.join("");
2434
+ const nullIndex = data.indexOf(0);
2435
+ const sliced = nullIndex >= 0 ? data.subarray(0, nullIndex) : data;
2436
+ return new TextDecoder("ascii").decode(sliced);
2293
2437
  }
2294
2438
 
2295
2439
  // src/readers/jpeg.ts
2296
2440
  var APP1_MARKER2 = 225;
2297
2441
  var COM_MARKER2 = 254;
2298
- var EXIF_HEADER2 = new Uint8Array([69, 120, 105, 102, 0, 0]);
2442
+ function matchesExifHeader(data, offset) {
2443
+ return data[offset] === 69 && // E
2444
+ data[offset + 1] === 120 && // x
2445
+ data[offset + 2] === 105 && // i
2446
+ data[offset + 3] === 102 && // f
2447
+ data[offset + 4] === 0 && data[offset + 5] === 0;
2448
+ }
2299
2449
  function readJpegMetadata(data) {
2300
2450
  if (!isJpeg(data)) {
2301
2451
  return Result.error({ type: "invalidSignature" });
@@ -2307,6 +2457,15 @@ function readJpegMetadata(data) {
2307
2457
  const exifSegments = parseExifMetadataSegments(exifData);
2308
2458
  segments.push(...exifSegments);
2309
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
+ }
2310
2469
  const comSegment = findComSegment(data);
2311
2470
  if (comSegment) {
2312
2471
  const comData = data.slice(
@@ -2335,12 +2494,11 @@ function findApp1Segment(data) {
2335
2494
  offset++;
2336
2495
  continue;
2337
2496
  }
2338
- const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
2339
- if (marker === APP1_MARKER2) {
2497
+ const length = readUint16BE(data, offset + 2);
2498
+ if (marker === APP1_MARKER2 && length >= 8) {
2340
2499
  const headerStart = offset + 4;
2341
2500
  if (headerStart + 6 <= data.length) {
2342
- const header = data.slice(headerStart, headerStart + 6);
2343
- if (arraysEqual(header, EXIF_HEADER2)) {
2501
+ if (matchesExifHeader(data, headerStart)) {
2344
2502
  return {
2345
2503
  offset: headerStart + 6,
2346
2504
  length: length - 8
@@ -2356,6 +2514,35 @@ function findApp1Segment(data) {
2356
2514
  }
2357
2515
  return null;
2358
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
+ }
2359
2546
  function findComSegment(data) {
2360
2547
  let offset = 2;
2361
2548
  while (offset < data.length - 4) {
@@ -2368,8 +2555,8 @@ function findComSegment(data) {
2368
2555
  offset++;
2369
2556
  continue;
2370
2557
  }
2371
- const length = (data[offset + 2] ?? 0) << 8 | (data[offset + 3] ?? 0);
2372
- if (marker === COM_MARKER2) {
2558
+ const length = readUint16BE(data, offset + 2);
2559
+ if (marker === COM_MARKER2 && length >= 2) {
2373
2560
  return {
2374
2561
  offset: offset + 4,
2375
2562
  length: length - 2
@@ -2469,7 +2656,7 @@ function tryUtf8Decode(data) {
2469
2656
  }
2470
2657
  function parseITXtChunk(data) {
2471
2658
  let offset = 0;
2472
- const keywordEnd = findNull(data, offset);
2659
+ const keywordEnd = data.indexOf(0, offset);
2473
2660
  if (keywordEnd === -1) return null;
2474
2661
  const keyword = utf8Decode(data.slice(offset, keywordEnd));
2475
2662
  offset = keywordEnd + 1;
@@ -2479,11 +2666,11 @@ function parseITXtChunk(data) {
2479
2666
  if (offset >= data.length) return null;
2480
2667
  const compressionMethod = data[offset] ?? 0;
2481
2668
  offset += 1;
2482
- const langEnd = findNull(data, offset);
2669
+ const langEnd = data.indexOf(0, offset);
2483
2670
  if (langEnd === -1) return null;
2484
2671
  const languageTag = utf8Decode(data.slice(offset, langEnd));
2485
2672
  offset = langEnd + 1;
2486
- const transEnd = findNull(data, offset);
2673
+ const transEnd = data.indexOf(0, offset);
2487
2674
  if (transEnd === -1) return null;
2488
2675
  const translatedKeyword = utf8Decode(data.slice(offset, transEnd));
2489
2676
  offset = transEnd + 1;
@@ -2505,20 +2692,8 @@ function parseITXtChunk(data) {
2505
2692
  text
2506
2693
  };
2507
2694
  }
2508
- function findNull(data, offset) {
2509
- for (let i = offset; i < data.length; i++) {
2510
- if (data[i] === 0) {
2511
- return i;
2512
- }
2513
- }
2514
- return -1;
2515
- }
2516
2695
  function latin1Decode(data) {
2517
- let result = "";
2518
- for (let i = 0; i < data.length; i++) {
2519
- result += String.fromCharCode(data[i] ?? 0);
2520
- }
2521
- return result;
2696
+ return new TextDecoder("iso-8859-1").decode(data);
2522
2697
  }
2523
2698
  function utf8Decode(data) {
2524
2699
  return new TextDecoder("utf-8").decode(data);
@@ -2528,28 +2703,55 @@ function decompressZlib(_data) {
2528
2703
  }
2529
2704
 
2530
2705
  // src/readers/webp.ts
2531
- var EXIF_CHUNK_TYPE2 = new Uint8Array([69, 88, 73, 70]);
2532
2706
  function readWebpMetadata(data) {
2533
2707
  if (!isWebp(data)) {
2534
2708
  return Result.error({ type: "invalidSignature" });
2535
2709
  }
2710
+ const segments = [];
2536
2711
  const exifChunk = findExifChunk(data);
2537
- if (!exifChunk) {
2538
- return Result.ok([]);
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
+ });
2539
2730
  }
2540
- const exifData = data.slice(
2541
- exifChunk.offset,
2542
- exifChunk.offset + exifChunk.length
2543
- );
2544
- const segments = parseExifMetadataSegments(exifData);
2545
2731
  return Result.ok(segments);
2546
2732
  }
2547
2733
  function findExifChunk(data) {
2548
2734
  let offset = 12;
2549
- while (offset < data.length - 8) {
2550
- const chunkType = data.slice(offset, offset + 4);
2735
+ while (offset + 8 <= data.length) {
2551
2736
  const chunkSize = readUint32LE(data, offset + 4);
2552
- if (arraysEqual(chunkType, EXIF_CHUNK_TYPE2)) {
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;
2553
2755
  return {
2554
2756
  offset: offset + 8,
2555
2757
  length: chunkSize
@@ -2563,20 +2765,37 @@ function findExifChunk(data) {
2563
2765
 
2564
2766
  // src/utils/convert.ts
2565
2767
  function pngChunksToRecord(chunks) {
2566
- return Object.fromEntries(chunks.map((c) => [c.keyword, c.text]));
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
+ );
2567
2777
  }
2568
2778
  function segmentsToRecord(segments) {
2569
2779
  const record = {};
2570
2780
  for (const segment of segments) {
2571
2781
  const keyword = sourceToKeyword(segment.source);
2572
2782
  const text = segment.data;
2573
- if (segment.source.type === "exifUserComment" && text.startsWith("{")) {
2574
- const expanded = tryExpandNovelAIWebpFormat(text);
2783
+ if (segment.source.type === "xmpPacket") {
2784
+ const expanded = extractXmpEntries(text);
2575
2785
  if (expanded) {
2576
2786
  Object.assign(record, expanded);
2577
2787
  continue;
2578
2788
  }
2579
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
+ }
2580
2799
  record[keyword] = text;
2581
2800
  }
2582
2801
  return record;
@@ -2606,6 +2825,8 @@ function sourceToKeyword(source) {
2606
2825
  return source.prefix ?? "ImageDescription";
2607
2826
  case "exifMake":
2608
2827
  return source.prefix ?? "Make";
2828
+ case "xmpPacket":
2829
+ return "XML:com.adobe.xmp";
2609
2830
  }
2610
2831
  }
2611
2832
 
@@ -2777,6 +2998,24 @@ function convertComfyUISegmentsToPng(segments) {
2777
2998
  return tryParseExtendedFormat(segments) ?? tryParseSaveImagePlusFormat(segments) ?? [];
2778
2999
  }
2779
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
+
2780
3019
  // src/converters/easydiffusion.ts
2781
3020
  function convertEasyDiffusionPngToSegments(chunks) {
2782
3021
  const json = Object.fromEntries(
@@ -2830,12 +3069,11 @@ function convertNovelaiPngToSegments(chunks) {
2830
3069
  );
2831
3070
  }
2832
3071
  function buildUserCommentJson(chunks) {
2833
- return NOVELAI_KEY_ORDER.map((key) => {
2834
- const chunk = chunks.find((c) => c.keyword === key);
2835
- return chunk ? { [key]: chunk.text } : null;
2836
- }).filter((entry) => entry !== null).reduce(
2837
- (acc, entry) => Object.assign(acc, entry),
2838
- {}
3072
+ return Object.fromEntries(
3073
+ NOVELAI_KEY_ORDER.flatMap((key) => {
3074
+ const chunk = chunks.find((c) => c.keyword === key);
3075
+ return chunk ? [[key, chunk.text]] : [];
3076
+ })
2839
3077
  );
2840
3078
  }
2841
3079
  var NOVELAI_KEY_ORDER = [
@@ -3093,6 +3331,10 @@ var convertHfSpace = createFormatConverter(
3093
3331
  createPngToSegments("parameters"),
3094
3332
  createSegmentsToPng("parameters", "text-unicode-escape")
3095
3333
  );
3334
+ var convertDrawThings = createFormatConverter(
3335
+ convertDrawThingsPngToSegments,
3336
+ convertDrawThingsSegmentsToPng
3337
+ );
3096
3338
  var convertCivitai = createFormatConverter(
3097
3339
  convertCivitaiPngToSegments,
3098
3340
  convertCivitaiSegmentsToPng
@@ -3134,7 +3376,9 @@ var softwareConverters = {
3134
3376
  // InvokeAI
3135
3377
  invokeai: convertInvokeAI,
3136
3378
  // HuggingFace Space
3137
- "hf-space": convertHfSpace
3379
+ "hf-space": convertHfSpace,
3380
+ // Draw Things (XMP format)
3381
+ "draw-things": convertDrawThings
3138
3382
  };
3139
3383
 
3140
3384
  // src/api/write.ts
@@ -3260,7 +3504,8 @@ var softwareLabels = Object.freeze({
3260
3504
  "hf-space": "Hugging Face Space",
3261
3505
  easydiffusion: "Easy Diffusion",
3262
3506
  fooocus: "Fooocus",
3263
- "ruined-fooocus": "Ruined Fooocus"
3507
+ "ruined-fooocus": "Ruined Fooocus",
3508
+ "draw-things": "Draw Things"
3264
3509
  });
3265
3510
  export {
3266
3511
  embed,