Customize Presentation Shapes in Python

Overview

This article explains how to customize presentation shapes in Aspose.Slides by editing shape geometry through edit points and geometry paths. It shows how to work with GeometryPath to modify existing shapes, perform basic path editing operations, add or remove points, and apply updated geometry back to a shape.

It also demonstrates how to create custom and composite shapes, build shapes with curved corners, determine whether a shape geometry is closed, and extract shape elements for contour analysis.

Change a Shape Using Edit Points

In PowerPoint, you can use Edit Points to modify a square:

  • Move a corner inward or outward.
  • Adjust the curvature at a point.
  • Add new points.

Editing points lets you modify a shape’s outline or use an existing shape as the basis for a new one.

Shape Editing Tips

overview_image

When editing PowerPoint shapes, keep these concepts in mind:

  • A shape (or its path) can either be closed or open.
  • A shape’s path consists of anchor points connected by segments.
  • A segment can be straight or curved. Anchor points and control handles determine its shape.
  • Anchor points exist as corner points, straight points, or smooth points:
    • At a corner point, adjoining segments can meet at an angle.
    • At a smooth point, the two control handles are aligned and equally distant from the anchor point.
    • At a straight point, the two control handles are aligned but can be different distances from the anchor point.
  • Moving anchor points or adjusting their handles changes the shape’s outline.

To edit PowerPoint shapes through edit points, Aspose.Slides provides the GeometryPath class.

Simple Editing Operations

Use the following methods to edit a geometry path. Overloads that accept an index insert a segment at that zero-based position in the path’s segment array.

Operation Method
Append or insert a straight line. GeometryPath.line_to
Append or insert a cubic Bézier curve with two control points and an endpoint. GeometryPath.cubic_bezier_to
Append or insert a quadratic Bézier curve with one control point and an endpoint. GeometryPath.quadratic_bezier_to
Append an elliptical arc with the specified dimensions and angles in degrees. GeometryPath.arc_to
Close the current figure by connecting its endpoint to its starting point. GeometryPath.close_figure
Set the starting point of a figure. GeometryPath.move_to
Remove a segment at a zero-based index. GeometryPath.remove_at

Add Custom Points to a Shape

This example inserts two points into a rectangular shape using GeometryPath.line_to and applies the edited path with GeometryShape.set_geometry_path. The coordinates are local to the shape: both new points are at its center, 100 points from the left and 50 points from the top. Segment indices 1 and 4 place them on the top and bottom edges, pulling those edges inward.

import aspose.slides as slides

with slides.Presentation() as presentation:
    slide = presentation.slides[0]

    shape = slide.shapes.add_auto_shape(slides.ShapeType.RECTANGLE, 100, 100, 200, 100)

    geometry_path = shape.get_geometry_paths()[0]
    geometry_path.line_to(100, 50, 1)
    geometry_path.line_to(100, 50, 4)

    shape.set_geometry_path(geometry_path)

example1_image

Remove Points from a Shape

This example changes a heart-shaped outline by removing the segment at zero-based index 2 with GeometryPath.remove_at. Applying the modified path with GeometryShape.set_geometry_path updates the shape’s contour.

import aspose.slides as slides

with slides.Presentation() as presentation:
    slide = presentation.slides[0]

    shape = slide.shapes.add_auto_shape(slides.ShapeType.HEART, 100, 100, 300, 300)

    path = shape.get_geometry_paths()[0]
    path.remove_at(2)

    shape.set_geometry_path(path)

example2_image

Create a Custom Shape

This example builds a five-pointed star with GeometryPath. Alternating outer and inner radii of 100 and 50 points define the tips and indentations; a 72-degree step spaces the five tips evenly. The path is closed with GeometryPath.close_figure and applied to a rectangular shape using GeometryShape.set_geometry_path, replacing its outline.

import aspose.slides as slides
import aspose.pydrawing as draw
import math

points = []

outer_radius = 100
inner_radius = 50
angle_step = 72

for angle in range(-90, 270, angle_step):
    radians = angle * (math.pi / 180)
    x = outer_radius * math.cos(radians)
    y = outer_radius * math.sin(radians)
    points.append(slides.PointF(x + outer_radius, y + outer_radius))

    radians = math.pi * (angle + angle_step / 2) / 180.0
    x = inner_radius * math.cos(radians)
    y = inner_radius * math.sin(radians)
    points.append(slides.PointF(x + outer_radius, y + outer_radius))

star_path = slides.GeometryPath()
star_path.move_to(points[0])

for i in range(1, len(points)):
    star_path.line_to(points[i])

star_path.close_figure()

with slides.Presentation() as presentation:
    slide = presentation.slides[0]

    shape = slide.shapes.add_auto_shape(slides.ShapeType.RECTANGLE, 100, 100, outer_radius * 2, outer_radius * 2)
    shape.set_geometry_path(star_path)

example3_image

Create a Composite Custom Shape

This example defines two closed GeometryPath objects for the top and bottom thirds of a shape. Applying both paths with GeometryShape.set_geometry_paths creates a single composite shape with a gap across its middle third.

import aspose.slides as slides

with slides.Presentation() as presentation:
    slide = presentation.slides[0]

    shape = slide.shapes.add_auto_shape(slides.ShapeType.RECTANGLE, 100, 100, 200, 100)

    top_path = slides.GeometryPath()
    top_path.move_to(0, 0)
    top_path.line_to(shape.width, 0)
    top_path.line_to(shape.width, shape.height / 3)
    top_path.line_to(0, shape.height / 3)
    top_path.close_figure()

    bottom_path = slides.GeometryPath()
    bottom_path.move_to(0, shape.height / 3 * 2)
    bottom_path.line_to(shape.width, shape.height / 3 * 2)
    bottom_path.line_to(shape.width, shape.height)
    bottom_path.line_to(0, shape.height)
    bottom_path.close_figure()

    shape.set_geometry_paths([top_path, bottom_path])

example4_image

Create a Custom Shape with Curved Corners

This example combines straight segments with GeometryPath.arc_to to create a shape with inward-curved corners. Each corner size sets the arc’s width and height in points, and the -90-degree sweep draws a counterclockwise quarter ellipse. The completed path is applied with GeometryShape.set_geometry_path, and the presentation is saved in PPTX format.

import aspose.slides as slides
import aspose.pydrawing as draw

shape_x = 20
shape_y = 20
shape_width = 300
shape_height = 200

left_top_size = 50
right_top_size = 20
right_bottom_size = 40
left_bottom_size = 10

with slides.Presentation() as presentation:
    slide = presentation.slides[0]

    shape = slide.shapes.add_auto_shape(slides.ShapeType.CUSTOM, shape_x, shape_y, shape_width, shape_height)

    top_left = slides.PointF(left_top_size, 0)
    top_right = slides.PointF(shape_width - right_top_size, 0)
    right_bottom = slides.PointF(shape_width, shape_height - right_bottom_size)
    bottom_left = slides.PointF(left_bottom_size, shape_height)
    left_top = slides.PointF(0, left_top_size)

    geometry_path = slides.GeometryPath()
    geometry_path.move_to(top_left)
    geometry_path.line_to(top_right)
    geometry_path.arc_to(right_top_size, right_top_size, 180, -90)
    geometry_path.line_to(right_bottom)
    geometry_path.arc_to(right_bottom_size, right_bottom_size, -90, -90)
    geometry_path.line_to(bottom_left)
    geometry_path.arc_to(left_bottom_size, left_bottom_size, 0, -90)
    geometry_path.line_to(left_top)
    geometry_path.arc_to(left_top_size, left_top_size, 90, -90)
    geometry_path.close_figure()

    shape.set_geometry_path(geometry_path)

    presentation.save("output.pptx", slides.export.SaveFormat.PPTX)

Find Out If a Shape Geometry Is Closed

The following helper checks whether every nonempty geometry path ends with a closing segment. It reads GeometryPath.path_data and compares the final segment’s PathSegment.path_command with PathCommandType.CLOSE. It returns false when there are no nonempty paths. This checks the final command of each path; it does not inspect every subfigure within a path. The example prints True for a rectangle.

import aspose.slides as slides


def is_geometry_closed(geometry_shape):
    has_nonempty_path = False

    for geometry_path in geometry_shape.get_geometry_paths():
        segments = geometry_path.path_data
        if len(segments) == 0:
            continue

        has_nonempty_path = True
        last_segment = segments[len(segments) - 1]
        if last_segment.path_command != slides.PathCommandType.CLOSE:
            return False

    return has_nonempty_path


with slides.Presentation() as presentation:
    slide = presentation.slides[0]
    shape = slide.shapes.add_auto_shape(slides.ShapeType.RECTANGLE, 100, 100, 200, 100)
    print(is_geometry_closed(shape))

Extract and Inspect Shape Elements

Use GeometryShape.create_shape_elements to inspect an existing shape as elements containing path points and point types. Each returned ShapeElement exposes its path_points and path_types arrays. This representation is useful for contour analysis or passing geometry to custom drawing tools.

The example opens a presentation, finds the first geometry shape directly on the first slide, and prints its path segments. Use a presentation containing a shape with straight and curved segments to see both kinds of output. It does not search inside groups.

import aspose.slides as slides

with slides.Presentation("input.pptx") as presentation:
    slide = presentation.slides[0]
    shape = next((item for item in slide.shapes if isinstance(item, slides.GeometryShape)), None)

    if shape is None:
        print("No geometry shape found on the first slide.")
    else:
        for element in shape.create_shape_elements():
            points = element.path_points
            types = element.path_types
            print("Shape element:")

            i = 0
            while i < len(points):
                point_type = types[i] & 0x07  # Keep the segment type, excluding flags.
                if point_type == 0:
                    print(f"Start figure at {points[i]}")
                elif point_type == 1:
                    print(f"Line to {points[i]}")
                elif point_type == 3:
                    print(f"Cubic Bezier: controls {points[i]}, {points[i + 1]}; end {points[i + 2]}")
                    i += 2  # Consume both control points and the endpoint.

                if (types[i] & 0x80) != 0:
                    print("Close subpath back to its starting point.")
                i += 1

The arrays correspond by index: points[i] is a point and types[i] describes its role. Type 0 starts a figure, and type 1 draws a line from the previous point to the current point. A cubic Bézier segment starts at the previous endpoint and uses three consecutive type-3 entries: two control points followed by the new endpoint. The loop consumes these three entries together and checks the endpoint for closure.

Point types also contain flags. Mask with 0x07 to identify the segment type, and test 0x80 separately to detect a closed subpath. For example, 129 (0x81) means a line endpoint that closes a subpath, and 131 (0x83) means a Bézier endpoint that closes one. Comparing the raw value only with 1 or 3 would miss these cases. Other flags, such as dash (0x10) and marker (0x20), do not change the base segment type.

Coordinates and transformations. Points are measured in points (1/72 inch), with X increasing to the right and Y increasing downward. For an ungrouped shape, the extracted coordinates already include its x and y position and reflect its current width and height. For example, an unrotated rectangle at (100, 200) with size 120 × 80 has contour bounds (100, 200)–(220, 280). Do not add the shape’s position again. Subtract its X and Y to obtain coordinates relative to its unrotated frame.

The extracted points do not apply the shape’s rotation or horizontal and vertical flips. To reproduce the displayed contour, apply those transformations around the shape’s center. For grouped shapes, also account for the group coordinate mapping and transformations; do not assume that the extracted points are the final rendered slide coordinates or apply a translation or scale twice if it is already reflected in the child’s frame.

Inspection versus editing. Extracted arrays describe the contour; changing them does not update the original shape. Choose this approach when your analysis or drawing code needs point/type arrays. Use get_geometry_paths when you need geometry commands in the shape’s local coordinate system or want to edit its geometry. Apply edited paths through set_geometry_path or set_geometry_paths.

FAQ

What will happen to the fill and outline after replacing the geometry?

The style remains with the shape; only the contour changes. The fill and outline are automatically applied to the new geometry.

How do I correctly rotate a custom shape along with its geometry?

Use the shape’s rotation property; the geometry rotates with the shape because it’s bound to the shape’s own coordinate system.

Can I convert a custom shape to an image to “lock in” the result?

Yes. Export the required slide area or the shape itself to a raster format; this simplifies further work with complex geometries.