مقال تقني

تخطيط CSS متعدد الأعمدة من HTML إلى PDF مع HotPDF Delphi

يرتب HotPDF Component نص CSS متعدد الأعمدة عندما يحول THPDFHTMLImporter.RenderHTML5 ملف HTML إلى PDF داخل Delphi. يحل محرك HTML المُصفّح قيم column-count و column-width مقابل صندوق المحتوى، ويوازن آخر شظية من كل حاوي multicol ببحث ثنائي، ويملأ الصفحات الأسبق عموداً بعمود حتى أسفل الصفحة، ويدعم column-fill و column-span: all و column-rule

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

كيف يقرر HotPDF عدد الأعمدة الذي سيستخدمه؟

يحل HotPDF عدد الأعمدة بالقواعد نفسها للخوارزمية الزائفة في CSS Multi-column Layout Module Level 1 §3.4، مطبقة على عرض صندوق المحتوى W بعد إزالة الحشو والحدود. يُؤخذ column-count المعلن كما هو. وعند إعطاء column-width وحده يحسب المحرك N = Trunc((W + gap) / (column-width + gap)) بأرضية عمود واحد. وعند وجودهما معاً تكون النتيجة أصغر الاثنين، فيستطيع العدد المعلن أن يسقف العدد المشتق لكنه لا يضخمه أبداً

حالة ملموسة: صندوق محتوى 483pt‏ (A4 بهوامش جانبية 56pt مدورة) مع column-width: 140pt; column-gap: 12pt يعطي Trunc(495 / 152) = 3 أعمدة، كل منها بعرض (483 - 2 * 12) / 3 = 153pt. وإضافة column-count: 2 إلى القاعدة نفسها تنزلها إلى عمودين، لأن الأدنى يغلب. والمفتاح normal للفجوة يحل إلى 1em من حجم خط الحاوي، ويجري ذلك الحل وقت التخطيط لا وقت التحليل، فـ font-size معلن بعد column-gap في القاعدة نفسها ينتج الفجوة الصحيحة رغم ذلك

حل عدد الأعمدة متعددة في CSS لـ HotPDF من HTML إلى PDF داخل Delphi: يُستخدم column-count كما أعلن، وتشتق column-width القيمة N = Trunc((W + gap) / (column-width + gap))، ويفوز الأصغر عند ضبطهما معاً، وأي عمود دون 12pt ينهار إلى عمود واحد
يطبق المحرك الخوارزمية الزائفة لـ CSS على صندوق المحتوى، ويسقف العدد المشتق بمعلن، ويضيف حرس أرضية 12pt كي لا تنتج القوالب غير الموثوقة آلاف الأعمدة المجهريّة

حرس واحد ليس في المواصفة، وهو هناك عمداً. بعد حساب عرض العمود، أي قيمة دون 12pt تُنهي الحاوي عائدة إلى عمود واحد. ورقة أنماط تقول column-count: 9999، صادفةً أو لأنها جاءت من قالب غير موثوق، لكانت ستنتج آلاف الأعمدة بعرض سالب أو مجهري وكسّار سطور يحاول إدخال محرف واحد في كل سطر. والعودة إلى عمود واحد تبقي المخرجات مقروءة وكلفة التخطيط محدودة. والعدد الصفر أو السالب لا يصلان إلى ذلك البعد: لا يقبل المدقق سوى auto أو عدد صحيح موجب لـ column-count، وتأخذ column-width و column-gap‏ auto أو normal على التوالي، أو طولاً بالنقاط أو em

عرض قسم HTML متعدد الأعمدة إلى PDF داخل Delphi

لا تحتاج المخرجات متعددة الأعمدة إلى أي مفتاح على المستورد. يُنشأ THPDFHTMLImporter بمستند THotPDF الذي يرسم فيه، ويجب أن يكون BeginDoc قد جرى سلفاً، وتصل خصائص الأعمدة عبر التتالي العادي، في الترميز أو في ورقة أنماط المؤلف الممررة بوصفها الوسيط الثاني لـ RenderHTML5

uses
  SysUtils, HPDFDoc, HPDFHTMLImport;

procedure BuildBulletin(const FileName: string);
var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Html, Css: WideString;
begin
  Pdf := THotPDF.Create(nil);
  Importer := THPDFHTMLImporter.Create(Pdf);
  try
    Pdf.FileName := FileName;
    Pdf.BeginDoc;
    Importer.Margin := 56;
    Importer.BaseFontName := 'Arial';
    Importer.BaseFontSize := 10;

    Css := 'section.news { column-width: 140pt; column-gap: 12pt; ' +
           '  column-rule: 0.5pt solid #999999 } ' +
           'section.news h2 { column-span: all }';

    Html := '<section class="news">' +
            '<p>Release 4.2 ships the new export pipeline...</p>' +
            '<p>Licensing changes take effect on 1 March...</p>' +
            '<h2>Known issues</h2>' +
            '<p>Import of legacy archives &amp; templates...</p>' +
            '</section>';

    if not Importer.RenderHTML5(Html, Css) then
      raise Exception.Create('Nothing was emitted');
    Pdf.EndDoc;
  finally
    Importer.Free;
    Pdf.Free;
  end;
end;

لاحظ التهريب المضاعف في الفقرة الأخيرة:‏ &amp; داخل سلسلة باسكال هي كيان HTML لعلامة &، يفكه المستورد عائداً إلى محرف واحد. وبقية التتالي والميزانيات واستدعاءات الموارد السلوكية تجري تماماً كما في نظرة عامة على استيراد وسائط HTML5 المُصفّحة في HotPDF؛ فالأعمدة نمط تخطيط إضافي واحد داخل الملف التعريفي نفسه

موازنة الأعمدة ببحث ثنائي

يوازن HotPDF الأعمدة بالبحث عن أصغر ارتفاع عمود يجري فيه الملء الطمّاع وضعه كل سطر متبقٍ. الممر الطمّاع بسيط: امشِ الأسطر في العمود الأول حتى يتجاوز السطر التالي الارتفاع المرشح، ثم تابع في العمود الثاني، وهكذا. يبدأ البحث بأطول سطر مفرد حداً أدنى ومجموع أطوال كل الأسطر حداً أعلى، وينصّف المجال كل جولة، ويتوقف متى اقترب الحدان حتى 0.05pt

موازنة متعددة الأعمدة في HotPDF داخل Delphi: يجد بحث ثنائي بين أطول سطر مفرد ومجموع أطوال كل الأسطر أصغر ارتفاع عمود يضع ملؤه الطمّاع كل سطر، مع تقارب الحدين حتى 0.05pt
محكّ «يتسع كل سطر» رتيب في ارتفاع العمود، فيتقارب تنصيف المجال على أقصر شريط متوازن ويستهلك الملء الطمّاع الأخير كل سطر دون أسطر يتيمة

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

ماذا يحدث عندما تعبر الأعمدة فاصل صفحة؟

الأعمدة التي لا تتسع في الصفحة الحالية تُملأ تباعاً حتى أسفل الصفحة، ولا توازن إلا شظية الصفحة الأخيرة. هذا هو السلوك الذي يقرره CSS Multi-column Layout Level 1 §7.1 لـ column-fill: balance في السياقات المجزأة، وهو مظهر النشرة المطبوعة: أعمدة ممتلئة في كل صفحة عدا الأخيرة، حيث يقسم النص المتبقي بالتساوي بدل ترك عمود طال بجوار عمود فارغ

تمر كل شظية صفحة عبر الشظّاء نفسه باستقلال. إذا اتسعت الأسطر المتبقية في N أعمدة من الارتفاع المتاح وُزنت الشظية؛ وإلا امتلأ كل عمود حتى أسفل الصفحة وبدأت الحلقة صفحة جديدة. والارتفاع الطبيعي للحاوي كله، الذي يستخدمه المحرك في قرارات break-inside: avoid وقياس الامتداد، هو ارتفاع الشريط المتوازن بسعة غير محدودة، فالحاوي الذي يتسع في صفحة جديدة ككتلة متوازنة ينقل إليها سليماً بدل تقطيعه. وعندما يكون سطر مفرد أطول من صفحة كاملة يخفف المحرك القيد، ويضع السطر على أي حال أعلى صفحة، ويبلّغ بتشخيص hdkPaginationConstraint بموضوع multi-column

تقسيم صفحات متعدد الأعمدة في HotPDF من HTML إلى PDF داخل Delphi: تملأ الشظايا قبل الصفحة الأخيرة كل عمود حتى أسفل الصفحة، وتتوازن الشظية الأخيرة عبر الأعمدة، ويقسم column-span all حاوي إلى أشرطة متوازنة منفصلة حول عنصر بعرض كامل
لا توازن إلا الشظية الأخيرة من حاوي multicol؛ وتملأ الصفحات الأسبق تباعاً، وتكرر كل شظية الموازنة والقواعد والتقسيم باستقلال

column-fill: auto وصناديق ارتفاع ثابت

مع column-fill: auto و height محددة يملأ HotPDF الأعمدة بالترتيب، وارتفاع كل عمود هو أصغر المساحة المتبقية في الصفحة وارتفاع الحاوي ناقص حشوه وحدوده. والأدنى أهمية أكثر مما تبدو. الاكتفاء بالمساحة المتبقية في الصفحة سيسمح للعمود الأول بابتلاع كل سطر كلما زادت مساحة الصفحة على الصندوق. وبدون ارتفاع محدد يعود column-fill: auto إلى الموازنة كما تفعل المتصفحات، لأن حاوي multicol غير مقيد ليس له ارتفاع أعمدة يُملأ إليه؛ وفاتهت التنفيذ الأول ذلك المسار البديل، والتقطته إحدى فحوصات الانحدار الـ 27

column-span: all والفقرات المتداخلة

يدعم HotPDF ‏column-span: all على الأبناء المباشرين لحاوي متعدد الأعمدة، اتساقاً مع CSS Multi-column Layout Level 1 §6.1. يقسم الابن الممتد الحاوي إلى مقاطع: يشكل الأبناء قبله شريط أعمدة واحداً، ويعرض العنصر الممتد عبر العرض الكامل، وتبدأ الأبناء بعده شريطاً جديداً. ويزن كل شريط ويملأ ويحدَّد وتفصل صفحاته بنفسه، فعنوان في وسط قسم من ثلاثة أعمدة ينتج شريطين متوازنين بعنوان بعرض كامل بينهما، وهو التخطيط الذي يرسمه الناس فعلاً في النماذج الأولية

تطلب الوصول إلى ذلك إصلاح مشكلة أقل ظهوراً أولاً. كانت الحاوي مثل <div style="column-count:2"> بعدة أبناء <p> تعرض كل فقرة في عمود واحد مكدس، لأن كل فقرة رتبت ككتلة نص خاص بها ولم ينطبق إعلان الأعمدة على الأب قط. يجمع HotPDF الآن نص الأبناء الكتلية المتداخلة في تدفق واحد للحاوي. وعلى الجامع إدخال فاصل صلب عند كل حد كتلي وهو يفعل؛ وإسقاطه يدمج آخر كلمة في فقرة بأول كلمة في التالية على السطر نفسه، وهو خطأ سهل إفلاته حين يصادف أن نص الاختبار ينتهي بعلامة ترقيم

لذلك التدفق المدمج نطاق مقصود. لا يُستخدم إلا عندما تكون الشجرة الفرعية كلها تدفق نص عادي. الحاوي التي تحمل ضوابط نماذج أو صوراً أو جداول أو قوائم أو <hr> في أي مكان داخلها تبقى على مسار الكتل العادي وتعرض في عمود واحد، لأن جامع النص المدمج لا يستطيع التعبير عن هندستها. والجداول خصوصاً لها قواعد تقسيم صفحات خاصة، تغطيها مقالة معالجة rowspan وترويسات الجداول المكررة في HotPDF، وخلطها في شريط أعمدة سيكسر تلك الضمانات

أين يذهب قاعدة العمود وكيف تختبرها

يرسم HotPDF ‏column-rule على خط الوسط لكل فجوة، عند الحافة اليمنى لعمود مضافاً إليها نصف الفجوة، من أعلى الشريط إلى أسفله في كل شظية صفحة، كما يصف CSS Multi-column Layout Level 1 §4. العرض الافتراضي 1pt. واللون الافتراضي لون نص العنصر حين لا يعلن أو يكون currentColor. وتعامل الصيغة المختصرة column-rule بلا كلمة نمط معاملة solid، تماشياً مع القراءة الشائعة للصيغة المختصرة border، بينما يكبحان none و hidden القاعدة

هندسة الأعمدة ملحوظة دون تحليل الـ PDF. يطلق حدث OnLineLayout على THPDFHTMLImporter مرة لكل سطر مرسوم بسجل THPDFHTMLLineLayout، ولأن المحرك يرسم كل عمود منفصلاً يكون حقل X لكل سطر هو الحافة اليسرى لعموده. وتختلف الأعمدة المتجاورة بقدر عرض العمود مضافاً إليه الفجوة بالضبط، وهو ما يسهل به تمييز فجوة 12pt من فجوة 20pt في اختبار انحدار

type
  TColumnProbe = class
  public
    Lines: TStringList;
    constructor Create;
    destructor Destroy; override;
    procedure LineDrawn(Sender: TObject; const Line: THPDFHTMLLineLayout);
  end;

constructor TColumnProbe.Create;
begin
  inherited Create;
  Lines := TStringList.Create;
end;

destructor TColumnProbe.Destroy;
begin
  Lines.Free;
  inherited;
end;

procedure TColumnProbe.LineDrawn(Sender: TObject;
  const Line: THPDFHTMLLineLayout);
begin
  // ‏PageIndex تبدأ من الصفر؛ و X هي الحافة اليسرى لعمود السطر
  Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
    [Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;

// التوصيل، قبل RenderHTML5:
//   Probe := TColumnProbe.Create;
//   Importer.OnLineLayout := Probe.LineDrawn;

ملاحظتان عمليتان من كتابة اختبارات مقابل هذا الحدث.‏ PageIndex تبدأ من الصفر، وهي أول ما يخطئ فيه تثبيت مكتوب يدوياً عادة. و X يتضمن إزاحات المحاذاة، فللنص الممركز أو المحاذى يميناً قارن مواضع الأعمدة على تجهيزات محاذاة يساراً. والقاعدة نفسها لا تظهر في الحدث؛ ولإثبات موضعها امسح تدفق المحتوى غير المضغوط عن المعاملين الرأسيين m و l

حدود ومزالق تستحق المعرفة

قليل من السلوكيات تقريبات، والمعرفة بها قبل مراجعة تخطيط أفضل من بعدها. تباعد الفقرات داخل تدفق أعمدة مدمج يقرَّب بسطر واحد من ارتفاع سطر الحاوي عند كل حد كتلي، فالهوامش لكل فقرة داخل شريط أعمدة لا تستنسخ بدقة. وعندما يقسم column-span: all حاوياً يكرر كل مقطع حشو الحاوي وصندوق الخلفية، وهو ما يبقي المقاطع متصلة بصرياً لكنه ليس صندوقاً واحداً بالمعنى الصارم لـ CSS. وكل قيمة column-rule-style غير none و hidden تجتاز التحقق، لكن القاعدة تُرسم خطاً متصلاً

القيم غير المدعومة لا تفشل صامتة. ضبط StrictCSS على True يحول كل تشخيص إلى استثناء EHPDFHTMLUnsupportedCSS، بينما يتيح OnDiagnostic لوظيفة دفعية تسجيلها والمتابعة. ولاحظ أن النمط الصارم ينطبق على تشخيص تقسيم الصفحات أيضاً، فمستند بسطر أطول من صفحة سيستثنى تحت StrictCSS بدل تخفيف القيد بصمت

procedure TReportJob.HtmlDiagnostic(Sender: TObject;
  const Diagnostic: THPDFHTMLDiagnostic);
begin
  case Diagnostic.Kind of
    hdkUnsupportedProperty, hdkUnsupportedValue:
      FLog.Add('CSS ' + Diagnostic.Subject + ': ' + Diagnostic.MessageText);
    hdkPaginationConstraint:
      FLog.Add('Layout relaxed (' + Diagnostic.Subject + '): ' +
        Diagnostic.MessageText);
  else
    FLog.Add(Diagnostic.MessageText);
  end;
end;

// تأليف القوالب: افشل سريعاً على أي شيء سيتجاهله المحرك
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;

النمط الصارم هو الإعداد الصحيح أثناء كتابة قالب، إذ يرفض خطأ مطبعي مثل column-span: both فوراً بدل ترك العنوان يتدفق داخل الأعمدة بصمت. وفي الإنتاج يكون التسجيل عبر OnDiagnostic عادة المقايضة الأفضل، لأن إعلاناً غريباً واحداً في HTML مقدمة من عميل لا ينبغي أن يكلف المستند كله. والتشخيصات المتطابقة (النوع والموضوع والقيمة نفسها) تبلّغ مرة واحدة ما دامت تتسع في قائمة MaxDiagnostics التي افتراضها 256 مدخلاً، فإعلان يزوره التتالي لمئات العناصر لا يغرق السجل

FAQ

هل يدعم HotPDF الصيغة المختصرة columns؟

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

هل تستطيع حاوي متعددة الأعمدة أن تحمل صوراً أو جداول؟

تستطيع حملها، لكن HotPDF عندئذ يعرض تلك الحاوي في عمود واحد. لا يطبق تدفق الأعمدة إلا عندما يكون كل سليل تدفق نص عادي، لأن الصور والجداول والقوائم وضوابط النماذج تحتاج هندسة صناديق وقواعد تقسيم خاصة بها

لماذا تعرض الصفحة الأخيرة أعمدة متوازنة بينما الصفحات الأسبق ممتلئة؟

هذه هي قاعدة CSS لـ column-fill: balance في الوسائط المُصفّحة: لا توازن إلا الشظية الأخيرة. تملأ الصفحات الأسبق كل عمود حتى أسفل الصفحة فلا يهدر أي فراغ في وسط المستند

تخطيط متعدد الأعمدة والملف التعريفي لوسائط HTML5 المُصفّحة وبقية أنبوب HTML إلى PDF كلها من HotPDF Component لـ Delphi و C++Builder، التي تغطي أيضاً الرسم المباشر للـ PDF والنماذج والتشفير واستخراج النص