مقال تقني

Side-by-Side PDF Comparison in Delphi with PDFium Component

\n

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

تخطيط النموذج (Form Layout)

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

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

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;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  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 حتى لا يظهر أبدًا إطار يكون فيه صندوق أطول من الصناديق الأخرى مما قد يتسبب في وميض التخطيط

فتح مستند

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

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 := '';

  try
    PdfComponent.Active := True;
  except
    on E: EPdfError do
    begin
      if InputQuery('Password', 'Enter document password:', Password) then
      begin
        PdfComponent.Password := Password;
        PdfComponent.Active   := True;
      end
      else
        raise;
    end;
  end;

  if PdfComponent.Active then
  begin
    PdfViewComponent.PageNumber := 1;
    SetActivePdfView(PdfViewComponent);
  end;
end;

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

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

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

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

تُطبّق بعض العمليات على كافة اللوحات المرئية بدلاً من اللوحة النشطة فقط. يتحكم الحقل المنطقي 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 ما يُعرض على الشاشة. ولأغراض التنقل، فإنك تريد خاصية العرض وليس خاصية المستند

يمنح مربع اختيار يحمل اسمًا على غرار "مزامنة الصفحات" (Sync pages) المستخدم التحكم في المزامنة. عند إلغاء تحديده، سيتنقل كل لوح باستقلالية وسينهي المعالج عمله على الفور. هذه الاستقلالية مهمة لحالات الاستخدام التي تملك فيها كلا الوثيقتين عددًا مختلفًا من الصفحات، أو عندما يريد المستخدم العثور على فقرة مكافئة في ترجمة تبدأ من صفحة مختلفة. ففرض المزامنة دائمًا من شأنه أن يجعل الأداة أصعب في الاستخدام مقارنة بفتح ترتيب مكتبي بنافذتين بسيطتين

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

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

يحمل كل TPdfView خاصية تكبير خاصة به، في شكل 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;

يوفر عامل الضرب في 2 للعرض والارتفاع إخراجًا أكثر وضوحًا للمستندات التي تحتوي على نصوص دقيقة. ولا تعد أداة الحماية 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 عند تلك النقطة بدلاً من إظهاره عند بدء تشغيل التطبيق. وإذا أرسلت مُثبّت (installer)، فإن النهج الأكثر موثوقية يتمثل في نسخ ملف DLL إلى مجلد التطبيق أثناء عملية التثبيت بدلاً من الاعتماد على مجلد النظام الذي قد يمحوه المسؤول في وقت لاحق

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

للحصول على مزيد من التفاصيل حول مكوّن PDFium Component وواجهة برمجة التطبيقات الكاملة الخاصة به، قم بزيارة صفحة منتج Delphi PDFium Component

\n