diff --git a/src/taskpane/components/App.tsx b/src/taskpane/components/App.tsx index bbf4c8aa..3c3f450b 100644 --- a/src/taskpane/components/App.tsx +++ b/src/taskpane/components/App.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { useEffect, useState } from "react"; import MatchSizes from "./MatchSizes"; import MatchProperties from "./MatchProperties"; +import RoundImage from "./RoundImage"; import { makeStyles, Text, Subtitle1, tokens, Theme, Spinner } from "@fluentui/react-components"; import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons"; @@ -151,6 +152,8 @@ const App: React.FC = () => {
+
+
diff --git a/src/taskpane/components/RoundImage.tsx b/src/taskpane/components/RoundImage.tsx new file mode 100644 index 00000000..6ad59592 --- /dev/null +++ b/src/taskpane/components/RoundImage.tsx @@ -0,0 +1,217 @@ +import * as React from "react"; +import { + Button, + makeStyles, + tokens +} from "@fluentui/react-components"; +import { + CircleRegular +} from "@fluentui/react-icons"; +import { useStatusContext } from "./App"; + +const useStyles = makeStyles({ + container: { + display: "flex", + flexDirection: "column", + width: "100%", + }, + buttonGroup: { + display: "flex", + flexDirection: "column", + gap: "8px", + marginBottom: "2px", + }, + actionButton: { + justifyContent: "flex-start", + transitionProperty: "all", + transitionDuration: "200ms", + transitionTimingFunction: "cubic-bezier(0.33, 0, 0.67, 1)", + }, + statusContainer: { + marginTop: "4px", + padding: "8px 12px", + fontSize: "13px", + borderRadius: "6px", + backgroundColor: "#f3f2f1", // Light gray background + transition: "all 0.3s ease", + }, + successStatus: { + backgroundColor: "#DFF6DD", // Light green background + color: "#107C10", // Green text + }, + warningStatus: { + backgroundColor: "#FFF4CE", // Light yellow background + color: "#797673", // Dark gray text + }, + errorStatus: { + backgroundColor: "#FDE7E9", // Light red background + color: "#A80000", // Red text + }, + statusIcon: { + marginRight: "8px", + }, + statusText: { + display: "flex", + alignItems: "center", + }, +}); + +export const RoundImage: React.FC = () => { + const styles = useStyles(); + const { + statusMessage, setStatusMessage, + statusType, setStatusType, + isProcessing, setIsProcessing + } = useStatusContext(); + + const convertToRoundImage = async () => { + setIsProcessing(true); + try { + await PowerPoint.run(async (context) => { + // Get the selected shapes + const shapes = context.presentation.getSelectedShapes(); + shapes.load("items"); + await context.sync(); + + if (shapes.items.length === 0) { + setStatusMessage("No shapes are selected. Please select an image."); + setStatusType("warning"); + return; + } + + // Get the first selected shape + const shape = shapes.items[0]; + + // Load essential properties + shape.load(["width", "height", "left", "top", "zIndex"]); + await context.sync(); + + try { + // Get the current slide + const slide = context.presentation.getSelectedSlides().getItemAt(0); + + // Step 1: Create an elliptical mask with the same dimensions as the image + // @ts-ignore - Use type assertion to avoid TypeScript issues + const maskShape = slide.shapes.addGeometricShape("Ellipse" as any); + + // Position and size the mask to exactly match the image + maskShape.left = shape.left; + maskShape.top = shape.top; + maskShape.width = shape.width; + maskShape.height = shape.height; + + // Set mask appearance - white fill with no outline + maskShape.fill.setSolidColor("white"); + + // Try to set no outline if the API supports it + try { + // @ts-ignore - This property might not be in TypeScript definitions + maskShape.lineFormat.visible = false; + } catch (lineError) { + console.log("Could not set line format visibility", lineError); + } + + // Make sure the mask is on top of the image + // The z-index should be higher than the image + // @ts-ignore - zIndex might not be directly settable + if (maskShape.hasOwnProperty("zIndex")) { + // @ts-ignore - Using property that might not be in the type definition + maskShape.zIndex = shape.zIndex + 1; + } + + await context.sync(); + + // Step 2: Select both the mask and the image + // First, clear any current selection + // No direct "clearSelection" method in the API + // We'll try to select the image first, but need to use @ts-ignore for TypeScript + try { + // @ts-ignore - The select method may not be in the TypeScript definitions + shape.select(); + await context.sync(); + } catch (selectError) { + console.log("Could not select shape", selectError); + // If we can't select the shape, we'll just continue and instruct the user + } + + // Now try to add the mask to the selection + // @ts-ignore - This method might not exist or might have different name + try { + // Try different possible methods to select multiple shapes + // @ts-ignore + if (maskShape.hasOwnProperty("select")) { + // @ts-ignore - Method might exist but with different parameters + maskShape.select(false); // false = add to selection, not exclusive + } else { + // Alternative: try to create a multi-selection directly + // @ts-ignore - This might not be in the API + slide.shapes.select([shape, maskShape]); + } + await context.sync(); + } catch (selectError) { + console.error("Could not multi-select shapes", selectError); + // If multi-select fails, we need to guide the user to do it manually + setStatusMessage("Created mask shape. Please select both the image and the oval, then use the 'Merge Shapes > Intersect' command in PowerPoint."); + setStatusType("warning"); + return; + } + + // Step 3: Merge the shapes to create a masked image + // Unfortunately, the Office.js API doesn't directly expose shape merging functionality + // We would need to use PowerPoint's "Merge Shapes > Intersect" command + // Since we can't call this directly, we'll need to guide the user + + setStatusMessage("Created oval mask. Please use 'Merge Shapes > Intersect' command in PowerPoint to complete the conversion."); + setStatusType("success"); + } catch (error) { + console.error("Error creating mask:", error); + + // Simplified fallback approach if the main one fails + try { + // Just create an oval with the same dimensions + const slide = context.presentation.getSelectedSlides().getItemAt(0); + // @ts-ignore - Use type assertion to avoid TypeScript issues + const oval = slide.shapes.addGeometricShape("Ellipse" as any); + oval.left = shape.left; + oval.top = shape.top; + oval.width = shape.width; + oval.height = shape.height; + oval.fill.setSolidColor("white"); + + await context.sync(); + + setStatusMessage("Created a round mask. Select both shapes and use PowerPoint's 'Merge Shapes > Intersect' command."); + setStatusType("success"); + } catch (fallbackError) { + console.error("Fallback error:", fallbackError); + throw fallbackError; + } + } + }); + } catch (error) { + setStatusMessage(`Error: ${error.message}`); + setStatusType("error"); + console.error("Main error:", error); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+
+ +
+
+ ); +}; + +export default RoundImage; \ No newline at end of file