A reviewer opens the same contract in your Delphi viewer and in Adobe Acrobat. Acrobat's comment pane lists fourteen items; your review panel shows eleven. Nothing is wrong with your loop. The missing three are two replies — complete annotation objects linked to their parents through in-reply-to references — and one popup window that belongs to a sticky note you already counted once. PDF annotations are not a flat list of coloured rectangles: ISO 32000-1 §12.5 defines a web of dictionaries with subtypes, flags, appearance streams, and parent-child relations, and a review panel that ignores those relations will keep disagreeing with every other viewer the customer owns. This walkthrough builds an annotation review workflow on PDFium Component, the PDFium-based VCL/LCL component for Delphi, C++Builder, and Lazarus, around the points where real reviewed documents push back.
UK teams should align this pdfium component annotation review delphi workflow with local governance, audit, and data quality requirements before production release
Why your count never matches Acrobat's comment pane
Acrobat presents a curated view: markup annotations grouped into reply threads with popups folded into their parents. The raw annotation array on each page contains both more and less than that view suggests.
- Popup annotations are separate objects attached to a parent note — counting them doubles every sticky note.
- Replies are full Text annotations that reference a parent; filtering on visible marks alone silently drops the discussion thread.
- The Hidden and NoView flags remove an annotation from display, not from the array, so flag checks belong in the indexing pass.
- Link annotations live in the same array, and no reviewer considers a hyperlink to be a comment.
Settle the counting rule before writing any code and write it into the spec, because 'why does your panel show a different number than Acrobat' is the first support ticket a review feature generates.
Index everything once, then never re-parse a page
Filtering by author, type, or page must not trigger a fresh parse of page objects — on a 300-page document with heavy markup that turns every dropdown change into seconds of latency. The component exposes AnnotationCount and the indexed Annotation[] property against the currently loaded page, and the returned TPdfAnnotation record carries everything a list view needs: Subtype, Flags, Color, Rectangle, ContentsText, and AuthorText. Build the index in one sweep at open time:
procedure TReviewPanel.BuildIndex;
var
PageNo, i: Integer;
A: TPdfAnnotation;
begin
FItems.Clear;
for PageNo := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := PageNo;
for i := 0 to Pdf.AnnotationCount - 1 do
begin
A := Pdf.Annotation[i];
// Keep reviewer-relevant subtypes only; record the page and
// index pair because all later edits are addressed by it
if A.Subtype in [anText, anHighlight, anInk] then
FItems.Add(TReviewItem.Create(PageNo, i,
A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
end;
end;
end;
The pair worth underlining is (PageNo, i). Every mutation call later — recolour, delete — is addressed by page number plus annotation index, and indices shift when an annotation is removed. Plan to rebuild the affected page's entries after any deletion instead of patching indices in place; the rebuild costs milliseconds, whilst a stale index deletes the wrong reviewer's comment.
Reply threading deserves a slot in the index design even if the first release only counts replies instead of displaying them. Group items by their parent reference at build time so the panel can later fold a thread the way Acrobat does — reconstructing the grouping lazily during scrolling re-opens pages you already paid to parse. The same one-pass thinking applies to geometry: the Rectangle in each record is expressed in page space, so convert it to view coordinates in exactly one shared helper. Review panels accumulate coordinate bugs when selection, hit-testing, and painting each carry their own zoom-and-rotation arithmetic; a single conversion path keeps a highlight, its list entry, and its click target pointing at the same ink.
Recolouring markup and the appearance-stream veto
Changing a highlight from yellow to amber sounds like a one-liner, and sometimes it is. The complication is ISO 32000-1 §12.5.5: when an annotation carries an /AP appearance stream, conforming viewers render that pre-built stream, and the colour entry in the annotation dictionary becomes decoration. Because Acrobat writes appearance streams for essentially everything it creates, most annotations arriving from customers are in exactly this state. Recolouring is a read-modify-write through the Annotation[] property — and the component reports an engine veto honestly by raising EPdfError:
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF; // amber
A.ColorAlpha := 160;
try
Pdf.Annotation[Item.Index] := A;
except
on EPdfError do
begin
// The annotation owns a pre-rendered /AP stream; the dictionary
// color alone cannot change what viewers paint
Item.AppearanceLocked := True;
StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
end;
end;
Handle that exception every time. Skipping the guard means your panel shows the new colour in its own list whilst the page keeps painting the old one, and the discrepancy surfaces weeks later as a 'your viewer ignores my edits' report. When the appearance is locked, the honest options are to recolour your selection overlay instead of the annotation, or to flag the item as appearance-locked in the UI.
Deleting annotations without leaving ghosts
DeleteAnnotation detaches the object from the current page, but it does not rebuild the cached page appearance — paint immediately after the call and the deleted highlight is still visible. Re-render the page raster as part of the same operation:
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index); // raises EPdfError on failure
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
PaintPageBitmap(Bmp);
finally
Bmp.Free; // RenderPage hands bitmap ownership to the caller
end;
RebuildPageEntries(Item.PageNo); // indices after Item.Index shifted
Note the reAnnotations option in the render call: without it the raster excludes all remaining annotations, which looks like a mass deletion to the user. And note the Bmp.Free — the function-style RenderPage overload transfers bitmap ownership to the caller, so skipping the free leaks a full page raster on every delete.
Adding reviewer marks from your own UI
Creating annotations goes through CreateAnnotation, which takes a filled TPdfAnnotation record — subtype, rectangle, colour, contents, author — and adds it to the current page. Sticky notes (anText) are the simple case — position, contents, author, done. Ink annotations are the trap: the record's rectangle only bounds the drawing, and the actual strokes are arrays of points that must be attached separately through the engine's ink-stroke call (FPDFAnnot_AddInkStroke with FS_POINTF data), captured from mouse or pen input stroke by stroke. Creating an ink annotation from a rectangle alone produces an empty scribble that renders as nothing.
Decide the authorship policy at the same time: every mark your UI creates should carry a consistent AuthorText, because downstream filtering by reviewer is only as reliable as the names you write today.
Getting the review out of the viewer
Review data earns its keep when it leaves the viewer: a summary the project lead reads without opening the file, or a CSV that feeds a tracking sheet. Export from the index, not from a re-parse, and keep references stable — page number plus the annotation rectangle survives round-trips better than an array index that the next deletion invalidates.
A defensible export row carries page, subtype, author, creation info where present, contents text, and your own status column. For documents arriving from outside the team, it pays to run the same indexing pass during intake triage; the PDF intake workbench article shows that pattern, and form-field navigation covers the companion problem of reviewing documents that collect data instead of comments.
FAQ
Why does a highlight I recoloured still show its old colour?
The annotation almost certainly carries an /AP appearance stream, which conforming viewers paint in preference to the dictionary colour (ISO 32000-1 §12.5.5). Writing the record back through Annotation[] raises EPdfError in that case — treat the exception as the source of truth, not the colour you intended to set.
Why does the page still show an annotation I removed?
DeleteAnnotation updates the document model, not the cached raster. Re-render the page with RenderPage after a successful removal, and rebuild your index entries for that page because annotation indices shift downward after the removed slot.
Do flattened annotations appear in the annotation array?
No. Flattening converts annotation appearances into ordinary page content, so they stop being annotation objects at all. If a customer file shows visible markup but AnnotationCount is zero, flattening upstream is the usual explanation — there is nothing left to review programmatically.
The annotation API surface used in this article — enumeration, creation, recolouring, removal, and the render options that keep the display honest — ships with PDFium Component for Delphi, C++Builder, and Lazarus/FPC.