مقاله فنی

ساختار فایل PDF: هدر، بدنه، Xref و تریلر

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

وقتی چیزی فشرده نشده باشد، بایت‌ها خود به اندازه کافی برای خواندن در یک ویرایشگر متن ساده هستند. یک سند مینیمال تک صفحه‌ای که "Hello, World!" را ترسیم می‌کند، در کمتر از پانصد بایت جا می‌شود و هر عنصر ساختاری فرمت در آن قابل مشاهده است. در اینجا کل فایل، با چهار بخش مشخص شده، آمده است:

%PDF-1.0                          % Header
%âãÏÓ

1 0 obj                           % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

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

3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj

4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj

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

xref                              % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

trailer                           % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

چهار بخش، همیشه به این ترتیب در فایل قرار دارند: یک هدر، بدنه‌ای از آبجکت‌ها، یک جدول cross-reference، و یک تریلر. نکته این است که شما آنها را تقریباً به ترتیب معکوس می‌خوانید. ISO 32000-2 §7.5.1 دقیقاً همین آناتومی چهار بخشی را شرح می‌دهد، و دلیل دسترسی از-پشت-به-جلو (back-to-front) کاملاً کاربردی است: خواننده‌ای که مستقیماً به آبجکت مورد نیازش پرش می‌کند بسیار سریع‌تر از خواننده‌ای است که هر بایت را از بالا اسکن می‌کند، و این دسترسی تصادفی دقیقاً همان چیزی است که تریلر و جدول cross-reference برای فراهم کردن آن وجود دارند

هدر دو خط است، و دومی اهمیت دارد

خط اول %PDF-1.0 است. تا جایی که به سینتکس مربوط می‌شود، علامت درصد آن را به یک کامنت (توضیح) تبدیل می‌کند، اما خواننده‌ها آن را به عنوان امضای فایل در نظر می‌گیرند و شماره نسخه را از آن استخراج می‌کنند. مدیریت نسخه در عمل آزاد است. خواننده‌ای که برای PDF 2.0 ساخته شده است، فایلی را که ادعا می‌کند 1.0 است به راحتی باز می‌کند، و اکثر خواننده‌ها فایلی را امتحان می‌کنند که نسخه اعلام شده آن اشتباه است یا خط نسخه آن به جای اینکه در بایت صفر باشد، کمی در فایل پنهان شده است. این شماره راهنمایی درباره ویژگی‌های مورد انتظار است، نه یک دروازه (مانع)

خط دوم همان خطی است که افراد به‌طور تصادفی حذف می‌کنند و سپس یک بعدازظهر را صرف دیباگ می‌کنند. آن هم یک کامنت است، اما محتوای آن چهار بایت بالاتر از ASCII 127 است. آنها وجود دارند تا هر چیزی که فایل را در "حالت متن" (text mode) جابجا می‌کند، آن را به عنوان باینری تشخیص دهد و از بازنویسی پایان خطوط (line endings) دست بکشد. یک PDF دارای جریان‌های فشرده‌ای (compressed streams) است که بایت‌های آن ممکن است به‌طور تصادفی با بازگشت به ابتدای خط (carriage return) یا تغذیه خط (line feed) مطابقت داشته باشند؛ اگر یک ابزار انتقال آنها را بازنویسی کند، طول جریان ثبت شده در دیکشنری دیگر با بایت‌های روی دیسک مطابقت ندارد و فایل خراب (corrupt) می‌شود. این کامنت بایت-بالا یک دفاع چهل ساله در برابر FTP در حالت ASCII است، و هنوز هم در هر فایلی که یک ابزار جدی می‌نویسد وجود دارد زیرا شکستی که از آن جلوگیری می‌کند پنهان و کامل است

بدنه آبجکت‌ها را نگه می‌دارد، که هر کدام شماره‌گذاری شده‌اند

هر چیزی که سند را می‌سازد، در بدنه به عنوان یک توالی مسطح از آبجکت‌های غیرمستقیم (indirect objects) زندگی می‌کند. هر کدام با دو عدد صحیح و کلمه کلیدی obj باز می‌شود، محتوای خود را نگه می‌دارد و با endobj بسته می‌شود. آبجکت ۱ در نمونه بالا نود درخت-صفحه (page-tree node) است: 1 0 obj، سپس یک دیکشنری، سپس endobj. اولین عدد صحیح شماره آبجکت است، دومین عدد شماره نسل (generation number) است. نسل تقریباً همیشه در فایلی که به تازگی نوشته شده است صفر است؛ تنها زمانی بالا می‌رود که یک شماره آبجکت در ویرایش‌ها دوباره استفاده شود، که به اندازه کافی نادر است که می‌توانید نسل غیرصفر را به عنوان نشانه‌ای از عبور فایل از به‌روزرسانی‌های افزایشی (incremental updates) در نظر بگیرید. محتوای بین کلمات کلیدی در اینجا یک دیکشنری است که بین << و >> نوشته شده است، اما می‌تواند یک عدد، یک رشته، یک آرایه یا یک جریان (stream) نیز باشد

آنچه این را به جای یک لیست به یک گراف تبدیل می‌کند، توکن ارجاع 2 0 R است. این یعنی "آبجکت ۲، نسل صفر، هر کجای فایل که قرار است باشد." نود درخت-صفحه در بالا صفحه خود را شامل نمی‌شود؛ بلکه به آبجکت ۲ اشاره می‌کند، که با همان مکانیزم به منابع و جریان محتوای خود اشاره می‌کند. بدنه به هر ترتیبی که نویسنده راحت دانسته است چیده می‌شود و ارجاعات آن را به درختی با ریشه کاتالوگ (catalog) می‌دوزند. موقعیت در فایل هیچ معنایی ندارد. هویت از شماره آبجکت می‌آید و مکان از جدول cross-reference به دست می‌آید

جدول cross-reference فهرستی از آفست‌های بایت است

جدول xref چیزی است که شماره آبجکت‌ها را به موقعیت‌های فایل تبدیل می‌کند. این دلیل آن است که خواننده می‌تواند یک سند هزار صفحه‌ای را باز کرده و صفحه ۸۵۰ را بدون تجزیه ۸۴۹ صفحه قبل از آن رندر کند. هر ورودی دقیقاً محل شروع آبجکت خود را ثبت می‌کند که برحسب بایت از ابتدای فایل شمرده می‌شود:

xref
0 6                  % 6 entries, starting at object 0
0000000000 65535 f   % entry 0: head of the free list
0000000015 00000 n   % object 1 begins at byte 15
0000000074 00000 n   % object 2 begins at byte 74
0000000192 00000 n   % object 3 begins at byte 192
0000000291 00000 n   % object 4 begins at byte 291
0000000409 00000 n   % object 5 begins at byte 409

عرض ثابت عمدی است. هر ورودی دقیقاً بیست بایت است: یک آفست ده رقمی، یک فاصله، یک نسل پنج رقمی، یک فاصله، یک نوع یک کاراکتری، و یک پایان خط دو بایتی. از آنجایی که سطرها یکنواخت هستند، خواننده می‌تواند با محاسبات ریاضی به جای اسکن کردن، مستقیماً به ورودی آبجکت n ایندکس کند، بنابراین جدولی که دسترسی تصادفی به بدنه را فراهم می‌کند خود به صورت تصادفی قابل دسترسی است. خط 0 6 هدر یک زیربخش است: این می‌گوید که ورودی‌های بعدی شش آبجکت را از شماره ۰ توصیف می‌کنند

آبجکت ۰ خاص است و همیشه حضور دارد. نوع آن f برای آزاد (free) است، نسل آن 65535 است، و در راس لیست پیوندی شماره آبجکت‌های آزاد قرار دارد. در فایلی که هرگز ویرایش نشده است، لیست آزاد فقط همین یک ورودی است که یک تشریفات است. در طول به‌روزرسانی‌های افزایشی ارزش خود را نشان می‌دهد، زمانی که حذف یک آبجکت شماره آن را به این لیست اضافه می‌کند تا ویرایش بعدی بتواند آن را بازیابی کند. سایر ورودی‌ها از نوع n برای در-حال-استفاده (in-use) هستند، و شماره ده رقمی آنها آفستی است که برای خواندن تعریف آن آبجکت باید جستجو کنید

تریلر نقطه ورود است، و در انتها قرار دارد

تریلر اولین چیزی است که خواننده در واقع مصرف می‌کند، حتی اگر آخرین بار نوشته شده باشد. یک تجزیه‌کننده فایل را باز می‌کند، به انتها می‌رود و برای یافتن %%EOF به عقب برمی‌گردد. درست بالای آن startxref و به دنبال آن یک شماره قرار دارد، و این شماره آفست بایت کلمه کلیدی xref است. با آن، خواننده مستقیماً به جدول cross-reference پرش می‌کند بدون اینکه حتی یک آبجکت را اسکن کرده باشد:

trailer
<<
/Root 5 0 R          % the document catalog
/Size 6              % one more than the highest object number
>>
startxref
459                  % byte offset of the xref table
%%EOF

دیکشنری تریلر حامل دو مقداری است که خواننده قبل از انجام هر کار دیگری به آنها نیاز دارد. /Root به کاتالوگ سند اشاره می‌کند، اینجا آبجکت ۵، که بالای گراف آبجکت و مسیر رسیدن به درخت-صفحه است. /Size تعداد ورودی‌هایی است که جدول cross-reference باید داشته باشد، که به دلیل ورودی آزاد در اسلات صفر، یکی بیشتر از بالاترین شماره آبجکت است. از %%EOF کل دنباله خواندن شکل می‌گیرد: نشانگر را پیدا کنید، startxref را بخوانید تا جدول را بیابید، جدول را بارگیری کنید تا بفهمید هر آبجکت کجا زندگی می‌کند، /Root را بخوانید تا کاتالوگ را پیدا کنید، و آبجکت‌ها را بر اساس تقاضا از آنجا حل کنید. به هدر که در بالا قرار دارد، تا اواخر کار به ندرت رجوع می‌شود. نقشه در پایین چیزی است که خواننده ابتدا به آن نیاز دارد

به‌روزرسانی افزایشی به جای بازنویسی، نقشه دومی را ضمیمه می‌کند

آن طراحی اول-دم (tail-first) زمانی نتیجه می‌دهد که فایلی تغییر کند. یک PDF را می‌توان بدون بازنویسی هیچ یک از بایت‌هایی که از قبل روی دیسک هستند ویرایش کرد. آبجکت‌های جدید و اصلاح شده به انتها ضمیمه می‌شوند، و به دنبال آن یک بخش cross-reference تازه و یک تریلر تازه قرار می‌گیرد، و فایل اصلی در زیر بدون تغییر باقی می‌ماند. تنها قطعه جدید حسابداری، یک ورودی /Prev در تریلر جدید است که آفست بایت جدول cross-reference قبلی را نگه می‌دارد:

% ... original file, unchanged, ends here ...

6 0 obj                          % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj

xref                             % a second xref section, for the new object only
6 1
0000000612 00000 n

trailer
<<
/Root 5 0 R
/Size 7
/Prev 459                        % byte offset of the earlier xref table
>>
startxref
680                              % offset of this new xref section
%%EOF

خواننده همچنان از %%EOF نهایی شروع می‌کند، همچنان startxref را به جدیدترین جدول دنبال می‌کند، اما اکنون زنجیره /Prev را به سمت جداول قدیمی‌تر به عقب دنبال می‌کند و آنها را به گونه‌ای ادغام می‌کند که جدیدترین ورودی برای هر شماره آبجکتی برنده شود. بخش‌های cross-reference یک لیست پیوندی را در سراسر فایل تشکیل می‌دهند، که هر کدام برای آبجکت‌هایی که لمس می‌کند، مورد قبلی را لغو می‌کند. آبجکتی که یک ویرایش جایگزین کرده است هنوز هم به صورت فیزیکی در آفست قدیمی خود وجود دارد؛ تنها دیگر قابل دسترسی نیست، زیرا یک ورودی xref بعدی به جای جدیدتری اشاره می‌کند

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

هزینه آن رشد است. هر ویرایش ضمیمه می‌شود؛ هیچ چیزی در جای خود بازیابی نمی‌شود، بنابراین فایلی که بارها مورد بازبینی قرار گرفته است، آبجکت‌های مرده و زنجیره طولانی از بخش‌های xref را جمع‌آوری می‌کند. درمان یک بازنویسی کامل است: سند را بارگیری کرده و آن را تازه ذخیره کنید، که آبجکت‌های باقی‌مانده را مجدداً شماره‌گذاری می‌کند، موارد غیرقابل دسترس را رها می‌کند و یک جدول cross-reference تمیز واحد منتشر می‌کند. این دو استراتژی مستقیماً با یکدیگر معامله می‌کنند. ضمیمه کردن سریع است و امضاها و تاریخچه را حفظ می‌کند؛ بازنویسی کندتر است و هر دو را کنار می‌گذارد، در ازای یک فایل فشرده (compact)

خواندن چهار بخش در عمل

دانستن چیدمان برای دیباگ دستی اکثر مشکلات "این فایل باز نمی‌شود" کافی است. اگر یک خواننده PDF را رد کند، مقصران معمول در دو انتها هستند، نه در وسط. یک دانلود ناقص تریلر را از دست می‌دهد، بنابراین startxref یا %%EOF وجود ندارد و خواننده نقطه ورودی ندارد؛ خواننده‌های با مدارا برای بازسازی xref به اسکن کل فایل متوسل می‌شوند، که دقیقاً همان مسیر کندی است که جدول برای جلوگیری از آن در نظر گرفته شده بود. یک انتقال ناموفق در حالت-متن بایت‌های جریان را خراب می‌کند یا آفست‌ها دیگر با واقعیت مطابقت ندارند، و آبجکت‌ها از موقعیت اشتباه بارگیری می‌شوند. وقتی آفست‌های موجود در جدول دیگر به کلمات کلیدی واقعی obj اشاره نمی‌کنند، فایل از نظر ساختاری خراب است حتی اگر هر آبجکت به صورت جداگانه مشکلی نداشته باشد

برای کد جدید، درس چیدمان این است که اجازه دهید یک کتابخانه حسابداری بایت‌ها را در اختیار داشته باشد. آفست‌های موجود در جدول cross-reference باید با موقعیت‌های واقعی هر آبجکت تا بایت مطابقت داشته باشند، تریلر باید به جدول مناسب اشاره کند، و به‌روزرسانی‌های افزایشی باید به درستی از طریق /Prev زنجیره شوند. یک کامپوننت نیتیو مانند HotPDF Component برای Delphi و C++Builder هنگام نوشتن یک فایل همه این موارد را انجام می‌دهد، از جمله انتخاب بین ضمیمه کردن یک بازبینی افزایشی و بازنویسی یک نسخه فشرده. اگر می‌خواهید ببینید همان ساختار به جای کالبدشکافی از هیچ ساخته می‌شود، مقاله همراه درباره ساختن یک سند PDF از پایه به طور کامل مراحل انتشار هدر، آبجکت‌ها، xref و تریلر را به ترتیب طی می‌کند