Tehnički članak

PDFium progressive download i cancel u Delphi-ju (FPDFAvail)

PDFium Component otvara PDF koji se još uvek preuzima kroz TPdfProgressiveDocument, TPdf podklasu koja obavija PDFium FPDFAvail_* availability API. BeginProgressiveLoad pokreće sesiju, CheckDocumentAvailability prijavljuje koje bajt opsege PDFium još treba, OpenProgressiveDocument otvara fajl čim ga ima dovoljno, a CancelProgressiveLoad napušta prekinuto preuzimanje bez curenja nativnih hendlova. Teško nije srećna putanja. Pregledaču na nestabilnoj vezi desiće se da korisnici zatvore tab na 25 odsto, predomisle se i otvore isti link ponovo, i svaka od tih napuštenih sesija nosi jedan nativni availability handle, dva C zapisa povratnih poziva, stream adapter i skup zahteva za opsege u letu koje treba osloboditi baš pravim redosledom

Kako TPdfProgressiveDocument učitava PDF koji se još preuzima?

TPdfProgressiveDocument održava PDFium availability provajdera živim dok se random-access stream puni, i pre svakog koraka parsiranja pita tog provajdera da li su bajtovi koje želi prisutni. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) uzima potporni stream plus logičku veličinu udaljenog fajla, uvezuje IsDataAvail povratni poziv i AddSegment povratni poziv u dva zapisa i zove FPDFAvail_Create. Kad PDFium pita da li je opseg prisutan, komponenta odgovara da ako opseg leži unutar kontinualnog prefiksa opisanog sa AvailableByteCount ili unutar opsega koji je već dovršen kroz RangeRequests raspoređivač, a OnDataAvailable događaj može presudu preokrenuti za retko punjena spremišta. Svaki poziv CheckDocumentAvailability-a vraća jednu od tri TPdfDataAvailability vrednosti (pdaAvailable, pdaNotAvailable, pdaError) i predaje nazad opsege koje je PDFium tražio kao sortirani, spojeni TPdfDownloadRanges niz, već ubačen u red raspoređivača na rrpImmediate prioritetu

// FetchRange je vaš transport (HTTP Range GET, soket, čitač blobova):
// upisuje Size bajtova na Offset u Store i vraća koliko ih 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;
    // Naznake su već u redu; prvo upišite bajtove, pa tek onda 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. Plafon krugova bitan je jer mrtav link tera CheckDocumentAvailability da traži iste opsege zauvek, i neogrančena petlja pretvara mrežni kvar u zaleđeno sučelje. Redosled je bitan jer raspoređivač svoje stanje serijalizuje kritičnom sekcijom ali ne radi ništa za TStream.Position na potpornom store-u: transportna nit mora upisati bajtove odgovora u stream pre poziva CompleteRequest-a, jer u trenutku kada se dovršenje objavi PDFium može čitati taj opseg, a istovremeni pisci trebaju pozicionirani I/O ili svoju bravu

Availability petlja TPdfProgressiveDocument-a u PDFium Component: BeginProgressiveLoad stvara FPDFAvail provajdera, CheckDocumentAvailability predaje sortirane spojene naznake preuzimanja ubačene u red na rrpImmediate prioritetu, transport upisuje bajtove u store pre nego što CompleteRequest objavi svaki opseg PDFium-u, a petlja je ograničena na 64 kruga jer mrtav link i dalje traži iste opsege
Upišite bajtove, pa tek onda dovršite zahtev: u trenutku kada se dovršenje objavi PDFium može čitati taj opseg, i ništa vam ne čuva poziciju streama

Zašto AvailableByteCount odbija da ide unazad?

AvailableByteCount samo raste, i setter diže EPdfError sa „Available byte count cannot move backwards" kad pokušate da ga smanjite. Čim je IsDataAvail povratni poziv rekao PDFium-u da opseg postoji, parser je možda već pročitao i keširao objekte iz njega, pa bi povlačenje tih bajtova posle učinilo odgovore o dostupnosti nekonzistentnim sa onim što je PDFium već potrošio. Isti setter odbija vrednosti veće od LogicalFileSize-a i diže „No progressive load is active" van sesije, pa bajtovi koje već držite pre nego što učitavanje počne pripadaju argumentu AInitialAvailableByteCount u BeginProgressiveLoad-u, a ne dodeli svojstva učinjenoj pre vremena. Ako se vaše spremište za preuzimanje puni van reda, ne pokušavajte to izraziti kroz prefiks uopšte: dovršite opsege kroz raspoređivač ili odgovorite kroz OnDataAvailable

Kada se delimično preuzet PDF zaista može otvoriti?

Samo linearizovan PDF (ISO 32000-1 Aneks F, raspored „Fast Web View") otvara se pre nego što stigne ceo fajl; nelinearizovan i dalje treba svaki bajt. OpenProgressiveDocument proverava svojstvo Linearization (plnUnknown, plnNotLinearized, plnLinearized) i usmerava s tim u skladu: linearizovan fajl otvara se kroz FPDFAvail_GetDocument čim su prisutna sekcija prve stranice i tabele naznaka, dok se nelinearizovan otvara kroz FPDF_LoadCustomDocument na istom zapisu pristupa fajlu i tretira kao čitljiv samo kao celina. Usmeravanje postoji iz konkretnog razloga. Poziv FPDFAvail_GetDocument na nelinearizovanom fajlu može vratiti ne-null handle čiji je broj stranica nula, dokument koji deluje otvoren a prazan je. U sopstvenoj test jedinici komponente 51-stranični linearizovani fixture doseže pdaAvailable i otvara se sa kompletnim stablom stranica dok retko punjeno spremište za preuzimanje još ne pokriva fajl

Kako OpenProgressiveDocument usmerava delimično preuzimanje u PDFium Component: linearizovan fajl otvara se kroz FPDFAvail_GetDocument čim stignu sekcija prve stranice i tabele naznaka, nelinearizovan treba FPDF_LoadCustomDocument i svaki bajt, a LoadAvailablePage proverava dostupnost forme sa FPDFAvail_IsFormAvail pre provere stranice, izbegavajući zamku ne-null handlea sa nula stranica
Samo linearizovani fajlovi dobijaju prednost na startu; na sve ostalo FPDFAvail_GetDocument može vratiti dokument koji deluje otvoren a ima nula stranica, i baš to usmeravanje spreč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 sprovodi redosled koji PDFium očekuje: pre prve provere stranice prelazi preko CheckFormAvailability-a, koji obavija FPDFAvail_IsFormAvail, i tek onda zove FPDFAvail_IsPageAvail. Rezultat pfaNotPresent normalan je odgovor za dokument bez AcroForm-a i ništa ne blokira. Kad je stranica spremna, LoadAvailablePage čini je aktivnom, pa pregledač može prikazati stranicu 1 linearizovane brošure dok se ostale stranice još prevoze; FirstAvailablePageNumber govori koju stranicu rečnik linearizacije određuje za prvu, već pretvoreno iz nula-baziranog PDFium indeksa

Šta CancelProgressiveLoad oslobađa, i kojim redosledom?

CancelProgressiveLoad rastavlja sesiju u četiri koraka koja se ne mogu preurediti: otkažite raspoređivač opsega, zatvorite dokument, uništite availability handle pozivom FPDFAvail_Destroy, a potom uništite zapise povratnih poziva i oslobodite stream adapter. Otkazivanje raspoređivača prvo povećava njegov brojač generacije, ispušta svaki zahtev na čekanju i u letu, i okida OnCancelRequest za svaki u letu, pa transportno dovršenje koje sleti kasnije nosi staru generaciju i CompleteRequest vraća False a ništa ne dira. Dokument se mora zatvoriti pre nego što availability handle i adapter nestanu, jer PDFium može pozvati nazad provajdera pristupa fajlu dok zatvara dokument, i ako je adapter već nestao taj povratni poziv čita oslobođenu memoriju

Fiksan redosled rastavljanja CancelProgressiveLoad u PDFium Component: prvo otkažite raspoređivač opsega da kasna dovršenja udare u povećani brojač generacije i vrate False, zatvorite dokument pre nego što adapter pristupa fajlu nestane, uništite availability handle sa FPDFAvail_Destroy, i tek onda uništite zapise povratnih poziva i oslobodite stream adapter
Jedan idempotentan metod čisti i pokvaren start, otkazivanje korisnika i destruktor; uz radnu nit koja piše u store, vlasništvo nad streamom ostaje na vama
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // Raspoređivač živi koliko i FPdf, pa ga povežite jednom
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

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

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

Metod je idempotentan i jedina je putanja čišćenja za tri situacije: BeginProgressiveLoad koji padne napola konstrukcije, izričito otkazivanje korisnika i destruktor. BeginProgressiveLoad ga zove i pre početka, pa je ponovno pokretanje istog objekta na novom URL-u sigurno bez eksplicitnog otkazivanja. Jedna odluka o vlasništvu je na vama da je pogodite: ako radna nit piše u potporni stream, prosledite AOwnsStream = False i oslobodite stream sami nakon što se radna nit zaustavi, jer sa predatim vlasništvom otkazivanje oslobađa stream dok kasni upis još može biti na putu. Izuzeci podignuti unutar OnCancelRequest-a gutaju se po zahtevu, da jedan pokvaren transport ne može blokirati preostala otkazivanja

Kako lifecycle suite dokazuje da putanja otkazivanja ne curi?

PDFium Component lifecycle stress suite kroz svaki mešoviti ciklus vežba prekinuto mrežno preuzimanje. Svaki ciklus pokreće progressive load čije spremište drži samo četvrtinu bajtova fixture-a, zahteva pdaNotAvailable sa nepraznom listom naznaka, zove CancelProgressiveLoad i asertuje da objekat ne prijavljuje ni ProgressiveLoading ni Active; a zatim istu streaming putanju sprovodi do kraja sa punom dostupnošću, OpenProgressiveDocument, jednim renderom i zatvaranjem. Podrazumevani mešoviti izvođaj pokriva 100 merenih ciklusa sa 600 otvaranja, 2300 rendera i 100 progressive otkazivanja, a uzorkovana privatna memorija porasla je za 8,21 MiB prema budžetu od 32 MiB. Suite broji progressive otkazivanja odvojeno od otkazivanja render povratnih poziva, jer su napušteno preuzimanje i render petlja koja rano stane različiti događaji sa različitim kriterijumima prihvatanja

Gde progressive putanja prestaje da pomaže

Nekoliko granica vredi znati pre nego što na ovome sagradite pregledač. Svojstva kojima trebaju bajtovi original fajla odbijaju nepotpun progressive izvor umesto da nagađaju: ReadXmpPacket pada eksplicitno i validacija potpisa javlja Indeterminate dok ceo fajl nije prisutan. Podrazumevani test dostupnosti pretpostavlja kontinualan prefiks, pa transport koji vuče opsege van reda mora ih dovršiti kroz RangeRequests ili odgovoriti kroz OnDataAvailable, ili će PDFium i dalje tražiti bajtove koje već držite. Nelinearizovan fajl ne dobija ništa na vremenu do prve stranice, pa ako je brza prva slika bitna, linearizujte fajl na strani servera. I CancelProgressiveLoad sam ne zatvara vaše sokete; OnCancelRequest je kuka gde se to dešava

Za običnu stream-adapter putanju koja učitava kompletan lokalni fajl na zahtev, pogledajte streaming velikih PDF-ova na zahtev uz PDFium; za otvaranje PDF-a koji sedi unutar većeg bafera, pogledajte byte range učitavanje za ugrađene PDF-ove. Otkazivanje spalog rendera stranice koja je već učitana poseban je mehanizam, pokriven u tekstu o cancellable progressive renderovanju stranica. TPdfProgressiveDocument i njegov raspoređivač opsega stižu uz PDFium Component za Delphi i C++Builder