From 878aedb4f70254d024730ef6ee3f17a057d04a34 Mon Sep 17 00:00:00 2001 From: thot experiment <94414189+thot-experiment@users.noreply.github.com> Date: Thu, 1 May 2025 16:26:24 -0700 Subject: [PATCH] add svg metadata loading (#3719) Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com> --- browser_tests/assets/workflow.svg | 596 ++++++++++++++++++ browser_tests/fixtures/ComfyPage.ts | 1 + .../tests/loadWorkflowInMedia.spec.ts | 3 +- src/scripts/app.ts | 10 + src/scripts/metadata/svg.ts | 18 + 5 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 browser_tests/assets/workflow.svg create mode 100644 src/scripts/metadata/svg.ts diff --git a/browser_tests/assets/workflow.svg b/browser_tests/assets/workflow.svg new file mode 100644 index 000000000..4e0fc30f1 --- /dev/null +++ b/browser_tests/assets/workflow.svg @@ -0,0 +1,596 @@ + + + +{ + "workflow": { + "last_node_id": 11, + "last_link_id": 11, + "nodes": [ + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": { + "0": 26, + "1": 474 + }, + "size": { + "0": 315, + "1": 98 + }, + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 1 + ], + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 3, + 5 + ], + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 8 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "sdxl\\sd_xl_base_1.0_0.9vae.safetensors" + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": { + "0": 473, + "1": 609 + }, + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 2 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 896, + 1152, + 1 + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": { + "0": 863, + "1": 186 + }, + "size": { + "0": 315, + "1": 262 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 1 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 2 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 7 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 0, + "fixed", + 50, + 8, + "euler", + "normal", + 1 + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": { + "0": 413, + "1": 389 + }, + "size": { + "0": 425.27801513671875, + "1": 180.6060791015625 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 5 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 6 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "text, watermark" + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": { + "0": 415, + "1": 186 + }, + "size": { + "0": 422.84503173828125, + "1": 164.31304931640625 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 3 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 4 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "a clear and sparkling quartz crystal on top of a cherrywood table, dispersion optics" + ] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": { + "0": 1209, + "1": 188 + }, + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": 8 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 10 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 10, + "type": "SaveAnimatedWEBP", + "pos": { + "0": 1450, + "1": 190 + }, + "size": { + "0": 315, + "1": 154 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 10 + } + ], + "outputs": [], + "properties": {}, + "widgets_values": [ + "ComfyUI", + 6, + false, + 98, + "slowest" + ] + } + ], + "links": [ + [ + 1, + 4, + 0, + 3, + 0, + "MODEL" + ], + [ + 2, + 5, + 0, + 3, + 3, + "LATENT" + ], + [ + 3, + 4, + 1, + 6, + 0, + "CLIP" + ], + [ + 4, + 6, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 5, + 4, + 1, + 7, + 0, + "CLIP" + ], + [ + 6, + 7, + 0, + 3, + 2, + "CONDITIONING" + ], + [ + 7, + 3, + 0, + 8, + 0, + "LATENT" + ], + [ + 8, + 4, + 2, + 8, + 1, + "VAE" + ], + [ + 10, + 8, + 0, + 10, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.9090909090909091, + "offset": [ + -168.6014778137205, + 15.451593017578356 + ] + } + }, + "version": 0.4, + "widget_idx_map": { + "3": { + "seed": 0, + "sampler_name": 4, + "scheduler": 5 + } + } + }, + "prompt": { + "3": { + "inputs": { + "seed": 0, + "steps": 50, + "cfg": 8, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 1, + "model": [ + "4", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "5", + 0 + ] + }, + "class_type": "KSampler" + }, + "4": { + "inputs": { + "ckpt_name": "sdxl\\sd_xl_base_1.0_0.9vae.safetensors" + }, + "class_type": "CheckpointLoaderSimple" + }, + "5": { + "inputs": { + "width": 896, + "height": 1152, + "batch_size": 1 + }, + "class_type": "EmptyLatentImage" + }, + "6": { + "inputs": { + "text": "a clear and sparkling quartz crystal on top of a cherrywood table, dispersion optics", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode" + }, + "7": { + "inputs": { + "text": "text, watermark", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode" + }, + "8": { + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode" + }, + "10": { + "inputs": { + "filename_prefix": "ComfyUI", + "fps": 6, + "lossless": false, + "quality": 98, + "method": "slowest", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveAnimatedWEBP" + } + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index caee56dcf..d0ac6b276 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -490,6 +490,7 @@ export class ComfyPage { const getFileType = (fileName: string) => { if (fileName.endsWith('.png')) return 'image/png' + if (fileName.endsWith('.svg')) return 'image/svg+xml' if (fileName.endsWith('.webp')) return 'image/webp' if (fileName.endsWith('.webm')) return 'video/webm' if (fileName.endsWith('.json')) return 'application/json' diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts b/browser_tests/tests/loadWorkflowInMedia.spec.ts index 39df2d77b..813fe8b54 100644 --- a/browser_tests/tests/loadWorkflowInMedia.spec.ts +++ b/browser_tests/tests/loadWorkflowInMedia.spec.ts @@ -12,7 +12,8 @@ test.describe('Load Workflow in Media', () => { 'workflow.glb', 'workflow.mp4', 'workflow.mov', - 'workflow.m4v' + 'workflow.m4v', + 'workflow.svg' ] fileNames.forEach(async (fileName) => { test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 3a033453f..da803eb81 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -63,6 +63,7 @@ import { deserialiseAndCreate } from '@/utils/vintageClipboard' import { type ComfyApi, PromptExecutionError, api } from './api' import { defaultGraph } from './defaultGraph' import { pruneWidgets } from './domWidget' +import { getSvgMetadata } from './metadata/svg' import { getFlacMetadata, getLatentMetadata, @@ -1310,6 +1311,15 @@ export class ComfyApp { } else if (mp4Info.prompt) { this.loadApiJson(mp4Info.prompt, fileName) } + } else if (file.type === 'image/svg+xml' || file.name?.endsWith('.svg')) { + const svgInfo = await getSvgMetadata(file) + if (svgInfo.workflow) { + this.loadGraphData(svgInfo.workflow, true, true, fileName) + } else if (svgInfo.prompt) { + this.loadApiJson(svgInfo.prompt, fileName) + } else { + this.showErrorOnFileLoad(file) + } } else if ( file.type === 'model/gltf-binary' || file.name?.endsWith('.glb') diff --git a/src/scripts/metadata/svg.ts b/src/scripts/metadata/svg.ts new file mode 100644 index 000000000..90dc95b10 --- /dev/null +++ b/src/scripts/metadata/svg.ts @@ -0,0 +1,18 @@ +import { ComfyMetadata } from '@/types/metadataTypes' + +export async function getSvgMetadata(file: File): Promise { + const text = await file.text() + const metadataMatch = + /\s*\s*<\/metadata>/i.exec(text) + + if (metadataMatch && metadataMatch[1]) { + try { + return JSON.parse(metadataMatch[1].trim()) + } catch (error) { + console.error('Error parsing SVG metadata:', error) + return {} + } + } + + return {} +}