كتابة PDF من الصفر (01): مرحبا بالعالم - بناء PDF قابل للاستخدام
كتابة PDF من الصفر (01): مرحبا بالعالم - بناء PDF قابل للاستخدام
هدف السلسلة: فهم PDF كنوع من تنسيق الملفات القابل للقراءة - بدءًا من "مثال صغير يعمل"، ثم التوسع تدريجيًا إلى الرسوميات، والصفحات المتعددة، والضغط وإعادة استخدام الموارد.
فهرس السلسلة
- المقالة 01 (هذه المقالة): كتابة PDF صغير يدويًا (صفحة واحدة + سطر واحد من النص)، واستخدام الأدوات لإكماله ليصبح PDF قياسي يمكن فتحه
- المقالة 02: رسم خطوط/مستطيلات في تدفق المحتوى (فهم المسارات، الحدود، التعبئة)
- المقالة 03: PDF متعدد الصفحات (كيف تتشكل شجرة الصفحات)
- المقالة 04: الاقتراب من العالم الحقيقي (تدفق مضغوط، إعادة استخدام الموارد، الهياكل الاختيارية، إلخ)
لماذا يجب أن نفهم الهيكل الأساسي لـ PDF؟
PDF (تنسيق المستندات القابل للنقل) هو واحد من أكثر لغات وصف الصفحات شيوعًا اليوم. على عكس HTML/CSS الذي يفصل بين "المحتوى والعرض، وقابل لإعادة التدفق"، يركز PDF أكثر على ثبات التخطيط، وما تراه هو ما تحصل عليه - بغض النظر عن الجهاز الذي يتم فتحه عليه، يظل التنسيق ثابتًا.
هناك عدة فوائد عملية لفهم الهيكل الأساسي لـ PDF:
- تصحيح مشاكل إنشاء PDF: عندما يحدث خطأ أثناء إنشاء PDF باستخدام مكتبة كود، فإن فهم الهيكل الأساسي يمكن أن يساعدك في تحديد المشكلة بسرعة
- المعالجة الآلية: لاستخراج النصوص بكميات كبيرة، دمج الوثائق، إضافة علامات مائية، إلخ، بعد فهم الهيكل يمكنك إجراء العمليات بدقة
- التدقيق الأمني: فهم ما يمكن تضمينه في PDF (JavaScript، المرفقات، النماذج، إلخ) يساعد في التحليل الأمني
- تعلم تصميم تنسيقات الملفات: تصميم PDF "رسم الكائنات + الوصول العشوائي" هو مثال كلاسيكي يستحق التعلم
التحضيرات
ستكتب هذه المقالة ملف hello-broken.pdf "غير مكتمل الهيكل ولكن منطقي"، ثم تستخدم pdftk لإكمال الهيكل الأساسي تلقائيًا وإخراج hello.pdf.
- الأدوات المطلوبة: pdftk (أداة سطر أوامر مجانية، تدعم Windows/macOS/Linux)
- ملفات الإخراج:
hello-broken.pdf(مكتوب يدويًا)،hello.pdf(بعد الإصلاح يمكن فتحه)
المفاهيم الأساسية: الهيكل الثلاثي لـ PDF
أهم شيء لفهم PDF هو بناء نموذج ذهني ثلاثي:

1. طبقة الكائنات (محتوى المستند)
يتكون مستند PDF من العديد من الكائنات، وترتبط الكائنات ببعضها البعض باستخدام الإشارات غير المباشرة (مثل 2 0 R) لتشكيل صورة. أنواع الكائنات الشائعة:
| النوع | المثال | الوصف |
|---|---|---|
| اسم | /Page | اسم يبدأ بـ / |
| عدد صحيح/عدد عشري | 50، 36.0 | قيمة عددية |
| سلسلة | (Hello, World!) | محاطة بأقواس |
| مصفوفة | [0 0 612 792] | مجموعة مرتبة |
| قاموس | << /Type /Page >> | مجموعة من أزواج المفاتيح والقيم |
| إشارة غير مباشرة | 2 0 R | إشارة إلى الكائن 2 (رقم الجيل 0) |
| تدفق | stream...endstream | بيانات ثنائية (مثل تعليمات الرسم، الصور) |
2. طبقة المحتوى (محتوى الصفحة)
سلسلة التعليمات التي "ترسم النص/الرسوم على الصفحة"، عادة ما تكون مكتوبة في stream ... endstream. التنسيق هو: المعاملات تأتي أولاً، ثم المشغل.
/F0 36 Tf ← المعامل: /F0، 36 المشغل: Tf (تعيين الخط)
(Hello, World!) Tj ← المعامل: سلسلة المشغل: Tj (رسم النص)
3. طبقة هيكل الملف (هيكل الملف)
تمكن القارئ من الوصول العشوائي السريع إلى أي كائن، دون الحاجة لقراءة الملف من البداية إلى النهاية:
| العنصر | الوظيفة |
|---|---|
%PDF-1.x | رأس الملف، يحدد إصدار PDF |
xref | جدول الإشارات المتقاطعة: رقم الكائن → إزاحة البايت |
trailer | قاموس النهاية: يشير إلى الكائن الجذري /Root |
startxref | يشير إلى موقع بداية جدول الإشارات المتقاطعة |
%%EOF | علامة نهاية الملف |
ما هي الكائنات المطلوبة لـ PDF صغير؟
PDF "صغير ولكنه يمكنه عرض النص" له علاقات الإشارة بين الكائنات كما يلي:

قائمة الكائنات الصغيرة:
| الكائن | الوظيفة | الحقول الرئيسية |
|---|---|---|
| الفهرس | الكائن الجذري، مدخل الوثيقة | /Type /Catalog, /Pages |
| الصفحات | شجرة الصفحات | /Type /Pages, /Kids, /Count |
| الصفحة | صفحة واحدة | /Type /Page, /MediaBox, /Resources, /Contents, /Parent |
| الموارد | حاوية الموارد | /Font (قاموس الخطوط) |
| الخط | تعريف الخط | /Type /Font, /BaseFont, /Subtype |
| المحتويات | تدفق المحتوى | تدفق تعليمات الرسم |
التطبيق العملي: كتابة hello-broken.pdf
قم بإنشاء ملف hello-broken.pdf، والصق المحتوى التالي بالكامل فيه:
%PDF-1.0
1 0 obj
<< /Type /Pages
/Count 1
/Kids [2 0 R]
>>
endobj
2 0 obj
<< /Type /Page
/MediaBox [0 0 612 792]
/Resources 3 0 R
/Parent 1 0 R
/Contents [4 0 R]
>>
endobj
3 0 obj
<< /Font
<< /F0
<< /Type /Font
/BaseFont /Times-Italic
/Subtype /Type1 >>
>>
>>
endobj
4 0 obj
<< >>
stream
1. 0. 0. 1. 50. 700. cm
BT
/F0 36. Tf
(Hello, World!) Tj
ET
endstream
endobj
5 0 obj
<< /Type /Catalog
/Pages 1 0 R
>>
endobj
xref
0 6
trailer
<< /Size 6
/Root 5 0 R
>>
startxref
0
%%EOF
لماذا يعتبر هذا الملف "معطلاً"؟
لقد تعمدنا حذف أو ملء المحتوى بشكل خاطئ في العناصر التالية:
| العنصر المفقود/الخطأ | الوصف |
|---|---|
إزاحة xref | لم يتم ملء الإزاحة الحقيقية لكل كائن |
startxref | تم ملؤه بـ 0، وليس الموقع الحقيقي لـ xref |
/Length | لم يتم إعلان طول تدفق المحتوى |
| علامة ثنائية | تفتقر إلى سطر التعريف الثنائي في الرأس |
هذه كلها معلومات رئيسية يحتاجها القارئ، وغيابها قد يؤدي إلى عدم القدرة على الفتح أو الفتح مع الأخطاء.
شرح التعليمات الرئيسية لتدفق المحتوى
تدفق المحتوى بين 4 0 obj في stream ... endstream، يتم شرحه سطرًا بسطر:
1. 0. 0. 1. 50. 700. cm ← تعيين مصفوفة التحويل (لاحظ أن 1. تمثل العدد العشري 1.0)
BT ← بدء كائن النص
/F0 36. Tf ← اختيار الخط F0، حجم الخط 36pt
(Hello, World!) Tj ← رسم السلسلة
ET ← إنهاء كائن النص
مشغل مصفوفة التحويل cm
1 0 0 1 50 700 cm هو مصفوفة تحويل مكونة من 6 عناصر [a b c d e f]، تتوافق مع:
| a b 0 | | 1 0 0 |
| c d 0 | = | 0 1 0 |
| e f 1 | | 50 700 1 |
عندما يكون a=1, b=0, c=0, d=1، تكون هذه مصفوفة نقل نقية، حيث يتم نقل نقطة الأصل في نظام الإحداثيات (أي النقطة (0,0) التي سيتم رسمها لاحقًا) إلى (50, 700). إذا لم يتم النقل، فإن نقطة الأصل تكون في الزاوية السفلى اليسرى من الصفحة.
مشغلات النص
| المشغل | المعنى | المثال |
|---|---|---|
BT | بدء النص، بدء كائن النص | BT |
ET | إنهاء النص، إنهاء كائن النص | ET |
Tf | تعيين الخط وحجم الخط | /F0 36 Tf |
Tj | رسم السلسلة | (Hello!) Tj |
استخدام pdftk لإصلاح PDF ليصبح قابلًا للفتح
في الدليل الذي يحتوي على hello-broken.pdf، نفذ:
pdftk hello-broken.pdf output hello.pdf
استخدم أي قارئ PDF لفتح hello.pdf، يجب أن ترى النص "Hello, World!" (خط Times-Italic، 36pt، في الزاوية العليا اليسرى من الصفحة).
ماذا أكمل لك pdftk؟
| العناصر المكتملة | الوصف |
|---|---|
| سطر العلامة الثنائية | أضاف سطرًا غير قابل للطباعة بعد %PDF-1.0، لضمان التعرف عليه كملف ثنائي |
/Length | حساب وإضافة طول البايت لتدفق المحتوى |
جدول xref | حساب إزاحة البايت لكل كائن وإدخالها |
startxref | إدخال الموقع الحقيقي لبداية جدول xref |
لماذا نحتاج إلى xref / trailer / startxref؟
الهدف الأساسي: الوصول العشوائي
تخيل PDF مكون من 500 صفحة، إذا لم يكن هناك xref، يجب على القارئ قراءة من البداية حتى الصفحة 449 لعرض الصفحة 450 - وهذا بطيء جدًا.
مع وجود xref، يمكن للقارئ:
- قراءة
startxref→ العثور على موقع xref - قراءة
trailer→ العثور على الكائن الجذري/Root - من الكائن الجذري، تتبع الكائنات → القفز مباشرة إلى كائن الصفحة 450
- من خلال xref، تحقق من إزاحة البايت لذلك الكائن → انتقل مباشرة لقراءته
تتغير تعقيد الوقت من O(n) إلى O(1).
تمارين هذا المقال
ننصحك حقًا بتعديل hello-broken.pdf، ثم إعادة إصلاحه باستخدام pdftk، وملاحظة النتائج:
| التمرين | المحتوى المعدل | نقاط الملاحظة |
|---|---|---|
| A | تغيير (Hello, World!) إلى جملة إنجليزية أخرى | تغيير النص |
| B | تغيير 36 إلى 12 أو 72 | تغيير حجم الخط |
| C | تغيير 50 700 إلى 50 100 | نقل الموقع لأسفل (نقطة الأصل في نظام إحداثيات PDF في الزاوية السفلى اليسرى) |
| D | تغيير /Times-Italic إلى /Helvetica أو /Courier | تغيير الخط |
| E | تغيير /MediaBox [0 0 612 792] إلى [0 0 595 842] | تغيير الورق من US Letter إلى A4 |
تلميح: نقطة الأصل في نظام إحداثيات PDF في الصفحة الزاوية السفلى اليسرى، والمحور Y يتجه لأعلى.
(50, 700)تعني 50pt من اليسار، و700pt من الأسفل.
الأسئلة الشائعة
س: لماذا نستخدم خطوط Type1 المدمجة بدلاً من TrueType؟
ج: خطوط Type1 الأربعة عشر القياسية (Times، Helvetica، Courier، إلخ) هي خطوط يجب أن تكون مدمجة في قارئ PDF، ولا تحتاج إلى تضمين ملفات الخط، وهذا هو الأسهل. في السيناريوهات الحقيقية، عادة ما تحتاج إلى تضمين الخطوط لضمان التوافق عبر الأنظمة.
س: ما هي هذه الأرقام في /MediaBox [0 0 612 792]؟
ج: الوحدة هي النقطة (1 نقطة = 1/72 بوصة). 612 × 792 نقطة = 8.5 × 11 بوصة = ورق US Letter. A4 هو 595 × 842 نقطة.
س: ما هو رقم الجيل (مثل 0 في 2 0 R)?
ج: يستخدم للتحديث التدريجي. عندما يتم تعديل كائن، يزداد رقم الجيل بمقدار 1. عادةً ما تكون جميع أرقام الجيل في PDF الجديد 0.
المعاينة للمقالة التالية
في المقالة 02، سنواصل استخدام "كتابة تدفق المحتوى" لإضافة عمليات مسار الرسوم الأساسية:
m(moveto)،l(lineto): تعريف المسارS(stroke): رسم الحدودre(rectangle)،f(fill): رسم مستطيل وتعبئته
سيمكنك من رسم: نص العنوان + خط فاصل أفقي + إطار مستطيل على نفس الصفحة، من "القدرة على الكتابة" إلى "القدرة على رسم الأشكال".
