مقال تقني

مقارنة PDF جنبًا إلى جنب في Delphi مع PDFium Component

مستندان مفتوحان في آن واحد، ورقم الصفحة نفسه، وكل منهما في لوحة قابلة للتمرير خاصة به: هذا هو جوهر عارض المقارنة. ويقدّم PDFium Component ذلك عبر نموذج كائنات مباشر يملك فيه TPdf الملف ويملك TPdfView العرض. مستند واحد، وTPdf واحد، وTPdfView واحد. وإن أردت ثلاث لوحات فلديك ثلاثة أزواج. والأجزاء الصعبة ليست استدعاءات الـ API، بل حساب التخطيط عند تغيير حجم النافذة ومنطق مزامنة الصفحات حين تقرر أي عرض يتبع أيًّا

تخطيط النموذج

يحتوي نموذج VCL على ثلاث حاويات TScrollBox جنبًا إلى جنب، في كل منها TPdfView بمحاذاة alClient كي يملأ الحاوية. ويقع بين الحاويات مكوّنان من نوع TSplitter ليتمكن المستخدم من ضبط عرض الأعمدة أثناء التشغيل. ويحمل شريط أدوات أعلى اللوحات أزرار الفتح وعناصر التحكم في التكبير ومفتاح التبديل بين عرضين وثلاثة عروض

وضع العروض الثلاثة قيمة منطقية يتتبعها النموذج داخليًا. وعند انقلابها تعيد حساب العروض وتُظهر العمود الثالث أو تخفيه. وأبسط نهج هو مسح كل خصائص Align، وإخفاء الفواصل، ثم تعيين المواضع المطلقة:

مخطط تخطيط النموذج لعارض مقارنة PDF جنبًا إلى جنب في Delphi مبني على PDFium Component، ويعرض شريط أدوات وثلاث حاويات تمرير بلوحات TPdfView وفواصل في وضع العرضين ووضع العروض الثلاثة
كل لوحة هي حاوية تمرير بداخلها TPdfView، والتبديل بين عرضين وثلاثة عروض ليس سوى مجموعة مختلفة من إسنادات العرض
procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // طبّق القيمة نفسها (ClientHeight - ارتفاع شريط الأدوات) على قيم Height الثلاث
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

تعيين Align := alNone على الحاويات الثلاث قبل الحساب على الأعداد الصحيحة يمنع محرك القيود في VCL من مقاومة إسناداتك. وأعِد إظهار الفواصل بعد تحديد المواضع إن أردت تغيير الحجم بالسحب في وضع العرضين

ارتفاع كل حاوية تمرير هو منطقة العميل ناقص ارتفاع لوحة شريط الأدوات. ولأن شريط الأدوات مرسّى في الأعلى بـ alTop، فإن ClientHeight - PanelButtons.Height يعطيك المساحة الرأسية القابلة للاستخدام. أسنِد هذه القيمة إلى الحاويات الثلاث داخل استدعاء UpdateLayout نفسه حتى لا يمر إطار واحد تكون فيه حاوية أطول من الأخريات فتسبب ارتعاشًا في التخطيط

فتح مستند

يحتاج كل زوج من اللوحات إلى إجراء فتح خاص به. والنمط قصير: عطّل المكوّن، وعيّن اسم الملف، وفعّله، ثم افحص Active؛ وإن بقي False فاطلب كلمة مرور وأعد المحاولة. ولاحظ أن TPdfView.Active هو ما يتحكم في العرض، بينما TPdf.Active هو ما يفتح الملف فعليًا؛ وهما مستقلان. وتعيين PdfView.Active := True بينما TPdf المرتبط به غير مفعّل بعدُ غير ضار لكنه لا يعرض شيئًا

مخطط انسيابي لفتح مستند PDF بـ PDFium Component في Delphi، ويعرض الفحص الصامت لقيمة Active ومحاولة واحدة بكلمة مرور وحوار خطأ للملفات التالفة أو المحمية بكلمة مرور
يترك التحميل الفاشل قيمة Active على False دون إطلاق استثناء، ولذلك يفحصها المسار ويعيد المحاولة مرة بكلمة مرور ثم يبلّغ عن المشكلة بدل عرض لوحة فارغة
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // إخفاقات التحميل صامتة: تبقى Active على False بدل إطلاق استثناء.
  if not PdfComponent.Active then
  begin
    // الأرجح أنه ملف محمي بكلمة مرور؛ امنح المستخدم محاولة واحدة.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

افحص دائمًا PdfComponent.Active بعد الإسناد؛ فالملف التالف أو كلمة المرور الخاطئة تجعل التحميل يفشل بصمت دون إطلاق استثناء في المسار الافتراضي. وتعيين PdfViewComponent.PageNumber := 1 صراحةً بعد فتح ناجح يتجنّب بقاء رقم صفحة قديم من المستند السابق

حوار الرسالة في النهاية مقصود: فأنت تريد للملفات التالفة أو غير المدعومة أن تظهر فورًا بدل أن تُبتلع في صورة لوحة فارغة صامتة. فالمستخدم الذي لا يرى شيئًا لا يعرف إن كان الملف قد حُمّل وهو فارغ ببساطة، أم أن المكوّن رفضه. والإبلاغ عن الإخفاق يبقي الخطأ ظاهرًا

تتبّع اللوحة النشطة

عندما ينقر المستخدم داخل لوحة تصبح تلك اللوحة نشطة. ويتتبّع النموذج حقلًا خاصًا هو FActivePdfView: TPdfView. والتغذية الراجعة البصرية هي تغيير لون الحد على TScrollBox الحاوية: عيّنه إلى clHighlight للنشطة وإلى clWindow لغيرها. واربط ذلك بكل TPdfView.OnClick وبإجراء الفتح كي يتبع التركيز المستند الذي فتحته للتو

تنطبق بعض العمليات على كل اللوحات المرئية لا على النشطة وحدها. وتقود ذلك الفرعَ قيمةٌ منطقية على النموذج اسمها FAllViewsMode. وحين تكون صحيحة تنتشر تغييرات التكبير والتنقل بين الصفحات إلى كل لوحة فيها مستند مفعّل:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

تنقل متزامن بين الصفحات

التنقل المتزامن اختياري لكنه مفيد في مسارات مراجعة المستندات حيث يغطي الملفان نطاق الصفحات نفسه. ومكان هذا المنطق هو معالج حدث يُطلق بعد أن ينتقل المستخدم في أحد العروض. وحين يغيّر عرض المصدر قيمة PageNumber لديه، ينشر المعالج ذلك الرقم إلى العروض الأخرى بشرط واحد: أن يملك العرض الهدف هذا العدد من الصفحات على الأقل، وإلا فتجاوزه

خاصية PageNumber في TPdfView وفي TPdf مستقلتان. فـ TPdf.PageNumber يتتبّع الصفحة التي يعدّها مكوّن المستند حالية، بينما TPdfView.PageNumber يتتبّع ما يُعرض على الشاشة. ولأغراض التنقل تريد خاصية العرض لا خاصية المستند

ويمنح المستخدمَ التحكمَ مربعُ اختيار بعنوان من قبيل "مزامنة الصفحات". وحين لا يكون محددًا، تتنقل كل لوحة باستقلال ويخرج المعالج فورًا. وهذا الاستقلال مهم في الحالات التي يختلف فيها عدد صفحات المستندين، أو التي يريد المستخدم فيها العثور على المقطع المكافئ في ترجمة تبدأ من صفحة مختلفة. وفرض المزامنة دائمًا يجعل الأداة أصعب استخدامًا من مجرد ترتيب نافذتين على سطح المكتب

ثمة أمر ينبغي الانتباه له: تعيين PdfView.PageNumber برمجيًا داخل معالج المزامنة يُطلق بدوره حدث التغيير على ذلك العرض. فاحتَط من العودية اللانهائية براية منطقية ترفعها قبل الإسناد وتخفضها مباشرة بعده. والراية على مستوى النموذج لا على مستوى العرض، لأن العروض الثلاثة تتشارك المعالج نفسه

مخطط للتنقل المتزامن بين الصفحات في عارض مقارنة PDF بـ Delphi يستخدم PDFium Component، مع مربع اختيار المزامنة وحارس لعدد الصفحات في كل عرض هدف وراية حماية من العودية
ينتقل رقم الصفحة من عرض المصدر إلى كل عرض آخر فقط عند تفعيل المزامنة وحين يحتوي العرض الهدف فعلًا على تلك الصفحة

التكبير لكل لوحة

يحمل كل TPdfView خاصية Zoom خاصة به، وهي Double بالنسبة المئوية حيث يعني Zoom := 100 الحجم الفعلي (100%). وتعيينها يتجاوز أي FitMode مفعّل. ولزر ملاءمة العرض على اللوحة النشطة، اقرأ نسبة الملاءمة من PdfView.PageWidthZoom[PdfView.PageNumber] وأسندها. وللملاءمة على الصفحة استخدم PageZoom[PageNumber]. وكلتاهما خاصية مصفوفة مفهرسة برقم صفحة يبدأ من 1، فاحترس من رقم صفحة يساوي صفرًا قبل الوصول إليهما

عند تصدير الصفحة الحالية إلى صورة، اقرأ التدوير من العرض لكن استدعِ RenderPage على مكوّن TPdf لا على العرض. وصيغة الصورة النقطية من TPdf.RenderPage تأخذ أبعادًا صريحة بالبكسل إضافة إلى قيمة TRotation ومجموعة TRenderOptions. أما صيغة الدالة فتعيد TBitmap يملكه المستدعي وتحرره أنت بنفسك بعد الحفظ:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

المضاعِف 2x على العرض والارتفاع يعطي خرجًا أوضح للمستندات ذات النص الدقيق. وكتلة try/finally حول تحرير الصورة النقطية ليست اختيارية؛ فإلغاء TSaveDialog يمر بكتلة finally أيضًا، وأنت تريد تحرير الصورة النقطية مهما فعل المستخدم

متطلبات ملف DLL

يغلّف PDFium Component مكتبة pdfium الأصلية. فالعملية المضيفة بمعمارية 32 بت تحتاج إلى pdfium32.dll، والمضيف بمعمارية 64 بت يحتاج إلى pdfium64.dll. والنسخ التي تتضمن محرك JavaScript المسمى V8 تضيف اللاحقة v8 وتزن نحو 23-27 ميجابايت مقابل 5-6 ميجابايت للبنى القياسية. ولعارض مقارنة يعطّل تعبئة النماذج (Pdf.FormFill := False) تكفي البنية القياسية بلا V8 وتُبقي حزمة التوزيع أصغر

ضع ملف DLL في المجلد نفسه الذي فيه الملف التنفيذي، أو في أي مجلد ضمن PATH النظام. ويحمّله المكوّن عند الطلب حين يُفعَّل أول TPdf، ولذلك يظهر غيابه عند تلك اللحظة لا عند بدء التطبيق. وإن كنت تشحن مثبّتًا، فأوثق نهج هو نسخ ملف DLL إلى مجلد التطبيق أثناء التثبيت بدل الاعتماد على مجلد نظام قد ينظّفه مسؤول لاحقًا

بنى V8 مفيدة أساسًا حين تحتاج إلى التعامل مع إجراءات JavaScript داخل PDF، مثل تشغيل حقول الحساب أو معالجات الإرسال. أما عارض المقارنة السلبي فلا سبب لديه لتشغيل JavaScript؛ وتعيين Pdf.FormFill := False قبل Active := True يتخطى بيئة تعبئة النماذج بالكامل، ما يعني أيضًا عدم تهيئة محرك JS حتى مع استخدام البنية القياسية. وذلك هو الافتراضي الصحيح لعارض للقراءة فقط أيًّا كانت نسخة DLL التي تشحنها

لمزيد من التفاصيل عن PDFium Component وواجهته البرمجية الكاملة، تفضل بزيارة صفحة منتج Delphi PDFium Component