EmbedPDF

Shapes & drawing

Shapes are drawings on top of the page: rectangles, ellipses, lines and arrows, polylines, polygons and freehand ink. You give the geometry in points, and the engine draws the shape in the style you choose.

await page.annotations.create({
  subtype: 'square',
  box: { x: 72, y: 92, width: 200, height: 100 },
  color: '#dc143c',
  strokeWidth: 2,
});

Every shape takes the style fields: color, opacity, strokeWidth, borderStyle, dashArray, and interiorColor where the shape has an inside. Left out, a shape is a solid red line, 1 point wide, with no fill.

Rectangles and ellipses#

square draws a rectangle and circle an ellipse, inside box:

await page.annotations.create({
  subtype: 'circle',
  box: { x: 300, y: 112, width: 120, height: 80 },
  interiorColor: '#ffeb96',
});
FieldWhat it isDefault
boxThe box the shape fills. Required.
interiorColorThe fill, or null for none.none
cloudyIntensityA cloudy border instead of a straight one; see Cloudy borders.none
rotationTurns the shape, in degrees clockwise; see Turned shapes.none

The line is drawn inside box, so a thick border doesn’t make the shape bigger. A read also has rect: where the shape sits on the page, the upright box around all it draws. The engine works it out; see The shape’s rect.

Lines and arrows#

A line goes from one point to another, with an optional ending at each end:

await page.annotations.create({
  subtype: 'line',
  linePoints: { start: { x: 100, y: 392 }, end: { x: 320, y: 292 } },
  lineEndings: { start: 'none', end: 'closed-arrow' },
  strokeWidth: 2,
});
FieldWhat it isDefault
linePoints{ start, end }, two points { x, y }. Required.
lineEndings{ start, end }: what’s drawn at each end; see Line endings.{ start: 'none', end: 'none' }
interiorColorThe fill of closed endings, such as a solid arrowhead.none
intent'line-arrow' labels the line as an arrow. It changes nothing in the drawing.none
rotationTurns the line, in degrees clockwise; see Turned shapes.none

You don’t pass a rect for a line, a polyline, a polygon or ink: the engine works it out from what it draws (the points, the line width, the endings) and keeps it up to date when they change; see The shape’s rect.

A line with intent: 'line-dimension' and a measure is a distance measurement; see Measurements.

Polylines and polygons#

Both take a list of vertices. A polygon is closed and can be filled; a polyline stays open and can have endings, like a line:

await page.annotations.create({
  subtype: 'polygon',
  vertices: [
    { x: 70, y: 332 },
    { x: 170, y: 332 },
    { x: 120, y: 252 },
  ],
  interiorColor: '#c8e6ff',
});
 
await page.annotations.create({
  subtype: 'polyline',
  vertices: [
    { x: 230, y: 332 },
    { x: 290, y: 252 },
    { x: 350, y: 332 },
  ],
  lineEndings: { start: 'none', end: 'open-arrow' },
});
FieldWhat it isDefault
verticesThe points, in order. Required: at least three for a polygon, two for a polyline.
interiorColorA polygon’s fill. On a polyline, the fill of closed endings.none
lineEndingsPolyline only: what’s drawn at the first and the last point.{ start: 'none', end: 'none' }
cloudyIntensityPolygon only: a cloudy border.none
intentPolygon only: 'polygon-cloud' labels it as a cloud. It changes nothing in the drawing.none
rotationTurns the shape, in degrees clockwise; see Turned shapes.none

A polygon’s last point joins its first by itself. Fewer points than a shape needs fail with InvalidArg. Polylines and polygons can also be perimeter and area measurements; see Measurements.

Freehand drawing#

An ink annotation is what a pen draws: one or more strokes, each a list of points.

await page.annotations.create({
  subtype: 'ink',
  inkList: [
    [
      { x: 70, y: 712 },
      { x: 120, y: 612 },
      { x: 180, y: 702 },
    ],
    [
      { x: 200, y: 692 },
      { x: 260, y: 632 },
    ],
  ],
  strokeWidth: 3,
});
FieldWhat it isDefault
inkListThe strokes: one list of points per movement of the pen. Required.
strokeWidthThe pen’s width in points, more than 0: ink has no fill to show otherwise.1
intent'ink-highlight' labels it as a freehand highlighter.none
rotationTurns the drawing, in degrees clockwise; see Turned shapes.none

Ink has no fill, and its strokes have round ends. An inkList without strokes, or with a stroke without points, fails with InvalidArg. For a highlighter pen, combine intent: 'ink-highlight' with a wide strokeWidth, a light color and blendMode: 'multiply', so the text under it stays readable.

Line endings#

lineEndings on lines and polylines, and lineEnding on callouts, take these values:

ValueWhat’s drawn at the end
'none'Nothing.
'open-arrow'An arrowhead of two lines, pointing outward.
'closed-arrow'A triangle arrowhead, pointing outward.
'r-open-arrow'An open arrowhead, pointing back along the line.
'r-closed-arrow'A triangle arrowhead, pointing back.
'square'A square.
'circle'A circle.
'diamond'A diamond.
'butt'A short line across the end.
'slash'A short slanted line across the end.

The closed ones ('closed-arrow', 'r-closed-arrow', 'square', 'circle', 'diamond') are filled with interiorColor when you set one, and outlined when you don’t. Endings grow with the line: an arrowhead is 9 times the strokeWidth long.

Dashed lines#

Set borderStyle: 'dashed', and the pattern in dashArray if you want your own: lengths in points, of line then gap.

await page.annotations.update(shape.ref, { borderStyle: 'dashed', dashArray: [6, 3] });

[6, 3] draws 6 points of line, then 3 points of gap. Without a dashArray, dashes are 3 points on, 3 off. A dashArray on a 'solid' border is kept, and draws when the border becomes dashed.

Cloudy borders#

Squares, circles and polygons can have a cloudy border instead of a straight one, the bumpy outline reviewers draw around a part of a drawing. cloudyIntensity sets how big the bumps are, usually 1 or 2; null makes the border straight again.

await page.annotations.create({
  subtype: 'square',
  box: { x: 100, y: 412, width: 120, height: 80 },
  cloudyIntensity: 2,
  strokeWidth: 2,
});

The bumps stick out past the box. The engine makes room for them in rect, and box stays the shape itself.

Permissions#

Reading shapes needs doc.annotate.read. Creating, changing and deleting them needs the same permissions as any annotation; see Permissions.

Good to know#

Turned shapes#

A rectangle or an ellipse can be turned by any angle. Give the shape’s own box, before turning, as box, and the angle as rotation, in degrees clockwise (as a page turns, and as CSS rotate() turns):

await page.annotations.create({
  subtype: 'square',
  box: { x: 100, y: 412, width: 160, height: 80 },
  rotation: 30,
});

The shape turns about the middle of its box. The engine sets rect to the upright box around the turned shape, which is what a PDF app uses to find it. To move or resize a turned shape, change its box; to straighten it where it is, set rotation: null. Free text boxes, carets and stamps turn the same way.

At a quarter turn (90, 180 or 270), you can create a shape by where it stands instead: its rect and rotation. The engine works out the box, which is the rect with its sides swapped under 90 and 270:

// A 160 × 40 box, standing 40 wide and 160 tall
await page.annotations.create({
  subtype: 'square',
  rect: { x: 100, y: 100, width: 40, height: 160 },
  rotation: 90,
});

At any other angle, give the box: many boxes stand in the same rect, so a create by rect fails with InvalidArg. A create with both takes the box, as when you pass a read back.

Lines, polylines, polygons and ink turn the same way too. Give their points upright, and the angle as rotation; they turn about the middle of the box around the points:

await page.annotations.create({
  subtype: 'polygon',
  vertices: [
    { x: 100, y: 492 },
    { x: 260, y: 492 },
    { x: 180, y: 412 },
  ],
  rotation: 45,
});

A read gives the points back upright, with the turn. drawnPointsOf(annotation) gives them as the page shows them: one list for a line or a polygon, one per ink stroke. To move a turned shape, move its points; to straighten it where it is, set rotation: null. A measurement’s caption turns with its shape.

import { drawnPointsOf } from '@embedpdf/engine';
 
const [shown] = drawnPointsOf(polygon)!; // the corners where the page shows them

The shape’s rect#

rect is where the shape sits on the page: the upright box around all it draws, turned, with what its border adds (a cloud’s bumps) and, for lines, the line width and the endings. The engine works it out from box or the points, rotation and the style. The rect you read, sent back, changes nothing, so a read passed straight back works. A different rect moves or stretches the shape into it, as in Acrobat; see Move or resize by rect. You can also change the box or the points yourself.

Border styles#

A shape from another PDF app can read 'beveled' or 'inset'. Only form widgets draw those, so the engine draws such a shape with a solid line and keeps the style when you send it back unchanged; a write that sets one fails with InvalidArg. A cloudy border is never dashed.

How it’s stored#

For readers who know PDF: linePoints is /L, vertices is /Vertices, inkList is /InkList, lineEndings is /LE, interiorColor is /IC and cloudyIntensity is the intensity of a /BE cloudy border effect. For a cloudy shape the PDF’s own /Rect takes in the bumps and /RD says where the shape is inside it, as Acrobat writes it; the engine writes both from box. A turned shape keeps its box in /EMBD_Metadata, and its drawing is turned inside its /Rect, so every PDF app shows it turned.

A turned line, polyline, polygon or ink keeps its points turned in the file, as every PDF app reads them, and its turn and the box around its upright points in /EMBD_Metadata. When another app moves or redraws the points, the two no longer match: a read then gives the points as drawn, with rotation: null.

Was this page helpful?

Your feedback goes directly to the documentation team.