Spaces:
Sleeping
Sleeping
| 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() |