شیء شماره 1 با صفحه 1 برابر نیست. این واقعیت ساده، کدهای پردازش PDF بیشتری را نسبت به هر ویژگی قالببندی دیگری با مشکل مواجه کرده است؛ و برای درک دلیل آن، باید فراتر از ظاهر نمایش داده شده توسط نمایشگر رفت و به عمق نمودار شیئی که نمایشگر واقعاً میخواند نفوذ کرد
یک فایل PDF مجموعهای از اشیاء غیرمستقیم شمارهگذاری شده است. صفحه یکی از این اشیاء است، اما ترتیب نمایش آنها هیچ ارتباطی با مکان آنها در فایل یا شمارهای که دارند ندارد. ترتیب نمایش کاملاً توسط درخت /Pages تعیین میشود؛ ساختاری زنجیرهای که ریشه آن کاتالوگ سند است. اگر این درخت را نادیده بگیرید و اشیاء را به ترتیب عددی اسکن کنید، در تعداد قابل توجهی از فایلهای واقعی با ترتیب نادرست صفحات مواجه خواهید شد
درخت صفحه: مکانیسمی که واقعاً ترتیب را تعیین میکند
هر PDF با یک document catalog آغاز میشود، مطابق ISO 32000-2 §7.7.2. این catalog یک ورودی /Pages دارد که به گره ریشه درخت صفحه اشاره میکند. آن گره ریشه یک دیکشنری است با /Type /Pages، یک آرایه /Kids از ارجاعهای غیرمستقیم و یک /Count که تعداد کل صفحههای برگ زیر آن را میدهد. ترتیب نمایش چیزی جز پیمایش depth-first از چپ به راست روی همین درخت نیست
یک نمونه حداقل با تنها سه صفحه میتواند این موضوع را ملموستر کند:
%PDF-1.7
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [20 0 R 4 0 R 9 0 R] /Count 3 >>
endobj
% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
آرایه /Kids به صورت [20 0 R 4 0 R 9 0 R] خوانده میشود، پس شیء 20 صفحه 1 است، شیء 4 صفحه 2 است و شیء 9 صفحه 3 است. شمارهگذاری اشیاء اصلاً اهمیتی ندارد. هر کدی که اشیاء را به ترتیب عددی پیمایش کند و موارد دارای /Type /Page را جمع کند، روی همین فایل ترتیب اشتباه تولید میکند
چرا تولیدکنندهها چیدمانهای غیرمتوالی ایجاد میکنند؟ چندین دلیل وجود دارد. کتابخانههایی که پیش از نوشتن محتوا، شماره شیء را به همه صفحات اختصاص میدهند، آنها را بر اساس ترتیب ایجاد شمارهگذاری میکنند و سپس بایتهای واقعی را با هر ترتیبی که برای سریالساز مناسب است مینویسند. ابزارهای ادغام هنگام پیوند زدن اسناد، اشیاء اسناد منبع را مجدداً شمارهگذاری میکنند تا از تداخل جلوگیری شود. اشیاء صفحهای که مجدداً شمارهگذاری شدهاند در جدول اشیاء ادغام شده پراکنده میشوند، در حالی که فقط آرایه ریشه جدید /Kids ترتیب نمایش صحیح را نگه میدارد. بهروزرسانیهای افزایشی، اشیاء جدید را با شمارههای جدید به انتهای فایل پیوست میکنند، بنابراین صفحهای که به عنوان یک نسخه اصلاحی اضافه شده است، حتی اگر در ترتیب نمایش در رتبه اول باشد، در نزدیکی انتهای جریان بایتها قرار میگیرد
درخت تخت و زیردرختهای تو در تو
استاندارد اجازه میدهد درخت صفحات دو شکل داشته باشد. تولیدکنندههای ساده یک ساختار مسطح تولید میکنند: یک گره ریشه /Pages که در آرایه /Kids آن فقط گرههای برگ /Page قرار دارند. پیمایش این ساختار بسیار آسان است: فقط یک لایه وجود دارد و با یک بار اسکن انجام میشود
اسناد بزرگ معمولاً به جای آن از درختان متوازن استفاده میکنند. آرایه /Kids در گره ریشه /Pages حاوی گرههای میانی /Pages است و هر گره میانی آرایه /Kids خود را دارد. مقدار /Count در هر گره میانی تعداد کل صفحات برگ در زیردرخت خود را گزارش میدهد، به طوری که نمایشگر هنگام پرش به یک صفحه بر اساس ایندکس، میتواند بدون تجزیه هر شیء، از کل زیردرخت عبور کند. یک سند 1000 صفحهای با ساختار درخت متوازن که هر گره برگ آن شامل 10 صفحه است، میتواند با یک جستجوی دودویی از طریق سه یا چهار بار جستجوی دیکشنری، صفحه 750 را پیدا کند، بدون اینکه نیاز به اسکن 750 ورودی /Kids باشد
پیامد آن برای کد پردازش روشن است: نمیتوانید فرض کنید لایه اول /Kids فقط شامل اشیاء /Page است. هر فرزند باید بررسی شود. اگر /Type آن برابر /Pages بود، باید بهصورت بازگشتی وارد آن شوید. اگر /Type آن برابر /Page بود، آن یک برگ است. توقف روی لایه اول در هر سندی که تولیدکننده درخت تو در تو ساخته باشد، کل زیردرختها را بیصدا حذف میکند. اینکه نویسندگان PDF چرا اصلاً به سراغ درختهای عمیق میروند، flatten کردن چه چیزهایی را از بین میبرد و خراب شدن /Count در عمل چگونه خود را نشان میدهد در مقاله همراه ما درباره شکل درخت صفحه، fan-out و سلامت /Count توضیح داده شده است
ویژگیهای صفحه به ارث رسیده
درخت صفحه یک سازوکار اشتراک منابع هم دارد. بعضی ویژگیهای صفحه، یعنی /MediaBox، /CropBox، /Resources و /Rotate، طبق ISO 32000-2 §7.7.3.4 موروثی هستند. اگر دیکشنری /Page یکی از آنها را نداشته باشد، reader از زنجیره /Parent بالا میرود تا آن ویژگی را پیدا کند یا به ریشه برسد. قرار دادن یک دیکشنری فونت مشترک در گره ریشه /Pages بهجای کپی کردن آن در هر صفحه برگ، برای اسنادی که در سراسر فایل از یک تایپفیس استفاده میکنند میتواند حجم فایل را بهطور محسوسی کم کند
این قاعده ارثبری برای کدی که ویژگیهای صفحه را میخواند یک ظرافت ایجاد میکند. اگر /MediaBox را مستقیماً از یک شیء /Page بخوانید و نبودن آن کلید را خطا فرض کنید، اشتباه کردهاید؛ ممکن است کلید فقط به ارث رسیده باشد. کدی که میخواهد هندسه صفحه را درست resolve کند باید زنجیره والد را دنبال کند. همچنین به یک نگهبان چرخه نیاز دارد: فایل خراب میتواند ارجاع /Parentای داشته باشد که به گرهی که قبلاً دیدهاید برگردد و بدون بررسی اشیای بازدیدشده، حلقه بینهایت ایجاد کند
جدول xref و جریانهای ارجاع متقابل (cross-reference)
جستجوی شیء غیرمستقیم از طریق جدول ارجاع متقابل (یا جانشین آن — جریان ارجاع متقابل معرفی شده در PDF 1.5) انجام میشود. xref هر شماره شیء را به یک آفست بایت در فایل نگاشت میکند. یک خواننده مطیع مشخصات از xref برای پرش مستقیم به هر شیء استفاده میکند، بدون اینکه فایل را به ترتیب اسکن کند. این طراحی دسترسی تصادفی پرش سریع صفحه را ممکن میسازد: بیننده دایرکتوری را میخواند، ارجاع /Pages را از طریق xref پارس میکند، گره ریشه /Pages را میخواند، یک ورودی /Kids را پارس میکند و به همین ترتیب ادامه میدهد و فقط با اشیایی که نیاز دارد تماس برقرار میکند
بهروزرسانی افزایشی یک بخش xref جدید را به انتهای فایل اضافه میکند که دارای یک تریلر است که به بخش قبلی لینک داده شده است. اشیایی که در یک بازبینی بهروزرسانی میشوند، ورودیهای جدیدی در بخش xref الحاقی دریافت میکنند و بایتهای اصلی در جای خود باقی میمانند اما جایگزین میشوند. به همین دلیل است که حتی پس از اضافه شدن نسخههای اصلاحشده یادداشتها یا پر کردن فرم، سند PDF امضا شده دیجیتالی همچنان قابل تایید است: محدوده بایتهای امضا شده هرگز لمس نشدهاند و محتوای جدید در بخش الحاقی وجود دارد. درخت صفحه نیز میتواند بهروزرسانی شود، بنابراین افزودن یا حذف صفحات در یک بازبینی، گره ریشه /Pages جدید و آرایه /Kids اصلاح شده تولید میکند، در حالی که شیء گره ریشه قدیمی همچنان موقعیت اصلی خود را در فایل اشغال میکند
عدم پیمایش درخت چه مشکلاتی ایجاد میکند
الگوی شکست روش اسکن شیء خاموش است. سند خروجی منطقی به نظر میرسد: تعداد صفحات صحیح است، هر صفحه حاوی محتوای قابل شناسایی است، فقط ترتیب اشتباه است - و روش اشتباه بودن به تولیدکننده، تعداد دفعات ویرایش و اینکه آیا صفحهای از یک منبع خارجی ادغام شده است یا خیر بستگی دارد. مجموعه فایلهای آزمایشی تولید شده با یک ابزار منفرد ممکن است کاملاً پاس شوند، در حالی که فایلهای حاصل از ابزارهای متفاوت یا جریانهای کاری ادغام شکست میخورند. این عدم سازگاری دقیقاً دلیلی است که اصلاحات اکتشافی هرگز نمیتوانند جایگاه ثابتی پیدا کنند
فایلهای بهروزرسانی شده به صورت افزایشی (Incremental Update) بهویژه مستعد این مشکل هستند، زیرا صفحاتی که در ویرایشهای بعدی اضافه یا جابجا میشوند، شمارههای شیء بزرگتری دارند، در حالی که ترتیب نمایش توسط آرایه بهروزرسانی شده /Kids کنترل میشود. اسکنرهایی که اشیاء را به ترتیب عددی پردازش میکنند، این صفحات با شمارههای بزرگتر را در انتها قرار میدهند، صرفنظر از اینکه درخت مشخص کرده باشد به کجا تعلق دارند
روش اصلاح پیچیده نیست: از کاتالوگ شروع کنید، ارجاعات /Pages را تجزیه کنید، آرایه /Kids را به صورت بازگشتی پیمایش کنید و گرههای برگ را به ترتیب پیمایش خروجی دهید. این تعریف ترتیب نمایش است و ربطی به شماره شیء، آفست بایت یا ساختار فایل ندارد. بیشتر کتابخانههای بالغ PDF قبلاً این کار را از طریق تعداد صفحات و رابطهای صفحه با دسترسی مبتنی بر ایندکس به درستی پیادهسازی کردهاند؛ خطر در دور زدن مدل صفحه کتابخانه و دستکاری مستقیم کد سطح شیء است
یک ناهماهنگی ساختاری وجود دارد که ارزش مدیریت صریح را دارد: در فایلهای دارای قالب نادرست، مقدار /Count روی گرههای میانی /Pages ممکن است نادرست باشد. تکیه بر /Count برای بررسیهای مرزی و سپس متوقف شدن قبل از پیمایش کامل، در صورت کم بودن شمارش باعث از دست رفتن بیصدای صفحات میشود. برای اسناد مهم، ایمنتر است که از /Count صرفاً به عنوان یک راهنمای عملکرد برای پیشتخصیص ظرفیت یا جستجوی دودویی استفاده کنید و تعداد واقعی را از پیمایش واقعی استخراج کنید
مقاله بعدی