La mayoría de las respuestas a esta pregunta tienen cinco años, son solo para PDF y comienzan pidiéndote que midas coordenadas contra una página que no puedes ver. Aquí está la versión 2026: siete llamadas, las dos convenciones que deciden dónde se coloca realmente el marcado, y la única línea que necesita un contenedor Linux.

Todo lo siguiente se ejecuta en .NET 8 y .NET 10, en Windows y Linux, sin Adobe Acrobat ni Microsoft Office instalados — y el mismo código funciona en DOCX, XLSX, PPTX e imágenes. Sólo cambia la ruta del archivo.

La versión ejecutable está en GitHub: annotate-pdf-csharp-net.

Instalación

dotnet add package GroupDocs.Annotation

Sin una licencia, la biblioteca se ejecuta en modo de evaluación y la salida lleva marcas de evaluación. Una licencia temporal las elimina:

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

Abrir el documento

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

Una cosa que debes saber desde el principio: el anotador mantiene el archivo abierto hasta que se elimina, y no puede sobrescribir el archivo que está leyendo. Guarda en una ruta diferente, o en un archivo temporal que luego muevas a su lugar.

No midas coordenadas — busca el texto

Esta es la parte que la mayoría de los tutoriales omiten, y es la que hace que el código de anotación sobreviva a la siguiente versión del documento. GetDocumentInfo() devuelve cada página con su tamaño y sus líneas de texto, cada una con un cuadro:

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

Ahora la anotación puede colocarse en lo que el documento indique hoy, en lugar de donde lo indicaba el año pasado.

Añadir un resaltado con un comentario y una respuesta

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 es el comentario. Replies inicia un hilo — en un lector de PDF es una nota con una respuesta, en Word es un comentario con una respuesta debajo. User es lo que permite que una pasada posterior encuentre y elimine exactamente el marcado de este revisor y deje los de los demás sin tocar.

Las dos convenciones que atrapan a todos

Observa detenidamente las dos líneas que calculan top y bottom, y PageNumber.

Los números de página se cuentan dos veces. PageInfo.PageNumber es basado en 1. AnnotationBase.PageNumber es basado en 0. Mézclalos y cada anotación quedará desplazada una página — y las que están destinadas a la última página desaparecen, porque una anotación dirigida a una página que no existe se descarta al guardar, sin excepción ni advertencia. Si el marcado “no apareció”, verifica el índice de página antes que nada.

Dos orígenes. TextLineInfo y AreaAnnotation.Box se miden desde la esquina arriba-izquierda. El marcado de texto — resaltado, subrayado, tachado, ondulado — se mide desde la esquina abajo-izquierda, que es como funciona el propio PDF.

Miembro Origen Numeración de página
PageInfo, TextLineInfo arriba-izquierda basado en 1
AnnotationBase.PageNumber — basado en 0
AreaAnnotation.Box arriba-izquierda —
HighlightAnnotation.Points abajo-izquierda —
PreviewOptions.PageNumbers — basado en 1

Cuatro puntos forman un rectángulo, en este orden: arriba-izquierda, arriba-derecha, abajo-izquierda, abajo-derecha. Pueden seguir más rectángulos en la misma lista, lo que permite que un solo comentario cubra una oración que se envuelve en tres líneas.

Leer el marcado de vuelta

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() lee lo que el documento contiene realmente — incluidas las anotaciones que una persona añadió en Word o Acrobat, lo que permite que un viaje de ida y vuelta a través de otras herramientas funcione.

Una advertencia que vale la pena conocer: Id se asigna mientras se lee el documento, no se almacena en él. Es lo suficientemente estable como para eliminar o actualizar una anotación en la misma sesión, y no es una clave para guardar en una base de datos. Para mantener las anotaciones fuera del archivo, usa importación y exportación.

Eliminarlo de nuevo

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

Renderizar las 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 en tres lugares: una interfaz web que muestra el documento, una prueba que demuestra que el marcado se colocó donde pretendías, y un agente que necesita revisar su propio trabajo.

En Linux

El código no cambia. El contenedor 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/*

Las imágenes del runtime de .NET se distribuyen sin fuentes y el renderizado de texto las necesita. Esa única línea es la diferencia entre una salida correcta y páginas en blanco — y es la pregunta más común sobre Linux respecto a las bibliotecas de documentos en general.

A dónde ir a continuación

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"