Většina odpovědí na tuto otázku je stará pět let, funguje jen s PDF a začíná tím, že vás žádá o měření souřadnic vůči stránce, kterou nevidíte. Zde je verze z roku 2026: sedm volání, dvě konvence, které určují, kam se značení skutečně umístí, a jediný řádek, který potřebuje Linux kontejner.

Vše níže běží na .NET 8 a .NET 10, na Windows i Linuxu, bez Adobe Acrobat a bez nainstalovaného Microsoft Office — a stejný kód funguje s DOCX, XLSX, PPTX a obrázky. Pouze cesta k souboru se mění.

Spustitelná verze je na GitHubu:
annotate-pdf-csharp-net.

Instalace

dotnet add package GroupDocs.Annotation

Bez licence knihovna běží v režimu hodnocení a výstup nese značky hodnocení.
Dočasná licence je odstraní:

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

Otevření dokumentu

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

Jedna věc, kterou je dobré vědět hned na začátku: anotátor drží soubor otevřený, dokud není uvolněn, a nemůže přepsat soubor, který čte. Uložte do jiné cesty nebo do dočasného souboru, který po dokončení přesunete na místo.

Neměřte souřadnice — hledejte text

To je část, kterou většina tutoriálů přeskočí, a je to ta, která umožní, aby kód anotací přežil další verzi dokumentu. GetDocumentInfo() vrací každou stránku s její velikostí a řádky textu, každý s rámečkem:

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

Nyní lze anotaci umístit tam, kde dokument dnes říká, místo tam, kde to říkal loni.

Přidání zvýraznění s komentářem a odpovědí

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 je komentář. Replies zahajuje vlákno — v PDF čtečce je to poznámka s odpovědí, ve Wordu to je komentář s odpovědí pod ním. User umožňuje později najít a odstranit právě toto značení od daného recenzenta a nechat ostatní nedotčeny.

Dvě konvence, které zachytí všechny

Podívejte se pozorně na dva řádky, které počítají top a bottom, a na PageNumber.

Čísla stránek se počítají dvakrát. PageInfo.PageNumber je základ 1.
AnnotationBase.PageNumber je základ 0. Smícháte je a každá anotace bude o jednu stránku posunuta — a anotace určené pro poslední stránku zmizí, protože anotace odkazující na neexistující stránku je při uložení zahozena bez výjimky a bez varování. Pokud se značení „neobjevilo“, zkontrolujte index stránky jako první.

Dva počátky. TextLineInfo a AreaAnnotation.Box se měří od levého horního rohu. Textové značení — zvýraznění, podtržení, přeškrtnutí, vlnovka — se měří od levého dolního rohu, což tak funguje i v PDF.

Člen Počátek Číslování stránek
PageInfo, TextLineInfo levý horní základ 1
AnnotationBase.PageNumber — základ 0
AreaAnnotation.Box levý horní —
HighlightAnnotation.Points levý dolní —
PreviewOptions.PageNumbers — základ 1

Čtyři body tvoří jeden obdélník v tomto pořadí: levý horní, pravý horní, levý dolní, pravý dolní. Další obdélníky mohou následovat ve stejném seznamu, což je způsob, jak jeden komentář pokrývá větu, která se zalamuje přes tři řádky.

Načtení značení zpět

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() načte to, co dokument skutečně obsahuje — včetně anotací, které osoba přidala ve Wordu nebo Acrobat, což umožňuje provést zpětný průchod přes jiné nástroje.

Jedna důležitá poznámka: Id se při čtení dokumentu přiřadí, není v dokumentu uložené. Je dostatečně stabilní pro smazání nebo aktualizaci anotace ve stejné relaci, ale není vhodný jako klíč do databáze. Pokud chcete anotace uchovávat mimo soubor, použijte import a export.

Odstranění

annotator.Remove(1);                                      // podle id
annotator.Remove(annotator.Get()
    .Where(a => a.User?.Name == "Reviewer").ToList());     // nebo podle autora

Vykreslení stránek

PreviewOptions previewOptions = new PreviewOptions(n => File.Create($"page_{n}.png"));
previewOptions.PreviewFormat = PreviewFormats.PNG;
previewOptions.PageNumbers = new[] { 1, 2 };     // základ 1
previewOptions.RenderAnnotations = true;
previewOptions.Resolution = 144;

annotator.Document.GeneratePreview(previewOptions);

Užitečné ve třech situacích: webové UI, které zobrazuje dokument, test, který dokazuje, že značení dopadlo tam, kde jste zamýšleli, a agent, který potřebuje zkontrolovat svou vlastní práci.

Na Linuxu

Kód se nemění. Kontejner se mění:

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

Obrazy .NET runtime jsou dodávány bez fontů a vykreslování textu je vyžaduje. Ten jediný řádek je rozdíl mezi správným výstupem a prázdnými stránkami — a je to nejčastější otázka ohledně knihoven pro dokumenty na Linuxu.

Kam dál

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"