DEV Community

Mate Technologies
Mate Technologies

Posted on

Build a Pro Image Cropper App in Streamlit

In this tutorial, we’ll create Pro Image Cropper Plus, a Streamlit app for image cropping with batch export, undo/redo, presets, and zoom features. Perfect for content creators and designers.

Try the live app: Pro Image Cropper Plus

Source code: GitHub Repo

  1. Setup

Install the required packages:

pip install streamlit pillow streamlit-cropper streamlit-sortables
Enter fullscreen mode Exit fullscreen mode

Create a Python file, e.g., app.py.

  1. Import Libraries
import streamlit as st
from PIL import Image
from streamlit_cropper import st_cropper
from io import BytesIO
import zipfile
import os
from streamlit_sortables import sort_items
Enter fullscreen mode Exit fullscreen mode

Explanation:

streamlit – for the web app.

PIL – for image manipulation.

st_cropper – Streamlit widget for cropping images.

BytesIO and zipfile – for batch saving images.

sort_items – to reorder uploaded images.

  1. App Configuration
APP_NAME = "Pro Image Cropper Plus — Streamlit"

st.set_page_config(
    page_title=APP_NAME,
    layout="wide",
    initial_sidebar_state="expanded",
)
Enter fullscreen mode Exit fullscreen mode
  1. Optional: Dark Theme Styling
st.markdown("""
<style>
[data-testid="stFileUploaderDropzone"] {
    background-color: #020617 !important;
    border: 1px dashed #334155 !important;
}
[data-testid="stFileUploaderDropzone"] p,
[data-testid="stFileUploaderDropzone"] small {
    color: #ffffff !important;
}
button.preset:hover { background-color: #ef4444; color: white; }
</style>
""", unsafe_allow_html=True)
Enter fullscreen mode Exit fullscreen mode
  1. Session State Initialization
HISTORY_LIMIT = 10

def init_state():
    st.session_state.setdefault("images", [])
    st.session_state.setdefault("index", 0)
    st.session_state.setdefault("history", {})
    st.session_state.setdefault("redo", {})
    st.session_state.setdefault("crops", {})
    st.session_state.setdefault("preset", None)
    st.session_state.setdefault("custom_base", "")
    st.session_state.setdefault("zoom", 1.0)

init_state()
Enter fullscreen mode Exit fullscreen mode

Explanation: st.session_state lets us remember images, crops, zoom, and history between interactions.

  1. Presets for Social Media
PRESETS = {
    "Instagram Square": ((1,1), (1080,1080)),
    "Instagram Portrait": ((4,5), (1080,1350)),
    "YouTube Thumbnail": ((16,9), (1280,720)),
    "TikTok / Reels": ((9,16), (1080,1920)),
}
Enter fullscreen mode Exit fullscreen mode
  1. Helper Functions
def ext_from_format(fmt):
    return "jpg" if fmt == "JPEG" else fmt.lower()

def aspect_tuple(v):
    return {
        "Free": None,
        "1:1": (1,1),
        "16:9": (16,9),
        "4:5": (4,5),
        "9:16": (9,16)
    }[v]

def push_history(idx, img):
    h = st.session_state.history.setdefault(idx, [])
    h.append(img.copy())
    if len(h) > HISTORY_LIMIT:
        h.pop(0)
    st.session_state.redo[idx] = []

def undo(idx):
    h = st.session_state.history.get(idx, [])
    if h:
        st.session_state.redo.setdefault(idx, []).append(st.session_state.crops[idx])
        st.session_state.crops[idx] = h.pop()

def redo(idx):
    r = st.session_state.redo.get(idx, [])
    if r:
        st.session_state.history.setdefault(idx, []).append(st.session_state.crops[idx])
        st.session_state.crops[idx] = r.pop()

def filename_template(name, i, mode, custom_base=None):
    base = os.path.splitext(name)[0]
    if mode == "original":
        return base
    if mode == "original_cropped":
        return f"{base}_cropped"
    return custom_base or f"custom_{i+1}"
Enter fullscreen mode Exit fullscreen mode
  1. Sidebar Controls
with st.sidebar:
    st.header("Controls")
    uploads = st.file_uploader(
        "Open Images",
        type=["png","jpg","jpeg","gif"],
        accept_multiple_files=True
    )
Enter fullscreen mode Exit fullscreen mode

Reorder Images

    if uploads:
        names = [u.name for u in uploads]
        order = sort_items(names)
        reordered = [u for name in order for u in uploads if u.name == name]
        st.session_state.images = reordered
        st.session_state.index = 0
Enter fullscreen mode Exit fullscreen mode

Aspect Ratio, Zoom, Output Format

    aspect = st.selectbox("Aspect Ratio", ["Free","1:1","16:9","4:5","9:16"])
    st.session_state.zoom = st.slider("Zoom", 0.3, 3.0, st.session_state.zoom, 0.1)
    out_format = st.selectbox("Output Format", ["PNG","JPEG","GIF"])
    fname_mode = st.selectbox("Filename Template", ["original","original_cropped","custom"])
    if fname_mode == "custom":
        st.session_state.custom_base = st.text_input("Custom Base Name", st.session_state.custom_base)
Enter fullscreen mode Exit fullscreen mode

Preset Buttons

    st.subheader("Presets")
    preset_cols = st.columns(2)
    for i,(k,v) in enumerate(PRESETS.items()):
        if preset_cols[i%2].button(k, key=f"preset-{k}"):
            st.session_state["preset"] = k
Enter fullscreen mode Exit fullscreen mode
  1. Keyboard Shortcuts (Optional)
st.markdown("""
<script>
document.addEventListener("keydown", e=>{
 if(e.key=="j")document.getElementById("prev-btn")?.click();
 if(e.key=="k")document.getElementById("next-btn")?.click();
 if(e.key=="u")document.getElementById("undo-btn")?.click();
 if(e.key=="r")document.getElementById("redo-btn")?.click();
});
</script>
""", unsafe_allow_html=True)
Enter fullscreen mode Exit fullscreen mode
  1. Main Cropping Area
idx = st.session_state.index
file = st.session_state.images[idx]
original_img = Image.open(file).convert("RGB")
img = original_img.copy()

if st.session_state.zoom != 1.0:
    w,h = img.size
    img = img.resize((int(w*st.session_state.zoom), int(h*st.session_state.zoom)), Image.LANCZOS)
Enter fullscreen mode Exit fullscreen mode

Apply Presets or Aspect Ratios

if st.session_state.preset:
    preset_ratio, preset_size = PRESETS[st.session_state.preset]
else:
    preset_ratio = aspect_tuple(aspect)
    preset_size = None
Enter fullscreen mode Exit fullscreen mode

Crop the Image

cropped = st_cropper(
    img,
    aspect_ratio=preset_ratio,
    realtime_update=True
)

if preset_size:
    cropped = cropped.resize(preset_size, Image.LANCZOS)
Enter fullscreen mode Exit fullscreen mode
  1. Save & Preview Cropped Image
buf = BytesIO()
fmt = out_format
ext = ext_from_format(fmt)
save_img = cropped
if fmt == "GIF":
    save_img = save_img.convert("P", palette=Image.ADAPTIVE)
save_img.save(buf, format=fmt, save_all=(fmt=="GIF"))

st.download_button(
    "Save Current",
    data=buf.getvalue(),
    file_name=filename_template(file.name, idx, fname_mode, st.session_state.custom_base)+f".{ext}",
    mime=f"image/{ext}"
)
Enter fullscreen mode Exit fullscreen mode
  1. Batch Export as ZIP
if st.button("Batch Auto-Save (ZIP ALL)"):
    with st.spinner("Preparing ZIP..."):
        zip_buf = BytesIO()
        with zipfile.ZipFile(zip_buf, "w", zipfile.ZIP_DEFLATED) as z:
            for i, f in enumerate(st.session_state.images):
                im = st.session_state.crops.get(i, Image.open(f).convert("RGB"))
                b = BytesIO()
                im.save(b, format=fmt)
                name = filename_template(f.name, i, fname_mode, st.session_state.custom_base)
                z.writestr(f"{name}.{ext}", b.getvalue())
        zip_buf.seek(0)
        st.download_button("Download ZIP", data=zip_buf, file_name="cropped_images.zip", mime="application/zip")
Enter fullscreen mode Exit fullscreen mode
  1. Run the App
streamlit run app.py
Enter fullscreen mode Exit fullscreen mode

You now have a full-featured image cropping app with batch export, presets, undo/redo, zoom, and custom filenames!

Try it live: Pro Image Cropper Plus

Source code: GitHub Repo

Top comments (0)