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