مقال تقني

تصيير PDF متعدد المحركات في Delphi: المحرك المدمج و Cairo و PDFium مع PDFlibPas

لكل rasterizer خاص بـ PDF ملف يحرجه. في إحدى حالات الدعم كان ملفا لدورة فواتير مرافق خرجت فيه الشعارات ذات soft mask كمستطيلات سوداء — لكن في موقع العميل فقط، لأن بناءهم كان يصيّر عبر محرك مختلف عن منصة الاختبار. نادرا ما يمكن إصلاح أخطاء كهذه في كود التطبيق؛ الصفحة صحيحة، لكن المحرك يختلف معها. ما يمكن إصلاحه هو البنية: اجعل محرك التصيير قرارا وقت التشغيل، وتحقق من المحركات التي يحتويها الملف الثنائي المنشور فعلا، وسجل أي محرك أنتج كل صورة. صُمم PDFlibPas، مكتبة PDF من losLab لـ Delphi و C++Builder، لهذا النمط بالضبط — سطح استدعاء واحد للتصيير، وثلاثة محركات قابلة للاختيار.

ثلاثة rasterizers خلف سطح استدعاء واحد

ترقم المكتبة محركاتها: 1 هو المحرك المدمج (الافتراضي، مع خيارات تنعيم GDI+ على Windows)، و2 هو Cairo، و3 هو PDFium، وتُختار وقت التشغيل عبر SelectRenderer. تُحمّل المحركات الخارجية من DLLs تحدد مساراتها عبر SetCairoFileName و SetPDFiumFileName. أيا كان المحرك، فالاستدعاءات نفسها تنجز العمل — RenderPageToFile و RenderPageToStream و RenderDocumentToFile — لذلك تغير استراتيجية fallback عددا صحيحا واحدا، لا كود التصيير.

تحت السطح، نموذج الوجهة أوسع من bitmaps: فئة التصيير تدعم metafiles‏ (WMF و EMF و EMF+) و EPS وسياقات الجهاز المباشرة والطابعات ومخرجات HTML5، مع ظهور Cairo و PDFium كوجهات إضافية عند تجميعهما. تركيز هذه المقالة على الخرج raster، حيث تكون فروق المحركات أوضح.

لا تفترض وجود المحرك: افحص عند بدء التشغيل

دعم Cairo و PDFium ميزتا تجميع شرطي. ملف ثنائي بُني من دونهما لن يرمي استثناء عندما تختار المحرك 2 أو 3 — بل تعيد SelectRenderer شيئا غير ID المطلوب، وإذا تجاهلت قيمة الإرجاع فستستمر بالتصيير بما كان نشطا من قبل. النمط الموثوق هو فحص بدء التشغيل:

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // engine 1 is always present
  if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
    Result := Result + ', cairo';
  if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
    Result := Result + ', pdfium';
  PDF.SelectRenderer(1);                       // restore the default before real work
end;

سجل نتيجة الفحص مع كل مهمة. عندما يبلغ عميل عن اختلاف في التصيير، يكون أول سؤال تشخيصي: "أي محركات تحتويها هذه التثبيتة؟" وإجابة بسطر واحد في السجل أفضل من جلسة remote desktop.

عشر صيغ إخراج خلف عدد Options واحد

يختار معامل Options في استدعاءات التصيير ترميز الإخراج: 0 لـ BMP، و1 JPEG، و2 WMF، و3 EMF، و4 EPS، و5 PNG، و6 GIF، و7 TIFF، و8 EMF+، و9 HTML5. تعد PNG (5) الافتراض المعقول للمعاينات وصور الصفحات الأرشيفية؛ بينما يفوز JPEG (1) مع SetJPEGQuality في المسوح الفوتوغرافية التي يهم فيها الحجم.

تخفي صيغة واحدة متطلبا في stream. مسار BMP يرقع حقول الدقة في header بعد كتابة بيانات الصورة، فيعود seeking إلى offset 0x26 في الإخراج. إذا صيّرت BMP إلى stream أمامي فقط — wrapper ضغط أو network stream — فسيخفق الاستدعاء بطريقة تبدو مرتبطة بالمحرك وهي ليست كذلك. إذا كانت الوجهة غير القابلة للـ seek لا مفر منها، فصيّر PNG بدلا من ذلك، أو مرر إخراج BMP عبر memory stream.

الـ DPI الذي تمرره ليس الـ DPI الذي تحصل عليه

كل استدعاءات التصيير تأخذ معامل DPI، لكن الدقة الفعلية هي تلك القيمة مضروبة في مقياس التصيير العام. تبدأ SetRenderScale بقيمة 1.0؛ وما إن تتغير حتى تطبق بصمت على كل تصيير لاحق على تلك النسخة:

PDF.SetRenderScale(2.0);                    // every later render is doubled
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // effectively 300 DPI
PDF.SetRenderScale(1.0);                    // reset, or your thumbnails arrive huge

ينطبق استمرار الحالة نفسه على SetRenderCropType وإعداد جودة JPEG. في خدمة تصيّر thumbnails ومعاينات وصور print-resolution من نسخة مشتركة واحدة، تكون هذه الإعدادات العالقة سبب تذاكر "thumbnails أصبحت فجأة 40 MB". إما أن تعيد ضبط الحالة في بداية كل عملية، أو تخصص نسخة لكل profile إخراج.

اضبط المحرك الافتراضي قبل البحث عن آخر

نسبة مفاجئة من طلبات "نحتاج محركا مختلفا" هي في الحقيقة طلبات إعدادات جودة متنكرة. يعرّض المحرك المدمج سلوك التنعيم عبر SetGDIPlusOptions وعائلة SetRenderOptions الأوسع، ويمكن توجيه المحرك إلى runtime معين من GDI+ عبر SetGDIPlusFileName عندما تشحن بيئة نشر نسخة غير عادية. الخطوط المتكسرة عند DPI منخفض، والنص الضبابي في thumbnails، والشرائط في gradients تستجيب كلها لهذه المقابض — وتعديلها لا يكلف شيئا عند النشر، بينما إضافة Cairo أو PDFium إلى installer تضيف DLLs ومتغيرات بتّية والتزام تحديث.

لذلك يكون التسلسل العملي عند شكوى جودة: أولا أعد الإنتاج عند DPI وإعدادات scale الخاصة بالعميل بالضبط، ثم جرب خيارات التنعيم في المحرك المدمج، وبعدها فقط قارن الصفحة A/B عبر المحركات مع تثبيت كل شيء آخر. صيّر الصفحة نفسها إلى PNG عبر المحركات 1 و2 و3 بالقيمة نفسها من DPI، وأرفق الصور الثلاث بالقضية. في معظم الأوقات يتفق اثنان من الثلاثة، وهذا يخبرك هل الشاذ هو تفسير المستند أم توقع baseline — دليل يحسم نزاعات "يصيّر خطأ" أسرع بكثير من تقارير أخطاء مبنية على الصفات.

سلسلة fallback تشرح نفسها

مع الفحص وانضباط الحالة في مكانهما، تكون سلسلة fallback نفسها قصيرة. يستخدم كشف الفشل LastRenderError، الذي يحمل نص رسالة المحرك لآخر تصيير:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // built-in first
  PDF.RenderPageToFile(200, Page, 5, OutFile);      // 5 = PNG
  if PDF.LastRenderError = '' then Exit;
  LogEngineFailure('built-in', Page, PDF.LastRenderError);
  if PDF.SelectRenderer(3) = 3 then                 // PDFium as the heavy fallback
  begin
    PDF.RenderPageToFile(200, Page, 5, OutFile);
    if PDF.LastRenderError = '' then Exit;
    LogEngineFailure('pdfium', Page, PDF.LastRenderError);
  end;
  raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;

هناك نقطتا تصميم مقصودتان. تسجل السلسلة سبب كل انتقال، لأن عبارة "هذه الصفحة بدأت تسقط إلى PDFium منذ الإصدار 3.7" إشارة regression تريد تتبعها في المراقبة لا دفنها. وترتيب fallback سياسة يجب اختيارها لكل workload: المحرك المدمج يُنشر بلا DLLs إضافية، وهذا يجعله المحاولة الأولى الصحيحة في معظم التثبيتات، بينما المستندات الثقيلة بمجموعات transparency أو أنماط shading غير معتادة هي السبب الكلاسيكي الذي يدفع الفرق إلى توصيل محرك بديل أصلا. قِس على corpus الخاص بك؛ corpus هو من يكسب النقاش دائما.

بعد الصفحات المفردة: دفعات TIFF وسياقات جهاز مباشرة

قدرتان مجاورتان لاستدعاءات الصفحة المفردة تكملان الأدوات. تصيّر RenderAsMultipageTIFFToFile تعبير نطاق صفحات مباشرة إلى TIFF متعدد الصفحات — وهو الشكل الطبيعي للتسليم الأرشيفي إلى أنظمة إدارة مستندات أقدم من PDF. وترسم RenderPageToDC مباشرة على Windows device context لعناصر preview، وتحكمها ثلاثية إعدادات sticky خاصة بها (SetRenderDCOffset و SetRenderDCErasePage إضافة إلى crop type) تستحق انضباط إعادة الضبط نفسه الذي يستحقه scale factor. معاينة الشاشة ومسار الطباعة يملكان ما يكفي من الفخاخ الخاصة بهما لمقالة مخصصة، مرتبطة أدناه.

إجابات عن أسئلة التصيير

لماذا تعيد SelectRenderer(3) القيمة 0 على جهازي؟ إما أن الملف الثنائي المنشور جُمع من دون دعم PDFium، أو أن SetPDFiumFileName لم تصل إلى DLL قابل للتحميل — مسار خاطئ، بتّية خاطئة، أو dependencies مفقودة. نمط الفحص عند بدء التشغيل يميز بينهما: إذا أعاد استدعاء اسم الملف 0 أصلا، فالمشكلة في DLL.

أي محرك أسرع؟ لا توجد إجابة ثابتة عبر أنواع المستندات، ولهذا تحديدا تتيح المكتبة الاختيار لكل استدعاء. اختبر كل محرك على عينة من مستنداتك الحقيقية عند DPI الحقيقي، وأعد الاختبار عندما تتغير DLLs الخاصة بالمحركات أو مزيج المستندات.

هل تستطيع صفحات مختلفة من مستند واحد استخدام محركات مختلفة؟ نعم. يؤثر SelectRenderer في الاستدعاءات اللاحقة على النسخة، لذلك تستطيع سلسلة fallback إعادة محاولة صفحة عنيدة واحدة على محرك آخر بينما تبقى بقية المستند على الافتراضي.

تابع الاستكشاف

لمعالجة الرسم في preview واختيار الطابعة والتعامل مع DevMode، تابع مع مقالة print preview وسياق الجهاز. وإذا كانت التصييرات تغذي pipeline كثيف الحجم فوق ملفات كبيرة جدا، فإن الأسلوب المعتمد على المقابض في دليل Direct Access ينسجم جيدا مع تصيير الصفحات عبر DARenderPageToFile.

تفاصيل تغليف المحركات والصيغ المدعومة وبناءات التجربة موجودة في صفحة منتج PDFlibPas.