Image editor

This commit is contained in:
space-nuko
2023-05-13 14:54:07 -05:00
parent 4d90623505
commit f66df94c36
9 changed files with 272 additions and 123 deletions

2
klecks

Submodule klecks updated: 78c61a032e...a1cb8064a0

View File

@@ -409,6 +409,7 @@ export default class ComfyApp {
} }
setColor("IMAGE", "rebeccapurple") setColor("IMAGE", "rebeccapurple")
setColor("COMFY_IMAGE_FILE", "chartreuse")
setColor(BuiltInSlotType.EVENT, "lightseagreen") setColor(BuiltInSlotType.EVENT, "lightseagreen")
setColor(BuiltInSlotType.ACTION, "lightseagreen") setColor(BuiltInSlotType.ACTION, "lightseagreen")
} }

View File

@@ -1,14 +1,13 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { File as FileIcon } from "@gradio/icons";
import { ModifyUpload, Upload, blobToBase64, normalise_file } from "@gradio/upload";
import type { FileData as GradioFileData } from "@gradio/upload";
import UploadText from "$lib/components/gradio/app/UploadText.svelte"; import UploadText from "$lib/components/gradio/app/UploadText.svelte";
import { tick } from "svelte"; import type { GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
import notify from "$lib/notify"; import notify from "$lib/notify";
import { convertComfyOutputToComfyURL, type ComfyUploadImageAPIResponse, converGradioFileDataToComfyURL } from "$lib/utils"; import { convertComfyOutputEntryToGradio, convertComfyOutputToComfyURL, type ComfyUploadImageAPIResponse } from "$lib/utils";
import type { GalleryOutputEntry, MultiImageData } from "$lib/nodes/ComfyWidgetNodes"; import { Block, BlockLabel } from "@gradio/atoms";
import { File as FileIcon } from "@gradio/icons";
import type { FileData as GradioFileData } from "@gradio/upload";
import { ModifyUpload, Upload } from "@gradio/upload";
import { createEventDispatcher, tick } from "svelte";
export let value: GalleryOutputEntry[] | null = null; export let value: GalleryOutputEntry[] | null = null;
export let imgWidth: number = 0; export let imgWidth: number = 0;
@@ -29,7 +28,10 @@
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
change: GalleryOutputEntry[]; change: GalleryOutputEntry[];
upload: undefined; uploading: undefined;
uploaded: GalleryOutputEntry[];
upload_error: any;
load: undefined;
clear: undefined; clear: undefined;
}>(); }>();
@@ -49,13 +51,17 @@
} }
function onUpload() { function onUpload() {
dispatch("upload") dispatch("uploaded")
} }
function onClear() { function onClear() {
dispatch("clear") dispatch("clear")
} }
function onLoad() {
dispatch("load")
}
interface GradioUploadResponse { interface GradioUploadResponse {
error?: string; error?: string;
files?: Array<GalleryOutputEntry>; files?: Array<GalleryOutputEntry>;
@@ -64,6 +70,8 @@
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> { async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
console.debug("UPLOADILFES", root, files); console.debug("UPLOADILFES", root, files);
dispatch("uploading")
const url = `http://${location.hostname}:8188` // TODO make configurable const url = `http://${location.hostname}:8188` // TODO make configurable
const requests = files.map(async (file) => { const requests = files.map(async (file) => {
@@ -147,8 +155,11 @@
} }
value = response.files; value = response.files;
onChange(); dispatch("change")
onUpload(); dispatch("uploaded", value)
}).
catch(err => {
dispatch("upload_error", err)
}); });
} }
} }
@@ -157,20 +168,24 @@
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) { async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) {
_value = Array.isArray(detail) ? detail : [detail]; _value = Array.isArray(detail) ? detail : [detail];
await tick(); await tick();
onChange(); dispatch("change")
onUpload(); dispatch("load")
} }
function handle_clear(_e: CustomEvent<null>) { function handle_clear(_e: CustomEvent<null>) {
_value = null; _value = null;
value = []; value = [];
onChange(); dispatch("change")
onClear(); dispatch("clear")
} }
function convertGradioUpload(e: CustomEvent<GradioFileData[]>) { function convertGradioUpload(e: CustomEvent<GradioFileData[]>) {
_value = e.detail _value = e.detail
} }
function convertNodeValue(nodeValue: GalleryOutputEntry[]): GradioFileData[] {
return nodeValue.map(convertComfyOutputEntryToGradio);
}
</script> </script>
<div class="image-upload" {style}> <div class="image-upload" {style}>
@@ -189,11 +204,11 @@
Icon={FileIcon} Icon={FileIcon}
float={label != ""} float={label != ""}
/> />
{#if _value && _value.length > 0 && !pending_upload} {#if value && value.length > 0 && !pending_upload}
{@const firstImage = _value[0]} {@const firstImage = value[0]}
<ModifyUpload on:clear={handle_clear} absolute /> <ModifyUpload on:clear={handle_clear} absolute />
<img src={converGradioFileDataToComfyURL(firstImage)} <img src={convertComfyOutputToComfyURL(firstImage)}
alt={firstImage.orig_name} alt={firstImage.filename}
bind:this={imgElem} bind:this={imgElem}
bind:naturalWidth={imgWidth} bind:naturalWidth={imgWidth}
bind:naturalHeight={imgHeight} bind:naturalHeight={imgHeight}

View File

@@ -1,7 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Button } from "@gradio/button";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
export let showModal; // boolean export let showModal; // boolean
export let closeOnClick = true; // boolean
export let showCloseButton = true; // boolean
let dialog; // HTMLDialogElement let dialog; // HTMLDialogElement
@@ -11,7 +14,17 @@
$: if (dialog && showModal) dialog.showModal(); $: if (dialog && showModal) dialog.showModal();
function close() { function close(e: Event) {
if (!closeOnClick) {
e.preventDefault();
e.stopPropagation();
return false
}
doClose()
}
function doClose() {
dialog.close(); dialog.close();
dispatch("close") dispatch("close")
} }
@@ -21,13 +34,16 @@
<dialog <dialog
bind:this={dialog} bind:this={dialog}
on:close={close} on:close={close}
on:cancel={doClose}
on:click|self={close} on:click|self={close}
> >
<div on:click|stopPropagation> <div on:click|stopPropagation>
<slot name="header" /> <slot name="header" />
<slot /> <slot />
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->
<button autofocus on:click={close}>Close</button> {#if showCloseButton}
<Button variant="secondary" on:click={doClose}>Close</Button>
{/if}
</div> </div>
</dialog> </dialog>

View File

@@ -8,7 +8,7 @@ import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes"; import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
import type { NotifyOptions } from "$lib/notify"; import type { NotifyOptions } from "$lib/notify";
import type { FileData as GradioFileData } from "@gradio/upload"; import type { FileData as GradioFileData } from "@gradio/upload";
import { convertComfyOutputToGradio, uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils"; import { convertComfyOutputToGradio, reuploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
export class ComfyQueueEvents extends ComfyGraphNode { export class ComfyQueueEvents extends ComfyGraphNode {
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -638,10 +638,10 @@ export class ComfyUploadImageAction extends ComfyGraphNode {
type: this.properties.folderType || "output" type: this.properties.folderType || "output"
} }
this._promise = uploadImageToComfyUI(data) this._promise = reuploadImageToComfyUI(data, "input")
.then((json: ComfyUploadImageAPIResponse) => { .then((entry: GalleryOutputEntry) => {
console.debug("[UploadImageAction] Succeeded", json) console.debug("[UploadImageAction] Succeeded", entry)
this.properties.lastUploadedImageFile = json.name; this.properties.lastUploadedImageFile = entry.filename;
this.triggerSlot(1, this.properties.lastUploadedImageFile); this.triggerSlot(1, this.properties.lastUploadedImageFile);
this._promise = null; this._promise = null;
}) })

View File

@@ -1,7 +1,7 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget, type SerializedLGraphNode } from "@litegraph-ts/core"; import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { GalleryOutput } from "./ComfyWidgetNodes"; import type { GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils"; import { reuploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties { export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties {
images: GalleryOutput | null, images: GalleryOutput | null,
@@ -171,11 +171,11 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
this.properties.filenames[newIndex] = { filename: null, status: "uploading" } this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
this.onPropertyChanged("filenames", this.properties.filenames) this.onPropertyChanged("filenames", this.properties.filenames)
const promise = uploadImageToComfyUI(data) const promise = reuploadImageToComfyUI(data, "input")
.then((json: ComfyUploadImageAPIResponse) => { .then((entry: GalleryOutputEntry) => {
console.debug("Gottem", json) console.debug("Gottem", entry)
if (lastGenNumber === this.properties.genNumber) { if (lastGenNumber === this.properties.genNumber) {
this.properties.filenames[newIndex] = { filename: json.name, status: "cached" } this.properties.filenames[newIndex] = { filename: entry.filename, status: "cached" }
this.onPropertyChanged("filenames", this.properties.filenames) this.onPropertyChanged("filenames", this.properties.filenames)
} }
else { else {

View File

@@ -4,7 +4,7 @@ import type { SvelteComponentDev } from "svelte/internal";
import { Watch } from "@litegraph-ts/nodes-basic"; import { Watch } from "@litegraph-ts/nodes-basic";
import type IComfyInputSlot from "$lib/IComfyInputSlot"; import type IComfyInputSlot from "$lib/IComfyInputSlot";
import { writable, type Unsubscriber, type Writable, get } from "svelte/store"; import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
import { clamp, convertComfyOutputToGradio, range } from "$lib/utils" import { clamp, convertComfyOutputToGradio, range, type ComfyUploadImageType } from "$lib/utils"
import layoutState from "$lib/stores/layoutState"; import layoutState from "$lib/stores/layoutState";
import type { FileData as GradioFileData } from "@gradio/upload"; import type { FileData as GradioFileData } from "@gradio/upload";
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
@@ -604,7 +604,7 @@ export type GalleryOutput = {
export type GalleryOutputEntry = { export type GalleryOutputEntry = {
filename: string, filename: string,
subfolder: string, subfolder: string,
type: string type: ComfyUploadImageType
} }
export interface ComfyGalleryProperties extends ComfyWidgetProperties { export interface ComfyGalleryProperties extends ComfyWidgetProperties {
@@ -985,7 +985,7 @@ export type MultiImageData = FileNameOrGalleryData[];
export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties { export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties {
} }
export class ComfyImageEditorNode extends ComfyWidgetNode<MultiImageData> { export class ComfyImageEditorNode extends ComfyWidgetNode<GalleryOutputEntry[]> {
override properties: ComfyImageEditorNodeProperties = { override properties: ComfyImageEditorNodeProperties = {
defaultValue: [], defaultValue: [],
tags: [], tags: [],
@@ -1000,7 +1000,7 @@ export class ComfyImageEditorNode extends ComfyWidgetNode<MultiImageData> {
} }
override svelteComponentType = ImageEditorWidget; override svelteComponentType = ImageEditorWidget;
override defaultValue: MultiImageData = []; override defaultValue: GalleryOutputEntry[] = [];
override outputIndex = null; override outputIndex = null;
override changedIndex = null; override changedIndex = null;
override storeActionName = "store"; override storeActionName = "store";
@@ -1012,27 +1012,30 @@ export class ComfyImageEditorNode extends ComfyWidgetNode<MultiImageData> {
_value = null; _value = null;
override parseValue(value: any): MultiImageData { override parseValue(value: any): GalleryOutputEntry[] {
if (value == null) { if (value == null)
return [] return []
const isComfyImageSpec = (value: any): boolean => {
return value && typeof value === "object" && "filename" in value && "type" in value
} }
else if (typeof value === "string" && value !== "") { // Single filename
const prevValue = get(this.value) if (typeof value === "string") {
prevValue.push(value) // Single filename
if (prevValue.length > 2) return [{ filename: value, subfolder: "", type: "input" }]
prevValue.splice(0, 1)
return prevValue as MultiImageData
} }
else if (typeof value === "object" && "images" in value && value.images.length > 0) { else if (isComfyImageSpec(value)) {
const output = value as GalleryOutput // Single ComfyUI file
return [value] return [value]
} }
else if (Array.isArray(value) && value.every(s => typeof s === "string")) { else if (Array.isArray(value)) {
return value as MultiImageData if (value.every(v => typeof v === "string"))
} return value.map(filename => { return { filename, subfolder: "", type: "input" } })
else { else if (value.every(isComfyImageSpec))
return [] return value
} }
return []
} }
override formatValue(value: GradioFileData[]): string { override formatValue(value: GradioFileData[]): string {

View File

@@ -126,17 +126,19 @@ export const debounce = (callback: Function, wait = 250) => {
}; };
export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileData[] { export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileData[] {
return output.images.map(r => { return output.images.map(convertComfyOutputEntryToGradio);
const url = `http://${location.hostname}:8188` // TODO make configurable }
const params = new URLSearchParams(r)
const fileData: GradioFileData = { export function convertComfyOutputEntryToGradio(r: GalleryOutputEntry): GradioFileData {
name: r.filename, const url = `http://${location.hostname}:8188` // TODO make configurable
orig_name: r.filename, const params = new URLSearchParams(r)
is_file: false, const fileData: GradioFileData = {
data: url + "/view?" + params name: r.filename,
} orig_name: r.filename,
return fileData is_file: false,
}); data: url + "/view?" + params
}
return fileData
} }
export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): string { export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): string {
@@ -148,13 +150,13 @@ export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): str
return url + "/view?" + params return url + "/view?" + params
} }
export function converGradioFileDataToComfyURL(image: GradioFileData, type: "input" | "output" | "temp" = "input"): string { export function convertGradioFileDataToComfyURL(image: GradioFileData, type: ComfyUploadImageType = "input"): string {
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams({ filename: image.name, subfolder: "", type }) const params = new URLSearchParams({ filename: image.name, subfolder: "", type })
return `${baseUrl}/view?${params}` return `${baseUrl}/view?${params}`
} }
export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, type: "input" | "output" | "temp" = "input"): GalleryOutputEntry { export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, type: ComfyUploadImageType = "input"): GalleryOutputEntry {
if (!fileData.is_file) if (!fileData.is_file)
throw "Can't convert blob data to comfy output!" throw "Can't convert blob data to comfy output!"
@@ -191,26 +193,58 @@ export function jsonToJsObject(json: string): string {
}); });
} }
export type ComfyUploadImageType = "output" | "input" | "temp"
export interface ComfyUploadImageAPIResponse { export interface ComfyUploadImageAPIResponse {
name: string name: string, // Yes this is different from the "executed" event args
subfolder: string,
type: ComfyUploadImageType
} }
export async function uploadImageToComfyUI(data: GalleryOutputEntry): Promise<ComfyUploadImageAPIResponse> { /*
* Uploads an image into ComfyUI's `input` folder.
*/
export async function uploadImageToComfyUI(blob: Blob, filename: string, type: ComfyUploadImageType, subfolder: string = "", overwrite: boolean = false): Promise<GalleryOutputEntry> {
console.debug("[utils] Uploading image to ComfyUI", filename, blob.size)
const url = `http://${location.hostname}:8188` // TODO make configurable
const formData = new FormData();
formData.append("image", blob, filename);
formData.set("type", type)
formData.set("subfolder", subfolder)
formData.set("overwrite", String(overwrite))
const req = new Request(url + "/upload/image", {
body: formData,
method: 'POST'
});
return fetch(req)
.then((r) => r.json())
.then((resp) => {
return {
filename: resp.name,
subfolder: resp.subfolder,
type: resp.type
}
});
}
/*
* Copies an *EXISTING* image in a ComfyUI image folder into a different folder,
* for use with LoadImage etc.
*/
export async function reuploadImageToComfyUI(data: GalleryOutputEntry, type: ComfyUploadImageType): Promise<GalleryOutputEntry> {
if (data.type === type)
return data
const url = `http://${location.hostname}:8188` // TODO make configurable const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(data) const params = new URLSearchParams(data)
console.debug("[utils] Reuploading image into to ComfyUI input folder", data)
return fetch(url + "/view?" + params) return fetch(url + "/view?" + params)
.then((r) => r.blob()) .then((r) => r.blob())
.then((blob) => { .then((blob) => uploadImageToComfyUI(blob, data.filename, type))
console.debug("Fetchin", url, params)
const formData = new FormData();
formData.append("image", blob, data.filename);
return fetch(
new Request(url + "/upload/image", {
body: formData,
method: 'POST'
})
)
})
.then((r) => r.json())
} }

View File

@@ -10,13 +10,13 @@
import "klecks/style/style.scss"; import "klecks/style/style.scss";
import ImageUpload from "$lib/components/ImageUpload.svelte"; import ImageUpload from "$lib/components/ImageUpload.svelte";
import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils"; import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse, convertComfyOutputToComfyURL } from "$lib/utils";
import notify from "$lib/notify"; import notify from "$lib/notify";
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
let node: ComfyImageEditorNode | null = null; let node: ComfyImageEditorNode | null = null;
let nodeValue: Writable<MultiImageData> | null = null; let nodeValue: Writable<GalleryOutputEntry[]> | null = null;
let attrsChanged: Writable<number> | null = null; let attrsChanged: Writable<number> | null = null;
let leftUrl: string = "" let leftUrl: string = ""
let rightUrl: string = "" let rightUrl: string = ""
@@ -63,6 +63,8 @@
let kl: Klecks | null = null; let kl: Klecks | null = null;
function disposeEditor() { function disposeEditor() {
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
if (editorRoot) { if (editorRoot) {
while (editorRoot.firstChild) { while (editorRoot.firstChild) {
editorRoot.removeChild(editorRoot.firstChild); editorRoot.removeChild(editorRoot.firstChild);
@@ -73,25 +75,52 @@
showModal = false; showModal = false;
} }
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = fill,
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
return canvas;
}
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
return new Promise((resolve) => {
const e = new Image();
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
e.addEventListener("load", () => { resolve(e); });
e.src = imageURL;
return e;
});
}
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
const image = await loadImage(imageURL);
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
ctx.restore();
return [canvas, image.width, image.height];
}
const FILENAME: string = "ComfyUITemp.png"; const FILENAME: string = "ComfyUITemp.png";
const SUBFOLDER: string = "ComfyBox_Editor"; // const SUBFOLDER: string = "ComfyBox_Editor";
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) { async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
const blob = kl.getPNG(); const blob = kl.getPNG();
const formData = new FormData(); await uploadImageToComfyUI(blob, FILENAME, "input")
formData.append("image", blob, FILENAME); .then((entry: GalleryOutputEntry) => {
$nodeValue = [entry] // TODO more than one image
const entry: GalleryOutputEntry = { notify("Saved image to ComfyUI!", { type: "success" })
filename: FILENAME,
subfolder: SUBFOLDER,
type: "input"
}
await uploadImageToComfyUI(entry)
.then((resp: ComfyUploadImageAPIResponse) => {
entry.filename = resp.name;
$nodeValue = [entry]
onSuccess(); onSuccess();
}) })
.catch(err => { .catch(err => {
@@ -101,19 +130,7 @@
}) })
} }
function generateBlankImage(fill: string = "#fff"): HTMLCanvasElement { async function openImageEditor() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = fill,
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
return canvas;
}
function openImageEditor() {
if (!editorRoot) if (!editorRoot)
return; return;
@@ -124,17 +141,33 @@
kl = new Klecks({ kl = new Klecks({
embedUrl: url, embedUrl: url,
onSubmit: submitKlecksToComfyUI, onSubmit: submitKlecksToComfyUI,
targetEl: editorRoot.parentElement.parentElement targetEl: editorRoot,
warnOnPageClose: false
}); });
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
let canvas = null;
let width = 512;
let height = 512;
if ($nodeValue && $nodeValue.length > 0) {
const comfyImage = $nodeValue[0];
const comfyURL = convertComfyOutputToComfyURL(comfyImage);
[canvas, width, height] = await generateImageCanvas(comfyURL);
}
else {
canvas = generateBlankCanvas(width, height);
}
kl.openProject({ kl.openProject({
width: 512, width: width,
height: 512, height: height,
layers: [{ layers: [{
name: 'Image', name: 'Image',
opacity: 1, opacity: 1,
mixModeStr: 'source-over', mixModeStr: 'source-over',
image: generateBlankImage(), image: canvas
}] }]
}); });
@@ -143,24 +176,52 @@
}, 1000); }, 1000);
} }
function onUploadChanged(e: CustomEvent<GradioFileData[]>) { let status = "none"
let uploadError = null;
function onUploading() {
uploadError = null;
status = "uploading"
} }
function onUploaded(e: CustomEvent<GalleryOutputEntry[]>) {
uploadError = null;
status = "uploaded"
$nodeValue = e.detail;
}
function onClear() {
uploadError = null;
status = "none"
}
function onUploadError(e: CustomEvent<any>) {
status = "error"
uploadError = e.detail
}
function onChange(e: CustomEvent<GalleryOutputEntry[]>) {
// $nodeValue = e.detail;
}
$: canEdit = status === "none" || status === "uploaded";
</script> </script>
<div class="wrapper comfy-image-editor"> <div class="wrapper comfy-image-editor">
{#if isMobile} {#if isMobile}
<span>TODO mask editor</span> <span>TODO mask editor</span>
{:else} {:else}
<Modal bind:showModal on:close={disposeEditor}> <Modal bind:showModal closeOnClick={false} on:close={disposeEditor}>
<div id="klecks-loading-screen"> <div>
<span id="klecks-loading-screen-text"></span> <div id="klecks-loading-screen">
<span id="klecks-loading-screen-text"></span>
</div>
<div class="image-editor-root" bind:this={editorRoot} />
</div> </div>
<div class="image-editor-root" bind:this={editorRoot} />
</Modal> </Modal>
<div class="comfy-image-editor-panel"> <div class="comfy-image-editor-panel">
<ImageUpload value={$nodeValue} <ImageUpload value={$nodeValue}
{isMobile}
bind:imgWidth bind:imgWidth
bind:imgHeight bind:imgHeight
bind:imgElem bind:imgElem
@@ -168,13 +229,22 @@
elem_classes={[]} elem_classes={[]}
style={""} style={""}
label={"Image"} label={"Image"}
on:change={onUploadChanged} on:uploading={onUploading}
on:uploaded={onUploaded}
on:upload_error={onUploadError}
on:clear={onClear}
on:change={onChange}
/> />
<Block> <Block>
<BlockTitle>Image editor.</BlockTitle> <Button variant="primary" disabled={!canEdit} on:click={openImageEditor}>
<Button variant="secondary" on:click={openImageEditor}> Edit Image
Open
</Button> </Button>
<BlockTitle>Status: {status}</BlockTitle>
{#if uploadError}
<div>
Upload error: {uploadError}
</div>
{/if}
</Block> </Block>
</div> </div>
{/if} {/if}
@@ -185,6 +255,12 @@
width: 75vw; width: 75vw;
height: 75vh; height: 75vh;
overflow: hidden; overflow: hidden;
color: black;
:global(> .g-root) {
height: calc(100% - 59px);
}
} }
.comfy-image-editor { .comfy-image-editor {
@@ -192,4 +268,8 @@
overflow: hidden; overflow: hidden;
} }
} }
:global(.kl-popup) {
z-index: 999999999999;
}
</style> </style>