Spaces:
Sleeping
Sleeping
| 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=""" | |
| <div class="header-banner"> | |
| <h1>🔄 Image Rotator</h1> | |
| <p>Rotate your images with precision. Paste directly from the clipboard, drop files, and choose any angle.</p> | |
| </div> | |
| """ | |
| ) | |
| 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) | |