Initial commit of Round Image.

This commit is contained in:
2025-03-08 16:59:16 +01:00
parent abcecb933a
commit 65861403b9
2 changed files with 220 additions and 0 deletions
+3
View File
@@ -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<AppProps> = () => {
<MatchSizes />
<div style={{ marginTop: "8px" }}></div>
<MatchProperties />
<div style={{ marginTop: "8px" }}></div>
<RoundImage />
</div>
<div className={styles.section}>
+217
View File
@@ -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 (
<div className={styles.container}>
<div className={styles.buttonGroup}>
<Button
appearance="primary"
className={styles.actionButton}
onClick={convertToRoundImage}
icon={<CircleRegular />}
disabled={isProcessing}
>
Round Image
</Button>
</div>
</div>
);
};
export default RoundImage;