PDFium Component otvára PDF, ktoré sa ešte sťahuje, cez TPdfProgressiveDocument, podtriedu TPdf, ktorá obaľuje dostupnostné API FPDFAvail_* z PDFium. BeginProgressiveLoad začína session, CheckDocumentAvailability hlási, ktoré bajtové rozsahy PDFium ešte potrebuje, OpenProgressiveDocument otvorí súbor, keď existuje dosť bajtov, a CancelProgressiveLoad opustí prerušené sťahovanie bez úniku natívnych handle. Ťažká časť nie je šťastná cesta. Viewer na náhodnej connection uvidí užívateľov zatvárať tab na 25 percentach, meniť názor a otvárať ten istý link znova a každá z týchto prerušených session má natívny availability handle, dva callback recordy v C, stream adapter a sadu rozbehnutých range požiadaviek, ktoré sa musia uvoľniť v presne správnom poradí
Ako TPdfProgressiveDocument načítava PDF, ktoré sa ešte sťahuje?
TPdfProgressiveDocument drží availability providera PDFium nažive, kým sa plní random-access stream, a pred každým parse krokom sa providera pýta, či bajty, ktoré chce, sú prítomné. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) vezme backing stream plus logickú veľkosť vzdialeného súboru, zapojí callback IsDataAvail a callback AddSegment do dvoch recordov a zavolá FPDFAvail_Create. Keď sa PDFium pýta, či rozsah je prítomný, komponent odpovie áno, ak rozsah leží vnútri súvislého prefixu popísaného AvailableByteCount alebo vnútri rozsahu už dokončeného cez scheduler RangeRequests, a event OnDataAvailable môže verdikt pre riedke úložiská prevaliť. Každé volanie CheckDocumentAvailability vráti jednu z troch hodnôt TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) a podá rozsahy, o ktoré PDFium prosilo, ako utriedené, zlúčené pole TPdfDownloadRanges, už vo fronte schedulera s prioritou rrpImmediate
// FetchRange je váš transport (HTTP Range GET, socket, blob reader):
// zapíše Size bajtov na Offset do Store a vráti, koľko ich prišlo
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;
// Hinty sú už vo fronte; najprv zapíšte bajty, potom dokonč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 detaily v tomto cykle nesú váhu. Strop kôl sa počíta, lebo mŕtvy link prinúti CheckDocumentAvailability žiadať tie isté rozsahy naveky a neohraničený cyklus zmení sieťové zlyhanie na zavesené UI. Poradie sa počíta, lebo scheduler serializuje vlastný stav kritickou sekciou, ale pre TStream.Position na backing store nerobí nič: transportný thread musí zapísať bajty odpovede do streamu skôr, než zavolá CompleteRequest, pretože v momente, keď je dokončenie publikované, PDFium si ten rozsah môže prečítať, a súbežní piscovia potrebujú positioned I/O alebo vlastný zámok
Prečo AvailableByteCount odmieta ísť dozadu?
AvailableByteCount len rastie a setter vyhodí EPdfError s "Available byte count cannot move backwards", keď sa ho pokúsite zmenšiť. Keď callback IsDataAvail raz povedal PDFium, že rozsah existuje, parser si z neho mohol už prečítať a cacheovať objekty, takže stiahnutie tých bajtov potom urobí odpovede o dostupnosti nekonzistentné s tým, čo PDFium už skonzumoval. Ten istý setter odmietne hodnoty väčšie než LogicalFileSize a mimo session vyhodí "No progressive load is active", a preto bajty, ktoré už držíte, ešte pred začiatkom načítania patria do argumentu AInitialAvailableByteCount v BeginProgressiveLoad, nie do priradenia vlastnosti urobeného priskoro. Ak sa váš download store plní mimo poradia, nesnažte sa to vyjadriť cez prefix vôbec: dokončujte rozsahy cez scheduler alebo odpovedajte cez OnDataAvailable
Kedy sa čiastočne stiahnuté PDF naozaj otvorí?
Pred príchodom celého súboru sa otvorí len linearizované PDF (Annex F ISO 32000-1, rozloženie "Fast Web View"); nelinírové PDF ešte stále potrebuje každý bajt. OpenProgressiveDocument kontroluje vlastnosť Linearization (plnUnknown, plnNotLinearized, plnLinearized) a smeruje podľa nej: linearizovaný súbor sa otvorí cez FPDFAvail_GetDocument, hneď ako je prítomná sekcia prvej strany a hint tabuľky, zatiaľ čo nelinírový súbor sa otvorí cez FPDF_LoadCustomDocument na tom istom recorde prístupu k súboru a za čitateľný sa berie len ako celok. To smerovanie má konkrétny dôvod. Volanie FPDFAvail_GetDocument na nelinírovom súbore môže vrátiť nenulový handle s počtom strán nula, dokument, ktorý vyzerá otvorený a je prázdny. Vo vlastnej test suite komponentu dosiahne 51-stranová linearizovaná fixtúra pdaAvailable a otvorí sa s celým stromom strán, zatiaľ čo riedky download store stále nepokrýva súbor
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 teraz aktívna strana
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage berie číslo strany 1-based a vynucuje poradie, ktoré PDFium očakáva: pred prvou kontrolou strany spustí CheckFormAvailability, ktorá obaľuje FPDFAvail_IsFormAvail, a až potom volá FPDFAvail_IsPageAvail. Výsledok pfaNotPresent je normálna odpoveď pre dokument bez AcroForm a ničomu nebráni. Keď je strana hotová, urobí z nej LoadAvailablePage aktívnu stranu, takže viewer môže vykresliť stranu 1 linearizovanej brožúry, kým zvyšné strany sú stále na ceste; FirstAvailablePageNumber povie, ktorú stránku linearizačný slovník určuje za prvú, už prekonvertované z zero-based indexu PDFium
Čo CancelProgressiveLoad uvoľňuje a v akom poradí?
CancelProgressiveLoad rozoberá session v štyroch krokoch, ktoré nemožno preusporiadať: zrušiť range scheduler, zatvoriť dokument, zničiť availability handle cez FPDFAvail_Destroy a potom uvoľniť callback recordy a stream adapter. Zrušenie schedulera najprv zvýši jeho generačné počítadlo, zahodí každú čakajúcu aj rozbehnutú požiadavku a pre každú rozbehnutú spustí OnCancelRequest, takže transportné dokončenie, ktoré pristane neskôr, nesie starú generáciu a CompleteRequest vráti False bez toho, aby sa čohokoľvek dotklo. Dokument sa musí zatvoriť skôr, než odídu availability handle aj adapter, pretože PDFium môže pri zatváraní dokumentu volať späť do providera prístupu k súboru a keď už adapter neexistuje, ten callback číta uvoľnenú pamäť
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// Scheduler žije, dokiaľ FPdf, takže ho zapojte raz
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // váš kód: zatvorte ten socket alebo požiadavku
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
Metóda je idempotentná a je jediná upratovacia cesta pre tri situácie: BeginProgressiveLoad, ktoré padne v polovici stavby, výslovné rušenie užívateľom a deštruktor. BeginProgressiveLoad ju volá aj pred štartom, takže reštart toho istého objektu na nové URL je bezpečný bez výslovného cancelu. Jedno rozhodnutie o vlastníctve je na vás, aby bolo správne: ak worker thread zapisuje do backing stream, podajte AOwnsStream = False a stream uvoľnite sami potom, čo worker zastavil, lebo s odovzdaným vlastníctvom uvoľní cancel stream, kým neskorý zápis môže byť ešte na ceste. Výnimky vyhodené vo vnútri OnCancelRequest sa pre každú požiadavku prehltnú, takže jeden padajúci transport nemôže zablokovať zostávajúce rušenia
Ako lifecycle suite dokazuje, že cesta cancel neflowuje?
Stres suite lifecycle PDFium Component cvičí prerušené sťahovanie v štýle siete na každom zmiešanom cykle. Každý cyklus začína progressive load, ktorého store drží len štvrtinu bajtov fixtúry, vyžaduje pdaNotAvailable s neprázdnym zoznamom hintov, zavolá CancelProgressiveLoad a assertuje, že objekt nehlási ani ProgressiveLoading, ani Active; potom beží tá istá streaming cesta do konca s plnou dostupnosťou, OpenProgressiveDocument, render a zatvorenie. Predvolený zmiešaný beh pokrýva 100 meraných cyklov s 600 otvoreniami, 2300 rendermi a 100 progresívnymi rušeniami a vzorkovaná privátna pamäť rástla o 8,21 MiB proti rozpočtu 32 MiB. Suite počíta progresívne rušenia oddelene od rušení render callbackov, lebo prerušené sťahovanie a render cyklus, ktorý skončí skôr, sú odlišné udalosti s odlišnými kritériami prijatia
Kde progresívna cesta prestáva pomáhať
Pár limitov sa oplatí poznať, skôr než postavíte viewer na tomto základe. Funkcie, ktoré potrebujú pôvodné bajty súboru, odmietnu neúplný progresívny zdroj namiesto hádania: ReadXmpPacket padne výslovne a validácia podpisov hlási Indeterminate, dokiaľ nie je prítomný celý súbor. Predvolený dostupnostný test predpokladá súvislý prefix, takže transport sťahujúci rozsahy mimo poradia ich musí dokončiť cez RangeRequests alebo odpovedať cez OnDataAvailable, inak bude PDFium žiadať bajty, ktoré už držíte. Nelinírový súbor nezíska nič v čase do prvej strany, takže ak záleží na rýchlom prvom namaľovaní, linearizujte súbor na strane servera. A CancelProgressiveLoad nezatvára vaše sockety sám; OnCancelRequest je hák, kde sa to deje
Pre holú cestu stream adaptera, ktorá načíta kompletný lokálny súbor na požiadanie, pozrite streaming veľkých PDF na požiadanie s PDFium; pre otvorenie PDF, ktoré sedí vo väčšom bufferi, pozrite načítanie byte range pre vložené PDF. Rušenie pomalého renderu už načítanej strany je samostatný mechanizmus, popísaný v zrušiteľnom progresívnom renderovaní strán. TPdfProgressiveDocument aj jeho range scheduler idú s PDFium Component pre Delphi a C++Builder