مقال تقني

مراجعة تعليقات PDF في Delphi باستخدام PDFium Component

يفتح مراجع العقد نفسه في عارض Delphi لديك وفي Adobe Acrobat. تعرض لوحة التعليقات في Acrobat أربعة عشر عنصراً؛ بينما تعرض لوحة المراجعة لديك أحد عشر فقط. الحلقة التي كتبتها ليست خاطئة. العناصر الثلاثة المفقودة هي ردان، وهما كائنان كاملان من نوع annotation مرتبطان بوالديهما عبر مراجع in-reply-to، ونافذة popup واحدة تخص ملاحظة لاصقة سبق أن حسبتها. تعليقات PDF ليست قائمة مسطحة من مستطيلات ملونة: يعرّف ISO 32000-1 §12.5 شبكة من القواميس لها subtypes وflags وappearance streams وعلاقات parent-child، ولوحة مراجعة تتجاهل هذه العلاقات ستظل تخالف كل عارض آخر يملكه العميل. يبني هذا الدليل سير عمل مراجعة annotations فوق PDFium Component، مكوّن VCL/LCL المبني على PDFium لـ Delphi وC++Builder وLazarus، حول المواضع التي تدفع فيها مستندات المراجعة الحقيقية النظام إلى حدوده.

لماذا لا يطابق العدد لديك لوحة تعليقات Acrobat

يعرض Acrobat منظراً منسقاً: markup annotations مجمعة في سلاسل ردود، مع popups مطوية داخل آبائها. أما مصفوفة annotations الخام في كل صفحة فتحتوي على أكثر وأقل مما يوحي به ذلك المنظر.

  • Popup annotations كائنات منفصلة مرتبطة بملاحظة أصلية؛ حسابها يضاعف كل sticky note.
  • الردود هي Text annotations كاملة تشير إلى parent؛ التصفية على العلامات المرئية وحدها تسقط سلسلة النقاش بصمت.
  • علامتا Hidden وNoView تزيلان annotation من العرض، لا من المصفوفة، لذلك تنتمي فحوص flags إلى مرحلة الفهرسة.
  • Link annotations تعيش في المصفوفة نفسها، ولا يعتبر أي مراجع hyperlink تعليقاً.

ثبّت قاعدة العد قبل كتابة أي كود واكتبها في المواصفة، لأن سؤال "لماذا تعرض لوحتكم رقماً مختلفاً عن Acrobat" هو أول تذكرة دعم تولدها ميزة مراجعة.

افهرس كل شيء مرة واحدة، ثم لا تعيد تحليل الصفحة

التصفية حسب المؤلف أو النوع أو الصفحة يجب ألا تطلق تحليلاً جديداً لكائنات الصفحة؛ ففي مستند من 300 صفحة مليء بالعلامات يتحول كل تغيير في dropdown إلى ثوانٍ من التأخير. يوفّر المكوّن AnnotationCount والخاصية المفهرسة Annotation[] على الصفحة المحملة حالياً، ويحمل سجل TPdfAnnotation المعاد كل ما تحتاجه list view: Subtype وFlags وColor وRectangle وContentsText وAuthorText. ابنِ الفهرس بمرور واحد عند الفتح:

procedure TReviewPanel.BuildIndex;
var
  PageNo, i: Integer;
  A: TPdfAnnotation;
begin
  FItems.Clear;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    for i := 0 to Pdf.AnnotationCount - 1 do
    begin
      A := Pdf.Annotation[i];
      // Keep reviewer-relevant subtypes only; record the page and
      // index pair because all later edits are addressed by it
      if A.Subtype in [anText, anHighlight, anInk] then
        FItems.Add(TReviewItem.Create(PageNo, i,
          A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
    end;
  end;
end;

الزوج الذي يستحق التأكيد هو (PageNo, i). كل استدعاء تعديل لاحق، سواء إعادة تلوين أو حذف، يخاطَب برقم الصفحة وفهرس annotation، وتتزحزح الفهارس عندما يُزال annotation. خطط لإعادة بناء إدخالات الصفحة المتأثرة بعد أي حذف بدلاً من ترقيع الفهارس في مكانها؛ إعادة البناء تكلف أجزاء من الثانية، بينما فهرس قديم يحذف تعليق المراجع الخطأ.

تستحق سلاسل الردود مكاناً في تصميم الفهرس حتى لو كان الإصدار الأول يحسب الردود فقط بدلاً من عرضها. جمّع العناصر حسب مرجع parent أثناء البناء بحيث تستطيع اللوحة لاحقاً طي thread كما يفعل Acrobat؛ إعادة بناء التجميع بتكاسل أثناء التمرير تعيد فتح صفحات سبق أن دفعت ثمن تحليلها. وتنطبق الفكرة نفسها على الهندسة: Rectangle في كل سجل معبّر عنه في إحداثيات الصفحة، فحوّله إلى إحداثيات العرض في helper مشترك واحد فقط. تتراكم أخطاء الإحداثيات في لوحات المراجعة عندما يحمل كل من التحديد وhit-testing والرسم حسابات zoom-and-rotation الخاصة به؛ مسار تحويل واحد يبقي الإبراز وإدخال القائمة وهدف النقر موجهة إلى الحبر نفسه.

إعادة تلوين العلامات ورفض appearance stream

تغيير highlight من الأصفر إلى الكهرماني يبدو كسطر واحد، وأحياناً يكون كذلك. التعقيد يأتي من ISO 32000-1 §12.5.5: عندما يحمل annotation مسار appearance باسم /AP، ترسم العوارض المطابقة ذلك المسار المبني مسبقاً، وتصبح خانة اللون في قاموس annotation مجرد زخرفة. وبما أن Acrobat يكتب appearance streams تقريباً لكل ما ينشئه، فإن معظم تعليقات العملاء تصل بهذه الحالة. إعادة التلوين هي قراءة-تعديل-كتابة عبر الخاصية Annotation[]، ويبلّغ المكوّن عن رفض المحرك بصدق عبر رفع EPdfError:

A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // amber
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // The annotation owns a pre-rendered /AP stream; the dictionary
    // color alone cannot change what viewers paint
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

تعامل مع ذلك الاستثناء في كل مرة. تخطي الحارس يعني أن لوحتك تعرض اللون الجديد في قائمتها بينما تظل الصفحة ترسم اللون القديم، ويظهر التباين بعد أسابيع كتقرير "عارضكم يتجاهل تعديلاتي". عندما يكون appearance مقفلاً، فالخيارات الصادقة هي إعادة تلوين overlay التحديد لديك بدلاً من annotation، أو وسم العنصر في UI بأنه appearance-locked.

حذف annotations بلا أشباح

يقوم DeleteAnnotation بفصل الكائن عن الصفحة الحالية، لكنه لا يعيد بناء مظهر الصفحة المخزن مؤقتاً؛ ارسم مباشرة بعد الاستدعاء وستظل الـ highlight المحذوفة مرئية. أعد تصيير raster الصفحة كجزء من العملية نفسها:

Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // raises EPdfError on failure
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage hands bitmap ownership to the caller
end;
RebuildPageEntries(Item.PageNo);  // indices after Item.Index shifted

لاحظ خيار reAnnotations في استدعاء التصيير: بدونه يستبعد raster كل annotations المتبقية، فيبدو الأمر للمستخدم كحذف جماعي. ولاحظ Bmp.Free أيضاً؛ overload الدالي لـ RenderPage ينقل ملكية bitmap إلى المستدعي، فتخطي free يسرّب raster صفحة كاملة في كل حذف.

إضافة علامات مراجعين من UI الخاص بك

إنشاء annotations يمر عبر CreateAnnotation، الذي يأخذ سجل TPdfAnnotation مملوءاً: subtype وrectangle وcolor وcontents وauthor، ثم يضيفه إلى الصفحة الحالية. الملاحظات اللاصقة (anText) هي الحالة البسيطة: موضع ومحتوى ومؤلف وانتهى الأمر. أما ink annotations فهي الفخ: مستطيل السجل يحد الرسم فقط، بينما stroke الفعلي هو مصفوفات نقاط يجب إرفاقها منفصلة عبر استدعاء ink-stroke في المحرك (FPDFAnnot_AddInkStroke مع بيانات FS_POINTF)، ملتقطة من إدخال الفأرة أو القلم stroke بعد stroke. إنشاء ink annotation من مستطيل وحده ينتج خربشة فارغة لا ترسم شيئاً.

احسم سياسة التأليف في الوقت نفسه: كل علامة ينشئها UI يجب أن تحمل AuthorText متسقاً، لأن التصفية اللاحقة حسب المراجع لا تكون موثوقة إلا بقدر الأسماء التي تكتبها اليوم.

إخراج المراجعة من العارض

تصبح بيانات المراجعة مفيدة عندما تغادر العارض: ملخص يقرؤه قائد المشروع دون فتح الملف، أو CSV يغذي جدول تتبع. صدّر من الفهرس، لا من إعادة تحليل، وحافظ على مراجع مستقرة؛ رقم الصفحة مع مستطيل annotation ينجوان من round-trips أفضل من فهرس مصفوفة يبطله الحذف التالي.

يحمل صف التصدير القابل للدفاع الصفحة وsubtype والمؤلف ومعلومة الإنشاء إن وجدت ونص المحتوى وعمود الحالة الخاص بك. بالنسبة إلى المستندات القادمة من خارج الفريق، من المفيد تشغيل مرور الفهرسة نفسه أثناء intake triage؛ يوضح مقال PDF intake workbench ذلك النمط، ويغطي تنقل حقول النماذج المشكلة المرافقة لمراجعة مستندات تجمع بيانات بدلاً من التعليقات.

FAQ

لماذا يبقى highlight الذي أعدت تلوينه بلونه القديم؟

يكاد annotation أن يحمل /AP appearance stream، وترسمه العوارض المطابقة بدلاً من لون القاموس (ISO 32000-1 §12.5.5). الكتابة إلى السجل عبر Annotation[] ترفع EPdfError في هذه الحالة؛ تعامل مع الاستثناء كمصدر الحقيقة، لا مع اللون الذي كنت تنوي ضبطه.

لماذا لا تزال الصفحة تعرض annotation أزلته؟

يقوم DeleteAnnotation بتحديث نموذج المستند، لا raster المخزن مؤقتاً. أعد تصيير الصفحة باستخدام RenderPage بعد إزالة ناجحة، وأعد بناء إدخالات الفهرس لتلك الصفحة لأن فهارس annotations تنزاح إلى أسفل بعد الخانة المحذوفة.

هل تظهر annotations المسطّحة في مصفوفة annotations؟

لا. يحول flattening مظاهر annotations إلى محتوى صفحة عادي، فتتوقف عن كونها كائنات annotation أصلاً. إذا كان ملف العميل يعرض علامات مرئية لكن AnnotationCount يساوي صفراً، فالتسطيح upstream هو التفسير المعتاد؛ لم يبق شيء لمراجعته برمجياً.

سطح annotation API المستخدم في هذا المقال، من التعداد والإنشاء وإعادة التلوين والإزالة إلى خيارات التصيير التي تبقي العرض صادقاً، يأتي مع PDFium Component لـ Delphi وC++Builder وLazarus/FPC.