import gradio as gr from PIL import Image, ImageOps def process_rotation(image, angle, expand, fill_type, custom_color): if image is None: return None # Preprocess image to respect EXIF orientation metadata try: image = ImageOps.exif_transpose(image) except Exception as e: print(f"EXIF transpose failed: {e}") # Map fill type to color if fill_type == "Transparent": if image.mode != "RGBA": image = image.convert("RGBA") fill = (0, 0, 0, 0) elif fill_type == "Black": if image.mode == "RGBA": fill = (0, 0, 0, 255) else: fill = (0, 0, 0) elif fill_type == "White": if image.mode == "RGBA": fill = (255, 255, 255, 255) else: fill = (255, 255, 255) elif fill_type == "Custom Color": try: hex_val = custom_color.lstrip('#') rgb = tuple(int(hex_val[i:i+2], 16) for i in (0, 2, 4)) if image.mode == "RGBA": fill = rgb + (255,) else: fill = rgb except Exception as e: print(f"Color parsing failed: {e}. Defaulting to transparent/black.") fill = (0, 0, 0, 0) if image.mode == "RGBA" else (0, 0, 0) else: fill = (0, 0, 0, 0) if image.mode == "RGBA" else (0, 0, 0) # PIL rotate: angle is clockwise, PIL rotates counter-clockwise # So we pass -angle rotated_img = image.rotate(-angle, expand=expand, fillcolor=fill, resample=Image.Resampling.BICUBIC) return rotated_img def toggle_color_picker(fill_type): if fill_type == "Custom Color": return gr.update(visible=True) else: return gr.update(visible=False) def handle_image_upload(image, expand, fill_type, custom_color): rotated = process_rotation(image, 0, expand, fill_type, custom_color) return 0, rotated # Custom CSS for premium styling custom_css = """ .header-banner { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #db2777 100%); padding: 2.5rem 2rem; border-radius: 16px; text-align: center; color: white; margin-bottom: 2rem; box-shadow: 0 10px 30px -5px rgba(79, 70, 229, 0.3); } .header-banner h1 { font-size: 2.5rem; font-weight: 800; margin: 0; color: white !important; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); letter-spacing: -0.025em; } .header-banner p { font-size: 1.1rem; margin-top: 0.75rem; margin-bottom: 0; opacity: 0.9; font-weight: 400; } .rotate-btn { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important; font-weight: 600 !important; cursor: pointer; } .rotate-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(79, 70, 229, 0.2) !important; } .rotate-btn:active { transform: translateY(0); } .control-section { padding: 1rem; border-radius: 12px; background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.05); } """ theme = gr.themes.Soft( primary_hue="indigo", secondary_hue="violet", neutral_hue="slate", font=[gr.themes.GoogleFont("Outfit"), "sans-serif"], ) with gr.Blocks(title="Image Rotator") as demo: gr.HTML( value="""

🔄 Image Rotator

Rotate your images with precision. Paste directly from the clipboard, drop files, and choose any angle.

""" ) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 📂 Input Source") input_image = gr.Image( type="pil", sources=["upload", "clipboard"], label="Drag & Drop, Browse, or Paste Image (Ctrl+V)" ) with gr.Group(): gr.Markdown("### ⚙️ Quick Rotations") with gr.Row(): btn_ccw_90 = gr.Button("↺ 90° CCW", elem_classes=["rotate-btn"]) btn_cw_90 = gr.Button("↻ 90° CW", elem_classes=["rotate-btn"]) with gr.Row(): btn_rotate_180 = gr.Button("⇅ 180°", elem_classes=["rotate-btn"]) btn_reset = gr.Button("🗑️ Reset", variant="secondary", elem_classes=["rotate-btn"]) with gr.Column(scale=1): gr.Markdown("### 🎛️ Precision & Canvas Controls") angle_slider = gr.Slider( minimum=-360, maximum=360, value=0, step=1, label="Custom Angle (Degrees)" ) with gr.Accordion("Advanced Canvas Settings", open=True): expand_canvas = gr.Checkbox( value=True, label="Expand canvas to fit rotated image", info="If disabled, the image is cropped to its original dimensions." ) fill_type = gr.Radio( choices=["Transparent", "Black", "White", "Custom Color"], value="Transparent", label="Background Fill Color", info="Used for areas revealed by rotation (transparency requires PNG output)" ) custom_color = gr.ColorPicker( value="#4f46e5", label="Select Custom Background Color", visible=False ) gr.Markdown("### 🖼️ Rotated Preview") output_image = gr.Image( type="pil", label="Rotated Output (Preview & Download)", interactive=False ) # Event wiring # Toggling color picker visibility fill_type.change( fn=toggle_color_picker, inputs=[fill_type], outputs=[custom_color] ) # Quick Action Buttons change the slider btn_ccw_90.click( fn=lambda val: (val - 90) % 360, inputs=[angle_slider], outputs=[angle_slider] ) btn_cw_90.click( fn=lambda val: (val + 90) % 360, inputs=[angle_slider], outputs=[angle_slider] ) btn_rotate_180.click( fn=lambda val: (val + 180) % 360, inputs=[angle_slider], outputs=[angle_slider] ) btn_reset.click( fn=lambda: 0, inputs=[], outputs=[angle_slider] ) # Uploading/changing the input image resets the angle slider to 0 and updates the preview input_image.change( fn=handle_image_upload, inputs=[input_image, expand_canvas, fill_type, custom_color], outputs=[angle_slider, output_image] ) # Whenever rotation parameters change, compute new preview rotation_inputs = [input_image, angle_slider, expand_canvas, fill_type, custom_color] angle_slider.change( fn=process_rotation, inputs=rotation_inputs, outputs=[output_image] ) expand_canvas.change( fn=process_rotation, inputs=rotation_inputs, outputs=[output_image] ) fill_type.change( fn=process_rotation, inputs=rotation_inputs, outputs=[output_image] ) custom_color.change( fn=process_rotation, inputs=rotation_inputs, outputs=[output_image] ) if __name__ == "__main__": demo.launch(theme=theme, css=custom_css)