Start vanilla workflow conversion, better PNG parser based on catbox
userscript code
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { Progress, SerializedPrompt, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptOutputs } from "./components/ComfyApp";
|
import type { Progress, SerializedPrompt, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptOutputs, SerializedAppState } from "./components/ComfyApp";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type { ComfyImageLocation } from "$lib/utils";
|
import type { ComfyImageLocation } from "$lib/utils";
|
||||||
@@ -57,10 +57,14 @@ export type ComfyAPIHistoryResponse = {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SerializedComfyBoxPromptData = {
|
||||||
|
subgraphs: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export type ComfyPromptPNGInfo = {
|
export type ComfyPromptPNGInfo = {
|
||||||
workflow: SerializedLGraph,
|
workflow?: SerializedLGraph, // ComfyUI format
|
||||||
comfyBoxLayout: SerializedLayoutState,
|
comfyBoxWorkflow: SerializedAppState,
|
||||||
comfyBoxSubgraphs: string[],
|
comfyBoxPrompt: SerializedComfyBoxPromptData,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComfyBoxPromptExtraData = ComfyUIPromptExtraData & {
|
export type ComfyBoxPromptExtraData = ComfyUIPromptExtraData & {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core";
|
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core";
|
||||||
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
||||||
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
||||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
import { getPngMetadata, importA1111, parsePNGMetadata } from "$lib/pnginfo";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ import selectionState from "$lib/stores/selectionState";
|
|||||||
import layoutStates from "$lib/stores/layoutStates";
|
import layoutStates from "$lib/stores/layoutStates";
|
||||||
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||||
import workflowState from "$lib/stores/workflowState";
|
import workflowState from "$lib/stores/workflowState";
|
||||||
|
import convertVanillaWorkflow from "$lib/convertVanillaWorkflow";
|
||||||
|
|
||||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||||
|
|
||||||
@@ -72,8 +73,10 @@ export type A1111PromptAndInfo = {
|
|||||||
* Represents a single workflow that can be loaded into the program from JSON.
|
* Represents a single workflow that can be loaded into the program from JSON.
|
||||||
*/
|
*/
|
||||||
export type SerializedAppState = {
|
export type SerializedAppState = {
|
||||||
/** Program identifier, should always be "ComfyBox" */
|
/** For easy structural typing use */
|
||||||
createdBy: "ComfyBox",
|
comfyBoxWorkflow: true,
|
||||||
|
/** Program identifier, should be something like "ComfyBox" or "ComfyUI" */
|
||||||
|
createdBy: string,
|
||||||
/** Serial version, should be incremented on breaking changes */
|
/** Serial version, should be incremented on breaking changes */
|
||||||
version: number,
|
version: number,
|
||||||
/** Commit hash if found */
|
/** Commit hash if found */
|
||||||
@@ -138,6 +141,14 @@ type CanvasState = {
|
|||||||
canvas: ComfyGraphCanvas,
|
canvas: ComfyGraphCanvas,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isComfyBoxWorkflow(data: any): data is SerializedAppState {
|
||||||
|
return data != null && (typeof data === "object") && data.comfyBoxWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVanillaWorkflow(data: any): data is SerializedLGraph {
|
||||||
|
return data != null && (typeof data === "object") && data.last_node_id != null;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ComfyApp {
|
export default class ComfyApp {
|
||||||
api: ComfyAPI;
|
api: ComfyAPI;
|
||||||
|
|
||||||
@@ -237,6 +248,7 @@ export default class ComfyApp {
|
|||||||
const canvas = this.lCanvas.serialize();
|
const canvas = this.lCanvas.serialize();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
comfyBoxWorkflow: true,
|
||||||
createdBy: "ComfyBox",
|
createdBy: "ComfyBox",
|
||||||
version: COMFYBOX_SERIAL_VERSION,
|
version: COMFYBOX_SERIAL_VERSION,
|
||||||
commitHash: __GIT_COMMIT_HASH__,
|
commitHash: __GIT_COMMIT_HASH__,
|
||||||
@@ -399,7 +411,7 @@ export default class ComfyApp {
|
|||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflow && workflow.createdBy === "ComfyBox") {
|
if (workflow && typeof workflow.createdBy === "string") {
|
||||||
this.openWorkflow(workflow);
|
this.openWorkflow(workflow);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -537,6 +549,13 @@ export default class ComfyApp {
|
|||||||
return workflow;
|
return workflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openVanillaWorkflow(data: SerializedLGraph) {
|
||||||
|
const converted = convertVanillaWorkflow(data)
|
||||||
|
console.info("WORKFLWO", converted)
|
||||||
|
notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" })
|
||||||
|
// await this.openWorkflow(JSON.parse(pngInfo.workflow));
|
||||||
|
}
|
||||||
|
|
||||||
setActiveWorkflow(id: WorkflowInstID) {
|
setActiveWorkflow(id: WorkflowInstID) {
|
||||||
const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id)
|
const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id)
|
||||||
if (index === -1)
|
if (index === -1)
|
||||||
@@ -695,7 +714,7 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const p = this.graphToPrompt(workflow, tag);
|
const p = this.graphToPrompt(workflow, tag);
|
||||||
const l = workflow.layout.serialize();
|
const wf = this.serialize(workflow)
|
||||||
console.debug(graphToGraphVis(workflow.graph))
|
console.debug(graphToGraphVis(workflow.graph))
|
||||||
console.debug(promptToGraphVis(p))
|
console.debug(promptToGraphVis(p))
|
||||||
|
|
||||||
@@ -704,9 +723,10 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
const extraData: ComfyBoxPromptExtraData = {
|
const extraData: ComfyBoxPromptExtraData = {
|
||||||
extra_pnginfo: {
|
extra_pnginfo: {
|
||||||
workflow: p.workflow,
|
comfyBoxWorkflow: wf,
|
||||||
comfyBoxLayout: l,
|
comfyBoxPrompt: {
|
||||||
comfyBoxSubgraphs: [tag],
|
subgraphs: [tag]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
thumbnails
|
thumbnails
|
||||||
}
|
}
|
||||||
@@ -761,10 +781,14 @@ export default class ComfyApp {
|
|||||||
*/
|
*/
|
||||||
async handleFile(file: File) {
|
async handleFile(file: File) {
|
||||||
if (file.type === "image/png") {
|
if (file.type === "image/png") {
|
||||||
const pngInfo = await getPngMetadata(file);
|
const buffer = await file.arrayBuffer();
|
||||||
|
const pngInfo = await parsePNGMetadata(buffer);
|
||||||
if (pngInfo) {
|
if (pngInfo) {
|
||||||
if (pngInfo.comfyBoxConfig) {
|
if (pngInfo.comfyBoxWorkflow) {
|
||||||
await this.openWorkflow(JSON.parse(pngInfo.comfyBoxConfig));
|
await this.openWorkflow(JSON.parse(pngInfo.comfyBoxWorkflow));
|
||||||
|
} else if (pngInfo.workflow) {
|
||||||
|
const workflow = JSON.parse(pngInfo.workflow);
|
||||||
|
await this.openVanillaWorkflow(workflow);
|
||||||
} else if (pngInfo.parameters) {
|
} else if (pngInfo.parameters) {
|
||||||
const parsed = parseA1111(pngInfo.parameters)
|
const parsed = parseA1111(pngInfo.parameters)
|
||||||
if ("error" in parsed) {
|
if ("error" in parsed) {
|
||||||
@@ -787,7 +811,13 @@ export default class ComfyApp {
|
|||||||
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async () => {
|
reader.onload = async () => {
|
||||||
await this.openWorkflow(JSON.parse(reader.result as string));
|
const result = JSON.parse(reader.result as string)
|
||||||
|
if (isComfyBoxWorkflow(result)) {
|
||||||
|
await this.openWorkflow(result);
|
||||||
|
}
|
||||||
|
else if (isVanillaWorkflow(result)) {
|
||||||
|
await this.openVanillaWorkflow(result);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
dateStr = formatDate(date);
|
dateStr = formatDate(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs;
|
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
|
||||||
|
|
||||||
let message = "Prompt";
|
let message = "Prompt";
|
||||||
if (entry.workflowID != null) {
|
if (entry.workflowID != null) {
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div class="queue">
|
<div class="queue">
|
||||||
<DropZone {app} />
|
<!-- <DropZone {app} /> -->
|
||||||
<div class="queue-entries {mode}-mode" bind:this={queueList}>
|
<div class="queue-entries {mode}-mode" bind:this={queueList}>
|
||||||
{#if _entries.length > 0}
|
{#if _entries.length > 0}
|
||||||
{#each _entries as entry}
|
{#each _entries as entry}
|
||||||
@@ -305,8 +305,9 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
$pending-height: 200px;
|
$pending-height: 200px;
|
||||||
$bottom-bar-height: 70px;
|
$bottom-bar-height: 70px;
|
||||||
|
$workflow-tabs-height: 2.5rem;
|
||||||
$mode-buttons-height: 30px;
|
$mode-buttons-height: 30px;
|
||||||
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height});
|
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height} - #{$workflow-tabs-height} - 0.9rem);
|
||||||
|
|
||||||
.prompt-modal-header {
|
.prompt-modal-header {
|
||||||
padding-left: 0.2rem;
|
padding-left: 0.2rem;
|
||||||
|
|||||||
@@ -203,7 +203,7 @@
|
|||||||
{#if $workflowState.activeWorkflow != null}
|
{#if $workflowState.activeWorkflow != null}
|
||||||
<ComfyWorkflowView {app} workflow={$workflowState.activeWorkflow} />
|
<ComfyWorkflowView {app} workflow={$workflowState.activeWorkflow} />
|
||||||
{:else}
|
{:else}
|
||||||
<span>No workflow loaded</span>
|
<span style:color="var(--body-text-color)">No workflow loaded</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane bind:size={graphSize}>
|
<Pane bind:size={graphSize}>
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
|
<input bind:this={fileInput} id="comfy-file-input" type="file" accept="application/json,image/png" on:change={loadWorkflow} />
|
||||||
|
|
||||||
{#if appSetupPromise}
|
{#if appSetupPromise}
|
||||||
{#await appSetupPromise}
|
{#await appSetupPromise}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
let galleryStyle: Styles = {
|
let galleryStyle: Styles = {
|
||||||
grid_cols: [2],
|
grid_cols: [2],
|
||||||
object_fit: "cover",
|
object_fit: "contain",
|
||||||
height: "var(--size-96)"
|
height: "var(--size-96)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +165,11 @@
|
|||||||
|
|
||||||
> :global(.block) {
|
> :global(.block) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
:global(> .preview) {
|
||||||
|
height: 100%;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
src/lib/convertVanillaWorkflow.ts
Normal file
51
src/lib/convertVanillaWorkflow.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { LGraph, type SerializedLGraph } from "@litegraph-ts/core";
|
||||||
|
import type { SerializedAppState } from "./components/ComfyApp";
|
||||||
|
import layoutStates, { defaultWorkflowAttributes, type DragItemID, type SerializedDragEntry, type SerializedLayoutState } from "./stores/layoutStates";
|
||||||
|
import type { WorkflowAttributes } from "./stores/workflowState";
|
||||||
|
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The workflow type used by base ComfyUI
|
||||||
|
*/
|
||||||
|
export type ComfyVanillaWorkflow = SerializedLGraph;
|
||||||
|
|
||||||
|
function addLayoutToVanillaWorkflow(workflow: ComfyVanillaWorkflow): SerializedLayoutState {
|
||||||
|
// easier to create a real layout first and then serialize it, then have to
|
||||||
|
// deal with manually constructing the serialized state from the ground up
|
||||||
|
const layoutState = layoutStates.createRaw();
|
||||||
|
const graph = new LGraph();
|
||||||
|
graph.configure(workflow)
|
||||||
|
|
||||||
|
for (const node of graph.iterateNodesInOrder()) {
|
||||||
|
console.warn("NODE", node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return layoutState.serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function convertVanillaWorkflow(workflow: ComfyVanillaWorkflow): SerializedAppState {
|
||||||
|
const attrs: WorkflowAttributes = {
|
||||||
|
...defaultWorkflowAttributes,
|
||||||
|
title: "ComfyUI Workflow"
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas: SerializedGraphCanvasState = {
|
||||||
|
offset: [0, 0],
|
||||||
|
scale: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = addLayoutToVanillaWorkflow(workflow);
|
||||||
|
|
||||||
|
const appState: SerializedAppState = {
|
||||||
|
comfyBoxWorkflow: true,
|
||||||
|
createdBy: "ComfyUI", // ???
|
||||||
|
version: 1,
|
||||||
|
workflow,
|
||||||
|
attrs,
|
||||||
|
canvas,
|
||||||
|
layout,
|
||||||
|
commitHash: null
|
||||||
|
}
|
||||||
|
|
||||||
|
return appState
|
||||||
|
}
|
||||||
@@ -1,65 +1,158 @@
|
|||||||
import { LiteGraph, LGraph, LGraphNode } from "@litegraph-ts/core"
|
import { LiteGraph, LGraph, LGraphNode } from "@litegraph-ts/core"
|
||||||
import type ComfyAPI from "$lib/api"
|
import type ComfyAPI from "$lib/api"
|
||||||
|
|
||||||
class PNGMetadataPromise extends Promise<Record<string, string>> {
|
class Lazy<T> {
|
||||||
public cancelMethod: () => void;
|
thunk: () => T;
|
||||||
constructor(executor: (resolve: (value?: Record<string, string>) => void, reject: (reason?: any) => void) => void) {
|
cache: T | null;
|
||||||
super(executor);
|
|
||||||
|
|
||||||
|
constructor(thunk: () => T) {
|
||||||
|
this.thunk = thunk;
|
||||||
|
this.cache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//cancel the operation
|
get(): T {
|
||||||
public cancel() {
|
if (this.cache === null) {
|
||||||
if (this.cancelMethod) {
|
this.cache = this.thunk();
|
||||||
this.cancelMethod();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.cache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPngMetadata(file: File): PNGMetadataPromise {
|
interface PNGChunk {
|
||||||
return new PNGMetadataPromise((r, _) => {
|
type: string,
|
||||||
const reader = new FileReader();
|
data: Uint8Array,
|
||||||
reader.onload = (event: Event) => {
|
}
|
||||||
// Get the PNG data as a Uint8Array
|
|
||||||
const pngData = new Uint8Array((event.target as any).result);
|
|
||||||
const dataView = new DataView(pngData.buffer);
|
|
||||||
|
|
||||||
// Check that the PNG signature is present
|
enum ParseErrorKind {
|
||||||
if (dataView.getUint32(0) !== 0x89504e47) {
|
PNGMalformedHeader,
|
||||||
console.error("Not a valid PNG file");
|
PNGMalformedTextChunk,
|
||||||
r();
|
JPEGNoEXIF,
|
||||||
return;
|
JPEGNoUserComment,
|
||||||
}
|
JPEGMalformedUserComment,
|
||||||
|
UnexpectedEOF,
|
||||||
|
UnsupportedFileType,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
// Start searching for chunks after the PNG signature
|
interface ParseError {
|
||||||
let offset = 8;
|
kind: ParseErrorKind,
|
||||||
let txt_chunks = {};
|
error: any,
|
||||||
// Loop through the chunks in the PNG file
|
}
|
||||||
while (offset < pngData.length) {
|
|
||||||
// Get the length of the chunk
|
/*
|
||||||
const length = dataView.getUint32(offset);
|
* This function was taken from the hdg userscript
|
||||||
// Get the chunk type
|
*/
|
||||||
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
|
function* pngChunks(bytes: Uint8Array): Generator<PNGChunk, ParseError | null> {
|
||||||
if (type === "tEXt") {
|
const HEADER: number[] = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||||
// Get the keyword
|
const LENGTH_LEN = 4;
|
||||||
let keyword_end = offset + 8;
|
const TYPE_LEN = 4;
|
||||||
while (pngData[keyword_end] !== 0) {
|
const CRC_LEN = 4;
|
||||||
keyword_end++;
|
|
||||||
}
|
let view = new DataView(bytes.buffer);
|
||||||
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
|
let decoder = new TextDecoder("utf-8", { fatal: true });
|
||||||
// Get the text
|
let pos = 0;
|
||||||
const text = String.fromCharCode(...pngData.slice(keyword_end + 1, offset + 8 + length));
|
|
||||||
txt_chunks[keyword] = text;
|
for (let i = 0; i < HEADER.length; i++) {
|
||||||
|
if (bytes[i] != HEADER[i]) {
|
||||||
|
return {
|
||||||
|
kind: ParseErrorKind.PNGMalformedHeader,
|
||||||
|
error: `wrong PNG header: ${bytes.slice(0, HEADER.length)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos += HEADER.length;
|
||||||
|
|
||||||
|
while (pos < bytes.byteLength) {
|
||||||
|
try {
|
||||||
|
let len = view.getUint32(pos, false);
|
||||||
|
let type = decoder.decode(bytes.subarray(pos + LENGTH_LEN, pos + LENGTH_LEN + TYPE_LEN));
|
||||||
|
if (type.length < 4) {
|
||||||
|
return {
|
||||||
|
kind: ParseErrorKind.UnexpectedEOF,
|
||||||
|
error: "PNG parse error: unexpected EOF when parsing chunk type",
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
let start = pos + LENGTH_LEN + TYPE_LEN;
|
||||||
|
|
||||||
offset += 12 + length;
|
yield {
|
||||||
|
type,
|
||||||
|
data: bytes.subarray(start, start + len),
|
||||||
}
|
}
|
||||||
|
|
||||||
r(txt_chunks);
|
pos = start + len + CRC_LEN;
|
||||||
};
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
kind: ParseErrorKind.Other,
|
||||||
|
error: err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reader.readAsArrayBuffer(file);
|
return null;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
type PNGTextChunk = {
|
||||||
|
keyword: string,
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePNGTextChunk(data: Uint8Array): PNGTextChunk | ParseError {
|
||||||
|
let decoder = new TextDecoder("utf-8", { fatal: true });
|
||||||
|
|
||||||
|
let sep = data.findIndex(v => v === 0);
|
||||||
|
if (sep < 0) {
|
||||||
|
return {
|
||||||
|
kind: ParseErrorKind.PNGMalformedTextChunk,
|
||||||
|
error: "PNG parse error: no null separator in tEXt chunk",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let keyword = decoder.decode(data.subarray(0, sep));
|
||||||
|
let text = decoder.decode(data.subarray(sep + 1, data.byteLength));
|
||||||
|
return { keyword, text }
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
kind: ParseErrorKind.Other,
|
||||||
|
error: err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parsePNGMetadata(buf: ArrayBuffer): Promise<Record<string, string>> {
|
||||||
|
let bytes = new Uint8Array(buf);
|
||||||
|
const metadata = {}
|
||||||
|
let next: IteratorResult<PNGChunk, ParseError>;
|
||||||
|
|
||||||
|
let chunks = pngChunks(bytes);
|
||||||
|
do {
|
||||||
|
next = chunks.next();
|
||||||
|
if (!next.done) {
|
||||||
|
let chunk = next.value;
|
||||||
|
if ("kind" in chunk) {
|
||||||
|
console.warn("ignored a malformed PNG text chunk");
|
||||||
|
console.error(chunk.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.type !== "tEXt") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = parsePNGTextChunk(chunk.data);
|
||||||
|
if ("kind" in result) {
|
||||||
|
console.warn("ignored a malformed PNG text chunk");
|
||||||
|
console.error(result.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let textChunk = result;
|
||||||
|
metadata[textChunk.keyword] = textChunk.text
|
||||||
|
}
|
||||||
|
} while (next.value != null && !next.done)
|
||||||
|
|
||||||
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeIndex = { node: LGraphNode, index: number }
|
type NodeIndex = { node: LGraphNode, index: number }
|
||||||
|
|||||||
@@ -660,7 +660,7 @@ export interface WidgetLayout extends IDragItem {
|
|||||||
export type DragItemID = UUID;
|
export type DragItemID = UUID;
|
||||||
|
|
||||||
type LayoutStateOps = {
|
type LayoutStateOps = {
|
||||||
workflow: ComfyWorkflow,
|
workflow: ComfyWorkflow | null,
|
||||||
|
|
||||||
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index?: number) => ContainerLayout,
|
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index?: number) => ContainerLayout,
|
||||||
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index?: number) => WidgetLayout,
|
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index?: number) => WidgetLayout,
|
||||||
@@ -700,11 +700,7 @@ export type SerializedDragItem = {
|
|||||||
|
|
||||||
export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps;
|
export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps;
|
||||||
|
|
||||||
function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
|
function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateStore {
|
||||||
if (get(layoutStates).all[workflow.id] != null) {
|
|
||||||
throw new Error(`Layout state already created! ${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const store: Writable<LayoutState> = writable({
|
const store: Writable<LayoutState> = writable({
|
||||||
root: null,
|
root: null,
|
||||||
allItems: {},
|
allItems: {},
|
||||||
@@ -1160,7 +1156,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
|
|||||||
|
|
||||||
function notifyWorkflowModified() {
|
function notifyWorkflowModified() {
|
||||||
if (!get(store).isConfiguring)
|
if (!get(store).isConfiguring)
|
||||||
workflow.notifyModified();
|
workflow?.notifyModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
const layoutStateStore: WritableLayoutStateStore =
|
const layoutStateStore: WritableLayoutStateStore =
|
||||||
@@ -1185,6 +1181,16 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
|
|||||||
notifyWorkflowModified
|
notifyWorkflowModified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return layoutStateStore
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
|
||||||
|
if (get(layoutStates).all[workflow.id] != null) {
|
||||||
|
throw new Error(`Layout state already created! ${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutStateStore = createRaw(workflow);
|
||||||
|
|
||||||
layoutStates.update(s => {
|
layoutStates.update(s => {
|
||||||
s.all[workflow.id] = layoutStateStore;
|
s.all[workflow.id] = layoutStateStore;
|
||||||
return s;
|
return s;
|
||||||
@@ -1233,6 +1239,7 @@ export type LayoutStateStores = {
|
|||||||
|
|
||||||
export type LayoutStateStoresOps = {
|
export type LayoutStateStoresOps = {
|
||||||
create: (workflow: ComfyWorkflow) => WritableLayoutStateStore,
|
create: (workflow: ComfyWorkflow) => WritableLayoutStateStore,
|
||||||
|
createRaw: (workflow?: ComfyWorkflow | null) => WritableLayoutStateStore,
|
||||||
remove: (workflowID: WorkflowInstID) => void,
|
remove: (workflowID: WorkflowInstID) => void,
|
||||||
getLayout: (workflowID: WorkflowInstID) => WritableLayoutStateStore | null,
|
getLayout: (workflowID: WorkflowInstID) => WritableLayoutStateStore | null,
|
||||||
getLayoutByGraph: (graph: LGraph) => WritableLayoutStateStore | null,
|
getLayoutByGraph: (graph: LGraph) => WritableLayoutStateStore | null,
|
||||||
@@ -1249,6 +1256,7 @@ const store = writable({
|
|||||||
const layoutStates: WritableLayoutStateStores = {
|
const layoutStates: WritableLayoutStateStores = {
|
||||||
...store,
|
...store,
|
||||||
create,
|
create,
|
||||||
|
createRaw,
|
||||||
remove,
|
remove,
|
||||||
getLayout,
|
getLayout,
|
||||||
getLayoutByGraph,
|
getLayoutByGraph,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user