يستطيع HotPDF بناء مستند مُقسَّم إلى صفحات من شجرة تصريحية بدلًا من إحداثيات. فأنت تُجمِّع THPDFDOMDocument من أقسام، وأكوام، ونصوص، وقوائم، وجداول، وتُسلِّمها إلى THPDFDOMRenderer، ويقوم المُرسِّم بالقياس والتقسيم إلى صفحات ورسم عناصر الصفحة الثابتة، ويُصدر عند الطلب شجرة بنية PDF/UA التي تجعل النتيجة قابلة للوصول. وكود التخطيط لا يحسب إحداثية y أبدًا
وأي شخص صان مولّد تقارير قائمًا على الإحداثيات يعرف سبب أهمية هذا. فالنسخة الأولى تعمل. ثم ينمو عنوان عميل إلى ثلاثة أسطر، ويكتسب جدول صفوفًا، ويلتف عنوان مُترجَم، وحينها يصبح كل موضع y تالٍ خاطئًا. وتتراكم الإصلاحات كفحوصات فاصل صفحة يدوية مبعثرة عبر منطق الأعمال، ومتطلب PDF الموسوم الذي يصل بعد سنتين لا يمكن إضافته لاحقًا إلى كود لا يعرف أصلًا ما هي الفقرة
ما تملكه الشجرة، ولماذا الملكية صارمة
يفرض DOM الملكية الأحادية على كل مستوى: فالمستند يملك أقسامه، والقسم يملك متنه ورأسه وتذييله، وتملك الأكوام والحاويات والجداول عناصرها الفرعية. وإعادة الاستخدام تحدث عبر Clone أو عبر مصنع مُسجَّل، وليس أبدًا بإلحاق الكائن نفسه بأصلين. وهذه القاعدة ليست شكليات فارغة. فأي مكوّن يظهر مرتين في الشجرة سيُقاس مرتين بقيود مختلفة ويُحرَّر مرتين عند التفكيك
والنتيجة العملية للكود المستدعي هي أن الدوال المساعدة تُعيد نسخًا جديدة. فتسجيل مصنع عبر RegisterComponent واستدعاء CreateComponent يمنحك وصفة مسمّاة تُنتج مكوّنًا جديدًا في كل مرة، وهذه هي الطريقة التي تنتمي بها عناصر ثابتة متكررة مثل كتلة توقيع أو تذييل قانوني إلى الشجرة
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // إصدار شجرة بنية PDF/UA
Doc.Language := 'en-US';
Section := Doc.AddSection;
Section.PageWidth := 595; // A4 بالنقاط
Section.PageHeight := 842;
Section.MarginLeft := 56;
Section.MarginTop := 56;
Section.MarginRight := 56;
Section.MarginBottom := 56;
Section.Style.FontName := 'Helvetica';
Section.Style.FontSize := 10;
Section.Body.AddHeading('Annual maintenance report', 1);
Section.Body.AddText('Every asset inspected during the reporting ' +
'period is listed below, grouped by site.');
Section.Body.AddSpacer(12);
Table := THPDFDOMTable.Create('assets');
Table.AddColumn(3); // أوزان، لا عروض مطلقة
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // صف الترويسة
Row[0].Text := 'Asset';
Row[1].Text := 'Last service';
Row[2].Text := 'Status';
for I := 0 to High(Assets) do
begin
Row := Table.AddRow(16);
Row[0].Text := Assets[I].Name;
Row[1].Text := Assets[I].ServiceDate;
Row[2].Text := Assets[I].Status;
end;
Section.Body.Add(Table);
end;
كيف يتجنب التقسيم إلى صفحات التكلفة التربيعية؟
الطريقة الساذجة لتقسيم شجرة إلى صفحات هي استنساخ كل ما لم يتسع ونقله إلى الصفحة التالية. وفي جدول من عشرة آلاف صف، يستنسخ ذلك الصفوف المتبقية مرة لكل صفحة، ويحوّل مستندًا خطيًا إلى مستند تربيعي
وبدلًا من ذلك، يُقسِّم HotPDF بدقّة ضيقة. فالمُرسِّم في المستوى الأعلى يجتاز عناصر المتن الفرعية وفق الفهرس، ولا يستنسخ أبدًا قسمًا أو متنًا كاملًا. وفقط الأكوام والحاويات المتداخلة التي تتخطى فعليًا حدود صفحة يُستنسَخ الجزء الفرعي المتأثر منها، بينما يحمل النوعان الورقيان الثقيلان مؤشرًا لا نسخة: فاستمرارية النص تخزّن نطاق الأحرف من المصدر الذي لا يزال مستحقًا، واستمرارية الجدول تخزّن شريحة الصفوف التي لم تُوضَع بعد. وتبقى المستندات الطويلة خطية، وتتكلف الفقرات الطويلة التكلفة نفسها سواء انقسمت مرة واحدة أو خمس مرات
ويبقى القياس صادقًا بشأن الآثار الجانبية. إذ يُشترَط أن يكون THPDFLayoutElement.Measure خاليًا من أي آثار جانبية للرسم، بينما يمر التموضع الفعلي دائمًا عبر THotPDF.PlaceLayoutElement، وهو الإجراء المركزي نفسه الذي يعيد قياس الجزء الموضوع، ويُنشئ ملكية الفائض، ويسجّل التشخيصات. ولا يقرر مُرسِّم DOM سوى سياسة الصفحة الجديدة، وعناصر الصفحة الثابتة، والتباعد، وعمر الاستمراريات
قواعد ترويسة الجدول التي تمنع مستندًا لا نهائيًا
يبدو تكرار ترويسات الجدول عبر الصفحات أمرًا بسيطًا، لكنه يخفي نمطَي فشل. يشترط HotPDF أن تظهر صفوف الترويسة فقط في السلسلة الأولى من الصفوف المتتالية، وأن يتسع الانقسام الأول لجميع صفوف الترويسة إضافةً إلى صف متن واحد على الأقل. ومن دون القاعدة الثانية، فإن ترويسة أطول من المساحة المتبقية تُنتج صفحة لا تحتوي على شيء سوى الترويسة، تليها صفحة أخرى مطابقة، إلى ما لا نهاية
وتُعيد صفحات الاستمرار رسم الترويسة، وتُوسَم تلك النسخة المُعاد رسمها كعنصر مصنوع لا كمحتوى، وهي الإجابة الصحيحة لكل من إمكانية الوصول واستخراج النص. ويبقى صف الترويسة الأصلي في بنية الجدول المنطقية مرة واحدة بالضبط. وتخطّي هذا يجعل قارئ الشاشة يُعلن عناوين الأعمدة مجددًا في منتصف البيانات، ويُدرج مستخرِج النص صف ترويسة مكرَّرًا بين صفوف المتن
وثمة أيضًا سقف دفاعي على عمق الاستمرار، لأن أي مكوّن مخصَّص حر في تنفيذ Split بطريقة تُعيد دائمًا ذيلًا مكافئًا. ويتحقق المُرسِّم من الحد بعد فصل الذيل وقبل بدء الصفحة التالية، وتُحرِّر التكرارة الحالية الذيل في كتلة finally الخاصة بها، بحيث يفشل مكوّن خارجي سيئ السلوك برسالة خطأ قابلة للتشخيص بدلًا من ملء القرص
عنصر منطقي واحد، أجزاء صفحات عديدة
الوسم التلقائي هو الموضع الذي يجب أن يتفق فيه نموذج التقسيم إلى صفحات مع نموذج البنية. فالفقرة المنقسمة عبر صفحتين فقرة منطقية واحدة، لذا يجب أن تبقى عنصر بنية واحدًا. لكن معرّفات المحتوى الموسوم تكون لكل صفحة، لذا يحتاج كل جزء مرئي إلى MCID خاص به على الصفحة التي يظهر فيها
يحل HotPDF هذا الأمر بالإبقاء على عنصر بنية واحد وإلحاق مرجع محتوى موسوم بمصفوفته /K لكل جزء، مع زوج /Pg و/MCID الذي يُعرِّف الصفحة والمعرّف. ويشير موضع ParentTree لذلك الـ MCID إلى العنصر نفسه. وهذا بالضبط ما يتوقعه معيار ISO 14289، وهو سبب اختلاف استنساخات الاستمرار عن الاستنساخات العادية: فـ Clone العادي يعني محتوى منطقيًا جديدًا ويحصل على هوية دلالية جديدة، بينما يرث استنساخ الاستمرار الداخلي هوية المكوّن الذي يواصله
ويُبحث عن إعادة استخدام العناصر عبر فهرس للهويات الدلالية مُرتَّب حسب مؤشر المكوّن ومُستعلَم عنه بمقارنة ثنائية، ما يُبقي البحث لوغاريتميًا في الأشجار الكبيرة. ولا يحمل الفهرس سوى مراجع غير مالكة؛ ويبقى عمر كائنات البنية نفسها مرتبطًا برسم كائنات PDF البياني
قواعد البنية التي يفرضها المُرسِّم مسبقًا
عند تفعيل GenerateStructure، يُتحقَّق من عدة قواعد PDF/UA أثناء رسم الشجرة لا بعد وجود الملف. تبدأ العناوين من المستوى 1 ولا يجوز أن تتخطى المستويات. ولا يجوز أن يظهر LI إلا داخل L، ولا Lbl وLBody إلا داخل LI. وينتمي TR إلى جدول، وينتمي TH وTD إلى صف. وتُرفَض أي صورة توضيحية بلا نص بديل في وضع PDF/UA
والرفض المبكر هو الخيار المتعمَّد هنا. فأداة تحقق تُبلغ عن نص بديل مفقود بعد كتابة المستند تخبرك بأن دفعة من عشرة آلاف كشف حساب تحتاج إلى إعادة توليد؛ أما مُرسِّم يرفض المكوّن فيخبرك بأي مكوّن بالتحديد، بينما لا تزال البيانات التي أنتجته في النطاق. ويظل التحقق من المطابقة خطوة منفصلة تنتمي إلى خط الأنابيب، وآلياتها مشروحة في التحقق من PDF/A وPDF/X وPDF/UA
var
Pdf: THotPDF;
Renderer: THPDFDOMRenderer;
Stats: THPDFDOMRenderStatistics;
begin
Pdf := THotPDF.Create(nil);
Renderer := THPDFDOMRenderer.Create;
try
Pdf.FileName := 'maintenance-report.pdf';
Pdf.BeginDoc;
Stats := Renderer.Render(Doc, Pdf);
Pdf.EndDoc;
Writeln(Format('%d page(s), %d placement(s), %d split(s)',
[Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
[Stats.StructureElementCount, Stats.MarkedContentCount,
Stats.ArtifactCount]));
Writeln(Format('deepest continuation chain: %d',
[Stats.MaximumContinuationDepth]));
finally
Renderer.Free;
Doc.Free;
Pdf.Free;
end;
end;
وسجل الإحصائيات أكثر فائدة مما يبدو للوهلة الأولى. فارتفاع SplitCount بشكل حاد بعد تغيير قالب يعني عادة أن مكوّنًا بدأ يُقاس أطول من حاويته. وزحف MaximumContinuationDepth صعودًا إنذار مبكر لمكوّن تُحرز دالة Split فيه تقدمًا ضئيلًا جدًا لكل صفحة. ومقارنة ArtifactCount بعدد صفحات الاستمرار تؤكد أن الترويسات المتكررة وُسمت فعلًا كعناصر مصنوعة
أين يقع DOM إلى جانب واجهة البرمجة المباشرة
لا يحل DOM محل الرسم المباشر؛ بل يقع فوق كائنات الصفحة نفسها. وأي شيء يضعه المُرسِّم يمكن تشبيكه مع استدعاءات مباشرة على THotPDF، وهو أمر مهم حين يحتاج تقرير إلى عنصر واحد موضوع يدويًا مثل صورة توقيع في موقع دقيق. ويبقى إغلاق الصفحة تحت سيطرة AddPage وEndDoc، بحيث لا يحتفظ وضع التفريغ الفوري بأي صفحات مكتملة في الذاكرة، وتظل الذاكرة المقيمة محكومة بالاستمراريات الحالية، وموارد الخط، والرسم البياني العادي لكائنات المستند
اختر DOM حين يكون المحتوى قائمًا على البيانات والتخطيط قائمًا على قواعد، واحتفظ بالرسم المباشر للأعمال الفنية الثابتة. وإذا كانت مشكلتك الحالية تحديدًا هي تقسيم الجداول إلى صفحات، فيستحق النهج الأضيق في توليد الجداول في PDF القراءة أولًا، بينما يُوصَف السلوك على مستوى النص مثل التبرير في تبرير النص
ويُشحَن التخطيط التصريحي، والوسم التلقائي، وواجهة الرسم المباشر ضمن المكوّن نفسه لـ Delphi وC++Builder؛ وتوجد قائمة الميزات الكاملة على صفحة مكوّن HotPDF لـ PDF في Delphi