imagecrop / app.py
veryscared's picture
Update app.py
ccf89a9 verified
import zipfile
import tempfile
from pathlib import Path
from PIL import Image
import gradio as gr
def _unique_name(base_name, existing):
"""
Return a unique filename (base_name already includes extension).
If base_name exists in `existing`, append _1, _2, ... before the extension.
"""
if base_name not in existing:
existing.add(base_name)
return base_name
stem = Path(base_name).stem
suffix = Path(base_name).suffix
i = 1
while True:
candidate = f"{stem}_{i}{suffix}"
if candidate not in existing:
existing.add(candidate)
return candidate
i += 1
def center_crop_or_resize_to_target(images, out_names, target_w=None, target_h=None):
"""
If target_w/target_h are None: center-crop all images to the smallest width/height.
If target_w/target_h are provided:
- If an image is larger than the target in both dimensions: center-crop to the target.
- If an image is smaller in either dimension: scale up (no more than necessary) so
the image covers the target in both dimensions, then center-crop to the exact target.
Returns path to a ZIP file containing JPEGs named according to out_names.
"""
# Determine target dimensions if not provided
if target_w is None or target_h is None:
widths = [img.width for img in images]
heights = [img.height for img in images]
target_w = min(widths)
target_h = min(heights)
else:
# ensure ints
target_w = int(target_w)
target_h = int(target_h)
out_dir = Path(tempfile.mkdtemp(prefix="cropped_"))
saved_files = []
for img, out_name in zip(images, out_names):
# Work on a copy to avoid mutating original PIL Image objects held by caller
working = img
if working.mode != "RGB":
working = working.convert("RGB")
# If image is smaller than target in either dimension, scale up minimally
if working.width < target_w or working.height < target_h:
scale = max(target_w / working.width, target_h / working.height)
new_w = max(1, int(round(working.width * scale)))
new_h = max(1, int(round(working.height * scale)))
working = working.resize((new_w, new_h), resample=Image.LANCZOS)
# Now center-crop to target (if image is larger or equal)
left = max(0, (working.width - target_w) // 2)
top = max(0, (working.height - target_h) // 2)
right = left + target_w
bottom = top + target_h
cropped = working.crop((left, top, right, bottom))
out_path = out_dir / out_name
cropped.save(out_path, format="JPEG", quality=95)
saved_files.append(out_path)
# If we created a converted/resized copy (working), and it's a different object, close it
try:
if working is not img:
working.close()
except:
pass
zip_path = out_dir / "cropped_images.zip"
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for p in saved_files:
zf.write(p, arcname=p.name)
return str(zip_path)
def process_upload(filepaths, mode="smallest", width=None, height=None):
"""
filepaths: list of local file paths (gr.File with type='filepath' provides these)
mode: "smallest" or "specified"
width/height: integers (used only when mode == "specified")
Returns (zip_path or None, message)
"""
if not filepaths:
return None, "No files uploaded."
images = []
try:
# Load images in the same order as filepaths
for fp in filepaths:
img = Image.open(fp)
images.append(img.copy())
img.close()
except Exception as e:
for im in images:
try:
im.close()
except:
pass
return None, f"Error loading images: {e}"
# Build output names that match input stems, with .jpg extension, and ensure uniqueness
existing = set()
out_names = []
for fp in filepaths:
stem = Path(fp).stem
# sanitize stem: remove path separators and problematic characters
safe_stem = "".join(c for c in stem if c.isalnum() or c in (" ", "_", "-")).rstrip()
if not safe_stem:
safe_stem = "image"
base_name = f"{safe_stem}.jpg"
unique = _unique_name(base_name, existing)
out_names.append(unique)
# Validate and decide target dims
if mode == "specified":
try:
if width is None or height is None:
raise ValueError("Width and height must be provided for specified mode.")
target_w = int(width)
target_h = int(height)
if target_w <= 0 or target_h <= 0:
raise ValueError("Width and height must be positive integers.")
except Exception as e:
for im in images:
try:
im.close()
except:
pass
return None, f"Invalid dimensions: {e}"
zip_path = center_crop_or_resize_to_target(images, out_names, target_w=target_w, target_h=target_h)
else:
# smallest mode: compute min dims and crop
zip_path = center_crop_or_resize_to_target(images, out_names, target_w=None, target_h=None)
for im in images:
try:
im.close()
except:
pass
return zip_path, f"Done — {Path(zip_path).name}"
with gr.Blocks() as demo:
gr.Markdown("## Center-crop images to smallest dimensions or to specified dimensions and download as ZIP (preserve input filenames)")
with gr.Row():
inp = gr.File(label="Upload images", file_count="multiple", type="filepath")
with gr.Row():
mode = gr.Radio(choices=["Crop to smallest", "Specify dimensions"], value="Crop to smallest", label="Mode")
with gr.Row():
width = gr.Number(value=512, label="Target width (px)", precision=0)
height = gr.Number(value=512, label="Target height (px)", precision=0)
with gr.Row():
out_file = gr.File(label="Download ZIP")
btn = gr.Button("Crop and Package")
status = gr.Textbox(label="Status", interactive=False)
# Hide width/height when not needed (client-side)
def toggle_dims(selected):
show = selected == "Specify dimensions"
return gr.update(visible=show), gr.update(visible=show)
mode.change(fn=toggle_dims, inputs=[mode], outputs=[width, height])
def run(filepaths, selected_mode, w, h):
chosen = "specified" if selected_mode == "Specify dimensions" else "smallest"
zip_path, msg = process_upload(filepaths, mode=chosen, width=w, height=h)
if zip_path is None:
return None, msg
return zip_path, msg
btn.click(run, inputs=[inp, mode, width, height], outputs=[out_file, status])
if __name__ == "__main__":
demo.launch()