1aeef10ebe
- Add file-level JSDoc comments to components that were missing them - Ensure consistent documentation format across all components - Improve code maintainability and readability
175 lines
5.5 KiB
TypeScript
175 lines
5.5 KiB
TypeScript
/**
|
|
* @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<PowerPoint.Shape | null> => {
|
|
// 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<PowerPoint.Shape> => {
|
|
// 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<void> => {
|
|
// 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<void> => {
|
|
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 (
|
|
<div className={styles.container}>
|
|
<div className={styles.buttonGroup}>
|
|
<ActionButton
|
|
title="Round Image"
|
|
icon={<CircleRegular />}
|
|
onClick={convertToRoundImage}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RoundImage;
|