يعد ملف PDF في الأساس حاوية نص عادي. افتح معظم الملفات في محرر سداسي عشري (hex editor) وسيكون الجزء العلوي مقروءاً: تعليق بالإصدار، ثم سلسلة من الكائنات المرقمة، ثم فهرس صغير ومؤشر في الأسفل تماماً يخبر القارئ بمكان البدء. جرّد التنسيق من الضغط وسيكون قابلاً للوصول لدرجة أنه يمكنك كتابة مستند يعمل في محرر نصوص وفتحه في عارض المستندات. القيام بذلك مرة واحدة يعلمك المزيد عن كيفية تماسك PDF أكثر من أي قدر من قراءة المواصفات، لأنك تضطر لربط الكائنات ببعضها البعض يدوياً ويرفض الملف الفتح حتى تضبط هذا الربط بشكل صحيح
تبني هذه الجولة أصغر ملف PDF يمكنه بالفعل عرض شيء ما: صفحة واحدة، وكلمتي "Hello, World!" بخط مدمج، على ورق بحجم US Letter. الملف النهائي يحتاج إلى خمسة كائنات بالضبط وبعض أسطر حفظ السجلات حولها. سنكتب الكائنات أولاً، ثم نجمع الترويسة، وجدول الإحالات المرجعية (cross-reference)، والذيل (trailer) الذي يربطها في ملف سيقبله القارئ
الكائنات الخمسة التي يصر عليها العارض
لا يقوم القارئ بمسح ملف PDF من أعلى إلى أسفل بحثاً عن المحتوى. بل يبدأ من الذيل (trailer)، ويتبع مرجعاً إلى فهرس المستند (Catalog)، ويمشي في سلسلة من الكائنات من هناك. يجب أن يكون كل كائن في تلك السلسلة موجوداً وإلا سيفشل الفتح. بالنسبة لمستند من صفحة واحدة، تكون السلسلة قصيرة، ولكل رابط وظيفة واحدة:
- Catalog (الفهرس) هو الجذر. وهو الكائن الذي يشير إليه الذيل (trailer)، وإدخاله الوحيد المطلوب هنا هو مرجع إلى شجرة الصفحات (page tree)
- Pages (الصفحات) هي عقدة شجرة الصفحات. وهي تدرج الصفحات في المستند وتبلغ عن عددها
- Page (الصفحة) تصف صفحة فعلية واحدة: حجمها، والموارد التي ترسم بها، وتدفق المحتوى (content stream) الذي يلونها
- Content stream (تدفق المحتوى) يحمل عوامل الرسم، وأوامر اللاحقة (postfix) التي تضع النص والرسومات على تلك الصفحة
- Font (الخط) يعلن عن الخط الذي يشير إليه تدفق المحتوى. استخدم أحد الخطوط القياسية الـ 14 ولن تضطر إلى تضمين أي شيء
كل كائن مرقم وقابل للعنونة. يُكتب الكائن غير المباشر بصيغة N 0 obj ... endobj، حيث N هو رقم الكائن و 0 هو رقم التوليد (دائماً 0 في ملف تكتبه حديثاً). في أي مكان آخر في الملف، تشير إلى هذا الكائن بمرجع: 5 0 R تعني "الكائن 5". هذه المراجع هي الربط. يحتوي Catalog على 2 0 R في ترقيمنا للوصول إلى شجرة الصفحات، وتحتوي شجرة الصفحات على مرجع للأسفل إلى الصفحة، وهكذا. أخطئ في رقم وسيتتبع القارئ مؤشراً معلقاً نحو اللاشيء
الأسماء، القواميس، والتدفقات
ثلاث قطع من بناء الجملة (syntax) تحمل كل شيء تقريباً. يبدأ الاسم (name) بشرطة مائلة: /Type، /Page، /F0. الأسماء هي معرفات حساسة لحالة الأحرف، وليست سلاسل نصية، ويستخدمها PDF كمفاتيح قواميس (dictionary keys) ولوضع علامات تحدد ما هو الكائن. القاموس (dictionary) هو مجموعة من أزواج المفتاح-القيمة (key-value) محاطة بأقواس زاوية مزدوجة، حيث يكون كل مفتاح اسماً: << /Type /Page /MediaBox [0 0 612 792] >>. يمكن أن تكون القيم أرقاماً، أسماءً، مصفوفات بين أقواس مربعة، مراجع، أو قواميس متداخلة. معظم كائنات PDF هي قواميس
التدفق (stream) هو قاموس تتبعه كتلة من البايتات بين الكلمتين الأساسيتين stream و endstream. هناك تعيش عوامل رسم الصفحات، وفي الملفات الحقيقية حيث تعيش الصور المضغوطة والخطوط المضمنة أيضاً. يصف قاموس التدفق البايتات؛ في ملف الإنتاج يجب أن يحمل إدخال /Length يعطي عدد البايتات الدقيق، وغالباً /Filter مثل /FlateDecode عندما تكون البيانات مضغوطة. سنعتمد على أداة لملء /Length، لأن حساب البايتات يدوياً هو الجزء من هذا التمرين الذي لا يحمل فائدة تعليمية وفرصة عالية لخطأ بفارق واحد يؤدي لكسر الملف
كتابة الكائنات
إليك الكائنات الخمسة بالترتيب. تفاصيل الإحداثيات التي يجب وضعها في الاعتبار قبل قراءة تدفق المحتوى: يقيس PDF من الزاوية اليسرى السفلية للصفحة بالنقاط (points)، حيث تساوي النقطة الواحدة 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، Catalog، يوجه إدخاله /Pages نحو الكائن 2. الكائن 2، شجرة الصفحات، يدرج الكائن 3 في /Kids ويعلن /Count 1. الكائن 3، الصفحة، يوجه /Parent للأعلى نحو الكائن 2 (الشجرة والصفحة تشيران لبعضهما البعض، وهو أمر مطلوب)، ويحدد حجمه بـ /MediaBox، ويكشف الخط تحت الاسم المحلي /F0 في موارده (/Resources)، ويسمي الكائن 5 كمحتوى له. الكائن 4 هو الخط: /BaseFont /Helvetica يختار أحد الخطوط القياسية الـ 14 التي يمتلكها كل قارئ متوافق بالفعل، لذا لا يوجد شيء ليتم تضمينه. الكائن 5 هو تدفق المحتوى (content stream)
ماذا يقول تدفق المحتوى بالفعل
جسم التدفق هو برنامج صغير بلغة وصف الصفحات الخاصة بـ 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، ولكن القارئ يتعامل مع هذا التعليق المعين باعتباره توقيع التنسيق ويقرأ الإصدار منه. الكاتب الحقيقي يتبعه على الفور بسطر تعليق ثانٍ من بايتات عالية البت (high-bit bytes)، كإشارة لأدوات نقل الملفات بأن الملف ثنائي (binary) ويجب ألا يتم تشويهه كنص عادي
في نهاية الملف يأتي جدول الإحالات المرجعية (cross-reference table)، الفهرس الذي يجعل الوصول العشوائي ممكناً. يسجل هذا الجدول إزاحة البايت (byte offset) لكل كائن من بداية الملف، بحيث يمكن للقارئ الانتقال مباشرة إلى الكائن 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 المقابل، وتتحرك كل إزاحة بمجرد إضافة حرف في أي مكان أعلاها. الذيل (trailer) هو نقطة الدخول التي يستخدمها القارئ أخيراً وأولاً: /Root 1 0 R يسمي الـ Catalog، و /Size 6 ينص على عدد الكائنات، و startxref 408 يعطي إزاحة البايت لكلمة xref نفسها. يفتح القارئ الملف، وينتقل إلى النهاية، ويقرأ startxref، ويبحث عن جدول الإحالات المرجعية، ومن هناك يصل إلى الـ Catalog وكل ما دونه. تشير %%EOF إلى البايت الأخير
دع أداة تصلح حسابات البايت
الإزاحات أعلاه توضيحية؛ عملياً ستكون خاطئة بحلول الوقت الذي تنتهي فيه من الكتابة، لأنها تعتمد على التخطيط الدقيق للبايت في ملفك. بدلاً من إعادة حسابها، اكتب الهيكل بقيم نائبة ودع أداة مساعدة تعيد بناء جدول الإحالات المرجعية وأطوال التدفقات. الأداة المجانية عبر المنصات pdftk تفعل ذلك في تمريرة واحدة:
pdftk hello-draft.pdf output hello.pdf
إنها تحلل كائناتك، وتعيد حساب كل إزاحة بايت، وتملأ قيم /Length الصحيحة، وتكتب جدول xref وذيل صالحين، وتصدر hello.pdf. افتح هذا في أي عارض وستحصل على صفحة واحدة مكتوب عليها "Hello, World!" بخط Helvetica بحجم 36 نقطة بالقرب من الأعلى. تقوم Qpdf بنفس المهمة، والعديد من العارضين سيقومون أيضاً بإصلاح ملف مشوه قليلاً أثناء التنقل. الهدف من الاعتماد على أداة هنا ليس الكسل؛ بل لأن الحساب الرياضي للإزاحة هو الجزء الوحيد من التنسيق الذي لا يحتوي على محتوى مفاهيمي وأعلى معدل خطأ، لذا فإن أتمتته تجعل الهيكل هو الشيء الذي تتعلمه
لماذا يتوسع هذا ليناسب المستندات الحقيقية
لا شيء بخصوص تقرير مكون من مائة صفحة يغير الشكل الذي بنيته للتو. لا يزال الـ Catalog يجلس في الجذر، ولا تزال شجرة الصفحات تجمع الصفحات، ولا تزال كل صفحة تشير إلى مواردها وتدفق المحتوى الخاص بها. ما ينمو هو الاتساع، وليس العمود الفقري: تتفرع شجرة الصفحات حتى يتمكن القارئ من تخطي أشجار فرعية بأكملها، وتحمل تدفقات المحتوى مئات العوامل بدلاً من خمسة، ويتم تضمين الخطوط ككائنات تدفق خاصة بها مع جداول العرض والترميزات، وتصل الصور كتدفقات بمرشحات خاصة بالصور. تميل الملفات الحديثة أيضاً إلى حزم العديد من الكائنات في تدفقات كائنات مضغوطة واستبدال جدول xref العادي بتدفق إحالة مرجعية، وهذا هو السبب في أن فتح ملف PDF حقيقي في محرر نصوص عادة ما يظهر كجدار من البيانات الثنائية. النموذج الأساسي مطابق لذلك الموجود في ملفك اليدوي. للحصول على رسم بياني أوسع للكائنات وكيفية ارتباط الـ Catalog وشجرة الصفحات وقواميس الموارد عبر مستند أكبر، تبدأ الجولة المتعمقة في هيكل مستند PDF من حيث ينتهي هذا المقال، و تغطي نظرة عامة على هيكل الملف التحديثات الإضافية وكيفية تسلسل الذيل عبر المراجعات
من الكتابة اليدوية إلى المكتبة
تعتبر كتابة الكائنات يدوياً تمريناً تعليمياً، وليست أسلوب إنتاج. في اللحظة التي تحتاج فيها إلى خطوط حقيقية، أو نص ملتف، أو صور، أو أكثر من صفحة بسيطة، تصبح محاسبة البايتات التي أصلحتها pdftk لك هي المهمة بأكملها، وتريد مكتبة تتولى ذلك. لا تزال الكائنات الخمسة تُكتب، لكن المكتبة تحسب كل إزاحة، وتدير قواميس الخطوط والموارد، وتضغط تدفقات المحتوى دون أن تتبع بايتاً واحداً. في Delphi و C++Builder، يقلل مكون HotPDF هذا الملف بأكمله إلى بضع استدعاءات: قم بإعداد المستند، واستدعِ BeginDoc و SetFont و TextOut لوضع نفس التحية، ثم EndDoc لكتابة Catalog صحيح وشجرة صفحات و xref وذيل. إن فهم الكائنات الموجودة بالأسفل هو ما يسمح لك بالتفكير في المخرجات عندما لا يعرض المستند بالطريقة التي توقعتها