La plupart des réponses à cette question ont cinq ans, ne concernent que les PDF et commencent par vous demander de mesurer les coordonnées par rapport à une page que vous ne pouvez pas voir. Voici la version 2026 : sept appels, les deux conventions qui décident où le balisage atterrit réellement, et la seule ligne dont un conteneur Linux a besoin.
Tout ce qui suit fonctionne avec .NET 8 et .NET 10, sous Windows et Linux, sans Adobe Acrobat ni Microsoft Office installés — et le même code fonctionne sur DOCX, XLSX, PPTX et images. Seul le chemin du fichier change.
La version exécutable se trouve sur GitHub : annotate-pdf-csharp-net.
Installation
dotnet add package GroupDocs.Annotation
Sans licence, la bibliothèque s’exécute en mode évaluation et la sortie porte des marques d’évaluation. Une licence temporaire les supprime :
new License().SetLicense("GroupDocs.Annotation.lic");
Ouvrir le document
using (Annotator annotator = new Annotator("quarterly-review.pdf"))
{
// add, read or remove markup here
}
Une chose à savoir dès le départ : l’annotateur garde le fichier ouvert jusqu’à ce qu’il soit libéré, et il ne peut pas enregistrer par-dessus le fichier qu’il lit. Enregistrez‑le dans un autre chemin, ou dans un fichier temporaire que vous déplacerez ensuite à sa place.
Ne pas mesurer les coordonnées — rechercher le texte
C’est la partie que la plupart des tutoriels ignorent, et c’est celle qui permet au code d’annotation de survivre à la prochaine version du document. GetDocumentInfo() renvoie chaque page avec sa taille et ses lignes de texte, chacune avec une boîte :
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
}
}
}
Ainsi l’annotation peut être placée sur ce que le document indique aujourd’hui, au lieu de là où il l’indiquait l’an dernier.
Ajouter un surlignage avec un commentaire et une réponse
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 est le commentaire. Replies démarre un fil — dans un lecteur PDF c’est une note avec une réponse, dans Word c’est un commentaire avec une réponse en dessous. User permet à une passe ultérieure de retrouver et de supprimer exactement le balisage de cet examinateur tout en laissant les autres intacts.
Les deux conventions qui attrapent tout le monde
Observez attentivement les deux lignes qui calculent top et bottom, ainsi que PageNumber.
Les numéros de page sont comptés deux fois. PageInfo.PageNumber est à base 1. AnnotationBase.PageNumber est à base 0. Les mélanger fait que chaque annotation est décalée d’une page — et celles destinées à la dernière page disparaissent, car une annotation adressée à une page qui n’existe pas est simplement ignorée lors de l’enregistrement, sans exception ni avertissement. Si le balisage « n’apparaît pas », vérifiez d’abord l’indice de page.
Deux origines. TextLineInfo et AreaAnnotation.Box sont mesurés depuis le coin en haut à gauche. Le balisage de texte — surlignage, soulignement, barré, ondulé — est mesuré depuis le coin en bas à gauche, qui est ainsi que le PDF fonctionne.
| Membre | Origine | Numérotation des pages |
|---|---|---|
PageInfo, TextLineInfo |
en haut à gauche | à base 1 |
AnnotationBase.PageNumber |
— | à base 0 |
AreaAnnotation.Box |
en haut à gauche | — |
HighlightAnnotation.Points |
en bas à gauche | — |
PreviewOptions.PageNumbers |
— | à base 1 |
Quatre points forment un rectangle, dans cet ordre : haut‑gauche, haut‑droite, bas‑gauche, bas‑droite. D’autres rectangles peuvent suivre dans la même liste, ce qui permet à un seul commentaire de couvrir une phrase qui s’étend sur trois lignes.
Lire le balisage
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() lit ce que le document contient réellement — y compris les annotations qu’une personne a ajoutées dans Word ou Acrobat, ce qui rend possible un aller‑retour à travers d’autres outils.
Une mise en garde utile : Id est attribué pendant la lecture du document, il n’est pas stocké dans celui‑ci. Il est suffisamment stable pour supprimer ou mettre à jour une annotation dans la même session, mais ce n’est pas une clé à mettre dans une base de données. Pour conserver les annotations hors du fichier, utilisez l’importation et l’exportation.
Le supprimer à nouveau
annotator.Remove(1); // by id
annotator.Remove(annotator.Get()
.Where(a => a.User?.Name == "Reviewer").ToList()); // or by author
Rendre les pages
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);
Utile à trois endroits : une interface web qui affiche le document, un test qui prouve que le balisage a atterri où vous le vouliez, et un agent qui doit examiner son propre travail.
Sous Linux
Le code ne change pas. Le conteneur change :
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/*
Les images d’exécution .NET sont livrées sans polices et le rendu du texte en a besoin. Cette seule ligne fait la différence entre une sortie correcte et des pages blanches — et c’est la question Linux la plus fréquente concernant les bibliothèques de documents en général.
Où aller ensuite
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"
- Documentation : Annotate a PDF in C#
- Let an LLM review a contract and leave real comments — les mêmes appels, pilotés par un modèle
- Build a browser annotation UI — les mêmes appels, pilotés par une personne dans un navigateur