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 { useEffect, useState } from "react";
|
||||||
import MatchSizes from "./MatchSizes";
|
import MatchSizes from "./MatchSizes";
|
||||||
import MatchProperties from "./MatchProperties";
|
import MatchProperties from "./MatchProperties";
|
||||||
|
import RoundImage from "./RoundImage";
|
||||||
import { makeStyles, Text, Subtitle1, tokens, Theme, Spinner } from "@fluentui/react-components";
|
import { makeStyles, Text, Subtitle1, tokens, Theme, Spinner } from "@fluentui/react-components";
|
||||||
import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons";
|
import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons";
|
||||||
|
|
||||||
@@ -151,6 +152,8 @@ const App: React.FC<AppProps> = () => {
|
|||||||
<MatchSizes />
|
<MatchSizes />
|
||||||
<div style={{ marginTop: "8px" }}></div>
|
<div style={{ marginTop: "8px" }}></div>
|
||||||
<MatchProperties />
|
<MatchProperties />
|
||||||
|
<div style={{ marginTop: "8px" }}></div>
|
||||||
|
<RoundImage />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<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