A maioria das respostas a esta pergunta tem cinco anos, funciona apenas com PDF e começa pedindo que você meça coordenadas em uma página que não pode ver. Aqui está a versão de 2026: sete chamadas, as duas convenções que decidem onde a marcação realmente aparece e a única linha que um contêiner Linux precisa.

Tudo abaixo funciona em .NET 8 e .NET 10, no Windows e no Linux, sem Adobe Acrobat e sem Microsoft Office instalados — e o mesmo código funciona em DOCX, XLSX, PPTX e imagens. Apenas o caminho do arquivo muda.

A versão executável está no GitHub:
annotate-pdf-csharp-net.

Instalar

dotnet add package GroupDocs.Annotation

Sem uma licença a biblioteca roda em modo de avaliação e a saída traz marcas de avaliação. Uma
licença temporária as remove:

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

Abrir o documento

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

Uma coisa a saber de antemão: o anotador mantém o arquivo aberto até ser descartado, e não pode salvar sobre o arquivo que está lendo. Salve em um caminho diferente, ou em um arquivo temporário que você move para o local depois.

Não meça coordenadas — procure o texto

Esta é a parte que a maioria dos tutoriais ignora, e é a que faz o código de anotação sobreviver à próxima versão do documento. GetDocumentInfo() devolve cada página com seu tamanho e suas linhas de texto, cada uma com uma caixa:

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
        }
    }
}

Agora a anotação pode ser colocada onde o documento indica hoje, em vez de onde ele dizia no ano passado.

Adicionar um destaque com comentário e resposta

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 é o comentário. Replies inicia um thread — em um leitor de PDF é uma nota com resposta, no Word é um comentário com uma resposta abaixo dele. User permite que uma passagem posterior encontre e remova exatamente a marcação desse revisor e deixe as dos demais intocadas.

As duas convenções que pegam todo mundo

Observe atentamente as duas linhas que calculam top e bottom, e o PageNumber.

Os números de página são contados duas vezes. PageInfo.PageNumber é baseado em 1.
AnnotationBase.PageNumber é baseado em 0. Misturá‑los faz com que toda anotação fique deslocada em uma página — e as que deveriam aparecer na última página desaparecem, porque uma anotação direcionada a uma página que não existe é descartada ao salvar, sem exceção e sem aviso. Se a marcação “não apareceu”, verifique o índice da página antes de qualquer outra coisa.

Duas origens. TextLineInfo e AreaAnnotation.Box são medidos a partir do canto superior‑esquerdo. Marcação de texto — destaque, sublinhado, tachado, ondulado — é medida a partir do canto inferior‑esquerdo, que é como o PDF funciona.

Membro Origem Numeração de página
PageInfo, TextLineInfo top-left 1-based
AnnotationBase.PageNumber — 0-based
AreaAnnotation.Box top-left —
HighlightAnnotation.Points bottom-left —
PreviewOptions.PageNumbers — 1-based

Quatro pontos formam um retângulo, nesta ordem: superior‑esquerdo, superior‑direito, inferior‑esquerdo, inferior‑direito. Mais retângulos podem seguir na mesma lista, que é como um único comentário cobre uma frase que se estende por três linhas.

Ler a marcação de volta

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() lê o que o documento realmente contém — incluindo anotações que uma pessoa adicionou no Word ou no Acrobat, o que faz uma ida e volta por outras ferramentas funcionar.

Um detalhe importante: Id é atribuído enquanto o documento está sendo lido, não armazenado nele. É estável o suficiente para excluir ou atualizar uma anotação na mesma sessão, e não é uma chave para colocar em um banco de dados. Para manter anotações fora do arquivo, use importação e exportação.

Remover novamente

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

Renderizar as páginas

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);

Útil em três situações: uma UI web que mostra o documento, um teste que comprova que a marcação chegou onde você pretendia, e um agente que precisa ver seu próprio trabalho.

No Linux

O código não muda. O contêiner muda:

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/*

As imagens de runtime do .NET são distribuídas sem fontes e a renderização de texto precisa delas. Essa única linha faz a diferença entre saída correta e páginas em branco — e é a pergunta mais comum sobre bibliotecas de documentos no Linux em geral.

Para onde ir a seguir

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"