This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/web/headless/shape.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Headless shape annotations | Nutrient Web SDK

Nutrient Web SDK ships five shape annotation classes, RectangleAnnotation, EllipseAnnotation, LineAnnotation, PolylineAnnotation, and PolygonAnnotation. They all share the same color, stroke, and preset surface, so a single shape palette in your host app can drive every shape type without per-class branching.

Looking for the visual side of this? See the tools.contextual slot for replacing the inline shape toolbar that appears when shape mode is active. This page covers the programmatic surface you’ll call from inside that custom UI.

When to use this

Reach for the headless shape API when you’re building:

  • A custom shape palette in your host-app sidebar, separate from the default toolbar.
  • A click-to-place workflow where the user picks a shape and then clicks the canvas to drop it at a coordinate.
  • A bulk-creation script that lays out shapes from a coordinate list — for example, importing form field bounds from a server.
  • A measurement or markup overlay built on the geometric primitives.

Shape types and their geometry

Each shape annotation class has its own geometry shape. The differences are subtle, and picking the right one matters because it determines what’s serialized into the PDF.

ClassGeometryWhen to use
RectangleAnnotationboundingBox: RectAxis-aligned rectangles. Optional cloudy border via cloudyBorderIntensity.
EllipseAnnotationboundingBox: RectAxis-aligned ellipses. Same cloudy-border options as rectangles.
LineAnnotationstartPoint: Point, endPoint: Point, optional capsA single line segment with optional arrowheads on either end.
PolylineAnnotationpoints: List<Point>Open polyline through multiple points. Same caps options as lines.
PolygonAnnotationpoints: List<Point>Closed polygon. Point order matters, clockwise winds the fill correctly.

The geometry primitives Point and Rect live under NutrientViewer.Geometry.

Example: Placing a rectangle from a host-app button

Wire a custom toolbar button to drop a rectangle on the current page. The button uses the current annotation preset for stroke color and width, so it stays in sync with whatever the user picked in your color palette:

const instance = await NutrientViewer.load({
container: "#viewer",
document: "contract.pdf"
});
const { RectangleAnnotation } = NutrientViewer.Annotations;
const { Rect } = NutrientViewer.Geometry;
document.getElementById("add-rectangle").onclick = async () => {
const rectangle = new RectangleAnnotation({
pageIndex: instance.viewState.currentPageIndex,
boundingBox: new Rect({ left: 100, top: 100, width: 200, height: 100 }),
strokeColor: NutrientViewer.Color.fromHex("#1976D2"),
strokeWidth: 2
});
await instance.create(rectangle);
};

Example: Programmatic polygon construction

Build a polygon from a coordinate list, useful for importing precomputed regions from a server or for building shape overlays from analytical data:

const { PolygonAnnotation } = NutrientViewer.Annotations;
const { Point } = NutrientViewer.Geometry;
const { List } = NutrientViewer.Immutable;
async function createRegion(pageIndex, coordinates) {
const polygon = new PolygonAnnotation({
pageIndex,
points: List(coordinates.map(([x, y]) => new Point({ x, y }))),
strokeColor: NutrientViewer.Color.fromHex("#FF9800"),
fillColor: NutrientViewer.Color.fromHex("#FFE0B2"),
strokeWidth: 1.5
});
await instance.create(polygon);
}
await createRegion(0, [
[50, 50],
[200, 50],
[200, 200],
[50, 200]
]);

Polygon point order matters. List points clockwise so the fill renders inside the shape rather than outside.

Example: A line with arrowheads

Lines and polylines share the same cap options. Use the cap enums on NutrientViewer.LineCap to add arrowheads to either end. The lineCaps object has start and end fields, and both are optional, so omit either side to leave it uncapped:

const { LineAnnotation } = NutrientViewer.Annotations;
const { Point } = NutrientViewer.Geometry;
const arrow = new LineAnnotation({
pageIndex: 0,
startPoint: new Point({ x: 100, y: 100 }),
endPoint: new Point({ x: 300, y: 200 }),
strokeColor: NutrientViewer.Color.fromHex("#D32F2F"),
strokeWidth: 2,
lineCaps: {
// Omit `start` to leave that end uncapped.
end: NutrientViewer.LineCap.openArrow
}
});
await instance.create(arrow);

Available cap values on NutrientViewer.LineCap include square, circle, diamond, openArrow, closedArrow, butt, rOpenArrow, rClosedArrow, and slash — all camelCase.

Setting presets for shape modes

Set the active preset for a shape mode before the user enters drawing mode. That way, the next shape they draw uses your custom defaults. Use the callback form of setAnnotationPresets so you merge into the existing preset map instead of replacing every other preset:

instance.setAnnotationPresets((presets) => ({
...presets,
rectangle: {
...presets.rectangle,
strokeColor: NutrientViewer.Color.fromHex("#1976D2"),
strokeWidth: 2
},
ellipse: {
...presets.ellipse,
strokeColor: NutrientViewer.Color.fromHex("#388E3C"),
fillColor: NutrientViewer.Color.fromHex("#C8E6C9"),
strokeWidth: 2
}
}));

Passing a plain object (instance.setAnnotationPresets({ rectangle: ..., ellipse: ... })) replaces the entire preset map, dropping every other preset the SDK ships with. Always prefer the callback form when you want to update individual presets.

To switch between shape modes from a custom palette, set the interaction mode and the current preset together:

function selectShapeMode(name) {
instance.setCurrentAnnotationPreset(name);
instance.setViewState((v) =>
v.set("interactionMode", NutrientViewer.InteractionMode.SHAPE_RECTANGLE)
);
}