Initial commit of Round Image.
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user