مقاله فنی

دانلود تدریجی و لغو آن در PDFium در دلفی (FPDFAvail)

PDFium Component یک PDF را در حالی که هنوز در حال دانلود است از طریق TPdfProgressiveDocument باز می‌کند، زیرکلاسی از TPdf که API در دسترس‌بودن FPDFAvail_* در PDFium را می‌پوشاند. BeginProgressiveLoad نشست را شروع می‌کند، CheckDocumentAvailability گزارش می‌دهد PDFium هنوز به کدام بازه‌های بایتی نیاز دارد، OpenProgressiveDocument فایل را وقتی بایت‌های کافی موجود شد باز می‌کند، و CancelProgressiveLoad دانلود نیمه‌کاره را بدون نشت handleهای native رها می‌کند. بخش سخت مسیر خوش‌فرمان نیست. نمایشگری روی اتصال ناپایدار کاربرانی می‌بیند که در 25 درصد تب را می‌بندند، نظرشان عوض می‌شود و همان لینک را دوباره باز می‌کنند، و هر یک از آن نشست‌های رهاشده یک handle در دسترس‌بودن native، دو رکورد کال‌بک C، یک آداپتور استریم و مجموعه‌ای از درخواست‌های بازهٔ در پرواز دارد که باید دقیقاً به ترتیب درست آزاد شوند

TPdfProgressiveDocument چطور PDFی را که هنوز دارد دانلود می‌شود بار می‌کند؟

TPdfProgressiveDocument یک ارائه‌دهندهٔ در دسترس‌بودن PDFium را تا وقتی استریم دسترسی-تصادفی پر می‌شود زنده نگه می‌دارد و پیش از هر گام parse از آن ارائه‌دهنده می‌پرسد آیا بایت‌های مورد نظرش حاضرند. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) استریم پشتیبان به‌علاوهٔ اندازهٔ منطقی فایل راه دور را می‌گیرد، یک کال‌بک IsDataAvail و یک کال‌بک AddSegment را در دو رکورد سیم‌کشی می‌کند و FPDFAvail_Create را صدا می‌زند. وقتی PDFium می‌پرسد آیا بازه‌ای حاضر است، کامپوننت اگر بازه درون پیشوند پیوستهٔ توصیف‌شده توسط AvailableByteCount یا درون بازه‌ای که از قبل از طریق زمان‌بند RangeRequests کامل شده باشد جواب مثبت می‌دهد، و رویداد OnDataAvailable می‌تواند برای انبارهای پراکنده حکم را بازنویسی کند. هر فراخوانی CheckDocumentAvailability یکی از سه مقدار TPdfDataAvailability (pdaAvailable، pdaNotAvailable، pdaError) را برمی‌گرداند و بازه‌هایی که PDFium خواسته را به‌صورت آرایهٔ مرتب و ادغام‌شدهٔ TPdfDownloadRanges پس می‌دهد، که از قبل با اولویت rrpImmediate روی زمان‌بند صف شده‌اند

// FetchRange ترابورت شماست (HTTP Range GET، سوکت، blob reader):
// Size بایت را در Offset داخل Store می‌نویسد و برمی‌گرداند چند بایت رسید
function FetchRange(Store: TStream; Offset, Size: UInt64): UInt64; forward;

procedure OpenWhileDownloading(Pdf: TPdfProgressiveDocument; Store: TStream;
  RemoteSize: UInt64);
const
  MaxRounds = 64;
var
  Hints: TPdfDownloadRanges;
  State: TPdfDataAvailability;
  Request: TPdfRangeRequest;
  Round: Integer;
begin
  Pdf.BeginProgressiveLoad(Store, RemoteSize, False);
  State := pdaNotAvailable;
  for Round := 1 to MaxRounds do
  begin
    State := Pdf.CheckDocumentAvailability(Hints);
    if State <> pdaNotAvailable then
      Break;
    // hintها از قبل در صف‌اند؛ اول بایت‌ها را بنویس، بعد complete کن
    while Pdf.RangeRequests.TryDequeue(Request) do
      Pdf.RangeRequests.CompleteRequest(Request,
        FetchRange(Store, Request.Offset, Request.Size));
  end;
  if State <> pdaAvailable then
    raise EPdfError.Create('The document could not be discovered');
  Pdf.OpenProgressiveDocument;
end;

دو جزئیات در آن حلقه باربرند. سقف دورها مهم است چون یک لینک مرده باعث می‌شود CheckDocumentAvailability تا ابد همان بازه‌ها را بخواهد و حلقهٔ بی‌سقف یک شکست شبکه را به UIی گیرکرده تبدیل می‌کند. ترتیب مهم است چون زمان‌بند وضعیت خودش را با critical section سریال می‌کند اما برای TStream.Position روی انبار پشتیبان هیچ کاری نمی‌کند: thread ترابورت باید بایت‌های پاسخ را پیش از صدا زدن CompleteRequest در استریم بنویسد، چون از همان لحظه‌ای که یک تکمیل منتشر شود PDFium ممکن است آن بازه را بخواند، و نویسنده‌های هم‌زمان به I/O موقعیت‌دار یا قفل خودشان نیاز دارند

حلقهٔ در دسترس‌بودن TPdfProgressiveDocument در PDFium Component: BeginProgressiveLoad ارائه‌دهندهٔ FPDFAvail را می‌سازد، CheckDocumentAvailability hintهای دانلود مرتب و ادغام‌شده را که با اولویت rrpImmediate صف شده‌اند پس می‌دهد، ترابورت پیش از آنکه CompleteRequest هر بازه را به PDFium منتشر کند بایت‌ها را در انبار می‌نویسد، و حلقه در 64 دور سقف خورده چون لینک مرده همان بازه‌ها را مدام می‌خواهد
اول بایت‌ها را بنویسید، بعد درخواست را complete کنید: از لحظه‌ای که تکمیلی منتشر شود PDFium ممکن است آن بازه را بخواند، و هیچ چیزی موقعیت استریم را برایتان محافظت نمی‌کند

چرا AvailableByteCount حاضر نیست عقب برود؟

AvailableByteCount فقط بزرگ می‌شود و setter وقتی بخواهید کوچکش کنید EPdfError با پیام «Available byte count cannot move backwards» می‌دهد. وقتی یک بار کال‌بک IsDataAvail به PDFium گفته باشد بازه‌ای وجود دارد، پارسر ممکن است از قبل شیءهایی از آن خوانده و کش کرده باشد، پس پس گرفتن آن بایت‌ها بعدش جواب‌های در دسترس‌بودن را با چیزی که PDFium از قبل مصرف کرده ناسازگار می‌کند. همان setter مقادیر بزرگ‌تر از LogicalFileSize را رد می‌کند و بیرون از نشست «No progressive load is active» می‌دهد؛ به همین دلیل بایت‌هایی که پیش از شروع بارگذاری از قبل دارید باید در آرگومان AInitialAvailableByteCount مربوط به BeginProgressiveLoad بروند نه در انتساب پراپرتی‌ای که زود انجام شده. اگر انبار دانلودتان نامرتب پر می‌شود، اصلاً سعی نکنید آن را با پیشوند بیان کنید: بازه‌ها را از طریق زمان‌بند کامل کنید یا از طریق OnDataAvailable جواب بدهید

چه زمانی یک PDF نیمه‌دانلودشده واقعاً باز می‌شود؟

فقط یک PDF خطی‌شده (پیوست F از ISO 32000-1، چیدمان «Fast Web View») پیش از رسیدن کل فایل باز می‌شود؛ یک PDF خطی‌نشده هنوز به همهٔ بایت‌ها نیاز دارد. OpenProgressiveDocument پراپرتی Linearization (plnUnknown، plnNotLinearized، plnLinearized) را بررسی و بر اساسش مسیر می‌دهد: فایل خطی‌شده به‌محض حاضر بودن بخش صفحهٔ اول و جدول‌های hint از طریق FPDFAvail_GetDocument باز می‌شود، در حالی که فایل خطی‌نشده از طریق FPDF_LoadCustomDocument روی همان رکورد دسترسی فایل باز می‌شود و فقط به‌صورت کامل قابل‌خواندن تلقی می‌گردد. این مسیریابی دلیل ملموسی دارد. صدا زدن FPDFAvail_GetDocument روی فایل خطی‌نشده می‌تواند handleای غیر تهی برگرداند که تعداد صفحه‌اش صفر است؛ سندی که به نظر باز می‌آید و خالی است. در مجموعهٔ آزمون خود کامپوننت یک fixture خطی‌شدهٔ 51 صفحه‌ای به pdaAvailable می‌رسد و با درخت صفحه کاملش باز می‌شود در حالی که انبار دانلود پراکنده هنوز کل فایل را نپوشانده

OpenProgressiveDocument در PDFium Component دانلود جزئی را چطور مسیر می‌دهد: فایل خطی‌شده به‌محض رسیدن بخش صفحهٔ اول و جدول‌های hint از طریق FPDFAvail_GetDocument باز می‌شود، فایل خطی‌نشده به FPDF_LoadCustomDocument و همهٔ بایت‌ها نیاز دارد و LoadAvailablePage پیش از بررسی صفحه، در دسترس‌بودن فرم را با FPDFAvail_IsFormAvail بررسی می‌کند تا از دام handle غیرتهیِ صفحهٔ صفر پرهیز شود
فقط فایل‌های خطی‌شده سرآغاز می‌گیرند؛ روی هر چیز دیگر FPDFAvail_GetDocument می‌تواند سند باز به‌نظرِ صفر صفحه‌ای برگرداند، و دقیقاً همان است که مسیریابی جلویش را می‌گیرد
function WaitForPage(Pdf: TPdfProgressiveDocument; Store: TStream;
  PageNumber: Integer): Boolean;
var
  Hints: TPdfDownloadRanges;
  Request: TPdfRangeRequest;
  Round: Integer;
begin
  Result := False;
  for Round := 1 to 64 do
    case Pdf.LoadAvailablePage(PageNumber, Hints) of
      pdaAvailable:
        Exit(True);   // PageNumber حالا صفحهٔ فعال است
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

LoadAvailablePage شماره صفحهٔ یک‌مبنا می‌گیرد و ترتیبی که PDFium انتظارش دارد را اعمال می‌کند: پیش از اولین بررسی صفحه، CheckFormAvailability را اجرا می‌کند که FPDFAvail_IsFormAvail را می‌پوشاند و فقط بعد از آن FPDFAvail_IsPageAvail را صدا می‌زند. نتیجهٔ pfaNotPresent جواب معمول سند بدون AcroForm است و چیزی را بلاک نمی‌کند. وقتی صفحه آماده شد، LoadAvailablePage آن را صفحهٔ فعال می‌کند، پس نمایشگر می‌تواند صفحهٔ 1 یک بروشور خطی‌شده را رندر کند در حالی که بقیهٔ صفحه‌ها هنوز در راه‌اند؛ FirstAvailablePageNumber می‌گوید دیکشنری خطی‌سازی کدام صفحه را اول نام برده، که از قبل از ایندکس صفرمبنای PDFium تبدیل شده

CancelProgressiveLoad چه چیزی را آزاد می‌کند و با چه ترتیبی؟

CancelProgressiveLoad یک نشست را در چهار گام جمع می‌کند که قابل جابه‌جایی نیستند: لغو زمان‌بند بازه‌ها، بستن سند، نابود کردن handle در دسترس‌بودن با FPDFAvail_Destroy، و بعد دور ریختن رکوردهای کال‌بک و آزاد کردن آداپتور استریم. لغو زودهنگام زمان‌بند شمارندهٔ نسلش را جلو می‌برد، هر درخواست در صف و در پرواز را می‌اندازد و برای هر در پرواز OnCancelRequest را روشن می‌کند، پس تکمیلی از طرف ترابورت که دیرتر برسد نسل قدیمی را حمل و CompleteRequest بدون دست زدن به هیچ چیز False برمی‌گرداند. سند باید پیش از ناپدید شدن handle در دسترس‌بودن و آداپتور بسته شود چون PDFium هنگام بستن سند می‌تواند به ارائه‌دهندهٔ دسترسی فایل کال‌بک بزند، و اگر آداپتور از قبل رفته باشد آن کال‌بک حافظهٔ آزادشده را می‌خواند

ترتیب ثابت جمع‌آوری CancelProgressiveLoad در PDFium Component: اول زمان‌بند بازه‌ها را لغو کنید تا تکمیل‌های دیرهنگام به شمارندهٔ نسل جلوافتاده بخورند و False برگردانند، سند را پیش از ناپدید شدن آداپتور دسترسی فایل ببندید، handle در دسترس‌بودن را با FPDFAvail_Destroy نابود کنید و فقط بعد رکوردهای کال‌بک را دور بریزید و آداپتور استریم را آزاد کنید
یک متد idempotent شروع شکسته، لغو کاربر و destructor را یک‌سان جمع می‌کند؛ با thread کارگری که در انبار می‌نویسد، مالکیت استریم دست خودتان می‌ماند
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // scheduler به اندازهٔ FPdf عمر می‌کند، پس یک بار وصلش کنید
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // کد شما: آن سوکت یا درخواست را ببند
end;

procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
  FPdf.CancelProgressiveLoad;
  // ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;

این متد idempotent است و مسیر پاک‌سازی واحد برای سه حالت است: BeginProgressiveLoadای که در میانهٔ ساخت شکست می‌خورد، لغو صریح کاربر، و destructor. BeginProgressiveLoad پیش از شروع هم آن را صدا می‌زند، پس راه‌اندازی دوبارهٔ همان شیء روی URL جدید بدون لغو صریح امن است. یک تصمیم مالکیت هست که درست‌کردنش با شماست: اگر thread کارگری در استریم پشتیبان می‌نویسد، AOwnsStream = False پاس بدهید و استریم را بعد از توقف کارگر خودتان آزاد کنید، چون با تحویل مالکیت، لغو استریم را آزاد می‌کند در حالی که ممکن است هنوز نوشتنی دیرهنگام در راه باشد. exceptionهای داخل OnCancelRequest به‌ازای هر درخواست بلعیده می‌شوند تا ترابورت شکسته نتواند بقیهٔ لغوها را بلاک کند

مجموعهٔ چرخهٔ عمر چطور ثابت می‌کند مسیر لغو نشت ندارد؟

مجموعهٔ تست استرس چرخهٔ عمر PDFium Component یک دانلود نیمه‌کارهٔ شبیه‌شده به شبکه را در هر چرخهٔ مخلوط ورزش می‌دهد. هر چرخه بارگذاری تدریجی‌ای را شروع می‌کند که انبارش فقط یک‌چهارم بایت‌های fixture را دارد، انتظار pdaNotAvailable با فهرست hint غیرخالی دارد، CancelProgressiveLoad را صدا می‌زند و assert می‌گیرد که شیء نه ProgressiveLoading گزارش می‌کند نه Active؛ بعد همان مسیر استریمینگ را تا تمام شدن با در دسترس‌بودن کامل می‌رود، OpenProgressiveDocument، یک رندر و یک بستن. اجرای مخلوط پیش‌فرض 100 چرخهٔ اندازه‌گیری‌شده با 600 باز شدن، 2300 رندر و 100 لغو تدریجی را پوشش می‌دهد و حافظهٔ خصوصی نمونه‌برداری‌شده در برابر بودجهٔ 32 MiB فقط 8.21 MiB رشد کرد. مجموعه لغوهای تدریجی را جدا از لغوهای کال‌بک رندر می‌شمارد، چون دانلود رهاشده و حلقهٔ رندری که زود متوقف می‌شود دو رویداد متفاوت با معیارهای قبولی متفاوت‌اند

جایی که مسیر تدریجی دیگر کمک نمی‌کند

چند محدودیت هست که پیش از ساختن نمایشگری روی این پایه‌ها بدانید. ویژگی‌هایی که به بایت‌های فایل اصلی نیاز دارند از منبع تدریجی ناقص به‌جای حدس زدن سر باز می‌زنند: ReadXmpPacket صریحاً شکست می‌خورد و اعتبارسنجی امضا تا وقتی کل فایل حاضر نشده Indeterminate گزارش می‌کند. آزمون در دسترس‌بودن پیش‌فرض پیشوند پیوسته را فرض می‌کند، پس ترابورتی که بازه‌ها را نامرتب می‌گیرد باید آن‌ها را از طریق RangeRequests کامل کند یا از طریق OnDataAvailable جواب بدهد، وگرنه PDFium مدام بایت‌هایی را می‌خواهد که از قبل دارید. یک فایل خطی‌نشده در زمان تا اولین صفحه هیچ نمی‌برد، پس اگر اولین نقاشی سریع مهم است فایل را سمت سرور خطی کنید. و CancelProgressiveLoad خودش سوکت‌های شما را نمی‌بندد؛ OnCancelRequest همان قلابی است که آن‌جا اتفاق می‌افتد

برای مسیر سادهٔ آداپتور استریم که یک فایل محلی کامل را درخواستی بار می‌کند استریم کردن PDFهای بزرگ درخواستی با PDFium را ببینید؛ برای باز کردن PDFی که داخل بافر بزرگ‌تری نشسته بارگذاری بازهٔ بایتی برای PDFهای جاسازی‌شده را ببینید. لغو یک رندر کند از صفحه‌ای که از قبل بار شده سازوکار جداگانه‌ای است که در رندر تدریجی قابل لغو صفحه پوشش داده شده. TPdfProgressiveDocument و زمان‌بند بازه‌هایش همراه PDFium Component برای دلفی و C++Builder عرضه می‌شوند