Integrate klecks
This commit is contained in:
@@ -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>
|
||||
148
src/lib/widgets/ImageEditorWidget.svelte
Normal file
148
src/lib/widgets/ImageEditorWidget.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user