Integrate klecks

This commit is contained in:
space-nuko
2023-05-13 12:43:34 -05:00
parent 4fdd373286
commit ea8502521b
12 changed files with 2086 additions and 353 deletions

View File

@@ -1,80 +0,0 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import ImageComparison from "$lib/components/ImageComparison.svelte";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { SelectData } from "@gradio/utils";
import type { ComfyImageCompareNode, ImageCompareData } from "$lib/nodes/ComfyWidgetNodes";
import { convertFilenameToComfyURL } from "$lib/utils";
import { TabItem, Tabs } from "@gradio/tabs";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageCompareNode | null = null;
let nodeValue: Writable<ImageCompareData> | null = null;
let attrsChanged: Writable<number> | null = null;
let leftUrl: string = ""
let rightUrl: string = ""
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageCompareNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
const urlPattern = /^((http|https|ftp):\/\/)/;
$: updateUrls($nodeValue);
function updateUrls(value: ImageCompareData) {
leftUrl = ""
rightUrl = ""
console.warn("UPD", value)
if (typeof value[0] === "string") {
if (urlPattern.test(value[0]))
leftUrl = value[0]
else
leftUrl = convertFilenameToComfyURL(value[0])
}
if (typeof value[1] === "string") {
if (urlPattern.test(value[1]))
rightUrl = value[1]
else
rightUrl = convertFilenameToComfyURL(value[1])
}
}
</script>
<div class="wrapper comfy-compare-widget">
<Block>
<Tabs elem_classes={["gradio-tabs"]}>
<TabItem name="Slider">
<ImageComparison>
{#if leftUrl && leftUrl != ""}
{@const props = { slot: "first" }}
<img {...props} alt="Left" src={leftUrl} />
{/if}
{#if rightUrl && leftUrl != ""}
{@const props = { slot: "second" }}
<img {...props} alt="Right" src={rightUrl} />
{/if}
</ImageComparison>
</TabItem>
</Tabs>
</Block>
</div>
<style lang="scss">
.comfy-compare-widget {
max-width: 40rem;
img {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,148 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block, BlockTitle } from "@gradio/atoms";
import { get, type Writable, writable } from "svelte/store";
import Modal from "$lib/components/Modal.svelte";
import { Button } from "@gradio/button";
import type { ComfyImageEditorNode, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
import { Embed as Klecks, KL, KlApp, klHistory, type KlAppOptionsEmbed } from "klecks";
import "klecks/style/style.scss";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageEditorNode | null = null;
let nodeValue: Writable<MultiImageData> | null = null;
let attrsChanged: Writable<number> | null = null;
let leftUrl: string = ""
let rightUrl: string = ""
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageEditorNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
const urlPattern = /^((http|https|ftp):\/\/)/;
$: updateUrls($nodeValue);
function updateUrls(value: MultiImageData) {
// leftUrl = ""
// rightUrl = ""
// console.warn("UPD", value)
//
// if (typeof value[0] === "string") {
// if (urlPattern.test(value[0]))
// leftUrl = value[0]
// else
// leftUrl = convertFilenameToComfyURL(value[0])
// }
// if (typeof value[1] === "string") {
// if (urlPattern.test(value[1]))
// rightUrl = value[1]
// else
// rightUrl = convertFilenameToComfyURL(value[1])
// }
}
let editorRoot: HTMLDivElement | null = null;
let showModal = false;
let kl: Klecks | null = null;
function disposeEditor() {
if (editorRoot) {
while (editorRoot.firstChild) {
editorRoot.removeChild(editorRoot.firstChild);
}
}
kl = null;
showModal = false;
}
function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
const data = kl.getPNG();
}
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() {
if (!editorRoot)
return;
showModal = true;
const url = `http://${location.hostname}:8188` // TODO make configurable
kl = new Klecks({
embedUrl: url,
onSubmit: submitKlecksToComfyUI,
targetEl: editorRoot.parentElement.parentElement
});
kl.openProject({
width: 512,
height: 512,
layers: [{
name: 'Image',
opacity: 1,
mixModeStr: 'source-over',
image: generateBlankImage(),
}]
});
setTimeout(function () {
kl.klApp?.out("yo");
}, 1000);
}
</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>
</div>
<div class="image-editor-root" bind:this={editorRoot} />
</Modal>
<div class="comfy-image-editor-panel">
<Block>
<BlockTitle>Image editor.</BlockTitle>
<Button variant="secondary" on:click={openImageEditor}>
Open
</Button>
</Block>
</div>
{/if}
</div>
<style lang="scss">
.image-editor-root {
width: 75vw;
height: 75vh;
overflow: hidden;
}
.comfy-image-editor {
:global(> dialog) {
overflow: hidden;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { File as FileIcon } from "@gradio/icons";
import { ModifyUpload, Upload, blobToBase64, normalise_file } from "@gradio/upload";
import ImageUpload from "$lib/components/ImageUpload.svelte"
import type { WidgetLayout } from "$lib/stores/layoutState";
import type { Writable } from "svelte/store";
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
@@ -19,29 +19,12 @@
let dragging = false;
let pending_upload = false;
let old_value: Array<GradioFileData> | null = null;
let imgElem: HTMLImageElement | null = null
let imgWidth: number = 1;
let imgHeight: number = 1;
$: widget && setNodeValue(widget);
let _value: GradioFileData[] | null = null;
const root = "comf"
const root_url = "https//ComfyUI!"
$: _value = normalise_file($nodeValue, root, root_url);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageUploadNode
nodeValue = node.value;
propsChanged = node.propsChanged;
}
};
$: if (!(node && _value && _value.length > 0 && imgElem)) {
imgWidth = 1
imgHeight = 1
$: if (!(node && $nodeValue && $nodeValue.length > 0)) {
node.imageSize = [1, 1]
}
else if (imgWidth > 1 || imgHeight > 1) {
@@ -51,178 +34,30 @@
node.imageSize = [1, 1]
}
function onChange() {
$nodeValue = _value || []
}
function onUpload() {
}
function onClear() {
}
interface GradioUploadResponse {
error?: string;
files?: Array<string>;
}
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
console.debug("UPLOADILFES", root, files);
const url = `http://${location.hostname}:8188` // TODO make configurable
const requests = files.map(async (file) => {
const formData = new FormData();
formData.append("image", file, file.name);
return fetch(new Request(url + "/upload/image", {
body: formData,
method: 'POST'
}))
.then(r => r.json())
.catch(error => error);
});
return Promise.all(requests)
.then( (results) => {
const errors = []
const files = []
for (const r of results) {
if (r instanceof Error) {
errors.push(r.cause)
}
else {
// bare filename of image
files.push((r as ComfyUploadImageAPIResponse).name)
}
}
let error = null;
if (errors && errors.length > 0)
error = "Upload error(s):\n" + errors.join("\n");
return { error, files }
})
}
$: {
if (JSON.stringify(_value) !== JSON.stringify(old_value)) {
pending_upload = true;
old_value = _value;
if (_value == null)
_value = []
else if (!Array.isArray(_value))
_value = [_value]
const allBlobs = _value.map((file_data: GradioFileData) => file_data.blob)
if (allBlobs == null || allBlobs.length === 0) {
_value = null;
onChange();
pending_upload = false;
}
else if (!allBlobs.every(b => b != null)) {
_value = null;
pending_upload = false;
}
else {
let files = (Array.isArray(_value) ? _value : [_value]).map(
(file_data) => file_data.blob!
);
let upload_value = _value;
pending_upload = true;
upload_files(root, files).then((response) => {
if (JSON.stringify(upload_value) !== JSON.stringify(_value)) {
// value has changed since upload started
console.error("[ImageUploadWidget] value has changed since upload started", upload_value, _value)
return;
}
pending_upload = false;
_value.forEach(
(file_data: GradioFileData, i: number) => {
if (response.files) {
file_data.orig_name = file_data.name;
file_data.name = response.files[i];
file_data.is_file = true;
}
}
);
if (response.error) {
notify(response.error, { type: "error" })
}
$nodeValue = normalise_file(_value, root, root_url) as GradioFileData[];
onChange();
onUpload();
});
}
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageUploadNode
nodeValue = node.value;
propsChanged = node.propsChanged;
}
}
};
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) {
_value = Array.isArray(detail) ? detail : [detail];
await tick();
onChange();
onUpload();
}
function handle_clear(_e: CustomEvent<null>) {
_value = null;
onChange();
onClear();
}
function getImageUrl(image: GradioFileData) {
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams({ filename: image.name, subfolder: "", type: "input" })
return `${baseUrl}/view?${params}`
function onChange(e: CustomEvent<GradioFileData[]>) {
$nodeValue = e.detail || []
}
</script>
<div class="wrapper gradio-file comfy-image-upload" style={widget.attrs.style}>
{#if widget && node && nodeValue}
<Block
visible={true}
variant={($nodeValue === null || $nodeValue.length === 0) ? "dashed" : "solid"}
border_mode={dragging ? "focus" : "base"}
padding={true}
elem_id="comfy-image-upload-block"
elem_classes={widget.attrs.classes.split(",")}
>
<BlockLabel
label={widget.attrs.title}
show_label={widget.attrs.title != ""}
Icon={FileIcon}
float={widget.attrs.title != ""}
/>
{#if _value && _value.length > 0 && !pending_upload}
{@const firstImage = _value[0]}
<ModifyUpload on:clear={handle_clear} absolute />
<img src={getImageUrl(firstImage)}
alt={firstImage.orig_name}
bind:this={imgElem}
bind:naturalWidth={imgWidth}
bind:naturalHeight={imgHeight}
/>
{:else}
<Upload
file_count={node.properties.fileCount}
filetype="image/*"
on:change={({ detail }) => ($nodeValue = detail)}
on:load={handle_upload}
bind:dragging
on:clear
on:select
parse_to_data_url={false}
>
<UploadText type="file" />
</Upload>
{/if}
</Block>
<ImageUpload value={$nodeValue}
{isMobile}
bind:imgWidth
bind:imgHeight
bind:fileCount={node.properties.fileCount}
elem_classes={widget.attrs.classes.split(",")}
style={widget.attrs.style}
label={widget.attrs.title}
on:change={onChange}/>
{/if}
</div>