مقال تقني

تحويل HTML إلى PDF في Delphi: مسار HTML5 للوسائط المُقسَّمة إلى صفحات

يحوّل HotPDF HTML إلى صفحات PDF عبر THPDFHTMLImporter.RenderHTML5، وهو ملف تعريف مُرقَّم بالإصدار للوسائط المُقسَّمة إلى صفحات يتضمن DOM حقيقيًا، وتتالي CSS حقيقيًا، وجداول، وفواصل صفحات، وإمكانية اختيارية لتوليد AcroForm من عناصر تحكم نموذج HTML. يعمل بالكامل داخل العملية نفسها: بلا محرك متصفح، بلا Chromium، بلا محوّل خارجي، وبلا أي وصول إلى الملفات أو الشبكة افتراضيًا

سبب وجود نقطة دخول منفصلة بدلاً من تحسين RenderHTML هو التوافقية. فالطريقة الأقدم ما زالت ترسم الفواتير والخطابات في تطبيقات إنتاجية منذ سنوات، وأي DOM متساهل مزوّد بتتالي كامل سيغيّر مخرجات المستندات التي ضُبطت وفق السلوك القديم. لذلك تحتفظ الطريقة القديمة بدلالتها الدقيقة كما هي، بينما يعيش كل جديد خلف RenderHTML5، الذي يُبلّغ عن إصدار ملفه التعريفي عبر HTML5ProfileVersion ويُدرج معالمه المدعومة عبر HTML5ProfileMilestones

ما الذي يضيفه ملف تعريف HTML5 فعليًا؟

هناك أربع قدرات مهمة عمليًا. شجرة DOM بنيوية حيث تكون html و body حاويتين لا عقدتي نص. تتالي CSS بأولوية صحيحة. تخطيط جداول. وتحكم صريح في فواصل الصفحات، وهذا هو معنى "الوسائط المُقسَّمة إلى صفحات": فـ HTML صُمم من أجل نافذة عرض قابلة للتمرير، وتحويله إلى صفحات يتطلب تحديد أين يُسمح للمحتوى بالانكسار

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

ترتيب التتالي، مُفصَّلاً

أولوية CSS ليست ترتيبًا واحدًا. يطبّق HotPDF التصريحات العادية من القواعد المطابقة بترتيب الخصوصية ثم ترتيب المصدر، ثم يطبّق التصريحات العادية المضمّنة فوقها، ثم تصريحات !important من القواعد المطابقة، وأخيرًا !important المضمّنة. أربع مراحل، بهذا الترتيب

رسم تخطيطي يوضح كيف يحسم HotPDF كل خاصية CSS في Delphi عبر أربع مراحل تتالي مرتّبة، من مطابقة القواعد العادية وصولاً إلى تصريحات !important المضمّنة
المراحل اللاحقة تفوز بالتعادل عن قصد: التصريحات المضمّنة تتفوق على أوراق الأنماط، و!important يتفوق على كل ما أُعلن بشكل عادي

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

uses
  HPDFDoc, HPDFHTMLImport;

var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Html, Css: WideString;
begin
  Pdf := THotPDF.Create(nil);
  Importer := THPDFHTMLImporter.Create(Pdf);
  try
    Pdf.FileName := 'statement.pdf';
    Pdf.BeginDoc;

    Importer.Margin := 56;              // نقاط
    Importer.BaseFontName := 'Arial';
    Importer.BaseFontSize := 10;
    Importer.CreateAcroForms := False;  // عرض عناصر التحكم دون إنشاء حقول

    Css := '@page { margin: 56pt } ' +
           'table { border-collapse: collapse } ' +
           'tr.total { page-break-inside: avoid; font-weight: bold }';
    Html := LoadTemplate('statement.html');

    if not Importer.RenderHTML5(Html, Css) then
      raise Exception.Create('nothing was emitted - check the markup');

    Pdf.EndDoc;

    Writeln(Format('profile=%d nodes=%d rules=%d layout ops=%d',
      [Importer.Statistics.ProfileVersion,
       Importer.Statistics.ParsedNodeCount,
       Importer.Statistics.CSSRuleCount,
       Importer.Statistics.LayoutOperationCount]));
  finally
    Importer.Free;
    Pdf.Free;
  end;
end;

الوسيط الثاني هو ورقة أنماط تأليف تُطبَّق جنبًا إلى جنب مع أي أنماط يحملها المستند، وهي الطريقة النظيفة لفرض قواعد داخلية مثل هوامش الصفحة والتحكم في الأسطر اليتيمة دون تعديل كل قالب على حدة

لماذا لا يجلب المستورِد أي شيء إطلاقًا؟

أي محوّل HTML إلى PDF يحلّ المراجع الخارجية هو في الواقع محرك تزوير طلبات من جهة الخادم (SSRF) يرتدي زي مولّد مستندات. وجّهه إلى ترميز مُقدَّم من المستخدم وسيجلب بكل سرور http://169.254.169.254/ أو عنوان ملف من خادم تطبيقك ويضمّن النتيجة في ملف PDF يقوم أحدهم بتنزيله

لهذا السبب لا يقرأ HotPDF لا الملفات ولا الشبكة افتراضيًا. تصل الصور والموارد الأخرى بطريقتين فقط: كعناوين data: URI داخل الترميز، أو عبر رد النداء OnResource، حيث يقرر كودك ما تعنيه كل URI ويوفّر البايتات:

رسم تخطيطي يوضح كيف يُحل مستورِد HTML الخاص بـ HotPDF في Delphi كل عنوان URI للموارد محليًا، فيقبل عناوين data URI ورد نداء OnResource بينما يرفض جلب الملفات والشبكة خلف سقوف للعدد والبايتات
لا تصل إلى الصفحة إلا البايتات التي وصلت محليًا، وتزيد المراجع المرفوضة العداد، ولا يغادر أي طلب جلب العملية إطلاقًا
procedure TReportForm.SupplyResource(Sender: TObject;
  const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
  var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
  Handled := False;
  // اخدم فقط من دليل أصول محلي مُدرَج في قائمة السماح. لا تُمرّر
  // عنوان URI أبدًا إلى عميل HTTP، ولا تُلحقه بمسار
  // دون التحقق منه أولاً
  if StartsText('asset:', URI) then
  begin
    Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
    ContentType := 'image/png';
    Handled := Length(Data) > 0;
  end;
end;

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

أربعة حدود، ونمط فشل واحد

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

تُعاد تهيئة الإحصاءات في بداية كل استدعاء لـ RenderHTML5، بحيث تصف الأرقام عملية استيراد واحدة لا عمر كائن المستورِد بأكمله. وفي خدمة ترسم قوالب غير موثوقة، سجّل ParsedNodeCount و LayoutOperationCount و ResourceRejectCount لكل مستند: فارتفاع عدد الرفض عادة ما يعني أن كاتب القالب يشير إلى أصول لا يتعرف عليها رد النداء الخاص بك، وعدد عمليات تخطيط قريب من السقف يعني أن المستند الأكبر قليلاً التالي سيفشل

الاختيار بين استيراد HTML وواجهة التخطيط

يفوز استيراد HTML عندما يكون المحتوى مُؤلَّفًا كـ HTML من قِبل أشخاص ليسوا مطوّري Delphi، وعندما تتغير القوالب أكثر من الكود، أو عندما يجب إرسال المحتوى نفسه كبريد إلكتروني أيضًا. أما شجرة التخطيط التصريحية الموصوفة في تخطيط PDF التصريحي مع مخرجات موسومة فتفوز عندما يُولَّد المستند من بيانات بواسطة كودك الخاص، وعندما تحتاج إلى إصدار بنية PDF/UA تلقائيًا، أو عندما يكون التحكم الدقيق في التقسيم إلى صفحات أهم من سهولة التأليف

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

اضبط CreateAcroForms عن قصد. عند إيقافه، تُرسم عناصر تحكم نموذج HTML كرسم ثابت، وهذا ما يريده كشف حساب مطبوع. وعند تفعيله، تصبح حقول AcroForm تفاعلية حقيقية، وهذا ما يريده نموذج قابل للتعبئة، وسلوك الحقول الذي يلي ذلك موضح في إضافة حقول نموذج إلى ملف PDF محمّل

يستهدف استيراد HTML والتخطيط التصريحي وواجهة الرسم المباشر كائن المستند نفسه في Delphi و C++Builder؛ وتجد قائمة الميزات الكاملة على صفحة مكوّن HotPDF لـ PDF في Delphi