مقاله فنی

ساخت دستی یک PDF مینیمال: ۵ شیء که به آن‌ها نیاز دارید

یک فایل PDF در هسته خود یک ظرف متنی ساده (plain-text) است. اکثر فایل‌ها را در یک ویرایشگر هگز باز کنید و بالای آن قابل خواندن است: یک توضیح برای نسخه، سپس مجموعه‌ای از اشیاء شماره‌گذاری شده، بعد یک نمایه کوچک و یک اشاره‌گر در پایین‌ترین قسمت که به خواننده می‌گوید از کجا شروع کند. اگر فشرده‌سازی را حذف کنید، این فرمت به قدری قابل درک است که می‌توانید یک سند کارآمد را در یک ویرایشگر متنی تایپ کرده و در یک نمایشگر باز کنید. یک بار انجام این کار، بیش از هر مقدار مطالعه مستندات مشخصات، به شما می‌آموزد که PDF چگونه در کنار هم قرار می‌گیرد، زیرا مجبورید اشیاء را با دست به یکدیگر متصل کنید و فایل تا زمانی که اتصالات درست نباشند، باز نخواهد شد

این راهنما کوچک‌ترین فایل PDF را می‌سازد که واقعاً چیزی را رندر می‌کند: یک صفحه، کلمات "Hello, World!" با یک فونت داخلی، روی کاغذ US Letter. فایل نهایی به دقیقاً پنج شیء و چند خط کدنویسی اطراف آن‌ها نیاز دارد. ابتدا اشیاء را خواهیم نوشت، سپس هدر، جدول cross-reference و تریلر را مونتاژ می‌کنیم تا آن‌ها را به فایلی متصل کنیم که یک خواننده آن را بپذیرد

پنج شیء که یک نمایشگر بر آن‌ها اصرار دارد

یک خواننده برای یافتن محتوا از بالا به پایین فایل PDF را اسکن نمی‌کند. از تریلر شروع می‌کند، ارجاعی را به کاتالوگ سند دنبال می‌کند و از آنجا زنجیره‌ای از اشیاء را می‌پیماید. هر شیء روی آن زنجیره باید وجود داشته باشد در غیر این صورت باز شدن فایل با شکست مواجه می‌شود. برای یک سند یک صفحه‌ای، این زنجیره کوتاه است و هر حلقه یک وظیفه منفرد دارد:

  • Catalog ریشه است. این همان شیئی است که تریلر به آن اشاره می‌کند و تنها ورودی ضروری آن در اینجا، ارجاعی به درخت صفحه (page tree) است
  • Pages گره درخت صفحه است. صفحه‌های موجود در سند را فهرست می‌کند و تعداد آن‌ها را گزارش می‌دهد
  • Page یک صفحه فیزیکی را توصیف می‌کند: اندازه آن، منابعی که با آن رسم می‌شود، و اینکه کدام استریم محتوا (content stream) آن را نقاشی می‌کند
  • Content stream حاوی عملگرهای رسم است، دستورات پسوندی که متن و گرافیک را در آن صفحه قرار می‌دهند
  • Font تایپ‌فیسی را که استریم محتوا به آن ارجاع می‌دهد، اعلان می‌کند. از یکی از ۱۴ فونت استاندارد استفاده کنید و دیگر نیازی به جاسازی (embed) چیزی ندارید

هر شیء شماره‌گذاری شده و قابل آدرس‌دهی است. یک شیء غیرمستقیم (indirect object) به صورت N 0 obj ... endobj نوشته می‌شود، که در آن N شماره شیء و 0 شماره تولید (generation number) آن است (در فایلی که از ابتدا می‌نویسید همیشه 0 است). در هر جای دیگری از فایل، با یک ارجاع به آن شیء اشاره می‌کنید: 5 0 R به معنای "شیء 5" است. آن ارجاعات همان اتصالات هستند. کاتالوگ در شماره‌گذاری ما 2 0 R را برای رسیدن به درخت صفحه نگه می‌دارد، درخت صفحه ارجاعی را به صفحه پایین نگه می‌دارد، و الی آخر. یک شماره را اشتباه وارد کنید تا خواننده یک اشاره‌گر معلق (dangling pointer) را به سمت هیچ دنبال کند

نام‌ها، دیکشنری‌ها، و استریم‌ها

سه قطعه از سینتکس تقریباً همه چیز را حمل می‌کنند. یک نام (name) با اسلش شروع می‌شود: /Type، /Page، /F0. نام‌ها شناسه‌های حساس به حروف کوچک و بزرگ هستند، نه رشته، و PDF از آن‌ها برای کلیدهای دیکشنری و برای برچسب‌گذاری اینکه یک شیء چیست استفاده می‌کند. یک دیکشنری (dictionary) مجموعه‌ای از جفت‌های کلید-مقدار است که در براکت‌های زاویه‌دار دوتایی پیچیده شده‌اند، که در آن هر کلید یک نام است: << /Type /Page /MediaBox [0 0 612 792] >>. مقادیر می‌توانند اعداد، نام‌ها، آرایه‌ها در براکت‌های مربعی، ارجاعات، یا دیکشنری‌های تودرتو باشند. اکثر اشیاء PDF دیکشنری هستند

یک استریم (stream) یک دیکشنری است که با بلوکی از بایت‌ها بین کلمات کلیدی stream و endstream دنبال می‌شود. این جایی است که عملگرهای رسم صفحه قرار دارند و در فایل‌های واقعی، جایی است که تصاویر فشرده شده و فونت‌های جاسازی شده نیز قرار دارند. دیکشنری استریم بایت‌ها را توصیف می‌کند؛ در یک فایل تولیدی باید یک ورودی /Length داشته باشد که تعداد دقیق بایت‌ها را مشخص کند، و اغلب یک /Filter مانند /FlateDecode هنگامی که داده‌ها فشرده شده‌اند. ما قرار است برای پر کردن /Length به یک ابزار تکیه کنیم، زیرا شمردن بایت‌ها با دست بخشی از این تمرین است که هیچ دستاورد آموزشی ندارد و شانس بالایی برای خطای off-by-one دارد که فایل را خراب می‌کند

نوشتن اشیاء

در اینجا پنج شیء به ترتیب آورده شده است. جزئیات مختصاتی که باید قبل از خواندن استریم محتوا در ذهن داشته باشید: 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، کاتالوگ، ورودی /Pages خود را به سمت شیء 2 نشانه می‌گیرد. شیء 2، درخت صفحه، شیء 3 را در /Kids فهرست می‌کند و /Count 1 را اعلان می‌کند. شیء 3، صفحه، /Parent را به شیء 2 برمی‌گرداند (درخت و صفحه به یکدیگر ارجاع می‌دهند، که الزامی است)، خود را با /MediaBox اندازه می‌گیرد، فونت را تحت نام محلی /F0 در /Resources خود در معرض قرار می‌دهد، و شیء 5 را به عنوان محتوای خود نام می‌برد. شیء 4 فونت است: /BaseFont /Helvetica یکی از ۱۴ تایپ‌فیس استاندارد را انتخاب می‌کند که هر خواننده مطابق استانداردی از قبل آن را دارد، بنابراین چیزی برای جاسازی وجود ندارد. شیء 5 استریم محتوا است

استریم محتوا در واقع چه می‌گوید

بدنه استریم یک برنامه کوچک در زبان توصیف صفحه PDF است، که از نوع پسوندی (postfix) است: عملوندها اول می‌آیند، سپس عملگری که آن‌ها را مصرف می‌کند. پنج خط کار را انجام می‌دهند. BT و ET یک شیء متنی را باز و بسته می‌کنند؛ هر چیزی که متنی را تعیین موقعیت می‌کند یا نشان می‌دهد باید بین آن‌ها قرار گیرد. /F0 36 Tf فونت فعلی را به منبعی به نام /F0 در ۳۶ نقطه تنظیم می‌کند (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 یک نظر (comment) را آغاز می‌کند، اما یک خواننده با این نظر خاص به عنوان امضای فرمت رفتار می‌کند و نسخه را از آن می‌خواند. یک نویسنده واقعی بلافاصله با یک خط نظر دوم شامل بایت‌های high-bit آن را دنبال می‌کند، اشاره‌ای به ابزارهای انتقال فایل که فایل باینری است و نباید به عنوان متن دستکاری شود

در انتهای فایل، جدول cross-reference قرار می‌گیرد، نمایه‌ای که دسترسی تصادفی (random access) را ممکن می‌سازد. این جدول افست بایتی (byte offset) هر شیء را از ابتدای فایل ثبت می‌کند، بنابراین یک خواننده می‌تواند مستقیماً به شیء 3 بپرد بدون اینکه ابتدا اشیاء 1 و 2 را تجزیه کند. جدول سفت و سخت است: ورودی‌ها دارای عرض ثابت و ۲۰ بایت برای هر کدام از جمله پایان خط هستند، که با فرمت یک افست ۱۰ رقمی، یک تولید ۵ رقمی، یک کلمه کلیدی (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 را می‌خواند، به جدول cross-reference می‌رود، و از آنجا به کاتالوگ و همه چیز در پایین آن می‌رسد. %%EOF آخرین بایت را مشخص می‌کند

اجازه دهید یک ابزار شمارش بایت‌ها را اصلاح کند

افست‌های بالا نمایشی هستند؛ در عمل، تا زمانی که تایپ کردن را تمام کنید، اشتباه خواهند بود، زیرا به چیدمان دقیق بایتی فایل شما بستگی دارند. به جای محاسبه مجدد آن‌ها، ساختار را با مقادیر نگهدارنده (placeholder) بنویسید و اجازه دهید یک ابزار کمکی، جدول cross-reference و طول‌های استریم را بازسازی کند. برنامه رایگان و چندپلتفرمی pdftk این کار را در یک گذر انجام می‌دهد:

pdftk hello-draft.pdf output hello.pdf

آن اشیاء شما را تجزیه می‌کند، تک‌تک افست‌های بایتی را دوباره محاسبه می‌کند، مقادیر صحیح /Length را پر می‌کند، یک جدول xref و تریلر معتبر می‌نویسد، و hello.pdf را صادر می‌کند. آن را در هر نمایشگری باز کنید و یک صفحه با "Hello, World!" با فونت Helvetica اندازه ۳۶ نقطه در نزدیکی بالای صفحه دریافت خواهید کرد. Qpdf هم همین کار را انجام می‌دهد، و بسیاری از نمایشگرها نیز فایل‌های کمی ناقص را در حال پردازش ترمیم می‌کنند. نکته تکیه بر یک ابزار در اینجا تنبلی نیست؛ این است که محاسبات افست تنها بخشی از فرمت است که دارای محتوای مفهومی صفر و بالاترین نرخ خطا است، بنابراین خودکارسازی آن به ساختار اجازه می‌دهد همان چیزی باقی بماند که شما در حال یادگیری آن هستید

چرا این کار به اسناد واقعی گسترش می‌یابد

هیچ چیزی در مورد یک گزارش صد صفحه‌ای شکلی را که تازه ساخته‌اید تغییر نمی‌دهد. کاتالوگ هنوز در ریشه می‌نشیند، درخت صفحه هنوز صفحات را جمع‌آوری می‌کند، و هر صفحه هنوز به منابع و یک استریم محتوای خود اشاره می‌کند. چیزی که رشد می‌کند وسعت است، نه ستون فقرات: درخت صفحه شاخه می‌شود تا خواننده بتواند زیردرخت‌های کامل را نادیده بگیرد، استریم‌های محتوا صدها عملگر را به جای پنج مورد حمل می‌کنند، فونت‌ها به عنوان اشیاء استریم خود با جداول عرض و کدگذاری‌ها جاسازی می‌شوند، و تصاویر به عنوان استریم‌هایی با فیلترهای مخصوص تصویر وارد می‌شوند. فایل‌های مدرن نیز معمولاً بسیاری از اشیاء را در استریم‌های شیء فشرده‌سازی شده بسته‌بندی می‌کنند و جدول ساده xref را با یک استریم cross-reference جایگزین می‌کنند، به همین دلیل است که باز کردن یک PDF واقعی در یک ویرایشگر متنی معمولاً دیواری از داده‌های باینری را نشان می‌دهد. مدل زیرین کاملاً شبیه به فایلی است که شما به صورت دستی ساخته‌اید. برای مشاهده نمودار شیء وسیع‌تر و اینکه چگونه کاتالوگ، درخت صفحه و دیکشنری‌های منبع در سراسر یک سند بزرگ‌تر به هم مرتبط هستند، گشت و گذار عمیق در ساختار سند PDF از جایی که این مقاله پایان می‌یابد، شروع می‌شود و بررسی اجمالی ساختار فایل به‌روزرسانی‌های افزایشی و نحوه زنجیره‌ای شدن تریلر در میان بازبینی‌ها را پوشش می‌دهد

از نوشتن دستی تا یک کتابخانه

تایپ اشیاء با دست یک تمرین یادگیری است، نه یک تکنیک تولید. لحظه‌ای که به فونت‌های واقعی، متن پیچیده شده (wrapped text)، تصاویر، یا چیزی بیش از یک صفحه ساده نیاز پیدا کنید، ثبت بایت‌ها که pdftk برای شما اصلاح کرد، تمام کار می‌شود، و شما به کتابخانه‌ای نیاز دارید که مسئولیت آن را بپذیرد. همان پنج شیء همچنان نوشته می‌شوند، اما یک کتابخانه هر افست را محاسبه می‌کند، دیکشنری‌های فونت و منبع را مدیریت می‌کند، و بدون اینکه حتی یک بایت را دنبال کنید، استریم‌های محتوا را فشرده می‌سازد. در دلفی و C++Builder، کامپوننت HotPDF این کل فایل را به تعداد انگشت‌شماری از فراخوانی‌ها تقلیل می‌دهد: سند را تنظیم کنید، BeginDoc، SetFont و TextOut را برای قرار دادن همان پیام احوال‌پرسی فراخوانی کنید، سپس EndDoc را برای نوشتن یک کاتالوگ صحیح، درخت صفحه، xref، و تریلر. درک اشیاء زیرین همان چیزی است که به شما اجازه می‌دهد تا زمانی که یک سند به شکلی که انتظار داشتید رندر نمی‌شود، درباره خروجی استدلال کنید