Tehnički članak

PDFium progressive download i cancel u Delphiju (FPDFAvail)

PDFium Component otvara PDF koji se još skida kroz TPdfProgressiveDocument, TPdf podklasu koja omata PDFiumov availability API FPDFAvail_*. BeginProgressiveLoad pokreće sesiju, CheckDocumentAvailability javlja koje bajt raspone PDFium još treba, OpenProgressiveDocument otvara datoteku čim ima dovoljno bajtova, a CancelProgressiveLoad odustaje od prekinutog preuzimanja bez curenja nativnih handlea. Teško nije sretan put. Viewer na krhkoj vezi vidjet će korisnike kako zatvore tab na 25 posto, predomisle se i otvore isti link ponovno, i svaka od tih prekinutih sesija ima nativni availability handle, dva C callback zapisa, stream adapter i skup zahtjeva raspona u letu koje treba otpustiti točno pravim redoslijedom

Kako TPdfProgressiveDocument loada PDF koji se još skida?

TPdfProgressiveDocument drži PDFium availability providera živim dok se random-access stream puni, i pita providera prije svakog koraka parsiranja jesu li bajtovi koje hoće prisutni. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) uzima podupirući stream plus logičku veličinu udaljene datoteke, užara IsDataAvail callback i AddSegment callback u dva zapisa i zove FPDFAvail_Create. Kad PDFium pita postoji li raspon, komponenta odgovara da ako raspon leži unutar kontinuiranog prefiksa opisanog s AvailableByteCount ili unutar raspona već dovršenog kroz RangeRequests planer, a OnDataAvailable event presudu može nadjačati za rijetke storeove. Svaki poziv CheckDocumentAvailability vraća jednu od tri vrijednosti TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) i predaje rasponse koje je PDFium tražio kao sortirani, spojeni TPdfDownloadRanges niz, već u redu čekanja planera uz prioritet rrpImmediate

// FetchRange je Vaš transport (HTTP Range GET, socket, blob reader):
// zapisuje Size bajtova na Offset u Store i vraća koliko je stiglo
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;
    // Hintovi su već u redu čekanja; najprije zapišite bajtove, pa dovršite
    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;

Dva detalja u toj petlji nose teret. Strop krugova važan je jer mrtvi link tjera CheckDocumentAvailability da traži iste raspone dovijeka, a beskonačna petlja mrežnu grešku pretvara u objeseno sučelje. Redoslijed je važan jer planer svoje stanje serializira kritičnom sekcijom ali ništa ne radi za TStream.Position podupirućeg storea: transportna nit mora odgovorne bajtove zapisati u stream prije poziva CompleteRequest, jer od trenutka kad se dovršetak objavi PDFium može taj raspon čitati, a paralelni pisci trebaju pozicionirani I/O ili vlastiti lokot

Availability petlja TPdfProgressiveDocumenta u PDFium Componentu: BeginProgressiveLoad stvara FPDFAvail providera, CheckDocumentAvailability predaje sortirane spojene download hintove u redu čekanju uz prioritet rrpImmediate, transport zapisuje bajtove u store prije nego CompleteRequest objavi svaki raspon PDFiumu, a petlja je ograničena na 64 krugova jer mrtvi link i dalje traži iste raspone
Zapišite bajtove, pa dovršite zahtjev: od trenutka kad se dovršetak objavi PDFium može taj raspon čitati, a ništa ne čuva poziciju streama za Vas

Zašto AvailableByteCount odbija ići unatrag?

AvailableByteCount samo raste, a setter podiže EPdfError s "Available byte count cannot move backwards" kad pokušate smanjiti. Jednom kad je IsDataAvail callback rekao PDFiumu da raspon postoji, parser je možda već iz njega pročitao i keširao objekte, pa bi povlačenje tih bajtova poslije odgovore o dostupnosti učinilo nekonzistentnima s onim što je PDFium već progutao. Isti setter odbija vrijednosti veće od LogicalFileSize i izvan sesije podiže "No progressive load is active", pa bajtovi koje već držite prije početka loada pripadaju argumentu AInitialAvailableByteCount u BeginProgressiveLoad, a ne dodjeli svojstva učinjenoj prerano. Ako se Vaš download store puni izvan reda, nemojte to uopće pokušavati izraziti kroz prefiks: dovršite raspone kroz planer ili odgovorite kroz OnDataAvailable

Kad se djelomično skinuti PDF stvarno može otvoriti?

Samo linearizirani PDF (ISO 32000-1 Aneks F, "Fast Web View" raspored) otvara se prije nego stigne cijela datoteka; nelinearizirani PDF i dalje treba svaki bajt. OpenProgressiveDocument provjerava svojstvo Linearization (plnUnknown, plnNotLinearized, plnLinearized) i rutira sukladno tome: linearizirana datoteka otvara se kroz FPDFAvail_GetDocument čim su prisutne sekcija prve stranice i tablice hintova, dok se nelinearizirana otvara kroz FPDF_LoadCustomDocument na istom zapisu pristupa datoteci i tretira čitljivom samo kao cjelina. Rutiranje postoji iz konkretnog razloga. Poziv FPDFAvail_GetDocument na nelineariziranoj datoteci može vratiti ne-nil handle čiji je broj stranica nula, dokument koji izgleda otvoren a prazan je. U vlastitom test suiteu komponente 51-stranični linearizirani fixture doseže pdaAvailable i otvara se s punim stablom stranica dok rijetki download store još ne pokriva datoteku

Kako OpenProgressiveDocument rutira djelomično preuzimanje u PDFium Componentu: linearizirana datoteka otvara se kroz FPDFAvail_GetDocument čim stignu sekcija prve stranice i tablice hintova, nelinearizirana treba FPDF_LoadCustomDocument i svaki bajt, a LoadAvailablePage provjerava dostupnost forme s FPDFAvail_IsFormAvail prije provjere stranice, izbjegavajući zamku ne-nula handlea s nula stranica
Samo linearizirane datoteke dobivaju prednost u startu; na svemu ostalom FPDFAvail_GetDocument može vratiti otvoreno-izgledajući dokument s nula stranica, a to je točno ono čemu rutiranje otežava
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 je sada aktivna stranica
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

LoadAvailablePage uzima broj stranice od 1 i nameće redoslijed koji PDFium očekuje: prije prve provjere stranice izvodi CheckFormAvailability, koji omata FPDFAvail_IsFormAvail, i tek zatim zove FPDFAvail_IsPageAvail. Rezultat pfaNotPresent normalan je odgovor za dokument bez AcroForma i ništa ne blokira. Kad je stranica spremna, LoadAvailablePage čini je aktivnom stranicom, pa viewer može prikazati stranicu 1 lineariziranog brošure dok su ostale stranice još u tranzitu; FirstAvailablePageNumber govori koju stranicu rječnik linearizacije označava prvom, već pretvoreno iz PDFiumova indeksa od nule

Što CancelProgressiveLoad otpušta i u kojem redoslijedu?

CancelProgressiveLoad rastavlja sesiju u četiri koraka koja se ne mogu presložiti: otkazati raspon planer, zatvoriti dokument, uništiti availability handle s FPDFAvail_Destroy, pa otpustiti callback zapise i osloboditi stream adapter. Otkazivanje planera prvo podiže njegov brojač generacije, baca svaki na čekanju i u letu zahtjev i okida OnCancelRequest za svaki u letu, pa transportni dovršetak koji slijeti kasnije nosi staru generaciju i CompleteRequest vraća False a da ništa ne dira. Dokument se mora zatvoriti prije nego availability handle i adapter nestanu jer PDFium može zvati natrag u provider pristupa datoteci dok zatvara dokument, i ako je adapter već nestao, taj callback čita oslobođenu memoriju

Fiksni redoslijed rastavljanja CancelProgressiveLoad u PDFium Componentu: prvo otkazati raspon planer da kasni dovršetaci nađu podignuti brojač generacije i vrate False, zatvoriti dokument prije nego nestane adapter pristupa datoteci, uništiti availability handle s FPDFAvail_Destroy, i tek onda otpustiti callback zapise i osloboditi stream adapter
Jedna idempotentna metoda počišćava podjednako palu startu, korisničko otkazivanje i destruktor; s radnom niti koja piše u store, vlasništvo streama ostaje na Vama
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // Planer živi dok i FPdf, pa ga užarite jednom
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // Vaš kod: zatvorite taj socket ili zahtjev
end;

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

Metoda je idempotentna i jedina je staza čišćenja za tri situacije: BeginProgressiveLoad koji padne na pola konstrukcije, eksplicitno korisničko otkazivanje i destruktor. BeginProgressiveLoad nju zove i prije početka, pa ponovno pokretanje istog objekta na novom URL-u sigurno je bez eksplicitnog otkazivanja. Jedna odluka o vlasništvu je na Vama da je pogodite: ako radna nit piše u podupirući stream, pošaljite AOwnsStream = False i stream oslobodite sami nakon što se radna nit zaustavi, jer s predanim vlasništvom otkazivanje oslobađa stream dok kasni zapis može još biti na putu. Iznimke podignute unutar OnCancelRequest gutaju se po zahtjevu da jedna posrna transport ne blokira ostala otkazivanja

Kako lifecycle suite dokazuje da staza otkazivanja ne curi?

PDFium Component lifecycle stress suite izvodi prekinuto download-stil preuzimanje u svakom miješanom ciklusu. Svaki ciklus pokreće progressive load čiji store drži samo četvrtinu bajtova fixturu, zahtjeva pdaNotAvailable s nepraznim popisom hintova, zove CancelProgressiveLoad i tvrdi da objekt ne javlja ni ProgressiveLoading ni Active; zatim izvodi istu streaming stazu do kraja s punom dostupnošću, OpenProgressiveDocument, jednim renderom i zatvaranjem. Zadani miješani rad pokriva 100 mjerenih ciklusa sa 600 otvaranja, 2300 rendera i 100 progressive otkazivanja, a uzorkovana privatna memorija narasla je 8,21 MiB naspram budžeta od 32 MiB. Suite progressive otkazivanja broji odvojeno od render-callback otkazivanja, jer su prekinuto preuzimanje i render petlja koja staje rano različiti događaji s različitim kriterijima prihvaćanja

Gdje progressive put prestaje pomoći

Nekoliko granica vrijedi znati prije nego na ovome gradite viewer. Značajkama kojima trebaju izvorni bajtovi datoteke nepotpuni progressive izvor odbijaju umjesto da nagađaju: ReadXmpPacket pada eksplicitno, a validacija potpisa javlja Indeterminate dok cijela datoteka ne stigne. Zadani test dostupnosti pretpostavlja kontinuirani prefiks, pa transport koji dohvaća raspone izvan reda mora ih dovršavati kroz RangeRequests ili odgovarati kroz OnDataAvailable, ili će PDFium i dalje tražiti bajtove koje već držite. Nelinearizirana datoteka ne dobiva ništa u vremenu do prve stranice, pa ako je brzi prvi prikaz bitan, linearizirajte datoteku na strani servera. A CancelProgressiveLoad sam ne zatvara Vaše sockete; OnCancelRequest je kuka gdje se to događa

Za obični put stream adaptera koji loada kompletnu lokalnu datoteku na zahtjev, pogledajte streamanje velikih PDF-ova na zahtjev uz PDFium; za otvaranje PDF-a koji sjedi unutar većeg buffera, pogledajte byte range loadanje ugrađenih PDF-ova. Otkazivanje sporo rendera stranice koja je već učitana zaseban je mehanizam, obrađen u prekidivom progresivnom renderanju stranica. TPdfProgressiveDocument i njegov raspon planer isporučuju se s PDFium Componentom za Delphi i C++Builder