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("COMFY_IMAGE_FILE", "chartreuse")
|
||||
setColor(BuiltInSlotType.EVENT, "lightseagreen")
|
||||
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<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 { tick } from "svelte";
|
||||
import type { GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import notify from "$lib/notify";
|
||||
import { convertComfyOutputToComfyURL, type ComfyUploadImageAPIResponse, converGradioFileDataToComfyURL } from "$lib/utils";
|
||||
import type { GalleryOutputEntry, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import { convertComfyOutputEntryToGradio, convertComfyOutputToComfyURL, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||
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 imgWidth: number = 0;
|
||||
@@ -29,7 +28,10 @@
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: GalleryOutputEntry[];
|
||||
upload: undefined;
|
||||
uploading: undefined;
|
||||
uploaded: GalleryOutputEntry[];
|
||||
upload_error: any;
|
||||
load: undefined;
|
||||
clear: undefined;
|
||||
}>();
|
||||
|
||||
@@ -49,13 +51,17 @@
|
||||
}
|
||||
|
||||
function onUpload() {
|
||||
dispatch("upload")
|
||||
dispatch("uploaded")
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
dispatch("clear")
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
dispatch("load")
|
||||
}
|
||||
|
||||
interface GradioUploadResponse {
|
||||
error?: string;
|
||||
files?: Array<GalleryOutputEntry>;
|
||||
@@ -64,6 +70,8 @@
|
||||
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
|
||||
console.debug("UPLOADILFES", root, files);
|
||||
|
||||
dispatch("uploading")
|
||||
|
||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||
|
||||
const requests = files.map(async (file) => {
|
||||
@@ -147,8 +155,11 @@
|
||||
}
|
||||
|
||||
value = response.files;
|
||||
onChange();
|
||||
onUpload();
|
||||
dispatch("change")
|
||||
dispatch("uploaded", value)
|
||||
}).
|
||||
catch(err => {
|
||||
dispatch("upload_error", err)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -157,20 +168,24 @@
|
||||
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) {
|
||||
_value = Array.isArray(detail) ? detail : [detail];
|
||||
await tick();
|
||||
onChange();
|
||||
onUpload();
|
||||
dispatch("change")
|
||||
dispatch("load")
|
||||
}
|
||||
|
||||
function handle_clear(_e: CustomEvent<null>) {
|
||||
_value = null;
|
||||
value = [];
|
||||
onChange();
|
||||
onClear();
|
||||
dispatch("change")
|
||||
dispatch("clear")
|
||||
}
|
||||
|
||||
function convertGradioUpload(e: CustomEvent<GradioFileData[]>) {
|
||||
_value = e.detail
|
||||
}
|
||||
|
||||
function convertNodeValue(nodeValue: GalleryOutputEntry[]): GradioFileData[] {
|
||||
return nodeValue.map(convertComfyOutputEntryToGradio);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="image-upload" {style}>
|
||||
@@ -189,11 +204,11 @@
|
||||
Icon={FileIcon}
|
||||
float={label != ""}
|
||||
/>
|
||||
{#if _value && _value.length > 0 && !pending_upload}
|
||||
{@const firstImage = _value[0]}
|
||||
{#if value && value.length > 0 && !pending_upload}
|
||||
{@const firstImage = value[0]}
|
||||
<ModifyUpload on:clear={handle_clear} absolute />
|
||||
<img src={converGradioFileDataToComfyURL(firstImage)}
|
||||
alt={firstImage.orig_name}
|
||||
<img src={convertComfyOutputToComfyURL(firstImage)}
|
||||
alt={firstImage.filename}
|
||||
bind:this={imgElem}
|
||||
bind:naturalWidth={imgWidth}
|
||||
bind:naturalHeight={imgHeight}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "@gradio/button";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let showModal; // boolean
|
||||
export let closeOnClick = true; // boolean
|
||||
export let showCloseButton = true; // boolean
|
||||
|
||||
let dialog; // HTMLDialogElement
|
||||
|
||||
@@ -11,7 +14,17 @@
|
||||
|
||||
$: if (dialog && showModal) dialog.showModal();
|
||||
|
||||
function close() {
|
||||
function close(e: Event) {
|
||||
if (!closeOnClick) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false
|
||||
}
|
||||
|
||||
doClose()
|
||||
}
|
||||
|
||||
function doClose() {
|
||||
dialog.close();
|
||||
dispatch("close")
|
||||
}
|
||||
@@ -21,13 +34,16 @@
|
||||
<dialog
|
||||
bind:this={dialog}
|
||||
on:close={close}
|
||||
on:cancel={doClose}
|
||||
on:click|self={close}
|
||||
>
|
||||
<div on:click|stopPropagation>
|
||||
<slot name="header" />
|
||||
<slot />
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<button autofocus on:click={close}>Close</button>
|
||||
{#if showCloseButton}
|
||||
<Button variant="secondary" on:click={doClose}>Close</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"
|
||||
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
|
||||
import type { NotifyOptions } from "$lib/notify";
|
||||
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 {
|
||||
static slotLayout: SlotLayout = {
|
||||
@@ -638,10 +638,10 @@ export class ComfyUploadImageAction extends ComfyGraphNode {
|
||||
type: this.properties.folderType || "output"
|
||||
}
|
||||
|
||||
this._promise = uploadImageToComfyUI(data)
|
||||
.then((json: ComfyUploadImageAPIResponse) => {
|
||||
console.debug("[UploadImageAction] Succeeded", json)
|
||||
this.properties.lastUploadedImageFile = json.name;
|
||||
this._promise = reuploadImageToComfyUI(data, "input")
|
||||
.then((entry: GalleryOutputEntry) => {
|
||||
console.debug("[UploadImageAction] Succeeded", entry)
|
||||
this.properties.lastUploadedImageFile = entry.filename;
|
||||
this.triggerSlot(1, this.properties.lastUploadedImageFile);
|
||||
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 ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||
import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||
import type { GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
|
||||
import { reuploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||
|
||||
export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties {
|
||||
images: GalleryOutput | null,
|
||||
@@ -171,11 +171,11 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
||||
this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
|
||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
||||
|
||||
const promise = uploadImageToComfyUI(data)
|
||||
.then((json: ComfyUploadImageAPIResponse) => {
|
||||
console.debug("Gottem", json)
|
||||
const promise = reuploadImageToComfyUI(data, "input")
|
||||
.then((entry: GalleryOutputEntry) => {
|
||||
console.debug("Gottem", entry)
|
||||
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)
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SvelteComponentDev } from "svelte/internal";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
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 type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
@@ -604,7 +604,7 @@ export type GalleryOutput = {
|
||||
export type GalleryOutputEntry = {
|
||||
filename: string,
|
||||
subfolder: string,
|
||||
type: string
|
||||
type: ComfyUploadImageType
|
||||
}
|
||||
|
||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||
@@ -985,7 +985,7 @@ export type MultiImageData = FileNameOrGalleryData[];
|
||||
export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties {
|
||||
}
|
||||
|
||||
export class ComfyImageEditorNode extends ComfyWidgetNode<MultiImageData> {
|
||||
export class ComfyImageEditorNode extends ComfyWidgetNode<GalleryOutputEntry[]> {
|
||||
override properties: ComfyImageEditorNodeProperties = {
|
||||
defaultValue: [],
|
||||
tags: [],
|
||||
@@ -1000,7 +1000,7 @@ export class ComfyImageEditorNode extends ComfyWidgetNode<MultiImageData> {
|
||||
}
|
||||
|
||||
override svelteComponentType = ImageEditorWidget;
|
||||
override defaultValue: MultiImageData = [];
|
||||
override defaultValue: GalleryOutputEntry[] = [];
|
||||
override outputIndex = null;
|
||||
override changedIndex = null;
|
||||
override storeActionName = "store";
|
||||
@@ -1012,27 +1012,30 @@ export class ComfyImageEditorNode extends ComfyWidgetNode<MultiImageData> {
|
||||
|
||||
_value = null;
|
||||
|
||||
override parseValue(value: any): MultiImageData {
|
||||
if (value == null) {
|
||||
override parseValue(value: any): GalleryOutputEntry[] {
|
||||
if (value == null)
|
||||
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)
|
||||
prevValue.push(value)
|
||||
if (prevValue.length > 2)
|
||||
prevValue.splice(0, 1)
|
||||
return prevValue as MultiImageData
|
||||
|
||||
if (typeof value === "string") {
|
||||
// Single filename
|
||||
return [{ filename: value, subfolder: "", type: "input" }]
|
||||
}
|
||||
else if (typeof value === "object" && "images" in value && value.images.length > 0) {
|
||||
const output = value as GalleryOutput
|
||||
else if (isComfyImageSpec(value)) {
|
||||
// Single ComfyUI file
|
||||
return [value]
|
||||
}
|
||||
else if (Array.isArray(value) && value.every(s => typeof s === "string")) {
|
||||
return value as MultiImageData
|
||||
}
|
||||
else {
|
||||
return []
|
||||
else if (Array.isArray(value)) {
|
||||
if (value.every(v => typeof v === "string"))
|
||||
return value.map(filename => { return { filename, subfolder: "", type: "input" } })
|
||||
else if (value.every(isComfyImageSpec))
|
||||
return value
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
override formatValue(value: GradioFileData[]): string {
|
||||
|
||||
@@ -126,17 +126,19 @@ export const debounce = (callback: Function, wait = 250) => {
|
||||
};
|
||||
|
||||
export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileData[] {
|
||||
return output.images.map(r => {
|
||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||
const params = new URLSearchParams(r)
|
||||
const fileData: GradioFileData = {
|
||||
name: r.filename,
|
||||
orig_name: r.filename,
|
||||
is_file: false,
|
||||
data: url + "/view?" + params
|
||||
}
|
||||
return fileData
|
||||
});
|
||||
return output.images.map(convertComfyOutputEntryToGradio);
|
||||
}
|
||||
|
||||
export function convertComfyOutputEntryToGradio(r: GalleryOutputEntry): GradioFileData {
|
||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||
const params = new URLSearchParams(r)
|
||||
const fileData: GradioFileData = {
|
||||
name: r.filename,
|
||||
orig_name: r.filename,
|
||||
is_file: false,
|
||||
data: url + "/view?" + params
|
||||
}
|
||||
return fileData
|
||||
}
|
||||
|
||||
export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): string {
|
||||
@@ -148,13 +150,13 @@ export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): str
|
||||
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 params = new URLSearchParams({ filename: image.name, subfolder: "", type })
|
||||
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)
|
||||
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 {
|
||||
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 params = new URLSearchParams(data)
|
||||
|
||||
console.debug("[utils] Reuploading image into to ComfyUI input folder", data)
|
||||
|
||||
return fetch(url + "/view?" + params)
|
||||
.then((r) => r.blob())
|
||||
.then((blob) => {
|
||||
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())
|
||||
.then((blob) => uploadImageToComfyUI(blob, data.filename, type))
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
|
||||
import "klecks/style/style.scss";
|
||||
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";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfyImageEditorNode | null = null;
|
||||
let nodeValue: Writable<MultiImageData> | null = null;
|
||||
let nodeValue: Writable<GalleryOutputEntry[]> | null = null;
|
||||
let attrsChanged: Writable<number> | null = null;
|
||||
let leftUrl: string = ""
|
||||
let rightUrl: string = ""
|
||||
@@ -63,6 +63,8 @@
|
||||
let kl: Klecks | null = null;
|
||||
|
||||
function disposeEditor() {
|
||||
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
|
||||
|
||||
if (editorRoot) {
|
||||
while (editorRoot.firstChild) {
|
||||
editorRoot.removeChild(editorRoot.firstChild);
|
||||
@@ -73,25 +75,52 @@
|
||||
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 SUBFOLDER: string = "ComfyBox_Editor";
|
||||
// const SUBFOLDER: string = "ComfyBox_Editor";
|
||||
|
||||
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
||||
const blob = kl.getPNG();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("image", blob, FILENAME);
|
||||
|
||||
const entry: GalleryOutputEntry = {
|
||||
filename: FILENAME,
|
||||
subfolder: SUBFOLDER,
|
||||
type: "input"
|
||||
}
|
||||
|
||||
await uploadImageToComfyUI(entry)
|
||||
.then((resp: ComfyUploadImageAPIResponse) => {
|
||||
entry.filename = resp.name;
|
||||
$nodeValue = [entry]
|
||||
await uploadImageToComfyUI(blob, FILENAME, "input")
|
||||
.then((entry: GalleryOutputEntry) => {
|
||||
$nodeValue = [entry] // TODO more than one image
|
||||
notify("Saved image to ComfyUI!", { type: "success" })
|
||||
onSuccess();
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -101,19 +130,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
function generateBlankImage(fill: string = "#fff"): HTMLCanvasElement {
|
||||
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() {
|
||||
async function openImageEditor() {
|
||||
if (!editorRoot)
|
||||
return;
|
||||
|
||||
@@ -124,17 +141,33 @@
|
||||
kl = new Klecks({
|
||||
embedUrl: url,
|
||||
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({
|
||||
width: 512,
|
||||
height: 512,
|
||||
width: width,
|
||||
height: height,
|
||||
layers: [{
|
||||
name: 'Image',
|
||||
opacity: 1,
|
||||
mixModeStr: 'source-over',
|
||||
image: generateBlankImage(),
|
||||
image: canvas
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -143,24 +176,52 @@
|
||||
}, 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>
|
||||
|
||||
<div class="wrapper comfy-image-editor">
|
||||
{#if isMobile}
|
||||
<span>TODO mask editor</span>
|
||||
{:else}
|
||||
<Modal bind:showModal on:close={disposeEditor}>
|
||||
<div id="klecks-loading-screen">
|
||||
<span id="klecks-loading-screen-text"></span>
|
||||
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor}>
|
||||
<div>
|
||||
<div id="klecks-loading-screen">
|
||||
<span id="klecks-loading-screen-text"></span>
|
||||
</div>
|
||||
<div class="image-editor-root" bind:this={editorRoot} />
|
||||
</div>
|
||||
<div class="image-editor-root" bind:this={editorRoot} />
|
||||
</Modal>
|
||||
<div class="comfy-image-editor-panel">
|
||||
<ImageUpload value={$nodeValue}
|
||||
{isMobile}
|
||||
bind:imgWidth
|
||||
bind:imgHeight
|
||||
bind:imgElem
|
||||
@@ -168,13 +229,22 @@
|
||||
elem_classes={[]}
|
||||
style={""}
|
||||
label={"Image"}
|
||||
on:change={onUploadChanged}
|
||||
on:uploading={onUploading}
|
||||
on:uploaded={onUploaded}
|
||||
on:upload_error={onUploadError}
|
||||
on:clear={onClear}
|
||||
on:change={onChange}
|
||||
/>
|
||||
<Block>
|
||||
<BlockTitle>Image editor.</BlockTitle>
|
||||
<Button variant="secondary" on:click={openImageEditor}>
|
||||
Open
|
||||
<Button variant="primary" disabled={!canEdit} on:click={openImageEditor}>
|
||||
Edit Image
|
||||
</Button>
|
||||
<BlockTitle>Status: {status}</BlockTitle>
|
||||
{#if uploadError}
|
||||
<div>
|
||||
Upload error: {uploadError}
|
||||
</div>
|
||||
{/if}
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -185,6 +255,12 @@
|
||||
width: 75vw;
|
||||
height: 75vh;
|
||||
overflow: hidden;
|
||||
|
||||
color: black;
|
||||
|
||||
:global(> .g-root) {
|
||||
height: calc(100% - 59px);
|
||||
}
|
||||
}
|
||||
|
||||
.comfy-image-editor {
|
||||
@@ -192,4 +268,8 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.kl-popup) {
|
||||
z-index: 999999999999;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user