ImageRotate / app.py
MichaelRKessler's picture
Sync from GitHub via hub-sync
9158f99 verified
Raw
History Blame Contribute Delete
7.55 kB
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)