مقال تقني

بناء عارض PDF في Delphi مع PDFium Component

يتلخص عارض PDF في Delphi في مكونين والتوصيل بينهما. يمتلك TPdf المستند: فهو يفتح الملف، ويفك تشفيره، ويجيب على أسئلة حول عدد الصفحات والبيانات الوصفية. TPdfView هو عنصر التحكم المرئي الذي يرسم الصفحات على الشاشة ويتعامل مع التمرير، والتكبير/التصغير، والصفحة التي ينظر إليها المستخدم حاليًا. يغلف PDFium Component نفس محرك العرض الذي يتم شحنه داخل Chrome، وبالتالي فإن الصور الرمزية، وصقل الحواف، واللون الذي تحصل عليه على القماش يتطابق مع ما يراه المستخدمون بالفعل في متصفحهم. العمل ليس في العرض. بل يكمن في توصيل كائن المستند بالعرض، والتحميل دون التعطل بسبب ملف تالف أو محمي بكلمة مرور، وإعطاء المستخدم حفنة من عناصر التحكم التي تجعل العارض يبدو مكتملًا: تقليب الصفحة، وتغيير التكبير/التصغير، وملاءمة الصفحة مع النافذة

يستعرض هذا عملية التجميع بالترتيب الذي ستقوم بالبناء به فعليًا. كل شيء هنا يعرض صفحة واحدة في كل مرة، وهو ما تريده معظم مسارات عمل المستندات. إذا كنت بحاجة إلى تكديس الصفحات في عمود تمرير مستمر واحد، فهذا قرار تخطيط مختلف وليس المسار هنا

توصيل TPdf بـ TPdfView

أسقط TPdf و TPdfView على النموذج، ثم أخبر العرض بالمستند الذي يجب عرضه. هذا التعيين الفردي هو الرابط بأكمله بين المستند غير المرئي وعنصر التحكم الذي يرسمه

معمارية عارض PDF في Delphi يمتلك فيها TPdf المستند ويرسمه TPdfView ويربطهما إسناد خاصية واحد فوق ملف DLL الخاص بـ PDFium
يمتلك TPdf المستند بينما يرسمه TPdfView، ويسلك إسناد واحد الطريق بين الاثنين فوق محرك PDFium المشترك
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // تم إسقاط Pdf و PdfView في وقت التصميم.
  PdfView.Pdf := Pdf;                 // يعرض العرض ما يحمله هذا المستند
  PdfView.FitMode := pfmFitWidth;     // ابدأ المستخدم بتكبير معقول
end;

قبل تشغيل أي من هذا، يجب أن تكون مكتبة PDFium الأصلية موجودة على الجهاز. يستدعي PDFium Component ملف pdfium32.dll أو pdfium64.dll اعتمادًا على النظام الأساسي المستهدف، ويرفض المستند ببساطة الفتح إذا تعذر العثور على ملف DLL. قم بشحن ملف DLL المطابق بجانب الملف القابل للتنفيذ الخاص بك، أو ضعه حيث سيجده محمل النظام. لا توجد الإصدارات التي تدعم V8 إلا لملفات PDF التي تحمل JavaScript ترغب في تنفيذه، وهو ما لا يفعله عارض عادي، لذا استخدم ملف DLL القياسي ما لم يكن لديك سبب ملموس لعدم القيام بذلك

تحميل مستند دون الوثوق بالمدخلات

الغريزة هي تغليف التحميل في try/except ومعاملة الاستثناء المطروح كفشل. هذه الغريزة خاطئة هنا، والخطأ فيها ينتج عارضًا يبدو جيدًا حتى يسلمه شخص ما ملفًا مكسورًا. لا يؤدي تعيين Active := True إلى طرح استثناء عند فشل التحميل. يكتشف PDFium Component الخطأ الداخلي ويترك Active على الحالة False، لذا فإن الطريقة الصادقة الوحيدة لمعرفة ما إذا كان المستند قد فُتح هي إعادة قراءة الخاصية بعد تعيينها

تدفق قرار تحميل لعارض PDFium في Delphi حيث لا يرفع ضبط Active استثناءً أبدًا، وتعني القيمة false الصامتة كلمة مرور خاطئة أو ملفًا تالفًا، وتتبعها محاولة كلمة مرور واحدة
لا يرفع التفعيل استثناءً عند الإخفاق أبداً، فيقرأ العارض Active راجعةً ويرد على false الصامتة بمحاولة كلمة مرور واحدة
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // لا يطرح استثناء؛ الفشل يترك Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // يتتبع العرض صفحته الحالية بنفسه
  UpdatePageLabel;
end;

هناك شيئان يستحقان الاهتمام. الأول هو أن PageNumber موجود في كلا الكائنين وهما مستقلان. Pdf.PageNumber هو مفهوم المستند للصفحة الحالية؛ PdfView.PageNumber هي الصفحة التي يعرضها عنصر التحكم فعليًا، وهي الصفحة التي قمت بتعيينها لنقل المستخدم عبر الملف. تعيين أحدهما لا يحرك الآخر، لذلك يقوم العارض دائمًا بقيادة خاصية العرض. الثاني هو الفهرسة المستندة إلى 1: تعمل الصفحات من 1 إلى Pdf.PageCount، وليس من 0، مما يوقع أي شخص معتاد على المصفوفات المستندة إلى الصفر في الخطأ

التعامل مع ملف مشفر

تُطوى المستندات المشفرة في نفس مسار التحميل. إذا تم تعيين كلمة مرور الفتح قبل التنشيط، فسيتم فك تشفير المستند عند فتحه؛ وإذا كانت خاطئة أو مفقودة، فستظل Active بحالة False تمامًا كما تفعل مع ملف تالف. لذا فإن الاسترداد يتمثل في المطالبة بكلمة مرور ومحاولة التنشيط مرة أخرى

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // يجب ضبطها قبل Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

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

تصفح المستند

مع فتح المستند، يكون التنقل حسابيًا على PdfView.PageNumber محددًا بـ Pdf.PageCount. العمل الحقيقي الوحيد هو التثبيت، لذلك لا تدفع الأزرار الصفحة أبدًا خارج النطاق وتظل أزرار الأول والأخير معطلة في نهايات الملف

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// أزرار التنقل الأربعة تختزل إلى استدعاء واحد لكل منها
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

يعد مربع نص "انتقال إلى الصفحة N" هو نفس استدعاء GoToPage المغذى من عدد صحيح تم تحليله، ويغطي التثبيت الحالة التي يكتب فيها المستخدم 9999 في ملف مكون من عشر صفحات. احتفظ بـ UpdatePageLabel باعتباره المكان الوحيد الذي يكتب "الصفحة 3 من 12" حتى لا تخرج القراءة عن التزامن مع ما يظهره العرض أبدًا

التكبير/التصغير: النسب المئوية الصريحة وأوضاع الملاءمة

يصل التكبير/التصغير في TPdfView بنكهتين تتفاعلان، وفهم التفاعل هو الفرق بين تحكم التكبير الذي يتصرف بشكل جيد والتحكم الذي يحارب المستخدم. المسار المباشر هو خاصية Zoom، وهي نسبة مئوية حيث يعني 100 الحجم الفعلي. المسار الآخر هو FitMode، والذي يخبر العرض بحساب التكبير نيابة عنك والاستمرار في إعادة حسابه عند تغيير حجم النافذة

تفاعل التكبير و FitMode في عارض PDFium داخل Delphi حيث يمحو إسناد Zoom دقيق FitMode إلى pfmNone ويعيد اختيار وضع ملاءمة التكبير إلى العرض
إسناد تكبير دقيق يمسح وضع الملاءمة وانتقاء وضع ملاءمة يعيد حساب التكبير إلى الرؤية
// تكبيرات ثابتة
PdfView.Zoom := 100;     // الحجم الفعلي
PdfView.Zoom := 50;      // النصف
PdfView.Zoom := 200;     // الضعف

// اجعل العرض يضبط حجم الصفحة على النافذة ويحافظ عليه عند تغيير حجمها
PdfView.FitMode := pfmFitWidth;   // عرض الصفحة يملأ عنصر التحكم
PdfView.FitMode := pfmFitPage;    // الصفحة كاملة مرئية
PdfView.FitMode := pfmActualSize; // 1:1 مع نقاط المستند

إليك الجزء الذي يوقع الناس في الخطأ. يؤدي تعيين Zoom مباشرة إلى إعادة تعيين FitMode إلى pfmNone. هذا سلوك صحيح، وليس خطأ: في اللحظة التي يختار فيها المستخدم 150٪ بالضبط، لم يعد بإمكان العرض أن يحترم "الملاءمة للعرض"، لأن الطلبين يتعارضان. النتيجة على واجهة المستخدم الخاصة بك هي أن زر التكبير وزر ملاءمة الصفحة هما حالتان متنافيتان، ويجب أن يجعل شريط الأدوات الوضع النشط مرئيًا. عندما ينقر المستخدم على الملاءمة للصفحة، قم بتعيين FitMode؛ وعندما ينقرون على تكبير/تصغير رقمي، قم بتعيين Zoom واتركه يمسح وضع الملاءمة بمفرده

إذا كنت تفضل حساب قيمة الملاءمة بنفسك، ربما لبذر منزلق التكبير بالنسبة المئوية الحالية للملاءمة، فإن المساعدين لكل صفحة يمنحونك الأرقام دون تغيير الوضع. تُرجع PageWidthZoom[N] و PageZoom[N] و ActualSizeZoom[N] النسبة المئوية التي من شأنها ملاءمة الصفحة N للعرض، أو ملاءمتها بالكامل، أو عرضها بحجمها الفعلي

// زرع قراءة تكبير من قيمة الملاءمة للعرض للصفحة الحالية
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

ما يحتاجه العارض النهائي حقًا

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

من هذه القاعدة، تكون الإضافات تزايدية وليست هيكلية. يقرأ تحديد النص والبحث من نفس طبقة النص التي يبنيها PDFium بالفعل؛ وتكون البيانات الوصفية مثل Pdf.Title و Pdf.Author على بُعد قراءة خاصية واحدة؛ والتناوب والتدرج الرمادي عبارة عن خيارات عرض تمررها عندما ترسم صفحة إلى صورة نقطية. لا يغير أي من ذلك العمود الفقري الذي لديك هنا، وهو كائن المستند والعرض وتدفق التحميل ثم التنقل الذي يربط بينهما. اجعل ذلك العمود الفقري صحيحًا والباقي مجرد زخرفة

مكونات TPdf و TPdfView المستخدمة طوال الوقت هي جزء من PDFium Component لـ Delphi و C++Builder، والذي يحمل مرجع العارض الكامل على صفحة المنتج الخاصة به