רוב התשובות לשאלה זו הן משנת חמש שנים, רק PDF, ומתחילות בבקשה למדוד קואורדינטות נגד דף שאינך יכול לראות. הנה גרסת 2026: שבע קריאות, שתי הקונבנציות שמחליטות היכן הסימון בפועל נופל, והשורה היחידה שהקונטיינר של לינוקס צריך.

כל מה שלמטה רץ על .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);

שימושי בשלושה מקומות: ממשק משתמש אינטרנטי שמציג את המסמך, בדיקה שמוכיחה שהסימון נחת במקום שהכוונה הייתה, וסוכן שצריך לבדוק את העבודה שלו.

בלינוקס

הקוד אינו משתנה. הקונטיינר כן:

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 מגיעות ללא גופנים והצגת טקסט דורשת אותם. השורה היחידה היא ההבדל בין פלט נכון לעמודים ריקים — והיא השאלה הנפוצה ביותר בלינוקס לגבי ספריות מסמכים באופן כללי.

לאן לפנות הלאה

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"