مقاله فنی

فایل PDF بدون دیکشنری Pages: پیامدهای تجزیه

دیکشنری کاتالوگ PDF دقیقاً یک کلید ناوبری الزامی دارد: /Pages. این کلید باید به یک شیء غیرمستقیم از نوع /Pages اشاره کند که آن نیز به نوبه خود آرایه /Kids و مجموع صفحات /Count را نگه می‌دارد. این اشاره‌گر را بردارید و هیچ خواننده سازگاری نمی‌تواند حتی یک صفحه را در فایل پیدا کند. استاندارد ISO 32000-1 §7.7.2 در این مورد صریح است: کاتالوگ باید دارای یک ورودی /Pages باشد، و شیء ارجاع‌شده باید دارای نوع /Pages باشد. فایل‌هایی که این الزام را نقض می‌کنند صرفاً ناسازگار نیستند؛ آن‌ها از نظر ساختاری به گونه‌ای خراب هستند که اکثر تجزیه‌کننده‌ها با آن‌ها به درستی برخورد نمی‌کنند

آنچه مشخصات واقعاً می‌گوید

یک PDF سازگار حداقل دارای سه شیء است. شیء 1 کاتالوگ (Catalog) است، شیء 2 ریشه صفحات (Pages root) است، و شیء 3 به بعد دیکشنری‌های منفرد صفحه (Page) هستند. کاتالوگ به ریشه صفحات اشاره می‌کند؛ ریشه صفحات فرزندان خود را در /Kids لیست می‌نماید؛ هر صفحه یک ارجاع بازگشتی /Parent را حمل می‌کند. کل زنجیره از نظر طراحی دوطرفه است، بنابراین یک تجزیه‌کننده می‌تواند از هر دو طرف شروع کند و برای درختان متوازن در زمان O(log n) به هر صفحه‌ای پیمایش نماید

% Minimal conforming structure (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj

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

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

درخت صفحات (Pages) می‌تواند تودرتو باشد. یک سند با هزاران صفحه معمولاً صفحات را در اشیاء گره میانی که آن‌ها نیز نوع /Pages را حمل می‌کنند، گروه‌بندی می‌نماید، که هر کدام /Kids و /Count خاص خود را دارند که منعکس‌کننده زیردرخت زیر آن است. مقدار /Count در گره ریشه همیشه برابر با تعداد کل صفحات است. این تعداد همان چیزی است که نمایشگرها پیش از آنکه حتی یک صفحه را تجزیه کنند در فیلد شماره صفحه نمایش می‌دهند، زیرا خواندن یک عدد صحیح از شیء 2 بسیار ارزان‌تر از پیمایش کل درخت است

فایلی بدون صفحات چگونه به نظر می‌رسد

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

% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj

% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj

25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj

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

چرا تجزیه‌کننده‌ها از کار می‌افتند (Crash)

بیشتر تجزیه‌کننده‌های PDF جدول صفحه داخلی خود را در زمان بارگیری بر اساس مقدار /Count از ریشه صفحات تخصیص می‌دهند. وقتی آن ریشه وجود ندارد، تجزیه‌کننده یا مقدار صفر را می‌خواند، چیزی تخصیص نمی‌دهد، و سپس اولین باری که هر کدی درخواست صفحه 1 را می‌دهد، یک اشاره‌گر نال (null pointer) را از ارجاع خارج می‌کند (dereference)، یا داده‌های بی‌معنی را خوانده و یک بافر به شدت نادرست تخصیص می‌دهد. هیچ‌کدام از این دو پیامد خوشایند نیست. نقض دسترسی در 0x008E5D78 که در گزارش‌های خرابی (crash logs) ناشی از پردازش چنین فایلی نشان داده می‌شود دقیقاً همین است: یک خروج از ارجاع اشاره‌گر نال در داخل مسیر دسترسی به صفحه، که ناشی از عدم وجود ساختاری است که تجزیه‌کننده فرض می‌کرد همیشه آنجا خواهد بود

فرض طراحی زیربنایی معقول است. اکثریت قریب به اتفاق PDFهای موجود دارای یک دیکشنری صفحات (Pages) هستند. تجزیه‌کننده‌هایی که برای صرفه‌جویی در چند دستورالعمل از بررسی وجود صرف‌نظر می‌کنند، بی‌ملاحظه نیستند؛ آن‌ها در حال بهینه‌سازی برای حالت معمول هستند. فایل‌هایی که این بهینه‌سازی را مجازات می‌کنند آن‌قدر نادر هستند که کد تولید ممکن است هرگز با یکی از آن‌ها مواجه نشود تا زمانی که واقعاً این اتفاق بیفتد، که در آن نقطه اگر مهندس بخش §7.7.2 را نخوانده باشد، خرابی هم قابل‌بازآفرینی و هم گیج‌کننده است

بازیابی بدون درخت صفحات

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

خود این بررسی ارزان است. پیش از پیمایش اشاره‌گر /Pages کاتالوگ، تأیید کنید که اشاره‌گر وجود دارد، به یک شیء واقعی ختم می‌شود، و اینکه /Type شیء حل‌شده برابر با /Pages است. اگر هر یک از این سه شرط برآورده نشود، به اسکن خطی (linear scan) برگردید. اسکن برای اسناد بزرگ کندتر از پیمایش درخت است، زیرا به جای دنبال کردن یک مسیر متوازن هر هدر شیء را می‌خواند، اما کار می‌کند، و برای فایلی که از پیش بدشکل (malformed) شده است، صحت (correctness) بر سرعت ارجحیت دارد

یک مورد خاص که اسکن خطی به طور خودکار آن را حل نمی‌کند: ترتیب‌بندی صفحه. بدون آرایه /Kids برای تعریف دنباله، ترتیب "صحیح" توسط مشخصات تعریف نشده است. ترتیب شماره شیء پیش‌فرض واقع‌گرایانه (pragmatic) است؛ اگر پردازش با دقت فایل به اندازه کافی مهم باشد، بررسی اینکه آیا اشیاء صفحه دارای یک ارجاع صریح /StructParents یا حاشیه‌نویسی (annotation) هستند که حاکی از دنباله خواندن باشد، ارزش کار اضافی را دارد

پیامدها برای تولیدکننده‌های PDF

برای هر کسی که در حال نوشتن یک تولیدکننده PDF به جای تجزیه‌کننده است، درس بسیار محدود است: همیشه پیش از بستن فایل، ریشه صفحات (Pages) را منتشر کنید. کاتالوگی بدون ورودی /Pages تحت هیچ بازبینی از مشخصات یک PDF معتبر نیست. تولیدکننده‌هایی که اشیاء صفحه را در لحظه می‌سازند و درخت را در زمان نهایی‌سازی (finalization) مونتاژ می‌کنند (رویکردی که اکثر نویسندگان جریان (streaming) استفاده می‌کنند) تا زمانی که نهایی‌سازی واقعاً اجرا شود خوب هستند. حالت معمول خرابی یک استثنا یا بازگشت زودهنگام است که نوشتن را پیش از کامل شدن تریلر متوقف می‌کند و فایلی را باقی می‌گذارد که در برخی نمایشگرها (که دارای روش‌های ابتکاری بازیابی (recovery heuristics) هستند) باز می‌شود و در برخی دیگر (که این ویژگی را ندارند) شکست می‌خورد

فرمت‌های PDF/A و PDF/UA محدودیت‌های اضافی را بر روی درخت صفحه فراتر از آنچه مشخصات پایه ایجاب می‌کند تحمیل می‌نمایند، اما هیچ‌کدام الزام /Pages را کاهش نمی‌دهند. اعتبارسنجی (validator) که انطباق با ISO 19005 یا ISO 14289 را بررسی می‌کند، دیکشنری گمشده صفحات را به عنوان نقض مشخصات پایه، پیش از آنکه حتی به قوانین خاص نمایه (profile) برسد، تشخیص خواهد داد