مقاله فنی

خطی‌سازی PDF و Fast Web View: چگونه کار می‌کند

یک گزارش اسکن‌شده 80 مگابایتی را پشت یک لینک قرار دهید، آن را در یک مرورگر باز کنید، و ببینید چه اتفاقی می‌افتد: نمایشگر روی یک صفحه خالی می‌ماند تا زمانی که بخش بزرگی از آن بایت‌ها برسند، سپس صفحه اول را یکباره نقاشی می‌کند. به صفحه 40 بپرید و، در فایلی که بد ساخته شده است، ممکن است کل دانلود دوباره شروع شود. بخش ناامیدکننده این است که خواننده فقط صفحه اول را می‌خواست. خطی‌سازی (Linearization) پاسخ ساختاری به این مشکل است. این کار یک PDF را دوباره مرتب می‌سازد تا نمایشگر بتواند صفحه آغازین را از یک پیشوند کوچک از فایل رندر کند و بقیه را در صورت تقاضا بیاورد، که به همین دلیل است که Adobe این ویژگی را به عنوان "Fast Web View" بازاریابی می‌کند

هیچ‌کدام از این‌ها یک فرمت فایل متفاوت نیستند. یک PDF خطی‌شده یک PDF معمولی است که یک خواننده منطبق (conforming reader) آن را بدون برخورد خاصی باز خواهد کرد. این ترفند کاملاً در نحوه ترتیب‌بندی بایت‌ها و در دو ساختار اضافی است که فایل به همراه دارد. استاندارد ISO 32000-1 کل این آرایش را در ضمیمه F (Annex F) مشخص می‌کند، و وقتی که طرح را مشاهده کردید، این رفتار دیگر شبیه جادو به نظر نمی‌رسد و به صورت یک معامله آگاهانه بین ترتیب فایل برای تأخیر نقاشی اول (first-paint latency) به نظر می‌آید

خطی‌سازی در واقع چه چیزی را دوباره مرتب می‌کند

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

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

مجموعه اشیاء صفحه اول و دیکشنری پارامتر خطی‌سازی

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

ضمیمه F در مورد اینکه "صفحه اول" در اینجا چه معنایی دارد سخت‌گیر است. بخش صفحه اول باید شامل خود شیء صفحه، جریان‌های محتوای آن، و منابعی که این جریان‌ها ارجاع می‌دهند باشد، تا صفحه پس از دانلود شدن آن پیشوند خودکفا (self-sufficient) باشد. منابع مشترک، یعنی فونتی که در هر صفحه استفاده می‌شود یا لوگویی که در یک هدر تکرار می‌گردد، به طور ویژه اداره می‌شوند: آن‌ها آن‌قدر زود ظاهر می‌گردند که به صفحه اول خدمت کنند اما به عنوان مشترک (shared) علامت‌گذاری می‌شوند تا خواننده هنگام رندر کردن صفحه 30 آن‌ها را دوباره واکشی نکند. این تمایز بین اشیاء اختصاصی صفحه (page-private) و مشترک همان بخشی است که اکثر "بهینه‌سازهای" دست‌ساز آن را اشتباه انجام می‌دهند، و اشتباه انجام دادن آن چیزی است که فایلی تولید می‌کند که ادعا می‌نماید خطی‌شده است اما هنوز متوقف (stall) می‌شود

جریان‌های راهنما (Hint streams): نمایه‌ای که پرش صفحه را ارزان می‌کند

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

با توجه به این جداول، خواننده‌ای که صفحه 40 را می‌خواهد، فایل را به صورت متوالی تجزیه نمی‌کند. او به جدول راهنما رجوع می‌کند تا محدوده بایت‌هایی که صفحه 40 اشغال می‌کند را یاد بگیرد، دقیقاً همان محدوده را از سرور می‌خواهد، و پس از رسیدن آن بایت‌ها صفحه را رندر می‌سازد، و هر منبع مشترکی را که از قبل نگه نداشته است از طریق همان مکانیسم می‌کشد. جریان راهنما در واقع یک نقشه دسترسی تصادفی است که روی سند گذاشته شده است، و دلیل این است که یک فایل 500 صفحه‌ای به خوبی خطی‌شده روی یک پیوند کند واکنش‌گرا (responsive) احساس می‌شود در حالی که یک فایل بهینه‌نشده با همان اندازه این‌گونه نیست

چرا سرور باید همکاری کند

خطی‌سازی فرض می‌کند که انتقال می‌تواند برش‌های دلخواهی از فایل را تحویل دهد، و بررسی این فرض قبل از اینکه شما به فرمت به خاطر نتایج ضعیف اعتبار بدهید، ارزش دارد. مکانیسم ارائه بایت (byte-serving) HTTP است: خواننده درخواست‌های دامنه (range requests) صادر می‌کند و سرور با پاسخ‌های 206 Partial Content به آن‌ها پاسخ می‌دهد. اگر سرور تبلیغ Accept-Ranges: bytes را نکند، یا اگر یک پروکسی یا CDN در مقابل آن درخواست‌های دامنه را به انتقال‌های کامل فرو بپاشد (collapse)، خواننده هیچ راهی برای واکشی صفحه 40 به صورت مجزا ندارد و به دانلود کردن کل فایل برمی‌گردد. ساختار داخل PDF در این صورت کاملاً درست است و به طور کامل هدر رفته است

این همان خرابی است که اغلب به اشتباه با عنوان "خطی‌سازی کار نمی‌کند" تشخیص داده می‌شود. فایل خوب است؛ مسیر تحویل خوب نیست. قبل از اینکه سندی را دوباره بسازید، با یک درخواست شرطی تأیید کنید که میزبان در واقع محتوای جزئی (partial content) را برای URLای که خواننده وارد می‌کند، برمی‌گرداند. بسیاری از میزبان‌های استاتیک این کار را به صورت پیش‌فرض انجام می‌دهند، و بسیاری از سرورهای کاربردی (application servers) و لایه‌های کشینگ با پیکربندی اشتباه این کار را نمی‌کنند

به‌روزرسانی‌های افزایشی به طور بی‌صدا خطی‌سازی را می‌شکنند

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

اثر جانبی این است که فایل اکنون جدیدترین داده‌های ارجاع متقاطع خود را در دم دارد، بعد از بلوک صفحه اول که با دقت قرار داده شده است، و دیکشنری پارامتر خطی‌سازی در جلو چیدمانی را توصیف می‌کند که دیگر با فایل مطابقت ندارد. یک خواننده منطبق این عدم تطابق را تشخیص می‌دهد و با سند به عنوان یک PDF معمولی و خطی‌نشده برخورد می‌نماید. ویژگی Fast Web View از بین رفته است، اگرچه ساختار خطی‌شده اصلی هنوز در نیمه اول فایل نشسته است. اگر چندین به‌روزرسانی را الحاق کنید، هر کدام یک بازبینی دیگر را در انتها روی هم قرار می‌دهد و شکاف بین نمایه جلویی کهنه و وضعیت واقعی بازتر می‌گردد

اگر گردش کار شما هم به ویرایش و هم به Fast Web View نیاز دارد، این قانون به طور مستقیم از ساختار پیروی می‌کند: در حالی که سند در حال تغییر (flux) است به صورت افزایشی ویرایش نمایید، سپس یک بار در انتها دوباره خطی‌سازی کنید. یک بازنویسی کامل همان چیزی است که چیدمان را بازیابی می‌کند. در اصطلاح HotPDF، این بدان معنی است که یک ویرایش در حال پیشرفت از طریق BeginIncrementalUpdate و SaveIncrementalUpdate انجام می‌شود که یک دلتا را الحاق می‌کنند، در حالی که مرحله پایانی کل سند را بارگیری می‌سازد و آن را به صورت تازه با LoadFromFile به دنبال SaveLoadedDocument سریال‌سازی (serialize) می‌کند، که بازبینی‌های قدیمی انباشته‌شده را رها می‌سازد و یک چیدمان تمیز را منتشر می‌نماید. همین معامله در مورد جریان‌های شیء (object streams) نیز نشان داده می‌شود: فعال‌سازی UseObjectStreams به همراه UseXRefStream ارجاع متقاطع را فشرده می‌سازد و اشیاء را به طور محکم بسته‌بندی می‌کند، که به اندازه فایل کمک می‌نماید اما مانند هر انتخاب ساختاری، باید در طول آن بازنویسی نهایی اعمال شود تا اینکه به یک بازبینی الحاق‌شده پیچ گردد

// In-flight edits: append a delta, keep prior revisions intact.
// This leaves the file NOT linearized.
Pdf.BeginIncrementalUpdate('report.pdf');
Pdf.AddPage;
Pdf.CurrentPage.TextOut(72, 760, 0, 'Addendum');
Pdf.SaveIncrementalUpdate('report.pdf');

// Finishing step: full re-serialization produces one clean layout,
// dropping the stacked revisions. Re-run your linearizer on the output.
Pdf.LoadFromFile('report.pdf');
Pdf.SaveLoadedDocument('report-final.pdf');

کتابخانه HotPDF یک روال "خطی‌سازی" با یک فراخوانی (one-call) را در معرض دید قرار نمی‌دهد، بنابراین الگوی عملی این است که یک فایل تمیز و کاملاً بازنویسی‌شده تولید کنید و یک بهینه‌ساز اختصاصی را روی آن اجرا نمایید. ابزارهای خط فرمان مستقیماً تغییر ترتیب را اداره می‌کنند. ابزار qpdf یک فایل را با یک پرچم (flag) تکی به فرم خطی‌شده بازنویسی می‌کند:

qpdf --linearize report-final.pdf report-web.pdf

چگونه تشخیص دهیم که آیا یک فایل خطی‌شده است

به نام فایل یا ابزاری که ادعا می‌کند آن را تولید نموده است اعتماد نکنید؛ بایت‌ها را تأیید نمایید. مستقیم‌ترین بررسی سرِ فایل است: آن را باز کنید و به دنبال دیکشنری پارامتر خطی‌سازی به عنوان اولین شیء بعد از هدر بگردید، که کلید /Linearized را به همراه دارد. یک میانبر در سمت خواننده (reader-facing) کادر گفتگوی ویژگی‌های سند Acrobat است، که "Fast Web View: Yes" را تنها زمانی گزارش می‌دهد که ساختار واقعاً حضور داشته و به‌روز باشد

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

# Reports "File is linearized" and validates hint tables against the layout
qpdf --check report-web.pdf

# Dumps the linearization parameters and hint data in detail
qpdf --show-linearization report-web.pdf

مرحله اعتبارسنجی همان مرحله‌ای است که ارزش خود را ثابت می‌کند. عبوری که فقط وجود دیکشنری را تأیید کند با خوشحالی فایلی را که نمایه آن به افست‌های اشتباه اشاره می‌کند، برکت می‌دهد؛ بررسی که جداول راهنما را در برابر موقعیت‌های واقعی شیء تطبیق می‌دهد همان چیزی است که به شما می‌گوید بهینه‌سازی تحت درخواست‌های دامنه یک خواننده واقعی پایدار (hold up) خواهد ماند

خطی‌سازی همچنان ارزش اعمال کردن روی هر سند بزرگی را دارد که از طریق وب ارائه می‌شود، به خصوص برای خوانندگان تلفن همراه در اتصالات ناهموار، و برای نمایه از پیش بارگذاری‌شده (front-loaded)، چند درصد از حجم فایل را هزینه می‌برد. دو چیزی که باید به صورت مستقیم در نظر داشته باشید این است که ساختار داخل PDF و ارائه بایت در خارج از آن هر دو باید درست باشند، و هرگونه ویرایش پس از واقعیت، بهینه‌سازی را بی‌اثر می‌کند تا زمانی که فایل را بازنویسی کنید. با خطی‌سازی مجدد به عنوان آخرین مرحله در خط لوله (pipeline) رفتار نمایید، پس از آنکه هر تغییر دیگری حل‌وفصل شد. ارجاع متقاطع، جریان شیء و رفتار به‌روزرسانی افزایشی که در اینجا توضیح داده شد بخشی از مدل ساختاری است که ابزار HotPDF Component برای Delphi و C++Builder پیاده‌سازی می‌کند؛ برای پس‌زمینه گسترده‌تر چیدمان فایل به مقاله چگونگی ساختار یک PDF مراجعه کنید، و برای گردش کار به‌روزرسانی افزایشی و فایل بزرگ در کد، مقاله پردازش PDFهای بزرگ از Delphi را ببینید