Customize Presentation Shapes in Python via Java

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, inspect shape elements, and convert Java AWT text outlines into geometry paths.

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.lineTo
Append or insert a cubic Bézier curve with two control points and an endpoint. GeometryPath.cubicBezierTo
Append or insert a quadratic Bézier curve with one control point and an endpoint. GeometryPath.quadraticBezierTo
Append an elliptical arc with the specified dimensions and angles in degrees. GeometryPath.arcTo
Close the current figure by connecting its endpoint to its starting point. GeometryPath.closeFigure
Set the starting point of a figure. GeometryPath.moveTo
Remove a segment at a zero-based index. GeometryPath.removeAt

Add Custom Points to a Shape

This example inserts two points into a rectangular shape using GeometryPath.lineTo and applies the edited path with GeometryShape.setGeometryPath. 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 jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 200, 100)

    geometry_path = shape.getGeometryPaths()[0]
    geometry_path.lineTo(100, 50, 1)
    geometry_path.lineTo(100, 50, 4)

    shape.setGeometryPath(geometry_path)
finally:
    presentation.dispose()

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.removeAt. Applying the modified path with GeometryShape.setGeometryPath updates the shape’s contour.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Heart, 100, 100, 300, 300)

    geometry_path = shape.getGeometryPaths()[0]
    geometry_path.removeAt(2)

    shape.setGeometryPath(geometry_path)
finally:
    presentation.dispose()

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.closeFigure and applied to a rectangular shape using GeometryShape.setGeometryPath, replacing its outline.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, GeometryPath

import math

points = []
outer_radius = 100
inner_radius = 50
angle_step = 72

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

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

star_path = GeometryPath()
star_path.moveTo(*points[0])
for point in points[1:]:
    star_path.lineTo(*point)
star_path.closeFigure()

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, outer_radius * 2, outer_radius * 2)
    shape.setGeometryPath(star_path)
finally:
    presentation.dispose()

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.setGeometryPaths creates a single composite shape with a gap across its middle third.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, GeometryPath

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 200, 100)

    top_path = GeometryPath()
    top_path.moveTo(0, 0)
    top_path.lineTo(shape.getWidth(), 0)
    top_path.lineTo(shape.getWidth(), shape.getHeight() / 3)
    top_path.lineTo(0, shape.getHeight() / 3)
    top_path.closeFigure()

    bottom_path = GeometryPath()
    bottom_path.moveTo(0, shape.getHeight() / 3 * 2)
    bottom_path.lineTo(shape.getWidth(), shape.getHeight() / 3 * 2)
    bottom_path.lineTo(shape.getWidth(), shape.getHeight())
    bottom_path.lineTo(0, shape.getHeight())
    bottom_path.closeFigure()

    shape.setGeometryPaths([top_path, bottom_path])
finally:
    presentation.dispose()

example4_image

Create a Custom Shape with Curved Corners

This example combines straight segments with GeometryPath.arcTo 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.setGeometryPath, and the presentation is saved in PPTX format.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, GeometryPath, SaveFormat

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

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Custom, shape_x, shape_y, shape_width, shape_height)

    geometry_path = GeometryPath()
    geometry_path.moveTo(left_top_size, 0)
    geometry_path.lineTo(shape_width - right_top_size, 0)
    geometry_path.arcTo(right_top_size, right_top_size, 180, -90)
    geometry_path.lineTo(shape_width, shape_height - right_bottom_size)
    geometry_path.arcTo(right_bottom_size, right_bottom_size, -90, -90)
    geometry_path.lineTo(left_bottom_size, shape_height)
    geometry_path.arcTo(left_bottom_size, left_bottom_size, 0, -90)
    geometry_path.lineTo(0, left_top_size)
    geometry_path.arcTo(left_top_size, left_top_size, 90, -90)
    geometry_path.closeFigure()
    
    shape.setGeometryPath(geometry_path)

    presentation.save("output.pptx", SaveFormat.Pptx)
finally:
    presentation.dispose()

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.getPathData and compares the final segment’s PathSegment.getPathCommand 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 jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, PathCommandType

def is_geometry_closed(geometry_shape):
    has_nonempty_path = False
    for geometry_path in geometry_shape.getGeometryPaths():
        path_data = geometry_path.getPathData()
        if len(path_data) == 0:
            continue
        last_segment = path_data[-1]
        has_nonempty_path = True
        if last_segment.getPathCommand() != PathCommandType.Close:
            return False
    return has_nonempty_path

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)
    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 200, 100)
    closed = is_geometry_closed(shape)
    print(closed)
finally:
    presentation.dispose()

Extract and Inspect Shape Elements

Use GeometryShape.createShapeElements to inspect an existing shape as elements containing path points and point types. Cast each returned element to ShapeElement with jpype.JObject to retrieve the point and type arrays through getPathPoints and getPathTypes methods. This representation is useful for contour analysis or passing geometry to custom drawing tools.

The example opens input.pptx, 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 jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, GeometryShape, ShapeElement

presentation = Presentation("input.pptx")
try:
    slide = presentation.getSlides().get_Item(0)

    shape = None
    for candidate in slide.getShapes():
        if isinstance(candidate, GeometryShape):
            shape = candidate
            break

    if shape is None:
        print("No geometry shape found on the first slide.")
    else:
        for element in shape.createShapeElements():
            shape_element = jpype.JObject(element, ShapeElement)
            points = shape_element.getPathPoints()
            types = shape_element.getPathTypes()
            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:
                    print("Close subpath back to its starting point.")
                i += 1
finally:
    presentation.dispose()

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. Java stores these flags in signed bytes; masking preserves the intended bit values. 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 the position returned by getX and getY 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, returned by getRotation, 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 getGeometryPaths when you need geometry commands in the shape’s local coordinate system or want to edit its geometry. Apply edited paths through setGeometryPath or setGeometryPaths.

Convert a Java AWT Shape to GeometryPath

This example combines a rectangular outline with text converted to geometry. The outline’s setFillMode method disables its fill. A GlyphVector creates the text outline in a java.awt.Shape. Its PathIterator supplies move, line, quadratic, cubic, and closing segments, which the example replays on a GeometryPath.

The text uses a 40-point Arial font. The outline is offset 20 points horizontally and positioned with its visual top 10 points below the shape’s top edge. The text path is filled, while the original rectangle remains an outline. Applying both paths with GeometryShape.setGeometryPaths produces a single shape containing the rectangle and text.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, GeometryPath, PathFillModeType

from java.awt import Font
from java.awt.geom import PathIterator
from java.awt.image import BufferedImage

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 300, 100)

    # Get the geometry path of the shape.
    original_path = shape.getGeometryPaths()[0]
    original_path.setFillMode(PathFillModeType.None_)

    # Create a new graphics path with text.
    font = Font("Arial", Font.PLAIN, 40)
    text = "Text in shape"
    image = BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB)
    graphics = image.createGraphics()
    try:
        font_context = graphics.getFontRenderContext()
        glyph_vector = font.createGlyphVector(font_context, text)
        text_bounds = glyph_vector.getVisualBounds()
        baseline = -text_bounds.getY() + 10
        graphics_path = glyph_vector.getOutline(20.0, baseline)
    finally:
        graphics.dispose()

    # Convert the graphics path to a geometry path.
    text_path = GeometryPath()
    path_iterator = graphics_path.getPathIterator(None)
    points = jpype.JArray(jpype.JFloat)(6)
    while not path_iterator.isDone():
        segment_type = path_iterator.currentSegment(points)
        if segment_type == PathIterator.SEG_MOVETO:
            text_path.moveTo(points[0], points[1])
        elif segment_type == PathIterator.SEG_LINETO:
            text_path.lineTo(points[0], points[1])
        elif segment_type == PathIterator.SEG_QUADTO:
            text_path.quadraticBezierTo(points[0], points[1], points[2], points[3])
        elif segment_type == PathIterator.SEG_CUBICTO:
            text_path.cubicBezierTo(points[0], points[1], points[2], points[3], points[4], points[5])
        elif segment_type == PathIterator.SEG_CLOSE:
            text_path.closeFigure()
        path_iterator.next()
    text_path.setFillMode(PathFillModeType.Normal)

    # Apply the text path together with the original geometry path.
    shape.setGeometryPaths([original_path, text_path])
finally:
    presentation.dispose()

example5_image

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 setRotation method; 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.