# app.py from PIL import Image, ImageDraw import gradio as gr MAX_SIDE = 900 def fit_canvas(img): if img is None: return MAX_SIDE, MAX_SIDE w, h = img.size scale = MAX_SIDE / max(w, h) return int(w * scale), int(h * scale) def snap_last_stroke(val): """Convert any roughly-dragged stroke into a filled rectangle of its color.""" if not val or not val["layers"]: return val last = val["layers"][-1] box = last.getbbox() if box: mx, my = (box[0]+box[2])//2, (box[1]+box[3])//2 try: color = last.getpixel((mx, my)) except: color = (255,255,255,255) rect = Image.new("RGBA", last.size, (0,0,0,0)) ImageDraw.Draw(rect).rectangle(box, fill=color) val["layers"][-1] = rect base = val["background"].copy() for layer in val["layers"]: base.alpha_composite(layer) val["composite"] = base # collapse layers so editor.value is always the merged image val["background"], val["layers"] = base, [] return val def load_image(img): return gr.update(value=img, canvas_size=fit_canvas(img)) def update_brush(color_hex): return gr.update( brush=gr.Brush( colors=[color_hex], default_color=color_hex, default_size=80, color_mode="fixed" ) ) with gr.Blocks(css=".gradio-container { max-width:1200px; margin:auto; }") as demo: with gr.Row(): color_picker = gr.ColorPicker("#FFFFFF", label="Brush Color") uploader = gr.Image(type="pil", label="Upload Image") editor = gr.ImageEditor( type="pil", # emit PIL.Images so the download icon works brush=gr.Brush(colors=["#FFFFFF"], default_color="#FFFFFF", default_size=80, color_mode="fixed"), eraser=gr.Eraser(default_size=80), layers=False, canvas_size=(MAX_SIDE, MAX_SIDE), ) # snapping callback editor.apply(snap_last_stroke, inputs=editor, outputs=editor) # wire color picker & uploader color_picker.change(update_brush, color_picker, editor) uploader.change(load_image, uploader, editor) demo.launch(share=True)