|
<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" |
|
| "generating" |
|
| "streaming" = "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 = 450; |
|
|
|
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> |
|
|