مقال تقني

تكوين مكتبة PDFium: حين يستبدل Brotli ‏Skia بصمت

في PDFium Component for Delphi كان تفعيل BrotliEnabled أو IsolatePerDocument في TPdfLibraryConfiguration يبدّل بناء Skia المرفق إلى العارض AGG دون أي خطأ، لأن الخيارين يرفعان FPDF_LIBRARY_CONFIG إلى إصدارٍ يقرأ فيه PDFium ‏m_RendererType حرفياً. ومنذ v3.123.0 يبقى العارض الافتراضي افتراض الـ DLL نفسها، ومنذ v3.125.0 يرفع طلب Skia أو Fontations لا تستطيع الـ DLL خدمته EPdfError قابلاً للالتقاط بدل قتل العملية

لا أحد من العيبين أعلن نفسه. الأول أنتج صفحات بدت سليمة، مصيَّرة فقط بعارض نقطي مختلف، بحواف نص وتمويه مجاور يختلفان قليلاً عن البناء الذي شحنت واختبارت. والثاني أعلن نفسه، بصوت عالٍ، بإسقاط العملية المضيفة من داخل التهيئة الأصلية. وكلاهما يأتي من الموضع نفسه: بنية C مؤرشفة لا تُحتسب حقولها إلا حين يقول رقم الإصدار ذلك، وقيمها الصفرية ليست «غير مضبوطة» بل اختيارات حقيقية

كيف يقرر FPDF_LIBRARY_CONFIG أي عارض يستخدمه PDFium؟

لا يستشير FPDF_InitLibraryWithConfig حقل m_RendererType إلا عندما يكون حقل Version في البنية 4 فأعلى، ومن ذلك الإصدار يستخدم القيمة كما كُتبت تماماً. وتحت الإصدار 4 يتجاهل PDFium الحقل وينتقي افتراض البناء، وهو Skia في البناءات المترجمة بـ PDF_USE_SKIA و AGG فيما عداهما

وكل حقل لاحق يتبع النمط نفسه. كبرت البنية قدرةً واحدة في كل مرة، ووصلت كل قدرة مع رقم إصدار جديد. يبني PDFium Component البنية الأصلية في LoadLibrary من TPdfLibraryConfiguration خاصتك ويرفع الإصدار فقط إلى ما تتطلبه الخيارات التي ضبطتها

إصدار البنيةالحقل الذي يضيفهيضبطه
2m_pIsolate، m_v8EmbedderSlotيُكتب دائماً؛ ‏V8Isolate، V8EmbedderSlot
3m_pPlatform‏V8Platform ليست nil
4m_RendererType‏Renderer غير prpDefault
5m_FontLibraryType‏FontBackend غير pfbpDefault
6m_BrotliEnabled‏BrotliEnabled = True
7m_IsolatePerDocument‏IsolatePerDocument = True

الفخ في الصفين الأخيرين. الإصدارات تراكمية: بنية الإصدار 6 هي أيضاً بنية الإصدار 4 والإصدار 5، فيقرأ PDFium ‏m_RendererType و m_FontLibraryType رغم أنك لم تطلق سوى Brotli. وكل ما يجلس في ذلك الحقلين في تلك اللحظة يصير العارضَ ومرجعَ الخطوط، سواء قصدت اختارهما أم لا

سلم إصدارات FPDF_LIBRARY_CONFIG في PDFium Component من الإصدار 2 إلى 7 يعرض أي خيار من TPdfLibraryConfiguration يضيف m_RendererType و m_FontLibraryType و m_BrotliEnabled و m_IsolatePerDocument، ولماذا تجعل الإصدارات التراكمية حقل عارض مثبت الأصفار اختيار AGG متعمداً لا قيمة غير مضبوطة على أي بناء
كل خيار يرفع إصدار البنية ويبقى كل حقل سابق حياً، فيصل الصفر في m_RendererType إلى PDFium طلبَ AGG صريحاً

لماذا بدّل تفعيل Brotli العارض إلى AGG؟

قبل v3.123.0 كان PDFium Component يكتب FPDF_RENDERERTYPE_AGG في m_RendererType من أجل prpDefault، فأي تكوين يدفع البنية إلى الإصدار 6 أو 7 يفرض AGG على بناء Skia. بيئتا التشغيل pdfium.dll و pdfium.v8.dll المشحونتان مع المكوّن بناءا Skia، فأصاب هذا النشرَ الافتراضي لا نشراً نادراً

بدت المطابقة غير مؤذية حين كُتبت. عند الإصدار 2 أو 3 لا يُقرأ الحقل قط، فكان prpDefault فعلاً يعني «مهما فعلت الـ DLL». وبمجرد دخول BrotliEnabled ‏(الإصدار 6) أو IsolatePerDocument ‏(الإصدار 7) على المشهد حوّل الكود نفسه «بلا تفضيل» إلى طلب AGG صريح. لم يفشل شيء. تهيأ PDFium عادي، وصيّر كل صفحة، وأعاد لا رمز خطأ، لأنه من وجهة نظره طلب المستدعي AGG واستلم AGG

يجعل هاش البكسلات التبديلَ مرئياً حيث تفشل لقطات الشاشة. تصيير الصفحة الأولى للمستند النموذجي نفسه تحت ثلاثة تكوينات أعطى:

  • التكوين الافتراضي: هاش 502D77C3711B4ACF
  • ‏BrotliEnabled = True مع إبقاء Renderer على prpDefault: هاش F75B5EB4728ADE87
  • ‏prpAgg صريح: هاش F75B5EB4728ADE87، متطابق مع تشغيل Brotli

الإصلاح في v3.123.0 هو الدالة العامة PdfNativeRendererType، التي تحل TPdfRendererPreference إلى القيمة المكتوبة في m_RendererType. ‏prpAgg و prpSkia تقابلان واحداً بواحد. و prpDefault يقابل الآن Skia حين تصدّر الـ DLL المحملة FPDF_RenderPageSkia وإلى AGG خلاف ذلك. ذلك التصدير يُترجم تحت شرط PDF_USE_SKIA نفسه الذي يُترجم تحته افتراض Skia ذاته، مما يجعله خاصية البناء الوحيدة التي تستطيع رصدها من خارج الـ DLL. بعد الإصلاح ينتج تكوين Brotli الهاش نفسه الذي ينتجه الافتراضي

مقارنة هاش بكسلات في PDFium Component تعرض هاش تصيير Skia الافتراضي 502D77C3711B4ACF، وتكوين BrotliEnabled قبل v3.123.0 مطابقاً تشغيلاً صريحاً بـ prpAgg بالهاش F75B5EB4728ADE87، والغلاف المصلح يحل prpDefault عبر تصدير FPDF_RenderPageSkia عائداً إلى هاش Skia الأصلي
هاش البكسلات يمسك ما تخفيه لقطات الشاشة: كان تفعيل Brotli يصيّر كل صفحة بـ AGG، والافتراضي المصلح يطابق الآن التكوين غير الممسوس

لم يعرف مرجع الخطوط المشكلةَ نفسها قط. يُقرأ m_FontLibraryType من الإصدار 5، وقيمته الصفرية FPDF_FONTBACKENDTYPE_FREETYPE هي أيضاً افتراض PDFium حين لا يُقرأ الحقل أصلاً. فكتابة FreeType لـ pfbpDefault تستنسخ الافتراضي الأصلي تماماً. القيم الصفرية ليست خاطئة دوماً، لكنها ليست صائبة آلياً قط

مع v3.123.0 أو أحدث يفعل كود الإقلاع الذي ستكتبه طبيعياً ما يقوله:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // يجب أن يجري قبل أي شيء يحمل المكتبة الأصلية
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // يرفع FPDF_LIBRARY_CONFIG إلى الإصدار 6
  // يبقى Renderer ‏prpDefault: يُحل Skia على البناءات التي تصدّر
  // ‏FPDF_RenderPageSkia وإلى AGG على البناءات ذات AGG وحدها
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

وتذكر أن BrotliEnabled يجعل تدفقات /BrotliDecode لـ PDF 2.0 قابلة للفك فقط إذا بُنيت الـ DLL نفسها بـ PDF_ENABLE_BROTLI. العلم طلب، وعلى بناء بلا دعم Brotli ليس له أثر. ‏TPdfLibraryConfiguration.Hardened هي نفسها Default إلا أن AllowMachineTime فيها False، مما يمنع JavaScript المستند من قراءة الساعة الحقيقية؛ وهي نقطة انطلاق معقولة للمعالجة من جهة الخادم لملفات غير موثوقة

ماذا يحدث حين تطلب مرجعاً خلفياً لا تحويه الـ DLL؟

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

الحالتان الملموستان بناء AGG وحدها يستلم FPDF_RENDERERTYPE_SKIA، وبناء بلا Fontations يستلم FPDF_FONTBACKENDTYPE_FONTATIONS. بيئة التشغيل Skia المرفقة في المجموعة الثانية: تصيّر بـ Skia لكنها تستخدم FreeType للخطوط. طلب prpSkia مع pfbpFontations عليها أنتج External exception 80000003 على جانب Delphi. وحين يصادف مصحح الأخطاء أو معالج استثناء الإمساك بها، يبقى الوضع غير قابل للإنقاذ:

  • يبقى PDFium مهيأً نصفياً
  • الختم على التكوين على مستوى العملية موضوع سلفاً، فيرفض ConfigurePdfLibrary تكويناً مصححاً
  • إعادة المحاولة بتكوين مختلف في العملية نفسها لم تعد ممكنة

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

كيف يفحص PDFium Component مسبقاً Skia و Fontations

منذ v3.125.0 يتحقق LoadLibrary من التكوين بعد ربط تصديرات الـ DLL وقبل استدعاء FPDF_InitLibraryWithConfig، ويحوّل عارضاً أو مرجع خطوط غير مدعوم إلى EPdfError برسالة تسمي الإعداد المخالف والبدائل. وتُفرَّغ الـ DLL ويُفك ختم التكوين، فيستطيع المستدعي انتقاء إعدادات أخرى والتحميل مجدداً

يقرر الحكم نفسه في الدالة النقية PdfLibraryConfigurationSupportError، التي تأخذ التكوين زائد قيمتين منطقيتين تصفان البناء وتعيد سلسلة فارغة حين تكون التركيبة آمنة. ولأنها لا تلمس حالة أصلية يمكنك استدعاؤها من اختباراتك بأي تركيبة قدرات. وداخل LoadLibrary تأتي القيمتان المنطقيتان من صنفي دليل، وتستحقان درجتي ثقة مختلفتين:

  • ‏Skia يُكتشف من وجود تصدير FPDF_RenderPageSkia، الدليل نفسه الذي تستخدمه PdfNativeRendererType. التصدير والعارض Skia يترجمان تحت شرط واحد، فالفحص تام
  • ‏Fontations لا يملك تصديراً خاصاً به. الأثر الوحيد الذي يتركه صناديق الخطوط Rust التي يسحبها إلى الثنائي، فيمسح PDFium Component ملف المكتبة المحمل بحثاً عن أسماء الصناديق skrifa و read-fonts ‏(وأيضاً read_fonts). لا يجري المسح إلا عند طلب pfbpFontations، والملف الذي يتعذر قراءته يحتسب «بلا Fontations»

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

فك الختم مهم بقدر الفحص. يختم LoadLibrary التكوينَ في بداية التحميل نفسها، فمن دون إعادة الضبط كان رفض قدرةٍ يترك ConfigurePdfLibrary تجيب كل إعادة محاولة بـ EPdfError «PDFium library configuration is already sealed». يستدعي مسار الرفض UnloadLibrary أولاً؛ استدعاء FPDF_DestroyLibrary عندها آمن لأن PDFium لم تُهيأ بعد وتعود فوراً. أما إخفاقات التحميل الأخرى، كـ DLL مفقودة أو تباين معمارية، فتبقي الختم، فعلى حلقة إعادة المحاولة أن تميز بينهما:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // مؤهلة بالوحدة: ‏Windows.LoadLibrary تحمل الاسم نفسه
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // رفض القدرة يفرغ الـ DLL ويفك ختم التكوين.
      // و DLL أفشل التحميل أصلاً تبقى مختومة: إعادة المحاولة لا تنفع
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

لاحظ PDFium.LoadLibrary الصريحة. في وحدة تستخدم أيضاً Windows أو Winapi.Windows يتحول LoadLibrary غير المؤهلة إلى أي وحدة تظهر آخرَها في بند uses؛ وحين تكون هي دالة Win32 يفشل الاستدعاء عديم الوسائط ترجمةً بخطأ عدد وسائط لا يقول شيئاً عن PDFium

مخطط تدقيق LoadLibrary المسبق في PDFium Component حيث يختم ConfigurePdfLibrary التكوين، ويفحص تدقيق القدرة تصدير FPDF_RenderPageSkia وأدلة سلاسل skrifa، ويرفع الطلب غير المدعوم EPdfError قابلاً للالتقاط ويفك الختم لإعادة المحاولة، بينما DLL لا تُحمَّل قط تبقي PdfLibraryConfigurationSealed بقيمة True
يجري التحقق بعد ارتباط التصديرات وقبل التهيئة، فيفشل مرجع غائب EPdfError تستطيع التقاطه بدل CHECK أصلي يقتل العملية

تحقق يجري أبكر من ذلك

يرفض ConfigurePdfLibrary بعض التركيبات قبل أي DLL، كلها بـ EPdfError. ‏FontBackend صريح، بما فيه pfbpFreeType، يتطلب Renderer = prpSkia، لأن PDFium لا يستشير مرجع الخطوط إلا للعارض Skia. ‏IsolatePerDocument يتطلب أن تكون V8Isolate قيمة nil، لأن PDFium يبني isolate خاصاً به لكل مستند ويفشل CHECK أصلياً إن سلّمتَه واحداً أيضاً. والسلاسل الفارغة في UserFontPaths تُرفض. وأي استدعاء بعد أول محاولة تحميل يفشل بـ «PDFium library configuration is already sealed»

ولتلك القاعدة الأخيرة نتيجة عملية: لا تستطيع أن تمسح الـ DLL أولاً ثم تكوّنها بعد. ‏GetSkiaRenderCapabilities و V8FeaturesAvailable وفتح مستند وأغلب مداخل الاستدعاء الأخرى تستدعي LoadLibrary داخلياً، فتختم التكوين في الحال. ولا يعيد استدعاء UnloadLibrary لاحقاً فتحها أيضاً. كوّن أولاً، ثم حمّل، ثم اسأل، وهو بالضبط الترتيب الذي يجب أن يتبعه روتين تشخيص:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // نسخة، آمنة للتفتيش
  if not PDFium.Loaded then
  begin
    if PdfLibraryConfigurationSealed then
      Exit('PDFium failed to load; configuration is sealed');
    Exit('PDFium not loaded; configuration can still change');
  end;
  // الحل نفسه الذي طبقه LoadLibrary حين بنا FPDF_LIBRARY_CONFIG
  if PdfNativeRendererType(Config.Renderer,
    GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
    Renderer := 'Skia'
  else
    Renderer := 'AGG';
  Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
    [Renderer, BoolToStr(Config.BrotliEnabled, True),
     BoolToStr(Config.IsolatePerDocument, True)]);
end;

تسجيل ذلك السطر مرة عند الإقلاع رخيص، وهو أول ما تريده في تذكرة دعم تقول «النص يبدو مختلفاً على الخادم». ‏PDFium.Loaded مؤهلة للسبب نفسه الذي شأن LoadLibrary: داخل تابعة نموذج أو مكوّن يرتبط Loaded العارية بـ TComponent.Loaded

طريقان يخطئ فيهما مبنى C تكويني مؤرشف

كل بنية تكوين مؤرشفة، كانت FPDF_LIBRARY_CONFIG أو سجل cbSize من Win32 أو واجهة ABI لمكوّن إضافي، تفشل بطريقتين متناظرتين، وعلى الغلاف أن يحرس ضد الاثنتين. الأولى ملء حقل مع إبقاء الإصدار منخفضاً جداً؛ والثانية رفع الإصدار مع إبقاء حقل على قيمة صفرية تقرؤها المكتبة اختياراً متعمداً

  1. حقل مضبوط والإصدار منخفض جداً. اكتب m_BrotliEnabled = 1 في بنية الإصدار 2 فلن ينظر PDFium إليها قط. ينجح الاستدعاء وتبقى تدفقات Brotli غير قابلة للفك. والدفاع اشتقاق الإصدار من الحقول المستخدمة فعلاً، وهو ما يفعله LoadLibrary، لا ترميز واحد مثبت
  2. إصدار كافٍ وقيمة صفرية ذات معنى. ارفع الإصدار إلى 6 فيصبح كل حقل حتى الإصدار 6 حياً. ‏FillChar يثبّت m_RendererType على FPDF_RENDERERTYPE_AGG، وهو عارض حقيقي لا «غير مضبوط». والدفاع كتابة كل حقل يغطيه الإصدار المنتقى بقيمة مقصودة، وحل «الافتراضي» مقابل البناء الفعلي بدل افتراضه

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

مرجع سريع: تكوين مكتبة PDFium Component

  • استدعِ ConfigurePdfLibrary مرة واحدة، قبل أي شيء يحمل الـ DLL؛ وأي استعلام قدرة أو تحميل مستند يختمها
  • ارتقِ إلى v3.123.0 أو أحدث إن كنت تضبط BrotliEnabled أو IsolatePerDocument وتتوقع ناتج Skia من بيئات التشغيل المرفقة
  • أبقِ Renderer على prpDefault ما لم تكن تحتاج عارضاً نقطياً معيناً؛ إنه يحل إلى افتراض البناء عند كل إصدار بنية الآن
  • استخدم PdfNativeRendererType مع GetSkiaRenderCapabilities.PageRender لتسجيل أي عارض نشط فعلاً
  • توقع EPdfError لا انهياراً لـ prpSkia على DLL ذات AGG وحدها أو pfbpFontations على DLL بلا Fontations في v3.125.0 أو أحدث
  • بعد رفض قدرةٍ تكون PdfLibraryConfigurationSealed بقيمة False ويمكنك إعادة التكوين؛ وبعد فشل تحميل DLL تبقى True
  • عامل كشف Fontations استدلالاً واحفظ تراجعاً إلى FreeType
  • اكتب PDFium.LoadLibrary و PDFium.Loaded باسم الوحدة لتجنب تضارب الأسماء مع Win32 و TComponent

إن فشلت الـ DLL قبل أن يهم التكوين أصلاً، فابدأ بـ تشخيص إخفاقات تحميل PDFium DLL في Delphi، ولمعرفة كيف يجد المكوّن الثنائي الصحيح على كل منصة راجع تحميل مكتبة PDFium الأصلية على أي هدف. وحين يستقر العارض تغطي تكتيكات ذاكرة التصيير والتمرير الناعم كيف تبقي تصيير الصفحات سريعاً في عارض

يغلّف PDFium Component محرك PDFium لـ Delphi و C++Builder بفحوص تكوين كهذه، فيفشل التهيئة الأصلية استثناءً بيسكلي تستطيع معالجته بدل خروج من العملية. وتفاصيل المنتج والتنزيلات في صفحة منتج PDFium Component for Delphi