مقاله فنی

خروجی PDF خطی‌شده در Delphi: جداول Hint در HotPDF

HotPDF فایل‌های PDF خطی‌شده، همان چیدمانی که Acrobat آن را Fast Web View می‌نامد، را از طریق ویژگی LinearizeOutput روی THotPDF می‌نویسد. تنظیم آن پیش از BeginDoc باعث می‌شود HotPDF گراف شیء نهایی را بازآرایی کند تا یک reader آگاه به byte-range بتواند صفحه یک را پس از دریافت تنها بخش ابتدایی فایل نمایش دهد، به‌جای دانلود کل سند ابتدا. مکانیزم آن ISO 32000-1 Annex F است

دلیل اهمیت این موضوع بی‌ظاهر است. یک PDF عادی جدول cross-reference خود را در انتها می‌گذارد، پس یک viewer باید به آخرین بایت برسد پیش از آنکه بداند هرچیزی کجاست. یک گزارش اسکن‌شده ۲۰۰صفحه‌ای را به یک مرورگر بدهید و کاربر برای کل transfer به یک اسپینر خیره می‌شود، حتی اگر تنها چیزی که می‌خواست صفحه ۱ بود. خطی‌سازی این را با پرداخت هزینه در زمان نوشتن حل می‌کند. این مقاله دقیقاً درباره آن مسیر نوشتن است، پارتیشن‌بندی، حلقه اندازه‌گیری و محدودیت‌های سخت؛ برای پس‌زمینه مفهومی درباره اینکه Fast Web View چه چیزی برای شما می‌خرد، توضیح خطی‌سازی PDF و Fast Web View قبلی آن حوزه را پوشش می‌دهد

چیدمان خطی‌شده واقعاً چه چیزی را تضمین می‌کند

یک فایل خطی‌شده یک PDF معمولی با یک ترتیب فیزیکی بسیار خاص است، و هر تضمینی که ارائه می‌دهد از آن ترتیب می‌آید نه از هر نوع شیء جدید. HotPDF بخش‌ها را به ترتیبی که Annex F تجویز می‌کند منتشر می‌کند: دیکشنری پارامتر خطی‌سازی داخل ۱۰۲۴ بایت اول، یک جدول cross-reference زودهنگام، اشیاء سطح سند، stream اصلی hint، صفحه اول و اشیاء خصوصی آن، سپس صفحات باقی‌مانده، سپس اشیاء مشترک، سپس همه‌چیز دیگر، و در نهایت جدول cross-reference اصلی

پارتیشن‌بندی مشتق‌شده است، نه اعلام‌شده. HotPDF گراف ارجاع را از هر شیء صفحه پیمایش می‌کند و برای هر شیء indirect ثبت می‌کند چند صفحه به آن می‌رسند و کدام صفحه اول به آن رسیده. شیئی که دقیقاً یک صفحه استفاده می‌کند خصوصی آن صفحه می‌شود. شیئی که بیش از یکی به آن می‌رسد مشترک می‌شود. کاتالوگ، به‌علاوه هرچه زیر /ViewerPreferences، /OpenAction، /Threads و /AcroForm ارجاع می‌دهد، به‌علاوه دیکشنری رمزنگاری وقتی حفاظت فعال است، گروه سطح سند را تشکیل می‌دهند که باید مقدم بر همه‌چیز باشد. گره‌های درخت صفحه عمداً نگه داشته می‌شوند تا بخش صفحه اول را آلوده نکنند

دیکشنری پارامتر اعدادی را حمل می‌کند که یک reader پیش از آنکه هرچیز دیگری خوانده باشد نیاز دارد: /L برای طول کل فایل، /H برای offset و طول stream hint، /O برای شماره شیء صفحه اول، /E برای بایتی که بخش صفحه اول در آن پایان می‌یابد، /N برای تعداد صفحات و /T برای offset entry جدول cross-reference اصلی. هر یک از این‌ها یک offset بایت به فایلی است که در لحظه‌ای که نیاز به نوشتنشان دارید هنوز وجود ندارد

چرا offsetهای جدول hint باید همگرا شوند؟

چون اعدادی که در دیکشنری پارامتر هستند فایلی را توصیف می‌کنند که خودشان را در بر دارد، و تغییر هرکدام از آن‌ها فایل را تغییر می‌دهد. این دشواری مرکزی یک writer خطی‌شده است، و به همین دلیل HotPDF مکرراً اندازه می‌گیرد به‌جای یک‌بار نوشتن. /T را از ۶ رقم به ۷ رقم گسترش دهید و دیکشنری پارامتر یک بایت رشد می‌کند؛ هدر رشد می‌کند؛ هر شیء جابه‌جا می‌شود؛ جدول cross-reference اصلی حرکت می‌کند؛ /T اکنون به مقدار متفاوتی نیاز دارد. چیدمان باید پیش از commit شدن یک بایت واحد از خروجی واقعی به یک نقطه ثابت برسد

HotPDF این را با یک تکرار محدود مدیریت می‌کند. ابتدا هر شیء را در یک stream شمارشگر سریالایز می‌کند که طول را ثبت می‌کند بدون نگه داشتن بایت‌ها، پس هر شیء اندازه سریالایزشده معلومی دارد. سپس یک پاس چیدمان اجرا می‌کند که offsetها را به گروه سطح سند، stream hint، گروه صفحه اول، گروه‌های صفحات بعدی، گروه مشترک و باقی‌مانده تخصیص می‌دهد، و گزارش می‌دهد جدول cross-reference اصلی کجا فرود می‌آید. آن نتیجه به‌عنوان ورودی پاس بعدی بازخوراند می‌شود. حلقه به هشت تلاش محدود شده، و عدم همگرایی به‌جای تولید فایلی با offsetهای اشتباه اما قابل‌قبول‌به‌نظر، یک exception بلند می‌کند

CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
  CalculateLayout(CandidateMainOffset, FirstXRefData,
    HintOffset, EndFirstPage, NewMainOffset);
  if NewMainOffset = CandidateMainOffset then
    Break;
  CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
  raise Exception.Create('Linearization layout did not converge');

دو جزئیات حلقه را از دست‌وپا زدن نگه می‌دارند. دیکشنری پارامتر در یک slot ثابت ۳۸۴ بایتی نوشته می‌شود، با فاصله padding‌شده، بنابراین رشد خودش هرگز نمی‌تواند چیدمان را ناپایدار کند؛ اگر متن دیکشنری تا به‌حال از آن رزرو فراتر می‌رفت، HotPDF به‌جای جابه‌جا کردن خاموش همه‌چیز، exception بلند می‌کند. و پس از همگرایی HotPDF یک پاس چیدمان تأییدی دیگر اجرا می‌کند و طول stream hint را دوباره بررسی می‌کند، چون خود stream hint offsetهایی را کدگذاری می‌کند که فقط پس از استقرار چیدمان معلوم بودند. پاداش کل این اندازه‌گیری این است که HotPDF هرگز یک کپی دوم از سند را بافر نمی‌کند: به‌محض ثابت شدن offsetها، اشیاء مستقیماً به stream مقصد سریالایز می‌شوند، با یک assertion در هر مرز بخش که بایت‌های نوشته‌شده با offsetی که وعده داده شده مطابقت دارد

فعال کردن آن از Delphi

سطح API یک Boolean است، و تنها نیازمندی آن این است که آن را پیش از شروع تولید تنظیم کنید. LinearizeOutput به‌طور پیش‌فرض False است، و پاس چیدمان زمانی اجرا می‌شود که سند نوشته می‌شود، پس انتساب آن پس از EndDoc هیچ کاری انجام نمی‌دهد

var
  PDF: THotPDF;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.FileName := 'fast-view.pdf';
    PDF.Version := pdf17;
    PDF.LinearizeOutput := True;      // must precede BeginDoc
    PDF.BeginDoc;
    PDF.Canvas.TextOut(72, 72, 'First page');
    PDF.EndDoc;
  finally
    PDF.Free;
  end;
end;

یک هشدار استقرار بر همه‌چیز در سمت کد اولویت دارد. خطی‌سازی فقط وقتی سودآور است که transport از درخواست‌های HTTP range پشتیبانی کند. همان فایل را از یک endpoint که آن را کامل جریان می‌دهد، یا از یک پیکربندی CDN که Range را نادیده می‌گیرد ارائه کنید، و برای خودتان یک مسیر نوشتن کندتر و یک فایل بزرگ‌تر بدون هیچ سودی که کاربر ببیند خریده‌اید. سرور را پیش از کد بررسی کنید

چرا خطی‌سازی بر UseXRefStream و UseObjectStreams اولویت دارد؟

چون writer خطی‌شده نیاز دارد هر شیء offset بایت مستقیماً آدرس‌پذیر خودش را داشته باشد، و هر دوی این ویژگی‌ها آن را می‌گیرند. HotPDF بنابراین هر وقت LinearizeOutput فعال باشد جداول cross-reference متنی سنتی و اشیاء indirect بازنشده منتشر می‌کند، حتی اگر فراخواننده UseXRefStream یا UseObjectStreams را هم تنظیم کرده باشد. این یک اولویت عمدی است، نه یک تضاد که خودتان باید حل کنید

استدلال از جداول hint می‌آید. یک جدول hint توصیف می‌کند یک بخش صفحه کجا شروع می‌شود و چقدر طول دارد، پس یک reader می‌تواند دقیقاً آن بازه را درخواست کند. یک شیء بسته‌بندی‌شده داخل یک container /ObjStm اصلاً هیچ offset مستقلی ندارد؛ فقط به‌عنوان یک برش داخل stream فشرده دیگری وجود دارد که باید به‌عنوان یک واحد دریافت و باز شود. اگر برای اندازه فایل روی object stream حساب می‌کردید، بدانید خطی‌سازی و فشرده‌سازی اینجا به جهت‌های مخالف می‌کشند، و trade-off را در مقاله همراه درباره object streamها و به‌روزرسانی‌های افزایشی در HotPDF بخوانید. همان کشمکش فایل‌های hybrid-reference را شکل می‌دهد، که دقیقاً برای نگه داشتن readerهای قدیمی‌تر در کنار جداول مبتنی‌بر stream وجود دارند، همان‌طور که در مقاله hybrid cross-reference streamها در PDFهای تولیدشده توسط آفیس پوشش داده شده

یک کف نسخه هم وجود دارد. خطی‌سازی به PDF 1.2 یا بالاتر نیاز دارد. اگر نسخه انتخاب‌شده قدیمی‌تر باشد، HotPDF آن را به‌طور خودکار بالا می‌برد، مگر StrictVersionLock تنظیم شده باشد، که در آن صورت نوشتن به‌جای ارتقای خاموش سندی که عمداً پین کرده‌اید یک exception بلند می‌کند

دیوار ۴ گیگابایتی، و چرا HotPDF به‌جای بریدن رد می‌کند

جداول hint خطی‌سازی offsetها را به‌عنوان مقادیر ۳۲بیتی ذخیره می‌کنند، پس یک فایل خطی‌شده نمی‌تواند چیزی در یا فراتر از ۴ گیگابایت را آدرس‌دهی کند، و HotPDF چنین خروجی‌ای را با یک exception صریح رد می‌کند به‌جای نوشتن فایلی با offsetهای پیچیده‌شده. این محدودیت یک انتخاب پیاده‌سازی HotPDF نیست؛ عرض فیلدهایی است که Annex F تعریف می‌کند

بررسی در سه جا اعمال می‌شود، و هر سه اهمیت دارند. HotPDF هر شیء را به‌محض معلوم شدن طول سریالایزشده‌اش اعتبارسنجی می‌کند، طول هر بخش صفحه را هنگام ساخت entryهای hint اعتبارسنجی می‌کند، و طول نهایی فایل را پس از تعیین اندازه جدول cross-reference اصلی اعتبارسنجی می‌کند. شکست زودهنگام کل هدف است: یک جدول hint با یک offset خاموش بریده‌شده فایلی تولید می‌کند که در یک viewer که آن را کامل دانلود می‌کند به‌درستی باز می‌شود و فقط برای کلاینت byte-range‌ای که خطی‌سازی برای خدمت به آن وجود داشت شکست می‌خورد، که بدترین حالت شکست ممکن است چون viewer تست شما هرگز آن را بازتولید نمی‌کند. اگر خروجی چندگیگابایتی تولید می‌کنید، خطی‌سازی ابزار مناسب نیست، و رویکرد streaming که در یادداشت‌های Direct File API برای workflowهای PDF بزرگ شرح داده شده جهتی است که باید نگاه کنید

تشخیص خطی‌سازی روی فایلی که بارگذاری کرده‌اید

THotPDF.IsLoadedLinearized گزارش می‌دهد آیا سند در حال حاضر بارگذاری‌شده از پیش به فرم خطی‌شده نوشته شده، و از یک snapshot گرفته‌شده پیش از parsing پاسخ می‌دهد، نه از stream زنده. HotPDF ۱۰۲۴ بایت اول را از موقعیت صفر stream منبع می‌خواند، آن‌ها را برای اولین کلمه کلیدی obj و سپس برای یک entry /Linearized با مقدار ۱ اسکن می‌کند، و نتیجه boolean را کش می‌کند

var
  PDF: THotPDF;
  PageCount: Integer;
begin
  PDF := THotPDF.Create(nil);
  try
    PageCount := PDF.LoadFromFile('incoming.pdf');
    if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
      Writeln('Source is not Fast Web View ready');
  finally
    PDF.Free;
  end;
end;

دو محدودیت در آن توصیف تحمل‌کننده بار هستند. تشخیص نمی‌تواند به موقعیت stream تکیه کند، چون تا زمانی که کد برنامه سؤال را می‌پرسد parser آن را حرکت داده، و نمی‌تواند دوباره روی تقاضا بخواند چون LoadFromFile پس از پایان بارگذاری stream منبع داخلی را آزاد می‌کند. از این‌رو طراحی capture-before-parse-and-cache. اسکن همچنین عمداً درباره مقدار literal است: فقط /Linearized 1 یا یک فرم معادل عددی با کسر صفر‌کامل پذیرفته می‌شود، چون فایلی که دیکشنری پارامترش چیز دیگری بگوید وعده Annex F را نمی‌دهد

یک دام رکورد Delphi که ارزش دزدیدن دارد

رکوردهای محلی حاوی آرایه‌های پویا فیلدهای managed خود را مقداردهی اولیه می‌کنند و هیچ‌چیز دیگر، و اگر یک فیلد ساده Count کنار آرایه نگه دارید باید خودتان آن را پاک کنید. این پارتیشن‌بندی خطی‌سازی را در طول توسعه گاز گرفت، و این نوعی باگ است که یک روز هزینه دارد دقیقاً چون یک platform آن را پنهان می‌کند

type
  THPDFLinearIndexList = record
    Values: THPDFIntegerArray;  // managed field: cleared for you
    Count: Integer;             // plain field: whatever was on the stack
  end;

// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);

فیلد آرایه پویا reference-counted است، پس کامپایلر آن را صفر می‌کند. Count کنار آن یک عدد صحیح معمولی بدون چنین تضمینی است، و یک Count مقداردهی‌نشده اولین append را به یک اندیس دلبخواه می‌فرستد. تحت Win32 slot پشته اتفاقاً صفر نگه داشته بود، append در اندیس ۰ فرود آمد، و هر تست عبور کرد. تحت Win64 همان کد فراتر از انتهای آرایه نوشت. درس فراتر از خطی‌سازی تعمیم می‌یابد: وقتی یک رکورد فیلدهای managed و unmanaged را مخلوط می‌کند، Default(TRecord) را انتساب دهید و استدلال درباره اینکه کامپایلر کدام فیلدها را می‌پوشاند را متوقف کنید، و هرگز یک اجرای سبز Win32 را به‌عنوان مدرک درستی مقداردهی اولیه در نظر نگیرید

اعضای LinearizeOutput و IsLoadedLinearized که اینجا شرح داده شدند همراه با نسخه استاندارد HotPDF Component برای Delphi و C++Builder عرضه می‌شوند؛ صفحه محصول مرجع کامل ویژگی، شامل قوانین تعامل با cross-reference streamها، object streamها و قفل نسخه را حمل می‌کند