@enslo/sd-metadata 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rikuto Nakao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # sd-metadata
2
+
3
+ A TypeScript library to read and write metadata embedded in AI-generated images.
4
+
5
+ ## Features
6
+
7
+ - **Multi-format Support**: PNG (tEXt / iTXt), JPEG (COM / Exif), WebP (Exif)
8
+ - **Unified API**: Simple `read()` and `write()` functions work across all formats
9
+ - **TypeScript Native**: Written in TypeScript with full type definitions included
10
+ - **Zero Dependencies**: Works in Node.js and browsers without any external dependencies
11
+ - **Format Conversion**: Seamlessly convert metadata between PNG, JPEG, and WebP
12
+ - **Lossless Round-trip**: Preserves original metadata structure when converting back to native format
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @enslo/sd-metadata
18
+ ```
19
+
20
+ ## Tool Support
21
+
22
+ | Tool | PNG | JPEG | WebP |
23
+ | ------ | :---: | :----: | :----: |
24
+ | [NovelAI](https://novelai.net/) * | ✅ | 🔄️ | ✅ |
25
+ | [ComfyUI](https://github.com/comfyanonymous/ComfyUI) * | ✅ | 🔄️ | 🔄️ |
26
+ | [AUTOMATIC1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | ⚠️ | ⚠️ | ⚠️ |
27
+ | [Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) / [Forge Neo](https://github.com/neggles/sd-webui-forge-neoforge) | ✅ | ✅ | ✅ |
28
+ | [InvokeAI](https://github.com/invoke-ai/InvokeAI) | ✅ | 🔄️ | 🔄️ |
29
+ | [SwarmUI](https://github.com/Stability-AI/StableSwarmUI) * | ✅ | ✅ | ✅ |
30
+ | [Civitai](https://civitai.com/) | ⚠️ | ✅ | ⚠️ |
31
+ | [TensorArt](https://tensor.art/) | ✅ | 🔄️ | 🔄️ |
32
+ | [Stability Matrix](https://github.com/LykosAI/StabilityMatrix) | ✅ | 🔄️ | 🔄️ |
33
+ | [HuggingFace Space](https://huggingface.co/spaces) | ✅ | 🔄️ | 🔄️ |
34
+ | [Ruined Fooocus](https://github.com/runew0lf/RuinedFooocus) | ✅ | 🔄️ | 🔄️ |
35
+ | [Easy Diffusion](https://github.com/easydiffusion/easydiffusion) | ⚠️ | ⚠️ | ⚠️ |
36
+ | [Fooocus](https://github.com/lllyasviel/Fooocus) | ⚠️ | ⚠️ | ⚠️ |
37
+
38
+ **Legend:**
39
+
40
+ - ✅ **Fully Supported** - Formats natively supported by the tool, verified with sample files
41
+ - 🔄️ **Extended Support** - sd-metadata specific parsers, cross-format conversion supported
42
+ - ⚠️ **Experimental** - Implemented from reference code, not verified with samples
43
+
44
+ > [!NOTE]
45
+ > \* Tools with known limitations. See [Known Limitations](#known-limitations) for details.
46
+ >
47
+ > [!TIP]
48
+ > **Help us expand tool support!** We're actively collecting sample images from experimental tools (Easy Diffusion, Fooocus) and unsupported tools. If you have sample images generated by these or other AI tools, please consider contributing them! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
49
+
50
+ ## Usage
51
+
52
+ ### Node.js Usage
53
+
54
+ ```typescript
55
+ import { read, write } from 'sd-metadata';
56
+ import { readFileSync, writeFileSync } from 'fs';
57
+
58
+ // Read metadata from any supported format
59
+ const imageData = readFileSync('image.png');
60
+ const result = read(imageData);
61
+
62
+ if (result.status === 'success') {
63
+ console.log('Tool:', result.tool); // 'novelai', 'comfyui', etc.
64
+ console.log('Prompt:', result.prompt);
65
+ console.log('Model:', result.parameters?.model);
66
+ console.log('Size:', result.width, 'x', result.height);
67
+ }
68
+ ```
69
+
70
+ ### Browser Usage
71
+
72
+ ```typescript
73
+ import { read } from 'sd-metadata';
74
+
75
+ // Handle file input
76
+ const fileInput = document.querySelector('input[type="file"]');
77
+ fileInput.addEventListener('change', async (e) => {
78
+ const file = e.target.files[0];
79
+ const arrayBuffer = await file.arrayBuffer();
80
+ const imageData = new Uint8Array(arrayBuffer);
81
+
82
+ const result = read(imageData);
83
+
84
+ if (result.status === 'success') {
85
+ document.getElementById('tool').textContent = result.tool;
86
+ document.getElementById('prompt').textContent = result.prompt;
87
+ document.getElementById('model').textContent = result.parameters?.model || 'N/A';
88
+ }
89
+ });
90
+ ```
91
+
92
+ ### Format Conversion
93
+
94
+ Convert metadata between different image formats:
95
+
96
+ ```typescript
97
+ import { read, write } from 'sd-metadata';
98
+
99
+ // Read from PNG
100
+ const pngData = readFileSync('comfyui-output.png');
101
+ const metadata = read(pngData);
102
+
103
+ // Write to JPEG
104
+ const jpegData = readFileSync('target.jpg');
105
+ const result = write(jpegData, metadata);
106
+
107
+ if (result.type === 'success') {
108
+ writeFileSync('output.jpg', result.data);
109
+ console.log('Metadata converted from PNG to JPEG');
110
+ }
111
+ ```
112
+
113
+ ### Handling Different Result Types
114
+
115
+ ```typescript
116
+ import { read } from 'sd-metadata';
117
+
118
+ const result = read(imageData);
119
+
120
+ switch (result.status) {
121
+ case 'success':
122
+ // Metadata successfully parsed
123
+ console.log(`Generated by ${result.tool}`);
124
+ console.log(`Prompt: ${result.prompt}`);
125
+ break;
126
+
127
+ case 'unrecognized':
128
+ // Format detected but not recognized as AI-generated
129
+ console.log('Not an AI-generated image');
130
+ // You can still access raw metadata for debugging:
131
+ console.log('Raw chunks:', result.raw);
132
+ break;
133
+
134
+ case 'empty':
135
+ // No metadata found
136
+ console.log('No metadata in this image');
137
+ break;
138
+
139
+ case 'unsupportedFormat':
140
+ // Not a PNG, JPEG, or WebP file
141
+ console.log('Unsupported image format');
142
+ break;
143
+
144
+ case 'invalid':
145
+ // Corrupted or invalid image data
146
+ console.log('Error:', result.message);
147
+ break;
148
+ }
149
+ ```
150
+
151
+ ### Force Conversion for Unrecognized Formats
152
+
153
+ When you have unrecognized metadata but still want to convert it:
154
+
155
+ ```typescript
156
+ import { read, write } from 'sd-metadata';
157
+
158
+ const source = read(unknownImage);
159
+ // source.status === 'unrecognized'
160
+
161
+ // Force blind conversion (preserves all metadata chunks/segments)
162
+ const result = write(targetImage, source, { force: true });
163
+
164
+ if (result.type === 'success') {
165
+ // Metadata successfully converted even though format wasn't recognized
166
+ console.log('Forced conversion succeeded');
167
+ }
168
+ ```
169
+
170
+ ### Removing Metadata
171
+
172
+ To strip all metadata from an image:
173
+
174
+ ```typescript
175
+ import { write } from 'sd-metadata';
176
+
177
+ const result = write(imageData, { status: 'empty' });
178
+ if (result.type === 'success') {
179
+ writeFileSync('clean-image.png', result.data);
180
+ }
181
+ ```
182
+
183
+ ## API Reference
184
+
185
+ ### `read(data: Uint8Array): ParseResult`
186
+
187
+ Reads and parses metadata from an image file.
188
+
189
+ **Returns:**
190
+
191
+ - `{ status: 'success', tool, prompt, parameters, width, height, raw }` - Successfully parsed
192
+ - `{ status: 'unrecognized', raw }` - Image has metadata but not from a known AI tool
193
+ - `{ status: 'empty' }` - No metadata found
194
+ - `{ status: 'unsupportedFormat' }` - Not a PNG, JPEG, or WebP file
195
+ - `{ status: 'invalid', message }` - Corrupted or invalid image data
196
+
197
+ ### `write(data: Uint8Array, metadata: ParseResult, options?: WriteOptions): WriteResult`
198
+
199
+ Writes metadata to an image file.
200
+
201
+ **Parameters:**
202
+
203
+ - `data` - Target image file data (PNG, JPEG, or WebP)
204
+ - `metadata` - ParseResult from `read()`
205
+ - `status: 'success'` or `'empty'` - Can write directly
206
+ - `status: 'unrecognized'` - Requires `force: true` option
207
+ - `options` - Optional settings:
208
+ - `force?: boolean` - Required when writing `status: 'unrecognized'` metadata
209
+
210
+ **Returns:**
211
+
212
+ - `{ type: 'success', data }` - Successfully written
213
+ - `{ type: 'unsupportedFormat' }` - Target is not PNG, JPEG, or WebP
214
+ - `{ type: 'conversionFailed', message }` - Metadata conversion failed
215
+ - `{ type: 'writeFailed', message }` - Failed to write metadata to image
216
+
217
+ ## Known Limitations
218
+
219
+ > [!WARNING]
220
+ > **ComfyUI JPEG/WebP**: While reading supports major custom node formats (e.g., `save-image-extended`), writing always uses the `comfyui-saveimage-plus` format. This format provides the best information preservation and is compatible with ComfyUI's native drag-and-drop workflow loading.
221
+ >
222
+ > [!WARNING]
223
+ > **NovelAI WebP**: Auto-corrects corrupted UTF-8 in the Description field, which means WebP → PNG → WebP round-trip is not content-equivalent (but provides valid, readable metadata).
224
+ >
225
+ > [!WARNING]
226
+ > **SwarmUI PNG→JPEG/WebP**: PNG files contain both ComfyUI workflow and SwarmUI parameters. When converting to JPEG/WebP, only parameters are preserved to match the native format. Metadata is fully preserved, but the ComfyUI workflow in the `prompt` chunk is lost.
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ # Install dependencies
232
+ npm install
233
+
234
+ # Run tests
235
+ npm test
236
+
237
+ # Watch mode
238
+ npm run test:watch
239
+
240
+ # Test coverage
241
+ npm run test:coverage
242
+
243
+ # Build
244
+ npm run build
245
+
246
+ # Lint
247
+ npm run lint
248
+ ```
249
+
250
+ ## License
251
+
252
+ MIT
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Result type for explicit error handling
3
+ */
4
+ type Result<T, E> = {
5
+ ok: true;
6
+ value: T;
7
+ } | {
8
+ ok: false;
9
+ error: E;
10
+ };
11
+ /**
12
+ * Helper functions for Result type
13
+ */
14
+ declare const Result: {
15
+ ok: <T, E>(value: T) => Result<T, E>;
16
+ error: <T, E>(error: E) => Result<T, E>;
17
+ };
18
+ /**
19
+ * PNG text chunk (tEXt or iTXt)
20
+ */
21
+ type PngTextChunk = TExtChunk | ITXtChunk;
22
+ /**
23
+ * Source location of a metadata segment.
24
+ * Used for round-tripping: reading and writing back to the correct location.
25
+ */
26
+ type MetadataSegmentSource = {
27
+ type: 'exifUserComment';
28
+ } | {
29
+ type: 'exifImageDescription';
30
+ prefix?: string;
31
+ } | {
32
+ type: 'exifMake';
33
+ prefix?: string;
34
+ } | {
35
+ type: 'jpegCom';
36
+ };
37
+ /**
38
+ * A single metadata segment with source tracking
39
+ */
40
+ interface MetadataSegment {
41
+ /** Source location of this segment */
42
+ source: MetadataSegmentSource;
43
+ /** Raw data string */
44
+ data: string;
45
+ }
46
+ /**
47
+ * Raw metadata for write-back (preserves original format)
48
+ */
49
+ type RawMetadata = {
50
+ format: 'png';
51
+ chunks: PngTextChunk[];
52
+ } | {
53
+ format: 'jpeg';
54
+ segments: MetadataSegment[];
55
+ } | {
56
+ format: 'webp';
57
+ segments: MetadataSegment[];
58
+ };
59
+ /**
60
+ * tEXt chunk (Latin-1 encoded text)
61
+ */
62
+ interface TExtChunk {
63
+ type: 'tEXt';
64
+ /** Chunk keyword (e.g., 'parameters', 'Comment') */
65
+ keyword: string;
66
+ /** Text content */
67
+ text: string;
68
+ }
69
+ /**
70
+ * iTXt chunk (UTF-8 encoded international text)
71
+ */
72
+ interface ITXtChunk {
73
+ type: 'iTXt';
74
+ /** Chunk keyword */
75
+ keyword: string;
76
+ /** Compression flag (0=uncompressed, 1=compressed) */
77
+ compressionFlag: number;
78
+ /** Compression method (0=zlib/deflate) */
79
+ compressionMethod: number;
80
+ /** Language tag (BCP 47) */
81
+ languageTag: string;
82
+ /** Translated keyword */
83
+ translatedKeyword: string;
84
+ /** Text content */
85
+ text: string;
86
+ }
87
+ /**
88
+ * Known AI image generation software
89
+ */
90
+ type GenerationSoftware = 'novelai' | 'comfyui' | 'swarmui' | 'tensorart' | 'stability-matrix' | 'invokeai' | 'forge-neo' | 'forge' | 'sd-webui' | 'sd-next' | 'civitai' | 'hf-space' | 'easydiffusion' | 'fooocus' | 'ruined-fooocus';
91
+ /**
92
+ * Metadata format classification
93
+ *
94
+ * This represents the format/structure of the metadata, not the specific tool.
95
+ * Use this to determine which fields are available and how to interpret them.
96
+ */
97
+ type MetadataFormat = 'novelai' | 'comfyui' | 'a1111' | 'invokeai' | 'swarmui';
98
+ /**
99
+ * Base metadata fields shared by all tools
100
+ */
101
+ interface BaseMetadata {
102
+ /** Format classification (for type narrowing) */
103
+ type: MetadataFormat;
104
+ /** Positive prompt */
105
+ prompt: string;
106
+ /** Negative prompt */
107
+ negativePrompt: string;
108
+ /** Model settings */
109
+ model?: ModelSettings;
110
+ /** Sampling settings */
111
+ sampling?: SamplingSettings;
112
+ /** Hires.fix settings (if applied) */
113
+ hires?: HiresSettings;
114
+ /** Upscale settings (if applied) */
115
+ upscale?: UpscaleSettings;
116
+ /** Image width */
117
+ width: number;
118
+ /** Image height */
119
+ height: number;
120
+ }
121
+ /**
122
+ * NovelAI-specific metadata
123
+ */
124
+ interface NovelAIMetadata extends BaseMetadata {
125
+ type: 'novelai';
126
+ software: 'novelai';
127
+ /** V4 character prompts (when using character placement) */
128
+ characterPrompts?: CharacterPrompt[];
129
+ /** Use character coordinates for placement */
130
+ useCoords?: boolean;
131
+ /** Use character order */
132
+ useOrder?: boolean;
133
+ }
134
+ /**
135
+ * Character prompt with position (NovelAI V4)
136
+ */
137
+ interface CharacterPrompt {
138
+ /** Character-specific prompt */
139
+ prompt: string;
140
+ /** Character position (normalized 0-1) */
141
+ center?: {
142
+ x: number;
143
+ y: number;
144
+ };
145
+ }
146
+ /**
147
+ * ComfyUI-format metadata (ComfyUI, TensorArt, Stability Matrix)
148
+ *
149
+ * These tools use ComfyUI-compatible workflow format.
150
+ */
151
+ interface ComfyUIMetadata extends BaseMetadata {
152
+ type: 'comfyui';
153
+ software: 'comfyui' | 'tensorart' | 'stability-matrix';
154
+ /** Full workflow JSON (for reproducibility) */
155
+ workflow?: unknown;
156
+ }
157
+ /**
158
+ * A1111-format metadata (SD WebUI, Forge, Forge Neo, Civitai)
159
+ */
160
+ interface A1111Metadata extends BaseMetadata {
161
+ type: 'a1111';
162
+ software: 'sd-webui' | 'sd-next' | 'forge' | 'forge-neo' | 'civitai' | 'hf-space' | 'easydiffusion' | 'fooocus' | 'ruined-fooocus';
163
+ }
164
+ /**
165
+ * InvokeAI-specific metadata
166
+ */
167
+ interface InvokeAIMetadata extends BaseMetadata {
168
+ type: 'invokeai';
169
+ software: 'invokeai';
170
+ }
171
+ /**
172
+ * SwarmUI-specific metadata
173
+ */
174
+ interface SwarmUIMetadata extends BaseMetadata {
175
+ type: 'swarmui';
176
+ software: 'swarmui';
177
+ }
178
+ /**
179
+ * Unified generation metadata (discriminated union)
180
+ *
181
+ * Use `metadata.type` to narrow by format, or `metadata.software` for specific tool:
182
+ * ```typescript
183
+ * if (metadata.type === 'comfyui') {
184
+ * // TypeScript knows metadata.workflow exists
185
+ * }
186
+ * if (metadata.software === 'tensorart') {
187
+ * // Specific tool within comfyui format
188
+ * }
189
+ * ```
190
+ */
191
+ type GenerationMetadata = NovelAIMetadata | ComfyUIMetadata | A1111Metadata | InvokeAIMetadata | SwarmUIMetadata;
192
+ /**
193
+ * Model settings
194
+ */
195
+ interface ModelSettings {
196
+ /** Model name */
197
+ name?: string;
198
+ /** Model hash */
199
+ hash?: string;
200
+ /** VAE name */
201
+ vae?: string;
202
+ }
203
+ /**
204
+ * Sampling settings
205
+ */
206
+ interface SamplingSettings {
207
+ /** Sampler name */
208
+ sampler?: string;
209
+ /** Scheduler (sometimes included in sampler, sometimes separate) */
210
+ scheduler?: string;
211
+ /** Sampling steps */
212
+ steps?: number;
213
+ /** CFG scale */
214
+ cfg?: number;
215
+ /** Random seed */
216
+ seed?: number;
217
+ /** CLIP skip layers */
218
+ clipSkip?: number;
219
+ }
220
+ /**
221
+ * Hires.fix settings
222
+ */
223
+ interface HiresSettings {
224
+ /** Upscale factor */
225
+ scale?: number;
226
+ /** Upscaler name */
227
+ upscaler?: string;
228
+ /** Hires steps */
229
+ steps?: number;
230
+ /** Hires denoising strength */
231
+ denoise?: number;
232
+ }
233
+ /**
234
+ * Upscale settings (post-generation)
235
+ */
236
+ interface UpscaleSettings {
237
+ /** Upscaler name */
238
+ upscaler?: string;
239
+ /** Scale factor */
240
+ scale?: number;
241
+ }
242
+ /**
243
+ * Parse result with 4-status design
244
+ *
245
+ * - `success`: Parsing succeeded, metadata and raw data available
246
+ * - `empty`: No metadata found in the file
247
+ * - `unrecognized`: Metadata exists but format is not recognized
248
+ * - `invalid`: File is corrupted or not a valid image
249
+ */
250
+ type ParseResult = {
251
+ status: 'success';
252
+ metadata: GenerationMetadata;
253
+ raw: RawMetadata;
254
+ } | {
255
+ status: 'empty';
256
+ } | {
257
+ status: 'unrecognized';
258
+ raw: RawMetadata;
259
+ } | {
260
+ status: 'invalid';
261
+ message?: string;
262
+ };
263
+
264
+ /**
265
+ * sd-metadata - Read and write AI-generated image metadata
266
+ */
267
+
268
+ /**
269
+ * Result of the write operation
270
+ */
271
+ type WriteResult = Result<Uint8Array, {
272
+ type: 'unsupportedFormat';
273
+ } | {
274
+ type: 'conversionFailed';
275
+ message: string;
276
+ } | {
277
+ type: 'writeFailed';
278
+ message: string;
279
+ }>;
280
+ /**
281
+ * Options for write operation
282
+ */
283
+ interface WriteOptions {
284
+ /**
285
+ * Force blind conversion for unrecognized formats
286
+ *
287
+ * When true, converts raw chunks/segments between formats even when
288
+ * the generating software is unknown. Enables format conversion for
289
+ * unknown/future tools without parser implementation.
290
+ *
291
+ * When false (default), returns error for unrecognized formats.
292
+ *
293
+ * @default false
294
+ */
295
+ force?: boolean;
296
+ }
297
+ /**
298
+ * Read and parse metadata from an image
299
+ *
300
+ * Automatically detects the image format (PNG, JPEG, WebP) and parses
301
+ * any embedded generation metadata.
302
+ *
303
+ * @param data - Image file data
304
+ * @returns Parse result containing metadata and raw data
305
+ */
306
+ declare function read(data: Uint8Array): ParseResult;
307
+ /**
308
+ * Write metadata to an image
309
+ *
310
+ * Automatically detects the target image format and converts the metadata
311
+ * if necessary.
312
+ *
313
+ * @param data - Target image file data
314
+ * @param metadata - ParseResult from `read()` (must be 'success' or contain raw data)
315
+ * @param options - Write options (e.g., { force: true } for blind conversion)
316
+ * @returns New image data with embedded metadata
317
+ */
318
+ declare function write(data: Uint8Array, metadata: ParseResult, options?: WriteOptions): WriteResult;
319
+
320
+ export { type GenerationMetadata, type GenerationSoftware, type MetadataFormat, type ParseResult, type RawMetadata, type WriteOptions, type WriteResult, read, write };