معظم الإجابات على هذا السؤال تعود إلى خمس سنوات مضت، مخصصة للـ PDF فقط، وتبدأ بطلب قياس الإحداثيات مقابل صفحة لا يمكنك رؤيتها. إليك نسخة 2026: سبع استدعاءات، القاعدتان اللتان تحددان أين ستظهر العلامات فعليًا، والسطر الوحيد الذي يحتاجه حاوية لينكس.
كل ما يلي يعمل على .NET 8 و .NET 10، على ويندوز ولينكس، دون الحاجة إلى 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"
- التوثيق: Annotate a PDF in C#
- دع نموذج لغة كبير يراجع عقدًا ويترك تعليقات حقيقية — نفس الاستدعاءات، مدفوعة بنموذج
- بناء واجهة تعليقات في المتصفح — نفس الاستدعاءات، مدفوعة بشخص في المتصفح