@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/CHANGELOG.md +428 -0
- package/README.ja.md +9 -23
- package/README.md +10 -24
- package/dist/index.d.ts +4 -2
- package/dist/index.global.js +7 -7
- package/dist/index.js +467 -222
- package/dist/index.js.map +1 -1
- package/docs/types.ja.md +799 -0
- package/docs/types.md +799 -0
- package/package.json +11 -13
- package/LICENSE +0 -21
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
|
|
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
|
|
114
|
+
return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(
|
|
115
|
+
offset,
|
|
116
|
+
true
|
|
117
|
+
);
|
|
77
118
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
134
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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, "&");
|
|
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
|
|
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
|
-
|
|
283
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
610
|
-
newFileSize +=
|
|
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
|
|
709
|
+
let metadataWritten = false;
|
|
621
710
|
for (const chunk of chunks) {
|
|
622
711
|
output.set(chunk, offset);
|
|
623
712
|
offset += chunk.length;
|
|
624
|
-
if (!
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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 (!
|
|
631
|
-
|
|
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 =
|
|
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
|
|
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
|
|
651
|
-
const
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1715
|
+
return buildMetadata2(parsed.value);
|
|
1569
1716
|
}
|
|
1570
|
-
function
|
|
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
|
-
|
|
2000
|
-
|
|
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
|
|
2211
|
+
const length = readUint16BE(data, offset + 2);
|
|
2061
2212
|
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
2062
|
-
const height = (data
|
|
2063
|
-
const width = (data
|
|
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
|
|
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
|
|
2090
|
-
const hRaw = (data
|
|
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
|
|
2270
|
-
|
|
2271
|
-
|
|
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
|
|
2279
|
-
|
|
2280
|
-
|
|
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
|
|
2288
|
-
|
|
2289
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
2538
|
-
|
|
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
|
|
2550
|
-
const chunkType = data.slice(offset, offset + 4);
|
|
2735
|
+
while (offset + 8 <= data.length) {
|
|
2551
2736
|
const chunkSize = readUint32LE(data, offset + 4);
|
|
2552
|
-
if (
|
|
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(
|
|
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 === "
|
|
2574
|
-
const expanded =
|
|
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
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
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,
|