|
<script lang="ts" context="module">
|
|
export interface EditorData {
|
|
background: FileData | null;
|
|
layers: FileData[] | null;
|
|
composite: FileData | null;
|
|
}
|
|
|
|
export interface ImageBlobs {
|
|
background: FileData | null;
|
|
layers: FileData[];
|
|
composite: FileData | null;
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { createEventDispatcher } from "svelte";
|
|
import { type I18nFormatter } from "@gradio/utils";
|
|
import { prepare_files, type FileData, type Client } from "@gradio/client";
|
|
|
|
import ImageEditor from "./ImageEditor.svelte";
|
|
import Layers from "./layers/Layers.svelte";
|
|
import { type Brush as IBrush } from "./tools/Brush.svelte";
|
|
import { type Eraser } from "./tools/Brush.svelte";
|
|
|
|
import { Tools, Crop, Brush, Sources } from "./tools";
|
|
import { BlockLabel } from "@gradio/atoms";
|
|
import { Image as ImageIcon } from "@gradio/icons";
|
|
import { inject } from "./utils/parse_placeholder";
|
|
|
|
export let brush: IBrush | null;
|
|
export let eraser: Eraser | null;
|
|
export let sources: ("clipboard" | "webcam" | "upload")[];
|
|
export let crop_size: [number, number] | `${string}:${string}` | null = null;
|
|
export let i18n: I18nFormatter;
|
|
export let root: string;
|
|
export let label: string | undefined = undefined;
|
|
export let show_label: boolean;
|
|
export let changeable = false;
|
|
export let value: EditorData | null = {
|
|
background: null,
|
|
layers: [],
|
|
composite: null
|
|
};
|
|
export let transforms: "crop"[] = ["crop"];
|
|
export let layers: boolean;
|
|
export let accept_blobs: (a: any) => void;
|
|
export let status: "pending" | "complete" | "error" = "complete";
|
|
export let canvas_size: [number, number] | undefined;
|
|
export let realtime: boolean;
|
|
export let upload: Client["upload"];
|
|
export let stream_handler: Client["stream"];
|
|
export let dragging: boolean;
|
|
export let placeholder: string | undefined = undefined;
|
|
export let height = 400;
|
|
|
|
const dispatch = createEventDispatcher<{
|
|
clear?: never;
|
|
upload?: never;
|
|
change?: never;
|
|
}>();
|
|
|
|
let editor: ImageEditor;
|
|
|
|
function is_not_null(o: Blob | null): o is Blob {
|
|
return !!o;
|
|
}
|
|
|
|
function is_file_data(o: null | FileData): o is FileData {
|
|
return !!o;
|
|
}
|
|
|
|
$: if (bg) dispatch("upload");
|
|
|
|
export async function get_data(): Promise<ImageBlobs> {
|
|
const blobs = await editor.get_blobs();
|
|
|
|
const bg = blobs.background
|
|
? upload(
|
|
await prepare_files([new File([blobs.background], "background.png")]),
|
|
root
|
|
)
|
|
: Promise.resolve(null);
|
|
|
|
const layers = blobs.layers
|
|
.filter(is_not_null)
|
|
.map(async (blob, i) =>
|
|
upload(await prepare_files([new File([blob], `layer_${i}.png`)]), root)
|
|
);
|
|
|
|
const composite = blobs.composite
|
|
? upload(
|
|
await prepare_files([new File([blobs.composite], "composite.png")]),
|
|
root
|
|
)
|
|
: Promise.resolve(null);
|
|
|
|
const [background, composite_, ...layers_] = await Promise.all([
|
|
bg,
|
|
composite,
|
|
...layers
|
|
]);
|
|
|
|
return {
|
|
background: Array.isArray(background) ? background[0] : background,
|
|
layers: layers_
|
|
.flatMap((layer) => (Array.isArray(layer) ? layer : [layer]))
|
|
.filter(is_file_data),
|
|
composite: Array.isArray(composite_) ? composite_[0] : composite_
|
|
};
|
|
}
|
|
|
|
function handle_value(value: EditorData | null): void {
|
|
if (!editor) return;
|
|
if (value == null) {
|
|
editor.handle_remove();
|
|
}
|
|
}
|
|
|
|
$: handle_value(value);
|
|
|
|
$: crop_constraint = crop_size;
|
|
let bg = false;
|
|
let history = false;
|
|
|
|
export let image_id: null | string = null;
|
|
|
|
$: editor &&
|
|
editor.set_tool &&
|
|
(sources && sources.length
|
|
? editor.set_tool("bg")
|
|
: editor.set_tool("draw"));
|
|
|
|
type BinaryImages = [string, string, File, number | null][];
|
|
|
|
function nextframe(): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(() => resolve(), 30));
|
|
}
|
|
|
|
let uploading = false;
|
|
let pending = false;
|
|
async function handle_change(e: CustomEvent<Blob | any>): Promise<void> {
|
|
if (!realtime) return;
|
|
if (uploading) {
|
|
pending = true;
|
|
return;
|
|
}
|
|
|
|
uploading = true;
|
|
|
|
await nextframe();
|
|
const blobs = await editor.get_blobs();
|
|
|
|
const images: BinaryImages = [];
|
|
|
|
let id = Math.random().toString(36).substring(2);
|
|
|
|
if (blobs.background)
|
|
images.push([
|
|
id,
|
|
"background",
|
|
new File([blobs.background], "background.png"),
|
|
null
|
|
]);
|
|
if (blobs.composite)
|
|
images.push([
|
|
id,
|
|
"composite",
|
|
new File([blobs.composite], "composite.png"),
|
|
null
|
|
]);
|
|
blobs.layers.forEach((layer, i) => {
|
|
if (layer)
|
|
images.push([
|
|
id as string,
|
|
`layer`,
|
|
new File([layer], `layer_${i}.png`),
|
|
i
|
|
]);
|
|
});
|
|
|
|
await Promise.all(
|
|
images.map(async ([image_id, type, data, index]) => {
|
|
return accept_blobs({
|
|
binary: true,
|
|
data: { file: data, id: image_id, type, index }
|
|
});
|
|
})
|
|
);
|
|
image_id = id;
|
|
dispatch("change");
|
|
|
|
await nextframe();
|
|
uploading = false;
|
|
if (pending) {
|
|
pending = false;
|
|
uploading = false;
|
|
handle_change(e);
|
|
}
|
|
}
|
|
|
|
let active_mode: "webcam" | "color" | null = null;
|
|
let editor_height = height - 100;
|
|
|
|
$: [heading, paragraph] = placeholder ? inject(placeholder) : [false, false];
|
|
</script>
|
|
|
|
<BlockLabel
|
|
{show_label}
|
|
Icon={ImageIcon}
|
|
label={label || i18n("image.image")}
|
|
/>
|
|
<ImageEditor
|
|
{canvas_size}
|
|
crop_size={Array.isArray(crop_size) ? crop_size : undefined}
|
|
bind:this={editor}
|
|
bind:height={editor_height}
|
|
parent_height={height}
|
|
{changeable}
|
|
on:save
|
|
on:change={handle_change}
|
|
on:clear={() => dispatch("clear")}
|
|
bind:history
|
|
bind:bg
|
|
{sources}
|
|
crop_constraint={!!crop_constraint}
|
|
>
|
|
<Tools {i18n}>
|
|
<Layers layer_files={value?.layers || null} enable_layers={layers} />
|
|
|
|
<Sources
|
|
bind:dragging
|
|
{i18n}
|
|
{root}
|
|
{sources}
|
|
{upload}
|
|
{stream_handler}
|
|
bind:bg
|
|
bind:active_mode
|
|
background_file={value?.background || value?.composite || null}
|
|
></Sources>
|
|
|
|
{#if transforms.includes("crop")}
|
|
<Crop {crop_constraint} />
|
|
{/if}
|
|
{#if brush}
|
|
<Brush
|
|
color_mode={brush.color_mode}
|
|
default_color={brush.default_color}
|
|
default_size={brush.default_size}
|
|
colors={brush.colors}
|
|
mode="draw"
|
|
/>
|
|
{/if}
|
|
|
|
{#if brush && eraser}
|
|
<Brush default_size={eraser.default_size} mode="erase" />
|
|
{/if}
|
|
</Tools>
|
|
|
|
{#if !bg && !history && active_mode !== "webcam" && status !== "error"}
|
|
<div class="empty wrap" style:height={`${editor_height}px`}>
|
|
{#if sources && sources.length}
|
|
{#if heading || paragraph}
|
|
{#if heading}
|
|
<h2>{heading}</h2>
|
|
{/if}
|
|
{#if paragraph}
|
|
<p>{paragraph}</p>
|
|
{/if}
|
|
{:else}
|
|
<div>Upload an image</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if sources && sources.length && brush && !placeholder}
|
|
<div class="or">or</div>
|
|
{/if}
|
|
{#if brush && !placeholder}
|
|
<div>select the draw tool to start</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</ImageEditor>
|
|
|
|
<style>
|
|
h2 {
|
|
font-size: var(--text-xl);
|
|
}
|
|
|
|
p,
|
|
h2 {
|
|
white-space: pre-line;
|
|
}
|
|
|
|
.empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
position: absolute;
|
|
height: 100%;
|
|
width: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
margin: auto;
|
|
z-index: var(--layer-top);
|
|
text-align: center;
|
|
color: var(--body-text-color);
|
|
top: var(--size-8);
|
|
}
|
|
|
|
.wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
color: var(--block-label-text-color);
|
|
line-height: var(--line-md);
|
|
font-size: var(--text-md);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.or {
|
|
color: var(--body-text-color-subdued);
|
|
}
|
|
</style>
|
|
|