presenton/servers/fastapi/utils/image_utils.py

258 lines
8.3 KiB
Python

from typing import List
from PIL import Image, ImageDraw
from models.pptx_models import PptxObjectFitEnum, PptxObjectFitModel
def clip_image(
image: Image.Image,
width: int,
height: int,
focus_x: float = 50.0,
focus_y: float = 50.0,
) -> Image.Image:
img_width, img_height = image.size
img_aspect = img_width / img_height
box_aspect = width / height
if img_aspect > box_aspect:
new_height = height
new_width = int(new_height * img_aspect)
else:
new_width = width
new_height = int(new_width / img_aspect)
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
# Calculate clipping position based on focus
# Convert focus percentages (0-100) to position in the resized image
focus_x = max(0.0, min(100.0, focus_x)) # Clamp to 0-100 range
focus_y = max(0.0, min(100.0, focus_y)) # Clamp to 0-100 range
# Calculate the center point based on focus
center_x = int((new_width - width) * (focus_x / 100.0))
center_y = int((new_height - height) * (focus_y / 100.0))
# Calculate clipping box
left = center_x
top = center_y
right = left + width
bottom = top + height
clipped_image = resized_image.crop((left, top, right, bottom))
return clipped_image
def round_image_corners(image: Image.Image, radii: List[int]) -> Image.Image:
if len(radii) != 4:
raise ValueError(
"Image Border Radius - radii must contain exactly 4 values for each corner"
)
w, h = image.size
# Clamp border radius to not exceed half the width or height
max_radius = min(w // 2, h // 2)
clamped_radii = [min(radius, max_radius) for radius in radii]
# Ensure the image has an alpha channel (RGBA)
if image.mode != "RGBA":
image = image.convert("RGBA")
# Create a mask for the rounded corners (start with fully transparent)
rounded_mask = Image.new("L", image.size, 0)
# Create a rectangular mask (fully opaque)
rectangular_mask = Image.new("L", image.size, 255)
# Process each corner
for i, radius in enumerate(clamped_radii):
if radius > 0: # Only process if radius is positive
# Create a circle for this radius
circle = Image.new("L", (radius * 2, radius * 2), 0)
draw = ImageDraw.Draw(circle)
draw.ellipse((0, 0, radius * 2 - 1, radius * 2 - 1), fill=255)
# Calculate position based on corner index
if i == 0: # top-left
rounded_mask.paste(circle.crop((0, 0, radius, radius)), (0, 0))
rectangular_mask.paste(0, (0, 0, radius, radius))
elif i == 1: # top-right
rounded_mask.paste(
circle.crop((radius, 0, radius * 2, radius)), (w - radius, 0)
)
rectangular_mask.paste(0, (w - radius, 0, w, radius))
elif i == 2: # bottom-right
rounded_mask.paste(
circle.crop((radius, radius, radius * 2, radius * 2)),
(w - radius, h - radius),
)
rectangular_mask.paste(0, (w - radius, h - radius, w, h))
else: # bottom-left
rounded_mask.paste(
circle.crop((0, radius, radius, radius * 2)), (0, h - radius)
)
rectangular_mask.paste(0, (0, h - radius, radius, h))
# Get the original alpha channel
original_alpha = image.getchannel("A")
# Combine the rectangular mask with the rounded corners
corner_mask = Image.composite(rounded_mask, rectangular_mask, rounded_mask)
# Combine the corner mask with the original alpha channel
final_alpha = Image.composite(
original_alpha, Image.new("L", image.size, 0), corner_mask
)
# Create a new image with the modified alpha channel
result = Image.new("RGBA", image.size)
result.paste(image.convert("RGB"), (0, 0))
result.putalpha(final_alpha)
return result
def invert_image(img: Image.Image) -> Image.Image:
# Get image data
data = img.getdata()
# Process each pixel
new_data = []
for item in data:
# Get current pixel values
r, g, b, a = item
# Invert RGB values while preserving transparency
if a != 0: # Skip fully transparent pixels
new_data.append((255 - r, 255 - g, 255 - b, a))
else:
new_data.append((0, 0, 0, 0))
# Create new image with modified data
new_img = Image.new("RGBA", img.size)
new_img.putdata(new_data)
return new_img
def create_circle_image(
image: Image.Image,
) -> Image.Image:
# Convert to RGBA if not already
img = image.convert("RGBA")
# Get the original image size
size = img.size
# Use the smaller dimension for the circle
circle_size = min(size)
# Create a transparent image of the same size as original
mask = Image.new("RGBA", size, color=(0, 0, 0, 0))
draw = ImageDraw.Draw(mask)
# Calculate center position
center_x = size[0] // 2
center_y = size[1] // 2
radius = circle_size // 2
# Create a circular mask
draw.ellipse(
(
center_x - radius,
center_y - radius,
center_x + radius,
center_y + radius,
),
fill=(255, 255, 255, 255),
)
# Apply the circular mask
result = Image.composite(img, mask, mask)
return result
def set_image_opacity(image: Image.Image, opacity: float) -> Image.Image:
# Clamp opacity to valid range
opacity = max(0.0, min(1.0, opacity))
# Convert to RGBA if not already
if image.mode != "RGBA":
image = image.convert("RGBA")
# Get the original alpha channel
original_alpha = image.getchannel("A")
# Create new alpha channel with adjusted opacity
new_alpha = original_alpha.point(lambda x: int(x * opacity))
# Create new image with modified alpha channel
result = Image.new("RGBA", image.size)
result.paste(image.convert("RGB"), (0, 0))
result.putalpha(new_alpha)
return result
def fit_image(
image: Image.Image, width: int, height: int, object_fit: PptxObjectFitModel
) -> Image.Image:
if not object_fit.fit:
return image
img_width, img_height = image.size
img_aspect = img_width / img_height
box_aspect = width / height
if object_fit.fit == PptxObjectFitEnum.CONTAIN:
# Scale image to fit within the box while maintaining aspect ratio
if img_aspect > box_aspect:
new_width = width
new_height = int(width / img_aspect)
else:
new_height = height
new_width = int(height * img_aspect)
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
# Use focus point for positioning if available
focus_x = 50.0
focus_y = 50.0
if object_fit.focus and len(object_fit.focus) == 2:
focus_x, focus_y = object_fit.focus[0], object_fit.focus[1]
# Calculate paste position based on focus
paste_x = int((width - new_width) * (focus_x / 100.0))
paste_y = int((height - new_height) * (focus_y / 100.0))
result = Image.new("RGBA", (width, height), (0, 0, 0, 0))
result.paste(resized_image, (paste_x, paste_y))
return result
elif object_fit.fit == PptxObjectFitEnum.COVER:
# Scale image to cover the box while maintaining aspect ratio
if img_aspect > box_aspect:
new_height = height
new_width = int(height * img_aspect)
else:
new_width = width
new_height = int(width / img_aspect)
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
# Use focus point for positioning if available
focus_x = 50.0
focus_y = 50.0
if object_fit.focus and len(object_fit.focus) == 2:
focus_x, focus_y = object_fit.focus[0], object_fit.focus[1]
# Calculate paste position based on focus
paste_x = int((new_width - width) * (focus_x / 100.0))
paste_y = int((new_height - height) * (focus_y / 100.0))
# Clip the image to the box size
return resized_image.crop((paste_x, paste_y, paste_x + width, paste_y + height))
elif object_fit.fit == PptxObjectFitEnum.FILL:
# Stretch image to fill the box exactly
return image.resize((width, height), Image.LANCZOS)
return image