Image editor
This commit is contained in:
2
klecks
2
klecks
Submodule klecks updated: 78c61a032e...a1cb8064a0
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,28 +1012,31 @@ 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 if (value.every(isComfyImageSpec))
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override formatValue(value: GradioFileData[]): string {
|
override formatValue(value: GradioFileData[]): string {
|
||||||
return `Images: ${value?.length || 0}`
|
return `Images: ${value?.length || 0}`
|
||||||
|
|||||||
@@ -126,7 +126,10 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertComfyOutputEntryToGradio(r: GalleryOutputEntry): GradioFileData {
|
||||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
const params = new URLSearchParams(r)
|
const params = new URLSearchParams(r)
|
||||||
const fileData: GradioFileData = {
|
const fileData: GradioFileData = {
|
||||||
@@ -136,7 +139,6 @@ export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileDat
|
|||||||
data: url + "/view?" + params
|
data: url + "/view?" + params
|
||||||
}
|
}
|
||||||
return fileData
|
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())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
<div id="klecks-loading-screen">
|
<div id="klecks-loading-screen">
|
||||||
<span id="klecks-loading-screen-text"></span>
|
<span id="klecks-loading-screen-text"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="image-editor-root" bind:this={editorRoot} />
|
<div class="image-editor-root" bind:this={editorRoot} />
|
||||||
|
</div>
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user