La maggior parte delle risposte a questa domanda ha cinque anni, è solo per PDF e inizia chiedendoti di misurare le coordinate rispetto a una pagina che non puoi vedere. Ecco la versione 2026: sette chiamate, le due convenzioni che decidono dove il markup atterra effettivamente, e la singola riga di cui ha bisogno un container Linux.
Tutto quanto segue funziona su .NET 8 e .NET 10, su Windows e Linux, senza Adobe Acrobat né Microsoft Office installati — e lo stesso codice funziona su DOCX, XLSX, PPTX e immagini. Solo il percorso del file cambia.
La versione eseguibile è su GitHub: annotate-pdf-csharp-net.
Installazione
dotnet add package GroupDocs.Annotation
Senza licenza la libreria funziona in modalità di valutazione e l’output contiene marchi di valutazione. Una licenza temporanea li rimuove:
new License().SetLicense("GroupDocs.Annotation.lic");
Apri il documento
using (Annotator annotator = new Annotator("quarterly-review.pdf"))
{
// add, read or remove markup here
}
Una cosa da sapere subito: l’annotator mantiene il file aperto fino a quando non viene eliminato, e non può salvare sopra il file che sta leggendo. Salva in un percorso diverso, o in un file temporaneo che sposti al posto giusto in seguito.
Non misurare le coordinate — cerca il testo
Questa è la parte che la maggior parte dei tutorial salta, ed è quella che fa sì che il codice di annotazione sopravviva alla prossima versione del documento. GetDocumentInfo() restituisce ogni pagina con le sue dimensioni e le sue linee di testo, ognuna con un riquadro:
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
}
}
}
Ora l’annotazione può essere posizionata su ciò che il documento dice oggi, invece di dove lo diceva l’anno scorso.
Aggiungi un evidenziatore con un commento e una risposta
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 è il commento. Replies avvia una discussione — in un lettore PDF è una nota con una risposta, in Word è un commento con una risposta sotto. User è ciò che permette a un passaggio successivo di trovare e rimuovere esattamente il markup di questo revisore e lasciare intatti quelli di tutti gli altri.
Le due convenzioni che catturano tutti
Guarda attentamente le due righe che calcolano top e bottom, e PageNumber.
I numeri di pagina sono contati due volte. PageInfo.PageNumber è basata su 1. AnnotationBase.PageNumber è basata su 0. Mescolandoli, ogni annotazione risulta spostata di una pagina — e quelle destinate all’ultima pagina scompaiono, perché un’annotazione indirizzata a una pagina inesistente viene scartata al salvataggio, senza eccezione e senza avviso. Se il markup “non è apparso”, controlla l’indice della pagina prima di qualsiasi altra cosa.
Due origini. TextLineInfo e AreaAnnotation.Box sono misurati dal alto-sinistra. Il markup di testo — evidenziazione, sottolineatura, barrato, ondulato — è misurato dal basso-sinistra, che è così che funziona il PDF.
| Membro | Origine | Numerazione pagina |
|---|---|---|
PageInfo, TextLineInfo |
alto-sinistra | basata su 1 |
AnnotationBase.PageNumber |
— | basata su 0 |
AreaAnnotation.Box |
alto-sinistra | — |
HighlightAnnotation.Points |
basso-sinistra | — |
PreviewOptions.PageNumbers |
— | basata su 1 |
Quattro punti formano un rettangolo, in quest’ordine: alto-sinistra, alto-destra, basso-sinistra, basso-destra. Altri rettangoli possono seguire nella stessa lista, ed è così che un singolo commento copre una frase che si avvolge su tre linee.
Leggi il markup
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() legge ciò che il documento contiene realmente — incluse le annotazioni aggiunte da una persona in Word o Acrobat, che è ciò che rende possibile un viaggio di andata e ritorno attraverso altri strumenti.
Una avvertenza da conoscere: Id viene assegnato mentre il documento viene letto, non è memorizzato al suo interno. È sufficientemente stabile per eliminare o aggiornare un’annotazione nella stessa sessione, e non è una chiave da inserire in un database. Per tenere le annotazioni fuori dal file, usa importazione ed esportazione.
Rimuovilo di nuovo
annotator.Remove(1); // by id
annotator.Remove(annotator.Get()
.Where(a => a.User?.Name == "Reviewer").ToList()); // or by author
Renderizza le pagine
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 in tre contesti: un’interfaccia web che mostra il documento, un test che dimostra che il markup è atterrato dove intendevi, e un agente che deve guardare al proprio lavoro.
Su Linux
Il codice non cambia. Il container sì:
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/*
Le immagini runtime di .NET vengono fornite senza font e il rendering del testo ne ha bisogno. Quella singola riga è la differenza tra output corretto e pagine vuote — ed è la domanda Linux più comune riguardo alle librerie di documenti in generale.
Dove andare dopo
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"
- Documentazione: Annotare un PDF in C#
- Fai revisionare un contratto da un LLM e lascia commenti reali — le stesse chiamate, guidate da un modello
- Costruisci un’interfaccia di annotazione per browser — le stesse chiamate, guidate da una persona in un browser