Technical Article

إنشاء مستند PDF بسيط يدويًا: الكائنات الخمسة التي تحتاجها

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

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

الكائنات الخمسة التي لا يستغني عنها العارض

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

  • الكتالوج هو الجذر. هو الكائن الذي يشير إليه التذييل، ولا يحتاج هنا إلا إلى مرجع إلى شجرة الصفحات.
  • شجرة الصفحات هي عقدة الصفحات. تسرد الصفحات في المستند وتذكر عددها.
  • الصفحة تصف صفحة مادية واحدة: حجمها، والموارد التي ترسم بها، وتدفق المحتوى الذي يطبعها.
  • تدفق المحتوى يحمل أوامر الرسم، أي الأوامر اللاحقة التي تضع النص والرسوم على تلك الصفحة.
  • الخط يعلن نوع الخط الذي يشير إليه تدفق المحتوى. استخدم واحدًا من الخطوط القياسية الـ14 ولن تحتاج إلى تضمين أي شيء.

كل كائن يحمل رقمًا ويمكن الوصول إليه عبر مرجع. يُكتب الكائن غير المباشر بهذه الصيغة: N 0 obj ... endobj، حيث N هو رقم الكائن و0 هو رقم الجيل، وهو دائمًا 0 في ملف تكتبه من الصفر. وفي أي موضع آخر من الملف تشير إلى ذلك الكائن بمرجع: 5 0 R تعني "الكائن 5". تلك المراجع هي الأسلاك. الكتالوج يحمل 2 0 R في ترقيمنا للوصول إلى شجرة الصفحات، وشجرة الصفحات تحمل مرجعًا نزولًا إلى الصفحة، وهكذا. أخطئ الرقم وسيجد القارئ نفسه يتبع مرجعًا يتيمًا لا يصل إلى شيء.

الأسماء والقواميس والتدفقات

ثلاثة عناصر في الصياغة تحمل معظم العبء. يبدأ الاسم بشرطة مائلة: /Type، /Page، /F0. الأسماء معرّفات حساسة لحالة الأحرف وليست سلاسل نصية، ويستخدمها PDF كمفاتيح للقواميس وللدلالة على ماهية الكائن. القاموس مجموعة أزواج مفتاح وقيمة محاطة بعلامتي زاوية مزدوجتين، حيث يكون كل مفتاح اسمًا: << /Type /Page /MediaBox [0 0 612 792] >>. يمكن أن تكون القيم أرقامًا، أو أسماء، أو مصفوفات داخل أقواس مربعة، أو مراجع، أو قواميس متداخلة. معظم كائنات PDF هي قواميس.

التدفق هو قاموس يتبعه كتلة من البايتات بين الكلمتين stream وendstream. هناك تعيش أوامر رسم الصفحة، وفي الملفات الحقيقية تعيش أيضًا الصور المضغوطة والخطوط المضمّنة. يصف القاموس البايتات؛ وفي ملف إنتاج يجب أن يحتوي على إدخال /Length يعطي العدد الدقيق للبايتات، وغالبًا على /Filter مثل /FlateDecode عندما تكون البيانات مضغوطة. سنعتمد على أداة لملء /Length، لأن عدّ البايتات يدويًا هو الجزء من هذا التمرين الذي لا فائدة تعليمية له واحتمال الخطأ فيه كبير جدًا، خاصة خطأ زائد أو ناقص يكسر الملف.

كتابة الكائنات

إليك الكائنات الخمسة بالترتيب. قبل قراءة تدفق المحتوى، تذكّر تفصيل الإحداثيات: يقيس PDF من الزاوية السفلية اليسرى للصفحة بالنقاط، حيث تساوي النقطة الواحدة 1/72 بوصة، ويمتد المحور Y إلى الأعلى. صفحة US Letter مقاسها 612 × 792 نقطة، لذا تقع 50 700 قرب أعلى اليسار، لا أسفل الصفحة.

1 0 obj
<< /Type /Catalog
   /Pages 2 0 R
>>
endobj

2 0 obj
<< /Type /Pages
   /Kids [3 0 R]
   /Count 1
>>
endobj

3 0 obj
<< /Type /Page
   /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F0 4 0 R >> >>
   /Contents 5 0 R
>>
endobj

4 0 obj
<< /Type /Font
   /Subtype /Type1
   /BaseFont /Helvetica
>>
endobj

5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj

اقرأ المراجع وستظهر البنية بوضوح. الكائن 1، أي الكتالوج، يوجّه إدخال /Pages إلى الكائن 2. الكائن 2، أي شجرة الصفحات، يسرد الكائن 3 في /Kids ويعلن /Count 1. الكائن 3، أي الصفحة، يشير /Parent مرة أخرى إلى الكائن 2، فالشجرة والصفحة يشيران إلى بعضهما، وهذا مطلوب، ويضبط حجمه بواسطة /MediaBox، ويكشف الخط تحت الاسم المحلي /F0 في /Resources، ويعيّن الكائن 5 كمحتواه. الكائن 4 هو الخط: /BaseFont /Helvetica يختار واحدًا من الخطوط القياسية الـ14 التي يمتلكها كل قارئ متوافق بالفعل، لذلك لا حاجة إلى تضمين أي شيء. الكائن 5 هو تدفق المحتوى.

ما الذي يقوله تدفق المحتوى فعلاً

نص التدفق برنامج صغير بلغة وصف الصفحة في PDF، وهو بصيغة postfix: تأتي المعاملات أولًا ثم العامل الذي يستهلكها. خمس أسطر تنجز العمل. يفتح BT وET كائن النص ويغلقانه، وكل ما يضع موضع النص أو يعرضه يجب أن يقع بينهما. يضبط /F0 36 Tf الخط الحالي إلى المورد المسمى /F0 بحجم 36 نقطة، وTf تعني "تعيين خط النص وحجمه". ينقل 50 700 Td موضع النص إلى الإحداثيات (50، 700) داخل الصفحة. يعرض (Hello, World!) Tj السلسلة، التي يكتبها PDF كنص حرفي بين قوسين، مستخدمًا Tj لرسمها في الموضع الحالي. إذا حذفت BT/ET سيرفض قارئ صارم أوامر النص، وإذا نسيت تعيين خط قبل Tj فلن يوجد خط حالي للرسم به.

القيمة /Length 44 في قاموس التدفق هي عدد البايتات بين stream وendstream، ولا بد أن تكون دقيقة. هذه هي القيمة التي يجدر تسليمها إلى أداة بدل عدّ أسطر النهاية يدويًا، خاصة أن ما إذا كان محررك يكتب نهايات الأسطر بصيغة LF أو CRLF يغيّر المجموع.

الرأس وxref والتذييل

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

%PDF-1.7

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

في نهاية الملف يأتي جدول المراجع المتقاطعة، أي الفهرس الذي يجعل الوصول العشوائي ممكنًا. يسجّل إزاحة البايت لكل كائن من بداية الملف، لكي يتمكن القارئ من القفز مباشرة إلى الكائن 3 من دون تحليل الكائنين 1 و2 أولًا. الجدول صارم: الإدخالات ثابتة العرض، 20 بايتًا لكل إدخال بما في ذلك نهاية السطر، وتُنسق على هيئة إزاحة من 10 أرقام، وجيل من 5 أرقام، وكلمة مفتاحية (n للكائن المستخدم وf للكائن الحر)، ومحدد نهائي من بايتين. هذا شكل جدول صحيح لمدخلاتنا الستة (إذ إن الكائن 0 هو دائمًا رأس قائمة الكائنات الحرة):

xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
   /Root 1 0 R
>>
startxref
408
%%EOF

تلك الإزاحات هي الجزء الهش في كتابة PDF يدويًا. كل واحدة منها هي الموضع البايت الدقيق الذي يبدأ عنده N 0 obj الموافق، وتتغير كل الإزاحات في اللحظة التي تضيف فيها حرفًا واحدًا فوقها. التذييل هو نقطة الدخول التي يستخدمها القارئ أخيرًا وأولًا: يعرّف /Root 1 0 R الكتالوج، ويعلن /Size 6 عدد الكائنات، ويعطي startxref 408 إزاحة البايت للكلمة xref نفسها. يفتح القارئ الملف، يقفز إلى النهاية، يقرأ startxref، ينتقل إلى جدول المراجع المتقاطعة، ومن هناك يصل إلى الكتالوج وكل ما دونه. تشير %%EOF إلى آخر بايت.

دع أداة تصلح عدّ البايتات

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

pdftk hello-draft.pdf output hello.pdf

تحلل الأداة الكائنات، وتعيد حساب كل إزاحة بايت، وتملأ قيم /Length الصحيحة، وتكتب جدول xref وتذييلًا صالحين، ثم تنتج hello.pdf. افتحه في أي عارض وستحصل على صفحة واحدة فيها "Hello, World!" بخط Helvetica بحجم 36 نقطة قرب الأعلى. تؤدي qpdf المهمة نفسها، وكثير من العارضين يصلحون أيضًا ملفًا معطوبًا قليلًا أثناء الفتح. الفكرة من الاعتماد على أداة هنا ليست الكسل؛ بل أن حساب الإزاحات هو الجزء الوحيد من التنسيق الذي يفتقر إلى أي مضمون مفهومي، ومع ذلك يرتفع فيه معدل الخطأ، لذا فإن أتمتته تترك البنية نفسها هي ما تتعلمه.

لماذا يتوسع هذا إلى مستندات حقيقية

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

من الكتابة اليدوية إلى مكتبة برمجية

كتابة الكائنات يدويًا تمرين تعليمي، لا تقنية إنتاج. في اللحظة التي تحتاج فيها إلى خطوط حقيقية أو نص ملتف أو صور أو أكثر من صفحة تافهة، يصبح ضبط البايتات الذي أصلحه لك pdftk هو المهمة كلها، وتحتاج إلى مكتبة تتولى ذلك. لا تزال الكائنات الخمسة نفسها تُكتب، لكن المكتبة تحسب كل إزاحة، وتدير قواميس الخطوط والموارد، وتضغط تدفقات المحتوى من دون أن تتعقب بايتًا واحدًا بنفسك. في Delphi وC++Builder، يقلّص مكوّن HotPDF هذا الملف بأكمله إلى بضعة استدعاءات: هيّئ المستند، ثم استدعِ BeginDoc وSetFont وTextOut لوضع التحية نفسها، ثم EndDoc لكتابة كتالوج وشجرة صفحات وجدول xref وتذييل صحيحين. فهم الكائنات في الأسفل هو ما يمكّنك من تفسير الناتج عندما لا يظهر المستند كما توقعت.