مقاله فنی

مدل شیء منطقی PDF: انواع، ارجاعات و ساختار

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

این لایه منطقی PDF است که در بند 7.3 استاندارد ISO 32000-1 تعریف شده است، و یک سطح بالاتر از چیدمان فایل فیزیکی (هدر، بدنه، جدول ارجاع متقاطع و تریلر، که موضوع خودش را در بررسی فنی ساختار فایل PDF دارد) قرار دارد. مدل منطقی به این معناست که آن بایت‌ها پس از تجزیه چه معنایی دارند. یک نمایشگر فایل را برای یافتن تریلر به سمت عقب می‌خواند، آن را تا ریشه دنبال می‌کند، و از آنجا سند به صورت اشیایی که به اشیاء ارجاع می‌دهند باز می‌شود. این همان بخشی است که وقتی یک صفحه بدشکل را دیباگ می‌کنید، یک تجزیه‌کننده می‌نویسید، یا به یک کتابخانه برای مونتاژ یک سند اعتماد می‌کنید، درباره آن استدلال می‌نمایید

هشت نوع شیء، و نه هیچ‌چیز دیگر

فرمت PDF دقیقاً هشت نوع شیء اساسی را تعریف می‌کند. هر مقداری در یک سند یکی از آن‌هاست، که این همان چیزی است که فرمت را با وجود گستردگی‌اش قابل‌مدیریت نگه می‌دارد

بولی‌ها (Booleans) کلمات کلیدی true و false هستند. آن‌ها پرچم‌هایی (flags) را روشن و خاموش می‌کنند، مانند اینکه آیا یک حاشیه‌نویسی (annotation) چاپ می‌شود یا خیر

اعداد (Numbers) در دو نوع ارائه می‌شوند که مشخصات با آن‌ها به عنوان یک نوع رفتار می‌کند: اعداد صحیح مانند 42 و اعداد حقیقی مانند 3.14 یا -0.002. فرمت PDF هیچ نماد توانی (exponent notation) ندارد، بنابراین شما هرگز 1e6 را در یک فایل سازگار نخواهید دید. مختصات، اندازه فونت‌ها و زوایای چرخش همگی عدد هستند

رشته‌ها (Strings) دنباله‌ای از بایت‌ها را نگه می‌دارند که یا در پرانتز، (Hello)، یا در براکت‌های زاویه‌دار به صورت هگزادسیمال، <48656C6C6F> نوشته می‌شوند. هر دو نماد محتوای یکسانی را کدگذاری می‌کنند؛ هگز راه گریزی برای بایت‌های ناخوشایند داخل پرانتز است. رشته‌ها متن را حمل می‌کنند، اما آن‌ها در درجه اول بایت هستند، که این موضوع لحظه‌ای که با هر چیزی فراتر از ASCII کار می‌کنید اهمیت پیدا می‌کند

نام‌ها (Names) توکن‌های اتمی هستند که با یک اسلش معرفی می‌شوند: /Type، /Pages، /MediaBox. یک نام یک رشته نیست؛ بلکه یک شناسه است که به عنوان کلید دیکشنری یا یک مقدار شمارش‌شده استفاده می‌شود، و دو نام تنها در صورتی برابرند که بایت به بایت با هم تطابق داشته باشند. اسلش صرفاً سینتکس است، نه بخشی از نام. این موضوع باعث اشتباه تازه‌کارانی می‌شود که با /Times-Roman و رشته (Times-Roman) به عنوان موارد قابل‌تعویض رفتار می‌کنند؛ فرمت این‌گونه نیست

آرایه‌ها (Arrays) لیست‌های مرتب و ناهمگن در کروشه‌ها هستند: [0 0 612 792] یک مستطیل صفحه است، و یک آرایه می‌تواند انواع را آزادانه ترکیب کند، از جمله ارجاعات به اشیاء دیگر. دیکشنری‌ها (Dictionaries) اسب کاری (workhorse) این فرمت هستند. دیکشنری که بین << و >> نوشته می‌شود، کلیدهای نام (name keys) را به مقادیری از هر نوع نگاشت می‌دهد، و تقریباً هر ساختار معناداری در PDF، شامل صفحه، کاتالوگ، فونت، حاشیه‌نویسی، یک دیکشنری با کلید /Type است که اعلام می‌کند چه چیزی است

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

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

مقادیر مستقیم، اشیاء غیرمستقیم و ارجاعات

هر یک از این هشت نوع می‌تواند به دو روش ظاهر شود. یک شیء مستقیم (direct) در جای خود نوشته می‌شود، مانند 612 در داخل یک آرایه MediaBox. به یک شیء غیرمستقیم (indirect) هویتی داده می‌شود تا اشیاء دیگر بتوانند به آن اشاره کنند: دو عدد صحیح، یک شماره شیء و یک شماره نسل، که تعریف را در obj و endobj بسته‌بندی می‌کنند:

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

این شیء 12، نسل 0، یک دیکشنری فونت است. در هر جای دیگر فایل، یک شیء دیگر با یک ارجاع غیرمستقیم (indirect reference) به آن اشاره می‌کند: همان دو عدد که با کلمه کلیدی R دنبال می‌شوند، 12 0 R. این ارجاع یک اشاره‌گر است. هنگامی که دیکشنری منبع یک صفحه می‌گوید /Font << /F1 12 0 R >>، شیء 12 را به عنوان فونت پشت نام منبع /F1 نام می‌برد، بدون اینکه تعریف فونت را در صفحه کپی کند

شماره نسل برای حذف‌ها و استفاده مجدد وجود دارد. وقتی یک شیء آزاد می‌شود و اسلات آن مجدداً استفاده می‌گردد، نسل افزایش می‌یابد تا یک ارجاع قدیمی 12 0 R نتواند به مستأجر جدید اسلات 12 حل‌وفصل شود. فایل‌هایی که به تازگی نوشته شده‌اند تقریباً همگی نسل 0 هستند، اما فایلی که به شدت ویرایش شده است می‌تواند اعداد بالاتری را حمل کند، و تجزیه‌کننده‌ای که نسل را نادیده می‌گیرد در نهایت شیء اشتباه را خواهد خواند

ارجاع غیرمستقیم همان چیزی است که PDF را کارآمد و قابل‌ویرایش می‌سازد. یک فونت، تصویر یا فضای رنگی را می‌توان یک بار تعریف کرد و از صدها صفحه به آن ارجاع داد. یک تغییر کوچک را می‌توان به عنوان یک بازبینی (revision) جدید پیوست کرد که جایگزین یک شیء منفرد می‌شود تا اینکه کل فایل را بازنویسی کند. جدول ارجاع متقاطع اندیسی است که شماره یک شیء را به آفست بایت تبدیل می‌نماید، بنابراین خواننده مستقیماً بدون اسکن به 12 0 obj می‌پرد، اما این یک بهینه‌سازی فیزیکی است. از نظر منطقی، تنها چیزی که باید بدانید این است که 12 0 R به معنای "شیء شناسایی‌شده به عنوان 12 0" است

کاتالوگ: جایی که هر سندی از آنجا آغاز می‌شود

حل‌وفصل ارجاعات باید از جایی شروع شود، و آنجا ورودی /Root تریلر است که به کاتالوگ سند (document catalog) اشاره می‌کند: ریشه گراف شیء، یک دیکشنری با /Type /Catalog. خواننده ابتدا به آن می‌رسد زیرا تریلر ابتدا پیدا می‌شود، و از آنجا هر بخش دیگری از سند با دنبال کردن ارجاعات قابل دسترسی است

کاتالوگ تنها دو ورودی کاملاً الزامی را حمل می‌کند: /Type آن، و /Pages، یک ارجاع غیرمستقیم به ریشه درخت صفحه. بقیه موارد اختیاری هستند و رفتار سراسری سند را به جای محتوا توصیف می‌کنند: /Outlines به درخت نشانک‌ها (bookmark) اشاره می‌کند، /Names درخت‌های نام با کلید رشته‌ای را نگه می‌دارد، /Metadata به یک جریان فراداده XMP ارجاع می‌دهد، و /PageMode و /PageLayout پیشنهاد می‌کنند که یک نمایشگر چگونه باید سند را باز کند. هیچ‌کدام از آن‌ها برای رندر کردن یک صفحه مورد نیاز نیستند؛ آن‌ها تجربه پیرامون صفحات را پیکربندی می‌کنند. ساختارهای نشانک، فراداده و حاشیه‌نویسی که از کاتالوگ آویزان هستند، در مقاله‌ای با عنوان فراداده PDF، نشانک‌ها و حاشیه‌نویسی‌ها بررسی می‌شوند

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

Diagram of a PDF file's four physical sections: a version header, a body holding the document objects including the catalog and page tree, a cross-reference table of object offsets, and a trailer pointing at the root

درخت صفحه: سلسله‌مراتبی متوازن از صفحات

از /Pages سند به درخت صفحه شاخه می‌شود، جایی که انتخاب یک گراف به جای یک لیست مسطح توسط PDF نتیجه می‌دهد. صفحات به عنوان یک دنباله ساده ذخیره نمی‌شوند؛ آن‌ها از درختی آویزان هستند که گره‌های داخلی آن گره‌های درخت صفحه (page tree nodes) (/Type /Pages) و برگ‌های آن اشیاء صفحه (page objects) (/Type /Page) هستند. یک گره داخلی فرزندان خود را در آرایه /Kids لیست می‌کند و در /Count ثبت می‌نماید که چه تعداد صفحات برگ در زیر آن زندگی می‌کنند. هر گره به جز ریشه یک ارجاع بازگشتی /Parent به سمت بالا حمل می‌کند، بنابراین درخت در هر دو جهت پیمایش می‌شود

2 0 obj                                  % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj

3 0 obj                                  % a leaf page
<< /Type /Page /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 12 0 R >> >>
   /Contents 5 0 R >>
endobj

4 0 obj                                  % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj

در اینجا شیء 2 ریشه است، با سه صفحه در زیر آن: صفحه برگ 3، به علاوه دو صفحه دیگر که از طریق گره داخلی 4 قابل دسترسی هستند. مقدار /Count ریشه برابر با 3 باید با کل برگ‌های زیر آن برابر باشد، و شمارشی که با ساختار واقعی مخالف باشد روشی رایج است که در آن فایلی که با دست ویرایش شده به اشتباه می‌افتد. هدف از درخت محلی بودن دسترسی است. خواننده‌ای که صفحه 900 از یک سند هزار صفحه‌ای را باز می‌کند 900 شیء را نمی‌پیماید؛ او چند گره پایین می‌آید، زیرا یک درخت خوش‌ساخت (well-formed) کم‌عمق و متوازن باقی می‌ماند. ساختن چنین درختی با دست آن‌قدر پیچیده است که ارزش دیدن را از ابتدا تا انتها داشته باشد، کاری که راهنمای ساخت یک سند PDF از ابتدا انجام می‌دهد

درخت ارزش دوم خود را از طریق وراثت (inheritance) به دست می‌آورد. تعداد انگشت‌شماری از ویژگی‌های صفحه، شامل /Resources، /MediaBox، /CropBox و /Rotate، ممکن است روی یک گره داخلی تنظیم شوند و از صفحات جداگانه برداشته شوند، که سپس آن‌ها مقدار نزدیک‌ترین جد را به ارث می‌برند. /MediaBox را یک بار روی ریشه تنظیم کنید تا هر برگ اندازه صفحه یکسانی را بدون تکرار به دست آورد؛ صفحه‌ای که نیاز به متفاوت بودن دارد، اندازه خاص خود را اعلام می‌کند. این تنها جایی در مدل شیء است که در آن معنای یک مقدار به موقعیت شیء در درخت بستگی دارد، نه تنها به محتویات خودش

آنچه یک صفحه برگ در واقع نگه می‌دارد

یک شیء صفحه نقطه اتصال بین مدل ساختاری و محتوای قابل‌رویت است. ورودی /Contents آن به یک یا چند جریان محتوا ارجاع می‌دهد، که اپراتورهای ترسیمی (drawing operators) برای نقاشی متن و گرافیک روی صفحه هستند. دیکشنری /Resources آن نام فونت‌ها، تصاویر و فضاهای رنگی را که آن اپراتورها به آن‌ها تکیه می‌کنند نام می‌برد، و هر ورودی یک ارجاع غیرمستقیم به شیئی است که در بین صفحات به اشتراک گذاشته شده است. /MediaBox مستطیل صفحه را در نقطه (1/72 اینچ) ارائه می‌دهد، و ورودی‌هایی مانند /Rotate و /CropBox نحوه ارائه آن را تنظیم می‌کنند

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

چرا این مدل ارزش دانستن دارد

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

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