مقال تقني

عارض PDF لـ Lazarus وFree Pascal باستخدام PDFium

بدا النقل منتهياً يوم الجمعة. عارض PDF في Delphi، نُقل إلى Lazarus خلال يومين: بضعة تغييرات في أسماء الوحدات، وبعض كتل {$IFDEF FPC}، والمشروع يترجم بلا أخطاء. اختبار يوم الاثنين روى قصة مختلفة: مربع البحث لا يعيد نتائج لكلمات ظاهرة بوضوح على الصفحة، واسم ملف يحتوي على أحرف مشددة يرفض الفتح. لم يلمّح compiler output إلى أي فشل، لأن كليهما كان مشكلة string encoding، وعدم تطابق الترميز لا يُرى حتى تتدفق بيانات حقيقية. نقل عارض مبني على PDFium Component، الذي يوفّر إصداري VCL وLCL من مصدر واحد، عمل ميكانيكي في معظمه؛ والأجزاء غير الميكانيكية هي بالضبط موضوع هذا المقال.

Pascal نفسه، لكن حمولة النص مختلفة

أصبحت string الأصلية في Delphi بنمط UTF-16 منذ 2009. أما Lazarus وFree Pascal فيستخدمان UTF-8 افتراضياً في تطبيقات LCL. واجهات المكوّن التي تتعامل مع النص تتحدث UTF-16 عبر النوع WString، الذي يعرّفه build الخاص بـ FPC كمرادف لـ WideString؛ لذلك فإن كل حد يعبر فيه النص بين UI الخاص بـ LCL ومحرك PDF هو نقطة تحويل.

تتم التحويلات تلقائياً في الإسنادات المباشرة، لكن عادتين تمنعان فئة أخطاء صباح الاثنين. مرّر النص كما هو من دون معالجة على مستوى bytes: الكود الذي يقتطع عبارة بحث بإزاحة byte يعمل في Delphi، حيث Char واحد هو وحدة UTF-16 واحدة، ويفسد UTF-8 متعدد bytes في LCL. واختبر ببيانات غير ASCII من اليوم الأول: اسم ملف ألماني، وعبارة بحث تحتوي على em dash، لأن بيانات الاختبار ASCII فقط تجعل كل أخطاء الترميز غير مرئية.

طبقة conditional صغيرة بدلاً من fork

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

{$IFDEF FPC}
uses
  LCLType, Forms, Graphics, Controls;

type
  WString = WideString;   // component text APIs are UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

كل ما تحت هذه الكتلة، من معالجة المستندات والتنقل بين الصفحات إلى استدعاءات التصيير، يترجم بالطريقة نفسها في IDEين، لأن TPdf وTPdfView يقدمان السطح نفسه في إصداري VCL وLCL. الانضباط الذي يبقي ذلك صحيحاً بنيوي: منطق PDF المشترك يعيش في وحدات لا تحتوي على dialogs أو panels خاصة بإطار معين، وكل ما يختلف فعلاً، مثل print dialogs وfile pickers ذات أعراف المنصة، يعيش خلف interface رقيق منفذ مرة لكل framework. كتلة IFDEF أعلاه هي أيضاً المكان الصحيح لأي اختلاف منصة مستقبلي، ما يبقي بقية الكود خالية من compiler conditions المبعثرة.

ابنِ النموذج بالكود، لا بمصممين اثنين

Form streaming هو الموضع الذي تتعفن فيه المشاريع ثنائية IDE بصمت: ملف .dfm وملف .lfm يصفان "النموذج نفسه"، ثم ينجرفان property بعد property حتى يتصرف buildان بشكل مختلف لأسباب لا يستطيع أحد diff لها. بالنسبة إلى قلب العارض، يتجاوز البناء وقت التشغيل المشكلة كلها: تسلسل constructor واحد محفوظ ككود عادي:

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  Pdf := TPdf.Create(Self);

  PdfView := TPdfView.Create(Self);
  PdfView.Parent := Self;
  PdfView.Align := alClient;
  PdfView.Pdf := Pdf;
  PdfView.FitMode := pfmFitWidth;

  if ParamCount > 0 then
  begin
    Pdf.FileName := ParamStr(1);
    Pdf.Active := True;   // opens the document; PageCount valid after this
  end;
end;

أهمية التسلسل أقل من أهمية الربط: PdfView.Pdf := Pdf يربط control المرئي بمكوّن المستند، وبعد ذلك يتصرف التنقل عبر PageNumber والتكبير عبر FitMode بالطريقة نفسها في VCL وLCL. هناك سلوك عابر للأطر يجب معرفته قبل أن يجده المستخدمون: إسناد Zoom يدوياً يعيد FitMode إلى pfmNone في الإطارين. إذا كان toolbar لديك يقدم "fit width" كتفضيل ثابت، فأعد وضع fit mode بعد أي تغيير zoom برمجي، وإلا سيتوقف التفضيل عن كونه ثابتاً بصمت.

الـ binary الذي لم يحذرك منه IDE

يلف المكوّن محرك PDFium، الذي يأتي كـ platform binary، وتحميل binaries هو مصدر تقارير "يعمل في IDE ويفشل من الاختصار المثبت". ثلاث قواعد تغطي معظمها. يجب أن تتطابق bitness تماماً: executable 32-bit لا يستطيع تحميل مكتبة pdfium 64-bit، ورسالة الفشل التي ينتجها OS، مثل "module not found" في بعض إصدارات Windows، مضللة لأن الملف موجود فعلاً. حل مسار المكتبة نسبة إلى executable، لا إلى working directory؛ إطلاق IDE وإطلاق shell يختلفان تحديداً في working directory. واعرض فشل التحميل قبل فتح أول مستند، برسالة تسمي المسار والمعمارية المتوقعين؛ تذكرة دعم تقول "PDFium 64-bit binary missing at <path>" تُغلق خلال دقائق، أما "viewer crashes on startup" فلا.

قم بإصدار binary المحرك مع executable أيضاً. يتحرك PDFium بسرعة، وinstaller يرقّي التطبيق بينما يترك مكتبة أقدم على القرص ينتج crashes لا تستطيع أي آلة في مكتبك إعادة إنتاجها، لأن كل آلات مكتبك تملك الزوج المطابق. تعامل مع المكتبة كجزء من build artifact: installer نفسه، version stamp نفسه، وrollback نفسه.

تسجيل المكوّنات في Lazarus IDE

البناء وقت التشغيل لا يحتاج إلى design-time registration أصلاً، وهذا أبسط إعداد صحيح لتطبيق عارض. إذا أردت المكوّنات على palette في Lazarus، ثبّت الحزمة واترك وحدة التسجيل المخصصة (PDFiumLazReg) تقوم بالعمل؛ هذه الوحدة موسومة design-time في الحزمة تحديداً لأنها تشير إلى واجهات property-editor الخاصة بـ IDE التي يجب ألا تُربط أبداً في executable الذي تشحنه.

العَرَض عند الخطأ هنا هو تطبيق يعتمد بشكل غامض على حزم IDE، ويظهر ذلك كفشل نشر على آلات لم تر Lazarus من قبل.

الكلام وقارئات الشاشة خارج Windows

إذا كان أصل Delphi يوفر text-to-speech، فإن النقل يرث قرار منصة. SAPI، وهو backend المعتاد لـ TTS على Windows، موجود هناك فقط. builds الخاصة بـ Lazarus التي تستهدف Windows تحتفظ بسلوك SAPI وNVDA-compatible كاملاً، لذلك فإن نقل Windows إلى Windows لا يفقد شيئاً، ويتفاعل مستخدمو NVDA مع العارض بالطريقة نفسها في كلا IDEين.

أهداف Linux وmacOS تحتاج إلى speech backend مختلف موصول بواجهات القراءة نفسها، وهذا سبب قوي لإبقاء الكلام خلف interface منذ البداية. آلية ترتيب القراءة وتتبع الكلمات نفسها platform-neutral؛ طبقة إخراج الصوت فقط هي التي تتغير. يغطي مقال القارئ القابل للوصول تلك الآلية بعمق.

ما يجب اختباره قبل إعلان اكتمال النقل

يمر فحص parity الذي كشف regressions حقيقية بهذا الترتيب تقريباً: افتح مستنداً يحتوي مساره على أحرف غير ASCII؛ ابحث عن عبارة تحتوي على أحرف غير ASCII وتأكد من إبراز النتائج؛ شغّل mouse-wheel scroll وdrag selection والتنقل بلوحة المفاتيح بين الصفحات على كل target widget set، لأن focus وسلوك wheel هما أكثر مناطق LCL اعتماداً على widget set؛ افحص التصيير عند display scaling بنسبة 100% و150% و200%؛ وشغّل build المثبت، لا build IDE، على آلة بلا IDE، لأنه الاختبار الوحيد الذي يمارس binary resolution بصدق.

خصائص rendering throughput تنتقل بين الإصدارات، لذلك تنطبق استراتيجية التخزين المؤقت من مقال render cache وzoom performance بلا تغيير.

FAQ

هل إصدار LCL كامل الميزات مقارنة بإصدار VCL؟

السطح الأساسي، TPdf وTPdfView والتصيير والنماذج واستخراج النص وواجهات الوصولية، هو نفسه في كليهما. الاختلافات الحقيقية مرتبطة بالمنصة: إخراج الكلام عبر SAPI خاص بـ Windows، وdialogs الطباعة والملفات تتبع أعراف كل framework.

لماذا يتعطل build الخاص بـ Lazarus عند startup بينما يعمل build الخاص بـ Delphi؟

افحص binary resolution أولاً: عدم تطابق architecture بين executable ومكتبة pdfium، أو مسار تحميل افترض working directory الخاص بـ IDE. كلاهما ينتج فشلاً فورياً في startup يبدو كخلل مكوّن وهو في الحقيقة خلل نشر.

هل يمكنني الاحتفاظ بنموذج مشترك واحد بين IDEين؟

ملفات وصف النماذج لا تنتقل؛ .dfm و.lfm صيغتان مختلفتان بمجموعات خصائص مختلفة. بناء UI العارض وقت التشغيل كما هو موضح أعلاه يستبدل ملفي designer بمسار كود واحد ويزيل مشكلة الانجراف بالكامل.

إصدارات VCL وLCL الموضحة هنا تُشحن معاً باسم PDFium Component، مع source code وواجهات API عامة متطابقة لـ Delphi وC++Builder وLazarus/FPC.