يرتب 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 في القاعدة نفسها ينتج الفجوة الصحيحة رغم ذلك
حرس واحد ليس في المواصفة، وهو هناك عمداً. بعد حساب عرض العمود، أي قيمة دون 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 & 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;
لاحظ التهريب المضاعف في الفقرة الأخيرة: & داخل سلسلة باسكال هي كيان HTML لعلامة &، يفكه المستورد عائداً إلى محرف واحد. وبقية التتالي والميزانيات واستدعاءات الموارد السلوكية تجري تماماً كما في نظرة عامة على استيراد وسائط HTML5 المُصفّحة في HotPDF؛ فالأعمدة نمط تخطيط إضافي واحد داخل الملف التعريفي نفسه
موازنة الأعمدة ببحث ثنائي
يوازن HotPDF الأعمدة بالبحث عن أصغر ارتفاع عمود يجري فيه الملء الطمّاع وضعه كل سطر متبقٍ. الممر الطمّاع بسيط: امشِ الأسطر في العمود الأول حتى يتجاوز السطر التالي الارتفاع المرشح، ثم تابع في العمود الثاني، وهكذا. يبدأ البحث بأطول سطر مفرد حداً أدنى ومجموع أطوال كل الأسطر حداً أعلى، وينصّف المجال كل جولة، ويتوقف متى اقترب الحدان حتى 0.05pt
لا يعمل البحث الثنائي إلا إذا كان المحكّ رتيباً، وهو هنا كذلك: العمود الأطول لا يمكن أن يحمل أسطراً أقل من الأقصر، فمحكّ «يتسع كل سطر» يقلب من خاطئ إلى صحيح مرة واحدة بالضبط مع نمو الارتفاع. تلك الخاصية هي ما يضمن التقارب ويضمن أن الملء الطمّاع الأخير عند الارتفاع الموجود يستهلك كل سطر، بدل ترك سطر يتيم يطلق صفحة إضافية زائفة. وهي تفسر أيضاً تفصيلاً من النتيجة: الشريط المتوازن أقصر ما تسمح به صناديق الأسطر، وليس بالضرورة أعمدة متساوية، لأن الأسطر غير قابلة للتجزئة وقد ينتهي العمود الأخير بسطر أو سطرين أعلى من غيره
ماذا يحدث عندما تعبر الأعمدة فاصل صفحة؟
الأعمدة التي لا تتسع في الصفحة الحالية تُملأ تباعاً حتى أسفل الصفحة، ولا توازن إلا شظية الصفحة الأخيرة. هذا هو السلوك الذي يقرره CSS Multi-column Layout Level 1 §7.1 لـ column-fill: balance في السياقات المجزأة، وهو مظهر النشرة المطبوعة: أعمدة ممتلئة في كل صفحة عدا الأخيرة، حيث يقسم النص المتبقي بالتساوي بدل ترك عمود طال بجوار عمود فارغ
تمر كل شظية صفحة عبر الشظّاء نفسه باستقلال. إذا اتسعت الأسطر المتبقية في N أعمدة من الارتفاع المتاح وُزنت الشظية؛ وإلا امتلأ كل عمود حتى أسفل الصفحة وبدأت الحلقة صفحة جديدة. والارتفاع الطبيعي للحاوي كله، الذي يستخدمه المحرك في قرارات break-inside: avoid وقياس الامتداد، هو ارتفاع الشريط المتوازن بسعة غير محدودة، فالحاوي الذي يتسع في صفحة جديدة ككتلة متوازنة ينقل إليها سليماً بدل تقطيعه. وعندما يكون سطر مفرد أطول من صفحة كاملة يخفف المحرك القيد، ويضع السطر على أي حال أعلى صفحة، ويبلّغ بتشخيص hdkPaginationConstraint بموضوع multi-column
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 والنماذج والتشفير واستخراج النص