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
- Setup
Install the required packages:
pip install streamlit pillow streamlit-cropper streamlit-sortables
Create a Python file, e.g., app.py.
- 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
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.
- App Configuration
APP_NAME = "Pro Image Cropper Plus — Streamlit"
st.set_page_config(
page_title=APP_NAME,
layout="wide",
initial_sidebar_state="expanded",
)
- 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)
- 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()
Explanation: st.session_state lets us remember images, crops, zoom, and history between interactions.
- 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)),
}
- 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}"
- Sidebar Controls
with st.sidebar:
st.header("Controls")
uploads = st.file_uploader(
"Open Images",
type=["png","jpg","jpeg","gif"],
accept_multiple_files=True
)
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
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)
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
- 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)
- 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)
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
Crop the Image
cropped = st_cropper(
img,
aspect_ratio=preset_ratio,
realtime_update=True
)
if preset_size:
cropped = cropped.resize(preset_size, Image.LANCZOS)
- 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}"
)
- 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")
- Run the App
streamlit run app.py
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)