List Annotations
page.GetAnnotations() returns the annotations attached to a page, in the order they
appear in the page’s /Annots array. Use it to inspect a file before modifying it: to
find out whether a review comment is already there, to locate the form widgets, or to
check what a third-party tool wrote into the document.
Each element is a *model.PdfAnnotation holding the entries common to every annotation
subtype. The subtype-specific fields live in a separate struct reachable through
GetContext(), so identifying an annotation means type-switching on that value rather
than reading a subtype field.
Doing it
page, err := pdfReader.GetPage(pageNum)
if err != nil {
return err
}
annotations, err := page.GetAnnotations()
if err != nil {
return err
}
for _, annot := range annotations {
switch a := annot.GetContext().(type) {
case *model.PdfAnnotationSquare:
fmt.Printf("square, color %v\n", a.C)
case *model.PdfAnnotationText:
fmt.Printf("sticky note: %v\n", a.Contents)
}
}annot.String() is the quick alternative when you just want to look at a file. It
prints the Go type of the context followed by the raw annotation dictionary, which is
where the *model.PdfAnnotationSquare: Dict(...) lines in the output below come from.
The common entries are on the outer PdfAnnotation: Rect, Contents, AP, C,
Border, F. They are core.PdfObject values, not Go primitives, so unwrap them with
the core helpers (core.GetNumbersAsFloat, core.GetStringVal) before doing
arithmetic on them. The embedded struct means a.Rect works from the context value too.
Limitations
A page with no /Annots array returns nil, nil, and so does a page not attached to a
reader, since there is nothing to resolve and nobody to resolve it against. A nil slice is
not an error. Annotations you added yourself with AddAnnotation are returned either way,
because they go into the same cached slice the load fills.
Annotation subtypes UniPDF does not model are dropped from the result. The reader logs
Ignoring unknown annotation at debug level and omits the entry, so the returned slice
can be shorter than the /Annots array. Count the array yourself if you need the true
number.
GetContext() returns nil for an annotation whose dictionary has no /Subtype, which
some generators omit. The annotation is still returned with its common entries intact,
so a type switch over contexts needs to tolerate a nil case.
Two errors come from a malformed file rather than from your code: Annots not an array
when /Annots is some other object, and annotation not in an indirect object when an
element is neither a dictionary nor a reference to one. An /Annots entry that is the
null object is skipped without complaint.
Results are cached on the page after the first call, so repeated calls are cheap and
return the same slice. Widget annotations get linked to their form field during that
first load, which is what makes PdfAnnotationWidget usable from
the forms guides.
Run the example
The example takes one or more input files on the command line and prints a numbered
listing per page. listAnnotations does the reading; printAnnotations does the
formatting.
git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/annotations
go run pdf_list_annotations.go input.pdfIf this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.
View the full source
Sample output
Run against a document carrying a single square annotation:

-- Page 1
1. *model.PdfAnnotationSquare: Dict("Type": Annot, "Rect": [139, 450, 199, 490], "AP": Dict("N": Ref(15 0), ), "C": [0, 0, 0], )