Більшість відповідей на це питання старі п’ять років, лише для PDF і починаються з того, що просять вас вимірювати координати щодо сторінки, яку ви не бачите. Ось версія 2026 року: сім викликів, дві конвенції, які визначають, куди саме потрапляє розмітка, і один рядок, потрібний Linux‑контейнеру.

Все нижче працює на .NET 8 і .NET 10, на Windows і Linux, без встановлених Adobe Acrobat та Microsoft Office — і той самий код працює з DOCX, XLSX, PPTX та зображеннями. Змінюється лише шлях до файлу.

Запускова версія розміщена на GitHub:
annotate-pdf-csharp-net.

Встановлення

dotnet add package GroupDocs.Annotation

Без ліцензії бібліотека працює в режимі оцінки, і вихідний файл містить позначки оцінки.
Тимчасова ліцензія їх видаляє:

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

Відкрити документ

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

Одне, що треба знати заздалегідь: annotator тримає файл відкритим, доки не буде звільнений, і не може перезаписати файл, який читає. Збережіть у інший шлях або у тимчасовий файл, який потім перемістите на місце.

Не вимірюйте координати — шукайте текст

Це частина, яку пропускає більшість підручників, і саме вона дозволяє коду анотацій вижити в наступній версії документа. GetDocumentInfo() повертає кожну сторінку з її розміром і рядками тексту, кожен з яких має коробку:

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

Тепер анотація може бути розміщена там, де документ каже сьогодні, а не там, де він говорив минулого року.

Додати виділення з коментарем та відповіддю

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 — це коментар. Replies відкриває гілку — у PDF‑читачі це нотатка з відповіддю, у Word — коментар з відповіддю під ним. User дозволяє пізніше знайти і видалити саме цю анотацію конкретного рецензента, залишивши інші недоторканими.

Дві конвенції, які охоплюють усіх

Уважно подивіться на два рядки, які обчислюють top і bottom, і на PageNumber.

Номери сторінок рахуються двічі. PageInfo.PageNumber має з індексацією 1.
AnnotationBase.PageNumber має з індексацією 0. Змішайте їх, і кожна анотація буде зміщена на одну сторінку — а анотації, призначені для останньої сторінки, зникають, бо анотація, що посилається на неіснуючу сторінку, відкидається під час збереження без виключення і без попередження. Якщо розмітка «не з’явилася», спершу перевірте індекс сторінки.

Два походження. TextLineInfo і AreaAnnotation.Box вимірюються від верхнього лівого кута. Текстова розмітка — виділення, підкреслення, закреслення, хвилясте — вимірюється від нижнього лівого кута, що відповідає способу роботи PDF.

Член Походження Нумерація сторінок
PageInfo, TextLineInfo верхній лівий з індексацією 1
AnnotationBase.PageNumber — з індексацією 0
AreaAnnotation.Box верхній лівий —
HighlightAnnotation.Points нижній лівий —
PreviewOptions.PageNumbers — з індексацією 1

Чотири точки утворюють один прямокутник у такому порядку: верхній лівий, верхній правий, нижній лівий, нижній правий. Додаткові прямокутники можуть слідувати в тому ж списку, що дозволяє одній коментарі охопити речення, що переноситься на три рядки.

Прочитати розмітку назад

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() читає те, що документ фактично містить — включаючи анотації, додані користувачем у Word або Acrobat, що і забезпечує «круглий» прохід через інші інструменти.

Одна важлива заувага: Id присвоюється під час читання документа, а не зберігається в ньому. Він достатньо стабільний, щоб видалити або оновити анотацію в межах однієї сесії, і не є ключем для бази даних. Щоб зберігати анотації поза файлом, використовуйте імпорт та експорт.

Видалити її знову

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

Рендерити сторінки

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

Корисно в трьох випадках: веб‑інтерфейс, що показує документ, тест, що доводить, що розмітка опинилася там, куди ви планували, і агент, якому потрібно переглянути свою роботу.

На Linux

Код не змінюється. Змінюється контейнер:

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

Образи .NET runtime постачаються без шрифтів, а рендеринг тексту їх потребує. Цей один рядок — різниця між правильним виводом і порожніми сторінками — і це найпоширеніше питання про Linux‑документні бібліотеки загалом.

Куди далі

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"