/** * @file RoundImage.tsx * @description Component that provides functionality to create circular or rounded images * in PowerPoint presentations. This tool creates a mask shape that can be used with * PowerPoint's built-in shape intersection feature to crop images into a circular shape. */ import * as React from "react"; import { CircleRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; import ActionButton from "./ActionButton"; import { getErrorMessage, isPictureShape, getFirstSelectedSlide, selectShapesById } from "../types/office-types"; // Configuration constants const MASK_COLOR = "red"; const SUCCESS_MESSAGE = "Created mask shape. Please select both the image and the oval, then use the 'Shape Format > Merge Shapes > Intersect' command in PowerPoint."; export const RoundImage: React.FC = () => { const styles = useCommonStyles(); const { setStatusMessage, setStatusType } = useStatusContext(); /** * Validates that an image shape is selected * @param shapes The collection of selected shapes * @param context The PowerPoint request context * @returns The selected image shape or null if validation fails */ const validateShapeSelection = async ( shapes: PowerPoint.ShapeScopedCollection, context: PowerPoint.RequestContext ): Promise => { // Check if any shape is selected if (shapes.items.length === 0) { setStatusMessage("No shapes are selected. Please select an image."); setStatusType("warning"); return null; } // Get the first selected shape const shape = shapes.items[0]; // Load essential properties shape.load(["type"]); await context.sync(); // Ensure the shape is a picture using our type-safe utility if (!isPictureShape(shape)) { setStatusMessage("Please select an image."); setStatusType("warning"); return null; } return shape; }; /** * Creates an elliptical mask shape for the image * @param slide The slide to add the mask to * @param imageShape The image shape to mask * @param context The PowerPoint request context * @returns The created mask shape */ const createMaskShape = async ( slide: PowerPoint.Slide, imageShape: PowerPoint.Shape, context: PowerPoint.RequestContext ): Promise => { // Load current dimensions to maintain aspect ratio imageShape.load(["width", "height", "left", "top", "id"]); await context.sync(); // Create elliptical mask with proper type const maskShape = slide.shapes.addGeometricShape(PowerPoint.GeometricShapeType.ellipse); maskShape.load(["width", "height", "left", "top", "id"]); await context.sync(); // Position the mask to match the image maskShape.left = imageShape.left; imageShape.lineFormat.load(["weight"]); await context.sync(); maskShape.top = imageShape.top; maskShape.width = imageShape.width; maskShape.height = imageShape.height; // Style the mask maskShape.fill.setSolidColor(MASK_COLOR); maskShape.lineFormat.visible = false; return maskShape; }; /** * Applies the mask to the image and selects both shapes * @param slide The slide containing the shapes * @param imageShape The image shape to mask * @param maskShape The mask shape * @param context The PowerPoint request context */ const applyMaskToImage = async ( slide: PowerPoint.Slide, imageShape: PowerPoint.Shape, maskShape: PowerPoint.Shape, context: PowerPoint.RequestContext ): Promise => { // Store original dimensions to maintain after selection const width = imageShape.width; const height = imageShape.height; // Select both shapes for the user to apply the intersection selectShapesById(slide, [imageShape.id, maskShape.id]); // Ensure we maintain the same size imageShape.width = width; imageShape.height = height; await context.sync(); // Update status message with instructions setStatusMessage(SUCCESS_MESSAGE); setStatusType("warning"); }; /** * Main function to convert an image to a round image */ const convertToRoundImage = async (): Promise => { try { await PowerPoint.run(async (context) => { // Get the selected shapes const shapes = context.presentation.getSelectedShapes(); shapes.load("items"); await context.sync(); // Validate selection and get the image shape const imageShape = await validateShapeSelection(shapes, context); if (!imageShape) { return; } // Get the current slide using our type-safe utility const slide = getFirstSelectedSlide(context); // Create the mask shape const maskShape = await createMaskShape(slide, imageShape, context); // Apply the mask and select both shapes await applyMaskToImage(slide, imageShape, maskShape, context); }); } catch (error: unknown) { const errorMessage = getErrorMessage(error); setStatusMessage(`Error: ${errorMessage}`); setStatusType("error"); console.error("Round image error:", error); } }; return (
} onClick={convertToRoundImage} />
); }; export default RoundImage;