Adding shape annotations to PDFs programmatically enables teams to automate visual markup, build diagram creation tools, and implement technical drawing workflows. Whether you’re creating architectural markups, highlighting important document regions, building callout systems for technical documentation, implementing diagram annotation tools, or creating visual review workflows, shape annotations provide precise geometric control. Shape annotations include lines with customizable endings (arrows, circles, diamonds), circles (ellipses) for highlighting regions, and squares (rectangles) for boundary marking, all with configurable colors and border widths.

How Nutrient helps you achieve this

Nutrient Python SDK handles PDF shape annotation structures and geometric rendering. With the SDK, you don’t need to worry about:

  • Parsing shape annotation dictionaries and path construction
  • Managing stroke and fill operations with appearance streams
  • Handling geometric calculations and coordinate transformations
  • Complex line ending styles and cap rendering

Instead, Nutrient provides an API that handles all the complexity behind the scenes, letting you focus on your business logic.

Complete implementation

Below is a complete working example that demonstrates adding various shape annotations to a PDF. The following lines set up the Python application. The import statements bring in all necessary classes from the Nutrient SDK:

from nutrient_sdk import Document
from nutrient_sdk import PdfEditor
from nutrient_sdk import Color
from nutrient_sdk import PdfLineEndingStyle
from nutrient_sdk import NutrientException

Working with shape annotations

The main() function defines the entry point that will contain the shape annotation creation logic. The Document.open() call opens the PDF document. The context manager(opens in a new tab) syntax ensures the document is automatically closed when you’re done, preventing resource leaks. The following code creates a PDF editor, accesses the page collection, ensures at least one page exists by adding a letter-size page (612×792 points) if the document is empty, and retrieves the annotation collection from the first page:

def main():
try:
with Document.open("input.pdf") as document:
editor = PdfEditor.edit(document)
pages = editor.get_page_collection()
if pages.get_count() == 0:
pages.add(612.0, 792.0)
page = pages.get_first()
annotations = page.get_annotation_collection()

Adding a line annotation

The following code creates a simple horizontal line annotation using the add_line() method. The first four parameters define the line’s geometry with start coordinates (50, 700) and end coordinates (200, 700), creating a 150-point horizontal line near the top of the page. The method accepts an author name and content strings for metadata. Lines are created with default black color (ARGB 255, 0, 0, 0) and 1-point line width unless explicitly customized:

line = annotations.add_line(
50.0, 700.0, 200.0, 700.0, # start_x, start_y, end_x, end_y
"Author",
"Simple horizontal line"
)

Adding a line with arrow endings

The following code creates a diagonal line annotation and customizes its appearance using property assignments. The line spans from coordinates (50, 650) to (200, 600), creating a downward-sloping line. After creation, three properties are configured: end_cap sets the ending style to a filled arrowhead using PdfLineEndingStyle.CLOSED_ARROW; color applies a blue stroke using Color.from_argb() with ARGB values (255, 0, 0, 255); and line_width increases the stroke thickness to 2 points. Line ending styles apply to either the start point (start_cap) or end point (end_cap) of the line, enabling directional indicators for callouts and pointers:

arrow = annotations.add_line(
50.0, 650.0, 200.0, 600.0, # start_x, start_y, end_x, end_y
"Reviewer",
"Arrow pointing to important area"
)
# Optionally customize the line
arrow.end_cap = PdfLineEndingStyle.CLOSED_ARROW
arrow.color = Color.from_argb(255, 0, 0, 255)
arrow.line_width = 2.0

Available line ending styles from the PdfLineEndingStyle enumeration include:

  • NONE — No ending (default)
  • SQUARE — Square ending for boundary indicators
  • CIRCLE — Circular ending for connection points
  • DIAMOND — Diamond shape for decision points
  • OPEN_ARROW — Open arrowhead for directional indicators
  • CLOSED_ARROW — Filled arrowhead for emphasis
  • BUTT — Perpendicular line for measurement marks
  • REVERSE_OPEN_ARROW — Reverse open arrow for backward direction
  • REVERSE_CLOSED_ARROW — Reverse closed arrow for backward emphasis
  • SLASH — Slash mark for termination indicators

Adding a circle annotation

The following code creates a circle annotation using the add_circle() method. The four parameters define the bounding rectangle: x-coordinate (250), y-coordinate (550), width (100), and height (100). When width and height are equal, the annotation renders as a perfect circle; different dimensions create an ellipse. The method accepts an author name and content strings for metadata. Circles are created with the default black color (ARGB 255, 0, 0, 0) and 1-point line width. The property assignments customize the appearance: color applies a green stroke using ARGB values (255, 0, 128, 0), and line_width increases the stroke thickness to 2 points. Circle annotations are useful for highlighting regions, marking areas of interest, and creating visual boundaries:

circle = annotations.add_circle(
250.0, 550.0, 100.0, 100.0, # x, y, width, height
"Editor",
"Area of interest"
)
# Optionally customize the appearance
circle.color = Color.from_argb(255, 0, 128, 0)
circle.line_width = 2.0

Adding a square annotation

The following code creates a square annotation using the add_square() method. The four parameters define the bounding rectangle: x-coordinate (400), y-coordinate (550), width (150), and height (100). When width and height are equal, the annotation renders as a perfect square; different dimensions create a rectangle. The method accepts an author name and content strings for metadata. Squares are created with the default black color (ARGB 255, 0, 0, 0) and 1-point line width. The property assignments customize the appearance: color applies a purple stroke using ARGB values (255, 128, 0, 128), and line_width increases the stroke thickness to 2 points. Square annotations are useful for marking sections, creating boundaries, and highlighting rectangular regions:

square = annotations.add_square(
400.0, 550.0, 150.0, 100.0, # x, y, width, height
"Reviewer",
"Section to review"
)
# Optionally customize the appearance
square.color = Color.from_argb(255, 128, 0, 128)
square.line_width = 2.0

Combining multiple shapes

The following code demonstrates creating a unified visual markup system using multiple shape annotations with consistent styling. The pattern creates three coordinated shapes: a directional line with an open arrowhead pointing from (50, 500) to (150, 450); an elliptical circle highlighting a region at (150, 420) with dimensions 100×60 points; and a rectangular selection box at (260, 420) with matching dimensions. All three shapes share consistent visual properties: orange color using ARGB values (255, 255, 165, 0) and 1.5-point line width. This approach creates cohesive visual markup systems where multiple shapes work together to annotate complex content, build callout structures for technical documentation, or create coordinated diagram elements:

callout_line = annotations.add_line(
50.0, 500.0, 150.0, 450.0, # start_x, start_y, end_x, end_y
"Author", "Callout line"
)
callout_line.end_cap = PdfLineEndingStyle.OPEN_ARROW
callout_line.color = Color.from_argb(255, 255, 165, 0)
callout_line.line_width = 1.5
highlight_circle = annotations.add_circle(
150.0, 420.0, 100.0, 60.0, # x, y, width, height
"Author", "Highlighted area"
)
highlight_circle.color = Color.from_argb(255, 255, 165, 0)
highlight_circle.line_width = 1.5
selection_box = annotations.add_square(
260.0, 420.0, 100.0, 60.0, # x, y, width, height
"Author", "Selection box"
)
selection_box.color = Color.from_argb(255, 255, 165, 0)
selection_box.line_width = 1.5

Saving the document

The final code block saves the document with all shape annotations and closes the editor. The try-except block handles potential errors using NutrientException:

editor.save_as("output.pdf")
editor.close()
except NutrientException as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()

Conclusion

The shape annotation workflow consists of several key operations:

  1. Open the document using a context manager(opens in a new tab) for automatic resource cleanup.
  2. Create an editor and access the page collection.
  3. Ensure at least one page exists by adding a letter-size page if needed.
  4. Retrieve the annotation collection from the target page.
  5. Create line annotations with add_line() specifying start and end coordinates.
  6. Customize line endings using start_cap and end_cap properties with PdfLineEndingStyle values.
  7. Create circle annotations with add_circle() for highlighting regions (equal dimensions create perfect circles).
  8. Create square annotations with add_square() for boundary marking (equal dimensions create perfect squares).
  9. Combine multiple shapes with consistent styling for unified visual markup systems.
  10. Save and close the editor.

Nutrient handles shape annotation dictionary structures, geometric calculations, coordinate transformations, appearance stream generation, and stroke rendering so you don’t need to understand PDF shape annotation specifications or manage path construction manually. The shape annotation system provides precise geometric control for architectural markups, technical drawing workflows, diagram annotation tools, and visual review systems.