Skip to main content

Annotations

An annotation is an object that sits on top of a page rather than in its content: a sticky note, a link, a highlight, a stamp, an attached file. A reader draws it, and the person reading can often open, hide, move or print it separately from the page. Use an annotation when the mark is a comment on the page or something to click. When the mark is part of the page itself, draw it with XGraphics instead.

Every annotation type is in the PdfPinata.Pdf.Annotations namespace of the core PdfPinata package. You add one to a page with page.Annotations.Add(annotation).

Place an annotation on the page

An annotation's position is in PDF's own page coordinates: points measured up from the bottom-left corner of the page. XGraphics draws in coordinates measured down from the top left. To convert, use the graphics object's transformer:

  • gfx.Transformer.WorldToDefaultPage(XRect) returns the rectangle in page coordinates. Wrap it in a PdfRectangle to set Rectangle.
  • gfx.Transformer.WorldToDefaultPage(XPoint) converts a single point. Use it for the two ends of a line annotation.

The conversion also applies any transform you have set on the XGraphics, so the annotation lands on what you drew at the same coordinates.

Notes

A PdfTextAnnotation is a sticky note. The reader draws its icon and shows Contents in a pop-up. Title is the label in the pop-up's title bar, usually the author's name.

src/SampleApp/Demos/AnnotationsDemo.cs
var opened = new PdfTextAnnotation();
page.Annotations.Add(opened);
opened.Icon = PdfTextAnnotationIcon.Comment;
opened.Open = true;
opened.Color = XColors.CornflowerBlue;
opened.Opacity = 0.85;
opened.Title = "Open on arrival";
opened.Contents = "Open = true, so a reader shows the popup without being asked. "
+ "Color tints the note and Opacity applies to the whole annotation.";
opened.Rectangle = new PdfRectangle(
gfx.Transformer.WorldToDefaultPage(new XRect(56, 510, 20, 20)));

Icon takes a PdfTextAnnotationIcon: Comment, Help, Insert, Key, NewParagraph, Note or Paragraph. Open = true shows the pop-up when the document opens. Color tints the note, and Opacity applies to the whole annotation.

XGraphics has one method per kind of link. Each takes a rectangle in drawing coordinates and converts it for you:

src/SampleApp/Demos/AnnotationsDemo.cs
Link("The PdfPinata repository",
"gfx.AddWebLink(rect, url) - a URI action. PDFKit calls this link().",
rect => secondGfx.AddWebLink(rect, "https://github.com/PinataLabs/PdfPinata"));

Link("Jump to the parity table on page three",
"gfx.AddDocumentLink(rect, 3) - a GoTo by one-based page number.",
rect => secondGfx.AddDocumentLink(rect, 3));

Link("Back to the text markup on page one",
"gfx.AddNamedLink(rect, \"markup\") against gfx.AddNamedDestination on page one - "
+ "PDFKit's goTo().",
rect => secondGfx.AddNamedLink(rect, "markup"));
  • AddWebLink(rect, url) opens a web address.
  • AddDocumentLink(rect, pageNumber) goes to a page of the same document. The page number starts at 1.
  • AddNamedLink(rect, name) goes to a named destination. Create the destination with gfx.AddNamedDestination(name, point) on the page it belongs to.

Each method returns the PdfLinkAnnotation, so you can set Contents on it afterwards. PdfPage has the same methods, taking a PdfRectangle in page coordinates, plus AddFileLink for a file on disk. PdfPage.AddDocumentLink(rect, pageNumber, destinationTop) also says how far down the target page to land. A link is only a clickable area: draw the text and any underline yourself.

Highlight, underline and strike out

Four classes mark a run of text: PdfHighlightAnnotation, PdfUnderlineAnnotation, PdfStrikeOutAnnotation and PdfSquigglyAnnotation. Each covers one or more quadrilaterals that you add with AddQuad, in page coordinates. The demo measures the run of text with MeasureString, converts the box, and adds it:

src/SampleApp/Demos/AnnotationsDemo.cs
// The annotation has to be on the page before a quad is added: AddQuad builds the
// /QuadPoints array against the document that owns it, and rebuilds the appearance.
T Mark<T>(T annotation, string line, string run, XPoint baseline)
where T : PdfTextMarkupAnnotation
{
page.Annotations.Add(annotation);
annotation.AddQuad(gfx.Transformer.WorldToDefaultPage(RunOf(line, run, baseline, body)));
return annotation;
}

Text that wraps onto a second line needs one quadrilateral per line. Add them all to the same annotation; the annotation's rectangle grows to enclose them.

src/SampleApp/Demos/AnnotationsDemo.cs
var wrapped = new PdfHighlightAnnotation();
page.Annotations.Add(wrapped);
wrapped.Color = XColors.Gold;
wrapped.Opacity = 0.55;
wrapped.Title = "PdfPinata";
wrapped.Contents = "Two quads, one annotation.";
wrapped.AddQuad(gfx.Transformer.WorldToDefaultPage(
RunOf(First, "past the end of a line is one annotation with", firstAt, body)));
wrapped.AddQuad(gfx.Transformer.WorldToDefaultPage(
RunOf(Second, "two quadrilaterals in it", secondAt, body)));

Color and Opacity belong to the annotation, not to the page, so you can mark the same text twice. A highlight is drawn in the Multiply blend mode, so the text shows through the colour. If you add no quadrilaterals, the annotation marks its Rectangle. ClearQuads removes them all.

Shapes, lines and free text

Readers draw /Square, /Circle, /Line and /FreeText annotations only from an appearance stream, so PdfPinata draws that stream for you and redraws it when you change a property. None of the demos shows these four, so here is a short example:

// A rectangle with a red border and a pale fill.
PdfSquareAnnotation box = new PdfSquareAnnotation(document)
{
Rectangle = new PdfRectangle(gfx.Transformer.WorldToDefaultPage(new XRect(56, 600, 200, 80))),
Color = XColors.Firebrick,
Interior = XColors.MistyRose,
BorderWidth = 2,
Contents = "Check these figures",
};
page.Annotations.Add(box);

// An arrow between two points.
PdfLineAnnotation arrow = new PdfLineAnnotation(document);
arrow.SetLine(
gfx.Transformer.WorldToDefaultPage(new XPoint(300, 700)),
gfx.Transformer.WorldToDefaultPage(new XPoint(420, 640)));
arrow.EndEnding = PdfLineEnding.ClosedArrow;
arrow.Interior = XColors.Black;
page.Annotations.Add(arrow);

// Text shown on the page itself rather than in a pop-up.
PdfFreeTextAnnotation label = new PdfFreeTextAnnotation(document)
{
Rectangle = new PdfRectangle(gfx.Transformer.WorldToDefaultPage(new XRect(56, 720, 220, 40))),
Font = new XFont("Arial", 10), // any family your font resolver serves
TextColor = XColors.DarkBlue,
Contents = "Figures from the March survey.",
};
page.Annotations.Add(label);
  • PdfSquareAnnotation and PdfCircleAnnotation fill their Rectangle. Color is the border, Interior the fill and BorderWidth the border width in points. A circle is an ellipse that fits the rectangle.
  • PdfLineAnnotation goes from Start to End. SetLine moves both at once. StartEnding and EndEnding take a PdfLineEnding, such as OpenArrow, ClosedArrow, Circle or Diamond, and Interior fills them.
  • PdfFreeTextAnnotation draws Contents in Font and TextColor, wrapped to its rectangle. Alignment takes an XParagraphAlignment (from PdfPinata.Drawing.Layout). Color is the background, and the border uses TextColor.

Stamps

A PdfRubberStampAnnotation shows one of 15 standard stamps, such as Draft, Approved, Confidential or Final. The reader draws the stamp.

src/SampleApp/Demos/AnnotationsDemo.cs
for (var index = 0; index < stamps.Length; index++)
{
double x = 56 + index * 120;

var stamp = new PdfRubberStampAnnotation(document)
{
Icon = stamps[index],
Title = "PdfPinata",
Contents = stamps[index] + " stamp",
Rectangle = new PdfRectangle(
secondGfx.Transformer.WorldToDefaultPage(new XRect(x, 450, 104, 34)))
};
second.Annotations.Add(stamp);

secondGfx.DrawString(stamps[index].ToString(), noteFont, XBrushes.Black,
new XPoint(x, 498));
}

For a stamp with your own artwork, draw it on an XForm with XGraphics.FromForm and pass the form to SetAppearance.

File attachments

A PdfFileAttachmentAnnotation carries a file inside the PDF and shows an icon the reader can open it from. PdfEmbeddedFile holds the bytes, PdfFileSpecification names them, and the annotation points at the specification. Both types are in PdfPinata.Pdf.Advanced.

src/SampleApp/Demos/AnnotationsDemo.cs
var payload = Encoding.UTF8.GetBytes(
"This file is carried inside the PDF, as the /EF stream of a file specification.\r\n"
+ "Open the paperclip on the page to save it out again.\r\n");

var embedded = new PdfEmbeddedFile(document, payload)
{
MimeType = "text/plain"
};

var specification =
new PdfFileSpecification(document, "readme.txt", embedded);

var attachment = new PdfFileAttachmentAnnotation(document)
{
File = specification,
Icon = PdfFileAttachmentAnnotation.IconType.Paperclip,
Title = "PdfPinata",
Contents = "readme.txt, carried inside this document.",
Rectangle = new PdfRectangle(
secondGfx.Transformer.WorldToDefaultPage(new XRect(56, 350, 18, 18)))
};
second.Annotations.Add(attachment);

Icon takes Graph, PushPin, Paperclip or Tag. The constructor sets PdfAnnotationFlags.Locked, so a reader will not let the person drag the icon away.

Other annotation types

PdfGenericAnnotation takes any subtype name, for example new PdfGenericAnnotation(document, "/Polygon"). Write its entries through Elements, and give it a drawing with SetAppearance.

Flags takes PdfAnnotationFlags on any annotation: Hidden, Print, NoZoom, NoRotate, NoView, ReadOnly, Locked and others.

Things to know

  • Coordinates go up from the bottom. Every rectangle and point is in page coordinates. If an annotation appears in the wrong place, check that you converted it with gfx.Transformer.WorldToDefaultPage.
  • Some annotations are drawn by the reader. Note icons, attachment icons and standard stamps have no drawing in the file. A renderer that draws only appearance streams shows nothing for them.
  • A line sets its own rectangle. PdfLineAnnotation works out Rectangle from its ends and line endings. If you assign Rectangle, the line overwrites it.
  • An empty shape removes its drawing. A square or circle with no border and no fill, or a shape smaller than one point in either direction, has no appearance.
  • Free text needs a font resolver. A PdfFreeTextAnnotation draws text, so a font resolver must be registered before it is added to a page. See Installation.
  • Annotations read from a file are generic. In a document you opened, page.Annotations[index] returns a PdfGenericAnnotation, not the typed class. Its Subtype property says what kind it is, and Elements holds its entries.
  • Justified free text is stored as left-aligned. PDF has no code for justified text in a free text annotation. PdfPinata draws it justified, but a reader that redraws it aligns it left.
  • Annotations are appended. page.Annotations has Add and Remove, but no Insert.
  • Not everything is covered. There are no classes for polygon, polyline, ink or pop-up annotations. A line has no caption or leader lines, and free text has no callout line.

See it in action

The Annotations demo marks up text four ways, adds notes with every icon, three kinds of link, a file attachment and four stamps.

note

The demo's last page compares PdfPinata with PDFKit and lists line and free text annotations as missing. That entry is out of date: PdfLineAnnotation and PdfFreeTextAnnotation are available, as shown above.

The full Annotations demo
src/SampleApp/Demos/AnnotationsDemo.cs
const string Sans = "Liberation Sans";

var document = new PdfDocument();
document.Info.Title = "Annotations";

var titleFont = new XFont(Sans, 18, XFontStyle.Bold);
var headingFont = new XFont(Sans, 9, XFontStyle.Bold);
var body = new XFont(Sans, 11);
var noteFont = new XFont(Sans, 7.5);

// ---- Page one: text markup, and notes ----------------------------------------
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);

void Title(XGraphics on, string text)
{
on.DrawString(text, titleFont, XBrushes.Black, new XPoint(56, 68));
on.DrawLine(new XPen(XColors.SteelBlue, 1.5), 56, 78, 539, 78);
}

void Heading(XGraphics on, string text, double y)
{
on.DrawString(text.ToUpperInvariant(), headingFont, XBrushes.SteelBlue,
new XPoint(56, y));
on.DrawLine(XPens.LightGray, 56, y + 5, 539, y + 5);
}

void Note(XGraphics on, string text, double y)
{
on.DrawString(text, noteFont, XBrushes.DimGray, new XPoint(56, y));
}

Title(gfx, "Annotations");
Note(gfx, "An annotation is not page content. Nothing here is drawn by XGraphics - a reader "
+ "paints it, and can hide it.", 94);

// A markup annotation covers quadrilaterals given in default page space, so the run of
// text to be marked has to be measured and then converted out of drawing coordinates.
// The ratio below is how PdfPinata itself turns a font into a baseline offset.
double AscentOf(XFont font) => font.GetHeight() * font.CellAscent / font.CellSpace;

XRect RunOf(string line, string run, XPoint baseline, XFont font)
{
var start = line.IndexOf(run, StringComparison.Ordinal);
var before = gfx.MeasureString(line[..start], font).Width;
var width = gfx.MeasureString(run, font).Width;
var ascent = AscentOf(font);

// The box a reader would draw a selection over: from the ascender down past the
// baseline by what is left of the line.
return new XRect(baseline.X + before, baseline.Y - ascent, width,
font.GetHeight());
}

// The annotation has to be on the page before a quad is added: AddQuad builds the
// /QuadPoints array against the document that owns it, and rebuilds the appearance.
T Mark<T>(T annotation, string line, string run, XPoint baseline)
where T : PdfTextMarkupAnnotation
{
page.Annotations.Add(annotation);
annotation.AddQuad(gfx.Transformer.WorldToDefaultPage(RunOf(line, run, baseline, body)));
return annotation;
}

Heading(gfx, "Text markup", 124);

(string Line, string Run, Func<PdfTextMarkupAnnotation> Make, string Caption)[] markups =
{
("Highlight marks a run of text with a wash of colour.", "a wash of colour",
() => new PdfHighlightAnnotation(), "PdfHighlightAnnotation - PDFKit's highlight()"),
("Underline draws a line along the foot of the run.", "along the foot",
() => new PdfUnderlineAnnotation(), "PdfUnderlineAnnotation - PDFKit's underline()"),
("Strike out draws through the middle of it instead.", "through the middle",
() => new PdfStrikeOutAnnotation(), "PdfStrikeOutAnnotation - PDFKit's strike()"),
("Squiggly draws the wavy line a spell checker uses.", "the wavy line",
() => new PdfSquigglyAnnotation(), "PdfSquigglyAnnotation - PDFKit has no squiggly()")
};

double y = 154;
foreach (var each
in markups)
{
var baseline = new XPoint(56, y);
gfx.DrawString(each.Line, body, XBrushes.Black, baseline);
Mark(each.Make(), each.Line, each.Run, baseline);
Note(gfx, each.Caption, y + 11);
y += 40;
}

// Colour and opacity belong to the annotation rather than to the drawing, so the same
// text can be marked twice over without the page content knowing.
const string Twice = "One run, marked twice: green underneath and a strike over the top.";
var twiceAt = new XPoint(56, y);
gfx.DrawString(Twice, body, XBrushes.Black, twiceAt);
var green = Mark(new PdfHighlightAnnotation(), Twice, "marked twice",
twiceAt);
green.Color = XColors.LightGreen;
green.Contents = "Highlight with a colour of its own";
Mark(new PdfStrikeOutAnnotation(), Twice, "marked twice", twiceAt).Color = XColors.Crimson;
Note(gfx, "Color is the annotation's, not the page's. Opacity applies to the whole markup.",
y + 11);
y += 40;

// One annotation, two quadrilaterals. This is how a markup follows a selection that
// wraps: the quads are the lines, and /Rect becomes the box around both.
const string First = "A markup that runs past the end of a line is one annotation with";
const string Second = "two quadrilaterals in it, not two annotations.";
var firstAt = new XPoint(56, y);
var secondAt = new XPoint(56, y + 16);
gfx.DrawString(First, body, XBrushes.Black, firstAt);
gfx.DrawString(Second, body, XBrushes.Black, secondAt);

var wrapped = new PdfHighlightAnnotation();
page.Annotations.Add(wrapped);
wrapped.Color = XColors.Gold;
wrapped.Opacity = 0.55;
wrapped.Title = "PdfPinata";
wrapped.Contents = "Two quads, one annotation.";
wrapped.AddQuad(gfx.Transformer.WorldToDefaultPage(
RunOf(First, "past the end of a line is one annotation with", firstAt, body)));
wrapped.AddQuad(gfx.Transformer.WorldToDefaultPage(
RunOf(Second, "two quadrilaterals in it", secondAt, body)));
Note(gfx, "AddQuad twice. /Rect is recomputed as the box around every quad.", y + 27);

// ---- Notes -------------------------------------------------------------------
Heading(gfx, "Notes - PDFKit's note()", 424);
Note(gfx, "A note has no appearance of its own: the reader draws the icon, at whatever "
+ "size it likes.", 444);

PdfTextAnnotationIcon[] icons =
{
PdfTextAnnotationIcon.Comment,
PdfTextAnnotationIcon.Note,
PdfTextAnnotationIcon.Help,
PdfTextAnnotationIcon.Key,
PdfTextAnnotationIcon.Insert,
PdfTextAnnotationIcon.NewParagraph,
PdfTextAnnotationIcon.Paragraph
};

for (var index = 0; index < icons.Length; index++)
{
double x = 56 + index * 68;

var sticky = new PdfTextAnnotation();
page.Annotations.Add(sticky);
sticky.Icon = icons[index];
sticky.Title = "PdfPinata";
sticky.Subject = icons[index].ToString();
sticky.Contents = $"The {icons[index]} icon. Every note carries a title, a subject and "
+ "this text, which is what a reader shows in the popup.";
sticky.CreationDate = new DateTime(2026, 1, 1, 9, 0, 0, DateTimeKind.Utc);
sticky.Color = XColors.Goldenrod;
sticky.Rectangle = new PdfRectangle(
gfx.Transformer.WorldToDefaultPage(new XRect(x, 456, 20, 20)));

gfx.DrawString(icons[index].ToString(), noteFont, XBrushes.Black, new XPoint(x, 494));
}

var opened = new PdfTextAnnotation();
page.Annotations.Add(opened);
opened.Icon = PdfTextAnnotationIcon.Comment;
opened.Open = true;
opened.Color = XColors.CornflowerBlue;
opened.Opacity = 0.85;
opened.Title = "Open on arrival";
opened.Contents = "Open = true, so a reader shows the popup without being asked. "
+ "Color tints the note and Opacity applies to the whole annotation.";
opened.Rectangle = new PdfRectangle(
gfx.Transformer.WorldToDefaultPage(new XRect(56, 510, 20, 20)));
Note(gfx, "Open = true on this one - its popup should already be showing.", 544);

// The place page two links back to. A named destination is a name in the document's
// name tree, so a link can point at it without knowing a page number.
gfx.AddNamedDestination("markup", new XPoint(56, 124));

// ---- Page two: links, attachments, stamps ------------------------------------
var second = document.AddPage();
var secondGfx = XGraphics.FromPdfPage(second);

Title(secondGfx, "Links, attachments and stamps");

Heading(secondGfx, "Links - PDFKit's link() and goTo()", 116);

double linkY = 146;

void Link(string label, string caption, Action<XRect> add)
{
var size = secondGfx.MeasureString(label, body);
secondGfx.DrawString(label, body, XBrushes.MediumBlue, new XPoint(56, linkY));

// The underline is drawn by hand: a link annotation is a hot area, not a decoration.
secondGfx.DrawLine(new XPen(XColors.MediumBlue, 0.6),
56, linkY + 2, 56 + size.Width, linkY + 2);

var ascent = AscentOf(body);
add(new XRect(56, linkY - ascent, size.Width, body.GetHeight()));

Note(secondGfx, caption, linkY + 13);
linkY += 44;
}

Link("The PdfPinata repository",
"gfx.AddWebLink(rect, url) - a URI action. PDFKit calls this link().",
rect => secondGfx.AddWebLink(rect, "https://github.com/PinataLabs/PdfPinata"));

Link("Jump to the parity table on page three",
"gfx.AddDocumentLink(rect, 3) - a GoTo by one-based page number.",
rect => secondGfx.AddDocumentLink(rect, 3));

Link("Back to the text markup on page one",
"gfx.AddNamedLink(rect, \"markup\") against gfx.AddNamedDestination on page one - "
+ "PDFKit's goTo().",
rect => secondGfx.AddNamedLink(rect, "markup"));

// A link annotation is a PdfLinkAnnotation like any other, so the returned object can
// still be given the fields every annotation has.
var titledSize = secondGfx.MeasureString("A link with a tooltip", body);
secondGfx.DrawString("A link with a tooltip", body, XBrushes.MediumBlue,
new XPoint(56, linkY));
secondGfx.DrawLine(new XPen(XColors.MediumBlue, 0.6),
56, linkY + 2, 56 + titledSize.Width, linkY + 2);
var described = secondGfx.AddWebLink(
new XRect(56, linkY - AscentOf(body), titledSize.Width, body.GetHeight()),
"https://www.pdfa.org/");
described.Contents = "Shown as a tooltip while the pointer is over the link.";
Note(secondGfx, "AddWebLink returns the annotation, so /Contents can be set on it "
+ "afterwards.", linkY + 13);

// ---- An attachment -----------------------------------------------------------
Heading(secondGfx, "File attachment - PDFKit's fileAnnotation()", 330);

var payload = Encoding.UTF8.GetBytes(
"This file is carried inside the PDF, as the /EF stream of a file specification.\r\n"
+ "Open the paperclip on the page to save it out again.\r\n");

var embedded = new PdfEmbeddedFile(document, payload)
{
MimeType = "text/plain"
};

var specification =
new PdfFileSpecification(document, "readme.txt", embedded);

var attachment = new PdfFileAttachmentAnnotation(document)
{
File = specification,
Icon = PdfFileAttachmentAnnotation.IconType.Paperclip,
Title = "PdfPinata",
Contents = "readme.txt, carried inside this document.",
Rectangle = new PdfRectangle(
secondGfx.Transformer.WorldToDefaultPage(new XRect(56, 350, 18, 18)))
};
second.Annotations.Add(attachment);

Note(secondGfx, "PdfEmbeddedFile holds the bytes, PdfFileSpecification names them, and the "
+ "annotation points at it.", 384);
Note(secondGfx, "The constructor sets PdfAnnotationFlags.Locked, so a reader will not let "
+ "it be dragged off the page.", 396);
Note(secondGfx, "Like a note's, the paperclip is the reader's own drawing - so a renderer "
+ "that paints only appearance streams shows nothing above.", 408);

// ---- A rubber stamp ----------------------------------------------------------
Heading(secondGfx, "Rubber stamp - PDFKit has no equivalent", 430);

PdfRubberStampAnnotationIcon[] stamps =
{
PdfRubberStampAnnotationIcon.Draft,
PdfRubberStampAnnotationIcon.Confidential,
PdfRubberStampAnnotationIcon.ForComment,
PdfRubberStampAnnotationIcon.Final
};

for (var index = 0; index < stamps.Length; index++)
{
double x = 56 + index * 120;

var stamp = new PdfRubberStampAnnotation(document)
{
Icon = stamps[index],
Title = "PdfPinata",
Contents = stamps[index] + " stamp",
Rectangle = new PdfRectangle(
secondGfx.Transformer.WorldToDefaultPage(new XRect(x, 450, 104, 34)))
};
second.Annotations.Add(stamp);

secondGfx.DrawString(stamps[index].ToString(), noteFont, XBrushes.Black,
new XPoint(x, 498));
}

Note(secondGfx, "Fifteen standard names, drawn by the reader. A stamp with artwork of its "
+ "own would need an appearance stream.", 520);

// ---- Page three: what PDFKit has that this does not ---------------------------
var third = document.AddPage();
var thirdGfx = XGraphics.FromPdfPage(third);

Title(thirdGfx, "Parity with PDFKit's annotations");
Note(thirdGfx, "pdfkit.org/docs/annotations.html documents eleven methods. Nine of them "
+ "have something here; two do not.", 94);

var mono = new XFont("Source Code Pro", 8);

(string PdfKit, string Here)[] parity =
[
("note(x, y, w, h, contents)", "PdfTextAnnotation"),
("link(x, y, w, h, url)", "gfx.AddWebLink / PdfLinkAnnotation.CreateWebLink"),
("goTo(x, y, w, h, name)", "gfx.AddNamedLink / gfx.AddDocumentLink"),
("highlight(x, y, w, h)", "PdfHighlightAnnotation"),
("underline(x, y, w, h)", "PdfUnderlineAnnotation"),
("strike(x, y, w, h)", "PdfStrikeOutAnnotation"),
("fileAnnotation(x, y, w, h, file)", "PdfFileAttachmentAnnotation"),
("lineAnnotation(x1, y1, x2, y2)", "MISSING - no /Line annotation"),
("rectAnnotation(x, y, w, h)", "PdfSquareAnnotation"),
("ellipseAnnotation(x, y, w, h)", "PdfCircleAnnotation"),
("textAnnotation(x, y, w, h, text)", "MISSING - no /FreeText annotation")
];

double rowY = 132;
thirdGfx.DrawString("PDFKit", headingFont, XBrushes.SteelBlue, new XPoint(56, rowY));
thirdGfx.DrawString("PdfPinata", headingFont, XBrushes.SteelBlue, new XPoint(260, rowY));
rowY += 6;
thirdGfx.DrawLine(XPens.LightGray, 56, rowY, 539, rowY);
rowY += 18;

foreach (var row in parity)
{
var missing = row.Here.StartsWith("MISSING", StringComparison.Ordinal);
XBrush brush = missing ? XBrushes.Crimson : XBrushes.Black;

thirdGfx.DrawString(row.PdfKit, mono, XBrushes.Black, new XPoint(56, rowY));
thirdGfx.DrawString(row.Here, mono, brush, new XPoint(260, rowY));
rowY += 17;
}

rowY += 20;
Heading(thirdGfx, "And what this has that PDFKit does not", rowY);
rowY += 26;

(string What, string Why)[] extras =
[
("PdfSquigglyAnnotation", "the fourth text markup subtype"),
("PdfRubberStampAnnotation", "fifteen standard stamp names"),
("PdfAnnotation.Opacity", "/CA, applied to the whole annotation"),
("PdfAnnotation.Flags", "Hidden, Print, Locked, NoZoom and the rest"),
("PdfTextMarkupAnnotation.AddQuad", "many quads under one annotation")
];

foreach (var row in extras)
{
thirdGfx.DrawString(row.What, mono, XBrushes.Black, new XPoint(56, rowY));
thirdGfx.DrawString(row.Why, noteFont, XBrushes.DimGray, new XPoint(260, rowY));
rowY += 17;
}

rowY += 22;
thirdGfx.DrawString(
"The two missing subtypes are appearance-bearing: a viewer will not draw a /Line from",
noteFont, XBrushes.DimGray, new XPoint(56, rowY));
rowY += 12;
thirdGfx.DrawString(
"its endpoints alone, so adding them means writing appearance streams the way",
noteFont, XBrushes.DimGray, new XPoint(56, rowY));
rowY += 12;
thirdGfx.DrawString(
"PdfTextMarkupAnnotation already does. Until then, draw the shape with XGraphics.",
noteFont, XBrushes.DimGray, new XPoint(56, rowY));
rowY += 18;
thirdGfx.DrawString(
"Nor can a caller supply one: PdfAnnotation is abstract with no public way to set",
noteFont, XBrushes.DimGray, new XPoint(56, rowY));
rowY += 12;
thirdGfx.DrawString(
"/Subtype, PdfGenericAnnotation is internal, and PdfAnnotations.Add takes neither -",
noteFont, XBrushes.DimGray, new XPoint(56, rowY));
rowY += 12;
thirdGfx.DrawString(
"so a subtype this library has no class for cannot be added through the typed API.",
noteFont, XBrushes.DimGray, new XPoint(56, rowY));