De meeste antwoorden op deze vraag zijn vijf jaar oud, alleen voor PDF, en beginnen met het vragen om coördinaten te meten ten opzichte van een pagina die je niet kunt zien. Hier is de 2026‑versie: zeven aanroepen, de twee conventies die bepalen waar de markup daadwerkelijk terechtkomt, en de ene regel die een Linux‑container nodig heeft.
Alles hieronder draait op .NET 8 en .NET 10, op Windows en Linux, zonder Adobe Acrobat en zonder Microsoft Office geïnstalleerd — en dezelfde code werkt op DOCX, XLSX, PPTX en afbeeldingen. Alleen het bestandspad verandert.
De uitvoerbare versie staat op GitHub:
annotate-pdf-csharp-net.
Installeren
dotnet add package GroupDocs.Annotation
Zonder licentie draait de bibliotheek in evaluatiemodus en bevat de output evaluatiemerkingen. Een tijdelijke licentie verwijdert deze:
new License().SetLicense("GroupDocs.Annotation.lic");
Open het document
using (Annotator annotator = new Annotator("quarterly-review.pdf"))
{
// add, read or remove markup here
}
Eén ding om meteen te weten: de annotator houdt het bestand open tot het wordt vrijgegeven, en hij kan niet overschrijven het bestand dat hij leest. Sla op naar een ander pad, of naar een tijdelijk bestand dat je daarna verplaatst.
Meet geen coördinaten — zoek naar de tekst
Dit is het deel dat de meeste tutorials overslaan, en het is het deel dat annotatiecode laat overleven in de volgende versie van het document. GetDocumentInfo() retourneert elke pagina met zijn grootte en zijn tekstregels, elk met een vak:
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
}
}
}
Nu kan de annotatie worden geplaatst op wat het document vandaag zegt, in plaats van waar het vorig jaar stond.
Voeg een markering toe met een opmerking en een antwoord
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 is de opmerking. Replies start een thread — in een PDF‑lezer is het een notitie met een reactie, in Word is het een opmerking met een antwoord eronder. User is wat een latere doorloop in staat stelt precies deze beoordelaar’s markup te vinden en te verwijderen en iedereen anders ongemoeid te laten.
De twee conventies die iedereen vangen
Kijk nauwkeurig naar de twee regels die top en bottom berekenen, en naar PageNumber.
Paginanummers worden twee keer geteld. PageInfo.PageNumber is 1-gebaseerd.
AnnotationBase.PageNumber is 0-gebaseerd. Meng ze en elke annotatie staat één pagina verkeerd — en de annotaties bedoeld voor de laatste pagina verdwijnen, omdat een annotatie gericht op een niet‑bestaande pagina wordt weggelaten bij het opslaan, zonder uitzondering en zonder waarschuwing. Als markup “niet verscheen”, controleer dan eerst de paginanaam.
Twee oorsprongen. TextLineInfo en AreaAnnotation.Box worden gemeten vanaf de linkerbovenhoek. Tekstmarkup — markering, onderstreping, doorhaling, golvende lijn — wordt gemeten vanaf de linkeronderhoek, zoals PDF zelf werkt.
| Lid | Oorsprong | Paginanummering |
|---|---|---|
PageInfo, TextLineInfo |
linkerbovenhoek | 1-gebaseerd |
AnnotationBase.PageNumber |
— | 0-gebaseerd |
AreaAnnotation.Box |
linkerbovenhoek | — |
HighlightAnnotation.Points |
linkeronderhoek | — |
PreviewOptions.PageNumbers |
— | 1-gebaseerd |
Vier punten vormen één rechthoek, in deze volgorde: linkerbovenhoek, rechterbovenhoek, linkeronderhoek, rechteronderhoek. Meer rechthoeken kunnen volgen in dezelfde lijst, wat de manier is waarop één opmerking een zin dekt die over drie regels wordt verdeeld.
Lees de markup terug
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() leest wat het document daadwerkelijk bevat — inclusief annotaties die iemand heeft toegevoegd in Word of Acrobat, wat een round‑trip door andere tools mogelijk maakt.
Een kanttekening die het waard is om te weten: Id wordt toegewezen terwijl het document wordt gelezen, niet opgeslagen in het document. Het is stabiel genoeg om een annotatie te verwijderen of bij te werken in dezelfde sessie, en het is geen sleutel om in een database te plaatsen. Om annotaties buiten het bestand te bewaren, gebruik import en export.
Verwijder het opnieuw
annotator.Remove(1); // by id
annotator.Remove(annotator.Get()
.Where(a => a.User?.Name == "Reviewer").ToList()); // or by author
Render de pagina’s
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);
Handig op drie plekken: een web‑UI die het document toont, een test die bewijst dat de markup terechtkwam waar je het bedoelde, en een agent die zijn eigen werk moet bekijken.
Op Linux
De code verandert niet. De container wel:
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/*
De .NET‑runtime‑images worden geleverd zonder lettertypen en tekstweergave heeft ze nodig. Die ene regel is het verschil tussen correcte output en lege pagina’s — en het is de meest voorkomende Linux‑vraag over documentbibliotheken in het algemeen.
Waar nu heen
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"
- Documentatie: PDF annoteren in C# — dezelfde aanroepen, aangestuurd door een model
- Laat een LLM een contract beoordelen en echte opmerkingen achterlaten — dezelfde aanroepen, aangestuurd door een model
- Bouw een browser‑annotatie‑UI — dezelfde aanroepen, aangestuurd door een persoon in een browser