مقال تقني

تظليل PDF غير إتلافي في Delphi: طبقة المراجعة في HotPDF

لا يجب أن يتحول مستطيل مرسوم حول فقرة أثناء المراجعة إلى علامة داخل ملف PDF. تعرض THPDFViewerModel في HotPDF الدالة AddHighlightRegion، وهي طريقة تحتفظ بكل تظليل كسجل في الذاكرة بدلًا من تغيير في المستند المحمَّل، بحيث يمكن للمراجِع وضع علامات على عشرات الصفحات بينما يبقى الملف على القرص مطابقًا بايتًا لبايت لما كان عليه. كبّر إلى 6400%، أدر الصفحة 90 درجة، وانتقل من ملاءمة العرض إلى ملاءمة الصفحة، وسيظل المستطيل نفسه يقع على الفقرة نفسها، لأن حسابات الإحداثيات تمر عبر هندسة الرسم الفعلية في اللحظة التي رُسمت فيها العلامة

تصطدم أدوات المراجعة المبنية حول عارض PDF بهذه المشكلة باستمرار. شاشة تصحيح (redlining)، مرور فحص جودة على فواتير مُولَّدة، سير عمل موافقة داخلي: كل هذه تحتاج إلى السماح لشخص بلفت الانتباه إلى منطقة في صفحة دون أن تتحول كل علامة مسودة إلى تغيير دائم في الملف، ودون اللجوء إلى نظام تعليقات توضيحية كامل فرعي لمجرد إظهار مربع ملون بينما لا يزال أحدهم يقرر ما إذا كانت العلامة مناسبة. يجيب HotPDF عن ذلك بطبقة تظليل مخصصة تقع بالكامل على جانب النموذج (Model) من الفصل الموصوف في بناء عارض PDF مخصص ببنية MVC في Delphi، وهذا أيضًا سبب إمكانية تشغيل قائمة التظليل نفسها من اختبار وحدة دون أي مقبض نافذة في الأفق

ما الذي تخزّنه AddHighlightRegion في HotPDF فعليًا؟

تخزّن AddHighlightRegion ثلاثة أشياء بالضبط لكل علامة: رقم صفحة يبدأ من صفر، وTHPDFRectangle بإحداثيات فضاء المستخدم في PDF، وTColor، وكلها معبأة كسجل THPDFViewerHighlight داخل THPDFViewerModel. استدعاء Viewer.HighlightRegion(PageIndex, PageRect, clYellow)، أو ما يعادله Model.AddHighlightRegion، يُلحق أحد هذه السجلات بمصفوفة خاصة ويُعيد فهرسه، وهذا الفهرس هو المقبض الوحيد الذي يحصل عليه المستدعي: لا يوجد كائن منفصل، ولا واجهة معدودة مرجعيًا، ولا شيء يجب تحريره. كل قدرة أخرى في هذه المقالة، رسم العلامة، وإعادة رسم إحداثياتها بعد تغيير التكبير، وحذفها، مبنية فوق ذلك السجل الصغير الواحد

يُطبَّع كل مستطيل ويُقصّ قبل قبوله. تبادل AddHighlightRegion الحافتين اليسرى واليمنى إذا سحب المراجِع من اليمين إلى اليسار، وتبادل الأعلى والأسفل لسحبة تصاعدية، ثم تقصّ النتيجة مقابل صندوق الوسائط (MediaBox) الخاص بالصفحة المسترجَع عبر GetLoadedPageBox. أي مستطيل ينتهي بعرض صفري، أو ارتفاع صفري، أو خارج الصفحة كليًا يُرفض فورًا: تُعيد الطريقة القيمة -1 ولا يُضاف شيء إلى القائمة. تلك القيمة المُعادة ليست زخرفية: دفعة من التظليلات المعاد بناؤها من ملف مراجعة خارجي، أو من إحداثيات قديمة بعد استبدال صفحة، يمكن أن تفقد إدخالات بصمت إذا لم يتحقق المستدعي من ذلك

كيف يبقى التظليل محاذيًا بعد التكبير أو الدوران؟

يبقى التظليل محاذيًا لأن HotPDF يخزّنه في فضاء صفحة PDF ويعيد إسقاطه في فضاء الشاشة عند كل إعادة رسم، بدلًا من تخزين مستطيل شاشة سيصبح قديمًا بمجرد تغيّر مستوى التكبير. تقوم THPDFViewerModel.PagePointToView ومعكوسها ViewPointToPage بذلك الإسقاط على مرحلتين: أولًا إدخال /Rotate الخاص بالصفحة نفسها، ثم ViewRotation المستقل الخاص بالعارض، الذي لا يُكتب مطلقًا مرة أخرى إلى PDF ولا يؤثر إلا على ما يعرضه العارض. التراجع عن التحويل عند رفع الماوس يُشغّل المرحلتين نفسيهما بترتيب عكسي، وهذا ما يسمح لتظليل مرسوم عند تكبير عالٍ على صفحة مُدارة 270 درجة بأن يقع في المكان الصحيح تمامًا بعد أن يعيد المراجِع ضبط العرض إلى ملاءمة الصفحة

دقة النقاط في البوصة المستخدمة لذلك الإسقاط تهم بقدر أهمية الدوران. يلتقط عارض HotPDF دقة النقاط في البوصة الدقيقة للصورة النقطية المعروضة حاليًا على الشاشة في FRenderedDPI فور كل عملية رسم، وتمرر ImageMouseUp القيمة نفسها إلى ViewPointToPage بحيث يُحوَّل إحداثي الماوس دائمًا باستخدام الدقة التي رُسم بها فعليًا، لا دقة مُعاد حسابها من خاصية التكبير الحالية. تحدّ CreatePageSnapshot وأقاربها دقة النقاط في البوصة إلى نطاق من 12 إلى 2400، لكن مسار الرسم التفاعلي لا يحمل سقفًا كهذا: يبلغ سلّم التكبير القياسي أعلاه 6400%، وهو ما يُحسَب إلى أكثر بكثير من 2400 نقطة في البوصة عند خط الأساس الافتراضي 96 نقطة في البوصة، لذا فإن إعادة استخدام حد بأسلوب اللقطة (snapshot) لتحويل الإحداثيات كان سيزيح كل تظليل عدة بكسلات عند أعلى نطاق التكبير. افتراضيان أصغران يكملان التفاعل: سحبة أقصر من بكسلين على أي محور تُعامل كنقرة ولا تنتج تظليلًا، ولا يمكن أن يبدأ التظليل حتى تُرسم صفحة واحدة فعليًا على الأقل، بما أن FRenderedDPI يبدأ من صفر

ربط التظليل التفاعلي بشاشة مراجعة

تفعيل التظليل التفاعلي مهمة بثلاث خصائص على عنصر التحكم THPDFViewer نفسه: اضبط InteractionMode على vimHighlight بدلًا من الافتراضي vimBrowse، واختر HighlightColor، الذي يكون افتراضيًا clYellow، وعالج OnMarqueeSelect لمعرفة ما رسمه المراجِع للتو. كل شيء آخر، التقاط الماوس، ورسم مستطيل التحديد المنقّط أثناء سحب المراجِع، وتحويل نقطة الإفلات مرة أخرى إلى فضاء الصفحة، واستدعاء AddHighlightRegion، يحدث داخل عنصر التحكم قبل إطلاق ذلك الحدث

type
  TReviewForm = class(TForm)
    Viewer: THPDFViewer;
    ReviewLog: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
      PageIndex: Integer; const PageRect: THPDFRectangle;
      HighlightIndex: Integer);
  end;

// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
  Viewer.PDFDocument := PdfDoc;
  Viewer.InteractionMode := vimHighlight;
  Viewer.HighlightColor := clLime;
  Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;

procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
  PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
  ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
    [PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
     PageRect.Right, PageRect.Top]));
end;

لا يُطلَق OnMarqueeSelect إلا لسحبة أنتجت تظليلًا فعليًا: نقرة صغيرة جدًا بحيث لا تُحتسب سحبة تمسح طبقة التحديد فورًا، وسحبة تقع خارج الصفحة كليًا تصل إلى AddHighlightRegion لكنها تُرفض هناك بالطريقة نفسها التي يُرفض بها استدعاء برمجي، لذا يبقى الحدث صامتًا في الحالتين. تفصيل تنفيذي يستحق المعرفة إذا بدا التظليل يتوقف عن الاستجابة عند حواف عنصر التحكم: التقاط الماوس ينتمي إلى THPDFViewer نفسه، وهو سليل لـTScrollBox، لا إلى TImage الداخلي الذي يعرض صورة الصفحة النقطية، وهذا ما يسمح لمراجِع بالسحب متجاوزًا حافة الصفحة المرسومة والحصول رغم ذلك على إفلات نظيف

إضافة التظليلات وحذفها وإعادة قراءتها من الشيفرة

لا يجب أن تأتي التظليلات من سحبة ماوس أصلًا. Viewer.HighlightRegion(PageIndex, PageRect, Color)، التي تصبّ في Model.AddHighlightRegion نفسها التي تستدعيها السحبة التفاعلية داخليًا، عامة تحديدًا كي تتمكن شاشة مراجعة من إعادة بناء التظليلات من بيانات تملكها بالفعل: تعليقات محمَّلة من قاعدة بيانات، أو نتائج من بحث نصي، أو علامات مستعادة من جلسة سابقة. ولأن الإحداثيات أرقام فضاء مستخدم PDF بسيطة، لا يعتمد شيء في هذا المسار على رسم صفحة أولًا، خلافًا للسحبة التفاعلية، التي تحتاج FRenderedDPI لتحمل قيمة حقيقية بالفعل

var
  I: Integer;
  Item: TPriorComment;    // your own record: PageIndex + PageRect
  NewIndex: Integer;
begin
  for I := 0 to PriorComments.Count - 1 do
  begin
    Item := TPriorComment(PriorComments[I]);
    NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
    if NewIndex < 0 then
      LogWarning('comment %d fell outside the page and was dropped', [I]);
  end;
end;

حذف تظليل واحد هو حيث يظهر التخزين المدعوم بمصفوفة بوضوح. تحذف RemoveHighlightRegion سجلًا واحدًا وتزيح كل سجل لاحق منزلة واحدة لسدّ الفجوة، ما يعني أن أي فهرس التُقط سابقًا، من حدث OnMarqueeSelect أو من تعداد سابق، لم يعد موثوقًا بمجرد حذف شيء يسبقه في القائمة. يُطلَق OnHighlightChange عند كل إضافة وحذف واستدعاء ClearHighlightRegions، لكنه لا يحمل أي معلومة عمّا تغيّر، لذا فإن النمط الآمن هو معاملته كإشارة لإعادة بناء أي قائمة تعرضها لوحة مراجعة من HighlightCount وTryGetHighlightRegion، بدلًا من ترقيع فهرس مخزَّن مؤقتًا في مكانه

procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
  I: Integer;
  Mark: THPDFViewerHighlight;
begin
  MarkList.Items.Clear;
  for I := 0 to Viewer.Model.HighlightCount - 1 do
    if Viewer.Model.TryGetHighlightRegion(I, Mark) then
      MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
        TObject(I));
end;

متى يجب أن تصبح العلامة تعليقًا توضيحيًا حقيقيًا للتظليل بدلًا من ذلك؟

يجب أن تصبح منطقة تظليل تعليقًا توضيحيًا حقيقيًا في اللحظة التي تحتاج فيها للبقاء خارج نطاق مثيل THPDFViewer ذلك الواحد. يعرض HotPDF أيضًا AddHighlightAnnotation لصفحة جديدة وAddLoadedHighlightAnnotation لمستند محمَّل بالفعل، ورغم التشابه شبه التام في الاسم، فإن هذه آلية مختلفة تمامًا: تكتب كلتاهما تعليقًا توضيحيًا فعليًا من نوع تظليل نص (text-markup) وفق ISO 32000-1 §12.5.6.10، أي /Subtype /Highlight في PDF، داخل مصفوفة /Annots الخاصة بالصفحة، مع /QuadPoints يحدد سلسلة الحروف بالضبط، وأي عارض PDF متوافق يرسمها بمجرد حفظ الملف، لا HotPDF وحده. الحد الفاصل نفسه بين الآليتين هو ما يقرر ما إذا كانت العلامة ستعود ذهابًا وإيابًا عبر XFDF: التعليق التوضيحي المُنشأ عبر AddLoadedHighlightAnnotation هو كائن PDF عادي تلتقطه ExportLoadedAnnotationsToXFDF وتسلّمه إلى Acrobat أو أداة مراجعة أخرى كترميز ISO 19444-1، المشروح في استيراد وتصدير تعليقات PDF التوضيحية كـXFDF في Delphi، بينما تكون منطقة أُضيفت عبر AddHighlightRegion غير مرئية لذلك التصدير لأنها لم تُكتب إلى رسم الكائنات إطلاقًا: فهي موجودة فقط طالما بقي THPDFViewerModel الذي أنشأها. العائلة الكاملة من أنواع التعليقات التوضيحية النصية والهندسية المتاحة على صفحة، وكيف يضع كل مستطيل كل نوع منها، مشروحة في مقالة التعليقات التوضيحية في PDF مع HotPDF في Delphi، والقاعدة العملية بسيطة: أبقِ العلامة قابلة للتخلص منها طالما لا يزال المستند قيد النقاش، والتزم بتعليق توضيحي بمجرد اتخاذ قرار نهائي

أين تتوقف طبقة التظليل

لا تحاول طبقة التظليل، من جهتها، أن تبدو كقلم تظليل شبه شفاف: ترسم RefreshDocument كل منطقة كمستطيل حدود بعرض بكسلين بلونها الخاص فوق صورة الصفحة النقطية المخزَّنة مؤقتًا، بالطريقة نفسها التي ترسم بها نتائج البحث، بدلًا من مزج تعبئة ملونة فوق النص أسفلها، لذا فإن مظهر التظليل الأصفر الكلاسيكي يجب رسمه في شيفرة التطبيق أو تأجيله إلى دفق مظهر (appearance stream) خاص بتعليق توضيحي مُرقّى. قدرة واحدة تستحق إعادة الاستخدام بمجرد وجود منطقة هي CreateCurrentPageRegionSnapshot، التي تأخذ THPDFRectangle نفسه الذي يحمله تظليل بالفعل وترسم تلك المنطقة فقط إلى صورة نقطية، مفيدة لإرفاق صورة معاينة صغيرة بتعليق مراجعة دون تصدير الصفحة كاملة. لا يحتاج بناء مراجعة إلى الاختيار بين الآليتين مسبقًا: اجعل كل علامة جديدة افتراضيًا منطقة THPDFViewerHighlight قابلة للتخلص طالما بقي خيط تعليق مفتوحًا، ولا تستدعِ AddLoadedHighlightAnnotation إلا بمجرد أن يحسم المراجِع أمرها، ما يبقي ملف PDF المحمَّل دون مساس أثناء التبادل ذهابًا وإيابًا الذي ينتج أكبر قدر من التغيّر. عنصر تحكم العارض الموصوف هنا جزء من مكوّن HotPDF القياسي لـDelphi وC++Builder، إلى جانب بقية واجهات التعليقات التوضيحية والنماذج المُشار إليها أعلاه