مقال تقني

إخراج PDF مُخطّط في Delphi: جداول التلميح في HotPDF

يكتب HotPDF ملفات PDF مُخطّطة، التخطيط الذي يُسمّيه Acrobat عرض الويب السريع، عبر خاصية LinearizeOutput في THotPDF. تعيينها قبل BeginDoc يجعل HotPDF يُعيد ترتيب رسم الكائنات المكتمل بحيث يستطيع قارئ يدرك نطاقات البايت عرض الصفحة الأولى بعد جلب الجزء الأولي فقط من الملف، بدلًا من تنزيل المستند بأكمله أولًا. الآلية موصوفة في الملحق F من ISO 32000-1

سبب أهمية هذا الأمر غير مبهر. ملف PDF عادي يضع جدول مراجعه المتقاطعة في النهاية، لذا يجب على العارض الوصول إلى البايت الأخير قبل أن يعرف مكان أي شيء. سلّم متصفحًا تقريرًا ممسوحًا ضوئيًا من 200 صفحة وسيحدّق المستخدم في مؤشر تحميل طوال النقل الكامل، رغم أن كل ما أراده هو الصفحة 1. التخطيط الحتمي يُصلح ذلك بدفع تكلفة عند وقت الكتابة. هذا المقال يتناول مسار الكتابة تحديدًا، التقسيم وحلقة القياس والحدود الصارمة؛ للخلفية المفاهيمية عمّا يجلبه عرض الويب السريع، يغطي الشرح السابق لتخطيط PDF وعرض الويب السريع ذلك المجال

ما الذي يضمنه التخطيط المُنظَّم فعليًا؟

الملف المُخطَّط هو ملف PDF عادي بترتيب فيزيائي محدد للغاية، وكل ضمان يقدّمه يأتي من ذلك الترتيب لا من أي نوع كائن جديد. يُصدر HotPDF الأجزاء بالتسلسل الذي يفرضه الملحق F: قاموس معاملات التخطيط داخل أول 1024 بايت، وجدول مراجع متقاطعة مبكر، وكائنات مستوى المستند، ودفق التلميح الأساسي، والصفحة الأولى وكائناتها الخاصة، ثم بقية الصفحات، ثم الكائنات المشتركة، ثم كل شيء آخر، وأخيرًا جدول المراجع المتقاطعة الرئيسي

التقسيم مُشتَق، لا مُعلَن. يتجول HotPDF في رسم المراجع من كل كائن صفحة ويُسجّل، لكل كائن غير مباشر، كم صفحة تصل إليه وأي صفحة وصلت إليه أولًا. كائن يستخدمه صفحة واحدة بالضبط يصبح خاصًا بتلك الصفحة. كائن تصل إليه أكثر من صفحة يصبح مشتركًا. الفهرس، بالإضافة إلى كل ما يشير إليه تحت /ViewerPreferences و/OpenAction و/Threads و/AcroForm، بالإضافة إلى قاموس التشفير عندما تكون الحماية مفعّلة، تُشكّل مجموعة مستوى المستند التي يجب أن تسبق كل شيء. تُحجَز عقد شجرة الصفحات عمدًا حتى لا تُلوّث قسم الصفحة الأولى

يحمل قاموس المعاملات الأرقام التي يحتاجها القارئ قبل أن يقرأ أي شيء آخر: /L لإجمالي طول الملف، و/H لإزاحة وطول دفق التلميح، و/O لرقم كائن الصفحة الأولى، و/E للبايت الذي ينتهي عنده قسم الصفحة الأولى، و/N لعدد الصفحات، و/T لإزاحة مدخل جدول المراجع المتقاطعة الرئيسي. كل واحد من هذه إزاحة بايت في ملف لا يوجد بعد في اللحظة التي تحتاج فيها إلى كتابته

لماذا يجب أن تتقارب إزاحات جدول التلميح؟

لأن الأرقام في قاموس المعاملات تصف الملف الذي يحويها، وتغيير أي منها يُغيّر الملف. هذه هي الصعوبة المركزية لكاتب مُخطَّط، وهي سبب قياس HotPDF مرارًا بدلًا من الكتابة مرة واحدة. وسّع /T من 6 أرقام إلى 7 وينمو قاموس المعاملات بايتًا واحدًا؛ تنمو الترويسة؛ تنزاح كل الكائنات؛ يتحرك جدول المراجع المتقاطعة الرئيسي؛ الآن يحتاج /T قيمة مختلفة. يجب أن يصل التخطيط إلى نقطة ثابتة قبل أن يُلتزم بأي بايت واحد من الإخراج الحقيقي

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

CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
  CalculateLayout(CandidateMainOffset, FirstXRefData,
    HintOffset, EndFirstPage, NewMainOffset);
  if NewMainOffset = CandidateMainOffset then
    Break;
  CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
  raise Exception.Create('Linearization layout did not converge');

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

تفعيلها من Delphi

سطح الواجهة البرمجية قيمة منطقية واحدة، ومتطلبها الوحيد هو أن تُعيّنها قبل بدء التوليد. تكون LinearizeOutput افتراضيًا False، وتُشغَّل مرحلة التخطيط عند كتابة المستند، لذا تعيينها بعد EndDoc لا يُحقق شيئًا

var
  PDF: THotPDF;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.FileName := 'fast-view.pdf';
    PDF.Version := pdf17;
    PDF.LinearizeOutput := True;      // must precede BeginDoc
    PDF.BeginDoc;
    PDF.Canvas.TextOut(72, 72, 'First page');
    PDF.EndDoc;
  finally
    PDF.Free;
  end;
end;

تحذير نشر واحد يتفوق على كل شيء في جانب الكود. التخطيط لا يجني ثماره إلا عندما يدعم النقل طلبات نطاق HTTP. قدّم نفس الملف من نقطة نهاية تُبثّه كاملًا، أو من تهيئة CDN تتجاهل Range، وستكون قد اشتريت لنفسك مسار كتابة أبطأ وملفًا أكبر دون أي فائدة ظاهرة للمستخدم. تحقّق من الخادم قبل أن تتحقق من الكود

لماذا يتجاوز التخطيط UseXRefStream وUseObjectStreams؟

لأن الكاتب المُخطَّط يحتاج أن يملك كل كائن إزاحة بايت قابلة للعنونة مباشرةً، وكلتا الميزتين تنزعان ذلك. لذا يُصدر HotPDF جداول مراجع متقاطعة نصية تقليدية وكائنات غير مباشرة غير مُحزَّمة كلما فُعِّلت LinearizeOutput، حتى لو عيّن المستدعي أيضًا UseXRefStream أو UseObjectStreams. هذا تجاوز متعمَّد، لا تعارض عليك حله بنفسك

المنطق ينبع من جداول التلميح. جدول التلميح يصف أين يبدأ قسم صفحة وطوله، بحيث يستطيع القارئ طلب ذلك النطاق بالضبط. الكائن المُحزَّم داخل حاوية /ObjStm ليس له إزاحة مستقلة إطلاقًا؛ يوجد فقط كشريحة داخل دفق مضغوط آخر يجب جلبه وفكّه كوحدة واحدة. إذا كنت تعتمد على دفقات الكائنات لحجم الملف، فافهم أن التخطيط والضغط يسحبان في اتجاهين متعاكسين هنا، واقرأ المفاضلة في المقال المرافق عن دفقات الكائنات والتحديثات التزايدية في HotPDF. نفس التوتر يُشكّل الملفات هجينة المرجع، الموجودة تحديدًا لإبقاء القراء الأقدم يعملون جنبًا إلى جنب مع الجداول القائمة على الدفقات، كما هو موضح في مقال دفقات المراجع المتقاطعة الهجينة في ملفات PDF المُولَّدة من Office

هناك أيضًا حد أدنى للإصدار. يتطلب التخطيط PDF 1.2 أو أحدث. إذا كان الإصدار المختار أقدم، يرفعه HotPDF تلقائيًا، ما لم يُعيَّن StrictVersionLock، وفي هذه الحالة تُثير الكتابة استثناءً بدلًا من ترقية مستند ثبّته عمدًا بهدوء

جدار الـ 4 غيغابايت، ولماذا يرفض HotPDF بدلًا من التقطيع

تُخزّن جداول تلميح التخطيط الإزاحات كقيم 32 بت، لذا لا يستطيع ملف مُخطَّط عنونة أي شيء عند 4 غيغابايت أو بعدها، ويرفض HotPDF هذا الإخراج باستثناء صريح بدلًا من كتابة ملف بإزاحات ملتفّة. الحد ليس خيار تنفيذ من HotPDF؛ إنه عرض الحقول التي يُعرّفها الملحق F

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

كشف التخطيط في ملف حمّلته

تُبلغ THotPDF.IsLoadedLinearized عمّا إذا كان المستند المُحمَّل حاليًا قد كُتب بالفعل بصيغة مُخطَّطة، وتُجيب من لقطة أُخذت قبل التحليل، لا من الدفق الحي. يقرأ HotPDF أول 1024 بايت من الموضع صفر في دفق المصدر، ويفحصها بحثًا عن أول كلمة مفتاحية obj ثم عن مدخل /Linearized بالقيمة 1، ويُخزّن النتيجة المنطقية مؤقتًا

var
  PDF: THotPDF;
  PageCount: Integer;
begin
  PDF := THotPDF.Create(nil);
  try
    PageCount := PDF.LoadFromFile('incoming.pdf');
    if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
      Writeln('Source is not Fast Web View ready');
  finally
    PDF.Free;
  end;
end;

قيدان في ذلك الوصف حاملان للحمل. الكشف لا يستطيع الاعتماد على موضع الدفق، لأنه بحلول الوقت الذي يسأل فيه كود التطبيق السؤال يكون المحلل قد حرّكه، ولا يستطيع إعادة القراءة عند الطلب لأن LoadFromFile تُحرّر دفق المصدر الداخلي بمجرد انتهاء التحميل. من هنا تصميم الالتقاط-قبل-التحليل-والتخزين المؤقت. الفحص أيضًا حرفي عمدًا بشأن القيمة: فقط /Linearized 1 أو شكل مكافئ رقميًا بجزء كسري كله أصفار مقبول، لأن ملفًا يقول قاموس معاملاته شيئًا آخر لا يفي بوعد الملحق F

فخ سجل في Delphi يستحق السرقة

السجلات المحلية التي تحتوي مصفوفات ديناميكية تُهيّئ حقولها المُدارة ولا شيء آخر، وإذا احتفظت بحقل Count عادي بجانب المصفوفة يجب أن تُصفّره بنفسك. هذا أوقع تقسيم التخطيط أثناء التطوير، وهو نوع الخلل الذي يُكلّف يومًا تحديدًا لأن منصة واحدة تُخفيه

type
  THPDFLinearIndexList = record
    Values: THPDFIntegerArray;  // managed field: cleared for you
    Count: Integer;             // plain field: whatever was on the stack
  end;

// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);

حقل المصفوفة الديناميكية مُعدود المراجع، لذا يُصفّره المترجم. أما Count بجانبه فهو عدد صحيح عادي بلا ضمان كهذا، وCount غير مُهيَّأ يُرسل أول إلحاق إلى فهرس عشوائي. تحت Win32، صدف أن فتحة المكدس تحمل صفرًا، فوقع الإلحاق عند الفهرس 0، ونجح كل اختبار. تحت Win64، كتب نفس الكود بعد نهاية المصفوفة. الدرس يتعمم بعيدًا عن التخطيط: عندما يخلط سجل بين حقول مُدارة وغير مُدارة، عيّن Default(TRecord) وتوقّف عن التفكير في أي الحقول يُغطّيها المترجم، ولا تعامل أبدًا تشغيلة Win32 ناجحة كدليل على صحة التهيئة

عضوا LinearizeOutput وIsLoadedLinearized الموصوفان هنا يُشحنان مع HotPDF Component القياسي لـ Delphi وC++Builder؛ صفحة المنتج تحمل المرجع الكامل للخصائص، بما في ذلك قواعد التفاعل مع دفقات المراجع المتقاطعة، ودفقات الكائنات، وقفل الإصدار