Die meisten Antworten auf diese Frage sind fünf Jahre alt, nur für PDF und beginnen damit, dass Sie Koordinaten gegen eine Seite messen sollen, die Sie nicht sehen können. Hier ist die Version 2026: sieben Aufrufe, die beiden Konventionen, die bestimmen, wo die Markierungen tatsächlich landen, und die eine Zeile, die ein Linux‑Container benötigt.

Alles unten läuft auf .NET 8 und .NET 10, unter Windows und Linux, ohne Adobe Acrobat und ohne Microsoft Office installiert — und derselbe Code funktioniert für DOCX, XLSX, PPTX und Bilder. Nur der Dateipfad ändert sich.

Die ausführbare Version befindet sich auf GitHub: annotate-pdf-csharp-net.

Installation

dotnet add package GroupDocs.Annotation

Ohne Lizenz läuft die Bibliothek im Evaluierungsmodus und die Ausgabe enthält Evaluierungsmarken. Eine temporäre Lizenz entfernt sie:

new License().SetLicense("GroupDocs.Annotation.lic");

Dokument öffnen

using (Annotator annotator = new Annotator("quarterly-review.pdf"))
{
    // add, read or remove markup here
}

Eine Sache, die Sie von vornherein wissen sollten: Der Annotator hält die Datei geöffnet, bis er verworfen wird, und er kann nicht über die Datei schreiben, die er gerade liest. Speichern Sie in einen anderen Pfad oder in eine temporäre Datei, die Sie anschließend an den Zielort verschieben.

Koordinaten nicht messen — nach dem Text suchen

Dies ist der Teil, den die meisten Tutorials überspringen, und er ist derjenige, der den Annotationscode die nächste Dokumentversion überleben lässt. GetDocumentInfo() liefert jede Seite mit ihrer Größe und ihren Textzeilen, jeweils mit einem Rechteck:

IDocumentInfo info = annotator.Document.GetDocumentInfo();

foreach (PageInfo page in info.PagesInfo)
{
    foreach (TextLineInfo line in page.TextLines)
    {
        if (line.Text.Contains("Hosting costs"))
        {
            // line.LeftIndent, line.TopIndent, line.Width, line.Height — points, from the top-left
        }
    }
}

Jetzt kann die Anmerkung dort platziert werden, wo das Dokument heute steht, anstatt dort, wo es letztes Jahr stand.

Ein Highlight mit Kommentar und Antwort hinzufügen

float left   = (float)line.LeftIndent;
float right  = (float)(line.LeftIndent + line.Width);
float top    = (float)(page.Height - line.TopIndent);
float bottom = (float)(page.Height - line.TopIndent - line.Height);

annotator.Add(new HighlightAnnotation
{
    PageNumber = page.PageNumber - 1,
    BackgroundColor = 0xFFE699,
    Opacity = 0.5,
    Message = "Check this against the invoice",
    CreatedOn = DateTime.Now,
    User = new User { Id = 1, Name = "Reviewer", Role = Role.Editor },
    Points = new List<Point>
    {
        new Point(left, top),    new Point(right, top),
        new Point(left, bottom), new Point(right, bottom)
    },
    Replies = new List<Reply>
    {
        new Reply { Comment = "Confirmed with finance.", RepliedOn = DateTime.Now }
    }
});

annotator.Save("result.pdf");

Message ist der Kommentar. Replies startet einen Thread — in einem PDF‑Reader ist es eine Notiz mit einer Antwort, in Word ist es ein Kommentar mit einer darunter liegenden Antwort. User ermöglicht es einem späteren Durchlauf, genau diese Markierung des Prüfers zu finden und zu entfernen und die Markierungen aller anderen unverändert zu lassen.

Die beiden Konventionen, die alle betreffen

Schauen Sie sich genau die beiden Zeilen an, die top und bottom berechnen, sowie PageNumber.

Seitenzahlen werden doppelt gezählt. PageInfo.PageNumber ist 1‑basiert.
AnnotationBase.PageNumber ist 0‑basiert. Wenn man sie mischt, ist jede Anmerkung um eine Seite verschoben — und die für die letzte Seite vorgesehenen verschwinden, weil eine Anmerkung, die sich auf eine nicht existierende Seite bezieht, beim Speichern verworfen wird, ohne Ausnahme und ohne Warnung. Wenn Markierungen „nicht erschienen“ sind, prüfen Sie zuerst den Seitenindex.

Zwei Ursprünge. TextLineInfo und AreaAnnotation.Box werden vom oberen linken Eckpunkt gemessen.
Textmarkierungen — Highlight, Unterstreichung, Durchstreichung, Wellstrich — werden vom unteren linken Eckpunkt gemessen, so wie PDF selbst funktioniert.

Element Ursprung Seitenzählung
PageInfo, TextLineInfo oben‑links 1‑basiert
AnnotationBase.PageNumber — 0‑basiert
AreaAnnotation.Box oben‑links —
HighlightAnnotation.Points unten‑links —
PreviewOptions.PageNumbers — 1‑basiert

Vier Punkte bilden ein Rechteck, in dieser Reihenfolge: oben‑links, oben‑rechts, unten‑links, unten‑rechts. Weitere Rechtecke können in derselben Liste folgen, was es einem einzelnen Kommentar ermöglicht, einen Satz zu umfassen, der über drei Zeilen hinweg umbrochen wird.

Markierungen zurücklesen

using (Annotator annotator = new Annotator("result.pdf"))
{
    foreach (AnnotationBase a in annotator.Get())
    {
        Console.WriteLine($"#{a.Id} {a.Type} on page {a.PageNumber + 1}: {a.Message}");

        foreach (Reply reply in a.Replies)
            Console.WriteLine($"   reply: {reply.Comment}");
    }
}

Get() liest, was das Dokument tatsächlich enthält — einschließlich Anmerkungen, die jemand in Word oder Acrobat hinzugefügt hat, was einen Rundlauf durch andere Werkzeuge ermöglicht.

Ein Hinweis, den man kennen sollte: Id wird zugewiesen, während das Dokument gelesen wird, nicht im Dokument gespeichert. Es ist stabil genug, um eine Anmerkung in derselben Sitzung zu löschen oder zu aktualisieren, und es ist kein Schlüssel für eine Datenbank. Um Anmerkungen außerhalb der Datei zu behalten, verwenden Sie Import und Export.

Erneut entfernen

annotator.Remove(1);                                      // by id
annotator.Remove(annotator.Get()
    .Where(a => a.User?.Name == "Reviewer").ToList());     // or by author

Seiten rendern

PreviewOptions previewOptions = new PreviewOptions(n => File.Create($"page_{n}.png"));
previewOptions.PreviewFormat = PreviewFormats.PNG;
previewOptions.PageNumbers = new[] { 1, 2 };     // 1-based
previewOptions.RenderAnnotations = true;
previewOptions.Resolution = 144;

annotator.Document.GeneratePreview(previewOptions);

Nützlich an drei Stellen: eine Web‑UI, die das Dokument anzeigt, ein Test, der beweist, dass die Markierung dort gelandet ist, wo Sie es beabsichtigt haben, und ein Agent, der seine eigene Arbeit prüfen muss.

Unter Linux

Der Code ändert sich nicht. Der Container tut es:

FROM mcr.microsoft.com/dotnet/runtime:8.0
RUN apt-get update \
 && apt-get install -y --no-install-recommends fontconfig fonts-liberation \
 && rm -rf /var/lib/apt/lists/*

Die .NET‑Runtime‑Images werden ohne Schriftarten ausgeliefert und die Textdarstellung benötigt sie. Diese eine Zeile ist der Unterschied zwischen korrekter Ausgabe und leeren Seiten — und sie ist die häufigste Linux‑Frage zu Dokumenten‑Bibliotheken im Allgemeinen.

Wie es weitergeht

git clone https://github.com/groupdocs-annotation/annotate-pdf-csharp-net.git
cd annotate-pdf-csharp-net
dotnet run --project src/AnnotatePdf -- highlight samples/quarterly-review.pdf --text "Hosting costs"