How to Work with PDF Annotations in Python
Aspose.PDF FOSS for Python exposes every annotation on a page as a live Annotation object through the page’s AnnotationCollection, so you can add markup, link, and 3D annotations, inspect their type-specific properties, and generate the appearance streams PDF viewers need to render them — all from pure Python. The library is installed from PyPI with pip.
Step-by-Step Guide
Step 1: Install the Package
Install the Aspose.PDF FOSS package from PyPI:
pip install aspose-pdf-foss-for-pythonVerify the installation:
import aspose_pdf
print("aspose_pdf OK")Step 2: Import Required Classes
Import Document to hold the PDF, plus the annotation classes you will use to create and inspect annotations:
from aspose_pdf import Document
from aspose_pdf.annotations import (
Annotation,
AnnotationType,
AnnotationFlags,
MarkupAnnotation,
LinkAnnotation,
)Step 3: Add an Annotation to a Page
Every Page exposes its annotations through the annotations property, an AnnotationCollection. Call add(subtype, rect, contents) with a subtype name, a (left, bottom, right, top) rectangle, and the annotation’s text content. Standard markup subtypes such as "Highlight" are returned as a MarkupAnnotation instance:
from aspose_pdf import Document
document = Document()
document.pages.add()
page = document.pages[0]
annotation = page.annotations.add(
"Highlight",
(100, 700, 300, 720),
"Key clause",
)
print(type(annotation).__name__) # MarkupAnnotation
print(annotation.contents) # Key clauseStep 4: Create Annotations from Enum Subtypes
AnnotationType enumerates every standard PDF annotation subtype (PDF 32000-1:2008, Table 169), so you can pass an enum member instead of a raw string. Type-specific data — such as a polygon’s vertices — goes in the properties dict and is read back with get_property:
from aspose_pdf import Document
from aspose_pdf.annotations import AnnotationType
document = Document()
document.pages.add()
page = document.pages[0]
annotation = page.annotations.add(
AnnotationType.POLYGON,
(0, 0, 10, 10),
"",
properties={"Vertices": [0, 0, 10, 0, 5, 10]},
)
print(annotation.subtype) # Polygon
print(annotation.get_property("Vertices")) # [0, 0, 10, 0, 5, 10]Step 5: Read and Iterate Existing Annotations
AnnotationCollection is iterable, so you can walk every annotation already on a page — including ones loaded from an existing PDF — and read the common properties every Annotation exposes (subtype, contents, rect, title, author, color):
from aspose_pdf import Document
document = Document()
document.pages.add()
page = document.pages[0]
page.annotations.add("Text", (50, 50, 70, 70), "First note")
page.annotations.add("Text", (80, 80, 100, 100), "Second note")
for annotation in page.annotations:
print(annotation.subtype, "-", annotation.contents)Step 6: Generate Annotation Appearance Streams
An annotation created without an explicit appearance_normal has no visible rendering (/AP /N) until one is generated. Call generate_appearance(force) on a single Annotation, or generate_appearances(force) on the whole AnnotationCollection to synthesise every missing appearance stream on the page at once:
from aspose_pdf import Document
from aspose_pdf.engine.cos import AnnotationName
document = Document()
document.pages.add()
page = document.pages[0]
stamp = page.annotations.add(
"Stamp",
(100, 100, 260, 150),
"",
properties={"Name": AnnotationName("Approved")},
)
if stamp.generate_appearance():
print(f"Appearance stream: {len(stamp.appearance_normal)} byte(s)")
# Regenerate every missing appearance stream on the page in one call:
count = page.annotations.generate_appearances(force=True)
print(f"Regenerated {count} appearance stream(s)")Step 7: Distinguish Annotation Subclasses
AnnotationCollection.add() dispatches to a specific subclass based on the subtype: markup-style subtypes (Highlight, Square, Stamp, and similar) come back as MarkupAnnotation, and "Link" comes back as LinkAnnotation. Both inherit every method and property from Annotation, so isinstance checks let you branch on annotation kind without inspecting subtype strings directly:
from aspose_pdf import Document
from aspose_pdf.annotations import LinkAnnotation, MarkupAnnotation
document = Document()
document.pages.add()
page = document.pages[0]
link = page.annotations.add("Link", (50, 750, 200, 770), "")
highlight = page.annotations.add("Highlight", (50, 700, 200, 720), "")
print(isinstance(link, LinkAnnotation)) # True
print(isinstance(highlight, MarkupAnnotation)) # TrueCommon Issues and Fixes
get_property returns None for a property I just set
properties keys are exact, case-sensitive PDF field names ("Vertices", "Name", and similar) — a typo or wrong case is silently ignored rather than raising. Pass a default argument to get_property(name, default) and check for it explicitly while debugging a new subtype.
generate_appearance() returns False
Not every subtype and property combination can be synthesised into an appearance stream by the built-in generator. Check has_appearance before assuming the call succeeded, and supply a pre-rendered appearance_normal (bytes) directly on add() for subtypes the generator does not cover.
Unexpected annotation subclass after add()
The subtype string (or AnnotationType member) you pass determines the returned class: markup subtypes come back as MarkupAnnotation, "Link" comes back as LinkAnnotation, and any other recognised subtype comes back as the base Annotation. Use isinstance() against MarkupAnnotation/LinkAnnotation rather than assuming a specific subtype string.
AnnotationFlags values don’t seem to change rendering
AnnotationFlags (PRINT, HIDDEN, NO_ZOOM, READ_ONLY, and similar) is a standard Python IntFlag enum for composing and interpreting an annotation’s behaviour bits with the | operator — it is a value type, not a property that Annotation.add() writes automatically. Combine the flags you need and pass them through the same type-specific properties mechanism used for other subtype-specific data.
Working with a page that has no annotations yet
page.annotations is always a valid (possibly empty) AnnotationCollection — you never need to check for None before calling add(), iterating, or calling clear().
Frequently Asked Questions
Which annotation subtypes does Aspose.PDF FOSS for Python support?
AnnotationType enumerates all 25 standard PDF 32000-1:2008 Table 169 subtypes, including TEXT, LINK, FREE_TEXT, LINE, SQUARE, CIRCLE, POLYGON, HIGHLIGHT, STAMP, INK, FILE_ATTACHMENT, REDACT, and more.
Does this library support 3D annotations?
Yes. PDF3DAnnotation, together with PDF3DArtwork, PDF3DContent, PDF3DView, PDF3DLightingScheme, and PDF3DRenderMode, models a minimal PDF 3D annotation surface (a rectangle, embedded artwork, and named views) for prerelease PDF 3D workflows. See the PDF3DAnnotation reference for its full property set.
How do I remove an annotation from a page?
Call page.annotations.delete(index) to remove one annotation by position, or page.annotations.clear() to remove every annotation on the page.
Can I insert an annotation at a specific position instead of appending it?
Yes — AnnotationCollection.insert(index, subtype, rect, contents, title, appearance_normal, properties) takes the same arguments as add() plus the target index.
How is an annotation’s colour represented?
The color property on Annotation (and its subclasses) is a tuple[float, ...] matching the PDF /C entry’s component count (empty when the colour is unset) — not a dedicated colour object.