Большинство ответов на этот вопрос пятилетние, только для 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
}
Важно знать сразу: аннотатор держит файл открытым до тех пор, пока не будет освобождён, и он не может сохранять поверх файла, из которого читает. Сохраняйте в другой путь или во временный файл, который потом переместите на место.
Не измеряйте координаты — ищите текст
Это часть, которую пропускают большинство руководств, а она позволяет коду аннотаций выжить после следующего обновления документа. 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‑based.
AnnotationBase.PageNumber — 0‑based. Смешайте их, и каждая аннотация будет смещена на одну страницу — а аннотации, предназначенные для последней страницы, исчезнут, потому что аннотация, адресованная несуществующей странице, отбрасывается при сохранении без исключения и без предупреждения. Если разметка «не появилась», проверьте индекс страницы в первую очередь.
Два начала координат. TextLineInfo и AreaAnnotation.Box измеряются от верхнего левого угла. Текстовая разметка — выделение, подчёркивание, зачеркивание, волнистая линия — измеряется от нижнего левого угла, как работает сам PDF.
| Member | Origin | Page numbering |
|---|---|---|
PageInfo, TextLineInfo |
top-left | 1-based |
AnnotationBase.PageNumber |
— | 0-based |
AreaAnnotation.Box |
top-left | — |
HighlightAnnotation.Points |
bottom-left | — |
PreviewOptions.PageNumbers |
— | 1-based |
Четыре точки образуют один прямоугольник в следующем порядке: верхний‑левый, верхний‑правый, нижний‑левый, нижний‑правый. Далее могут следовать другие прямоугольники в том же списке — так один комментарий покрывает предложение, которое переносится на три строки.
Чтение разметки обратно
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"
- Документация: Annotate a PDF in C#
- Let an LLM review a contract and leave real comments — те же вызовы, управляемые моделью
- Build a browser annotation UI — те же вызовы, управляемые человеком в браузере