PDFium Component otevírá PDF, které se ještě stahuje, přes TPdfProgressiveDocument, podtřídu TPdf obalující availability API PDFium FPDFAvail_*. BeginProgressiveLoad startuje session, CheckDocumentAvailability hlásí, které rozsahy bajtů PDFium ještě potřebuje, OpenProgressiveDocument otevře soubor, jakmile existuje dost bajtů, a CancelProgressiveLoad opustí přerušený download bez úniku nativních handle. Těžká část není happy path. Viewer na nedůvěryhodném spojení uvidí uživatele zavřít záložku na 25 procentech, rozmyslet si to a otevřít stejný odkaz znovu, a každá z těch přerušených session drží nativní availability handle, dva C callback záznamy, stream adapter a sadu rozsahových požadavků ve vzduchu, které se musí uvolnit přesně ve správném pořadí
Jak načítá TPdfProgressiveDocument PDF, které se ještě stahuje?
TPdfProgressiveDocument drží availability providera PDFium naživu, dokud se random-access stream plní, a ptá se providera před každým parsovacím krokem, zda jsou bajty, které chce, přítomné. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) bere podkladový stream plus logickou velikost vzdáleného souboru, zapojí callback IsDataAvail a callback AddSegment do dvou záznamů a zavolá FPDFAvail_Create. Když se PDFium ptá, zda je rozsah přítomný, komponenta odpoví ano, pokud rozsah leží uvnitř souvislého prefixu popsaného AvailableByteCount nebo uvnitř rozsahu už dokončeného přes scheduler RangeRequests, a událost OnDataAvailable může verdikt přebít pro sparse úložiště. Každé volání CheckDocumentAvailability vrátí jednu ze tří hodnot TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) a podá zpět rozsahy, o které PDFium požádal, jako setříděné, sloučené pole TPdfDownloadRanges už zaražené do fronty scheduleru s prioritou rrpImmediate
// FetchRange je váš transport (HTTP Range GET, socket, blob reader):
// zapíše Size bajtů na Offset do Store a vrátí, kolik jich dorazilo
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;
// Hints už jsou zaražené do fronty; nejdřív zapište bajty, pak dokončete
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 téhle smyčce nesou zatížení. Strop kol záleží, protože mrtvý odkaz přiměje CheckDocumentAvailability ptát se navěky na tytéž rozsahy a neohraničená smyčka změní síťové selhání na zavěšené UI. Pořadí záleží, protože si scheduler serializuje vlastní stav kritickou sekcí, ale pro TStream.Position podkladového úložiště nedělá nic: transportní vlákno musí zapsat odpovědní bajty do streamu dřív, než zavolá CompleteRequest, protože v momentě, kdy se dokončení publikuje, si PDFium tenhle rozsah může přečíst, a souběžní writeri potřebují pozicované I/O nebo vlastní zámek
Proč se AvailableByteCount odmítá posunout dozadu?
AvailableByteCount jen roste a setter vyhodí EPdfError s „Available byte count cannot move backwards", když se ho pokusíte zmenšit. Jakmile callback IsDataAvail řekl PDFium, že rozsah existuje, parser si z něj mohl už přečíst a cachovat objekty, takže pozdější odvolání těch bajtů by učinilo odpovědi o dostupnosti nekonzistentními s tím, co PDFium už skonzumoval. Týž setter odmítá hodnoty větší než LogicalFileSize a mimo session vyhazuje „No progressive load is active", proto bajty, které už držíte dřív, než začne načítání, patří do argumentu AInitialAvailableByteCount v BeginProgressiveLoad místo přiřazení property udělaného moc brzy. Pokud se váš download store plní mimo pořadí, nesnažte se to vyjádřit prefixem vůbec: rozsahy dokončujte přes scheduler nebo odpovídejte přes OnDataAvailable
Kdy se dá částečně stažené PDF vůbec otevřít?
Než dorazí celý soubor, otevře se jen linearizované PDF (Annex F normy ISO 32000-1, rozložení „Fast Web View"); nelinearizované PDF pořád potřebuje každý bajt. OpenProgressiveDocument zkontroluje property Linearization (plnUnknown, plnNotLinearized, plnLinearized) a směruje podle ní: linearizovaný soubor se otevře přes FPDFAvail_GetDocument, jakmile je přítomná sekce první stránky a hint tabulky, zatímco nelinearizovaný soubor se otevírá přes FPDF_LoadCustomDocument na témuž záznamu file access a jako čitelný se bere jen celý. Tohle směrování má konkrétní důvod. Zavolat FPDFAvail_GetDocument na nelinearizovaném souboru může vrátit nenulový handle s počtem stránek nula, dokument, který vypadá otevřený a je prázdný. Ve vlastní test suite komponenty dosáhne 51stránková linearizovaná fixture pdaAvailable a otevře se s celým stromem stránek, zatímco sparse download store pořád nepokrývá soubor
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 teď aktivní stránka
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage bere číslo stránky od 1 a vynucuje pořadí, které PDFium očekává: před první kontrolou stránky pustí CheckFormAvailability, které obaluje FPDFAvail_IsFormAvail, a teprve potom zavolá FPDFAvail_IsPageAvail. Výsledek pfaNotPresent je normální odpověď pro dokument bez AcroForm a nic neblokuje. Když je stránka hotová, udělá z ní LoadAvailablePage aktivní stránku, takže viewer může renderovat stránku 1 linearizovaného brožu, zatímco zbylé stránky jsou ještě na cestě; FirstAvailablePageNumber řekne, kterou stránku určuje linearizační slovník za první, už převedenou z nulového indexu PDFium
Co uvolňuje CancelProgressiveLoad a v jakém pořadí?
CancelProgressiveLoad rozebere session ve čtyřech krocích, které se nedají prohodit: zrušit range scheduler, zavřít dokument, zničit availability handle přes FPDFAvail_Destroy, pak uvolnit callback záznamy a stream adapter. Zrušení scheduleru jako prvního zvýší jeho počítadlo generace, upustí každý čekající a ve vzduchu visící požadavek a pro každý ve vzduchu visící vystřelí OnCancelRequest, takže transportní dokončení, které dopadne později, nese starou generaci a CompleteRequest vrátí False bez toho, aby se čehokoli dotklo. Dokument se musí zavřít dřív, než zmizí availability handle a adapter, protože PDFium může při zavírání dokumentu volat zpět do providera file access, a pokud je adapter už pryč, tenhle callback čte uvolněnou paměť
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// Scheduler žije, dokud žije FPdf, takže zapojte ho jednou
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // váš kód: zavřít tenhle socket nebo požadavek
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
Metoda je idempotentní a je jediná úklidová cesta pro tři situace: BeginProgressiveLoad, které selže napůl stavby, explicitní zrušení uživatelem a destruktor. BeginProgressiveLoad ji taky volá před startem, takže restart téhož objektu na novém URL je bezpečný bez explicitního zrušení. Jedno rozhodnutí o vlastnictví je na vás: pokud worker vlákno zapisuje do podkladového streamu, předejte AOwnsStream = False a stream uvolněte sami po zastavení workeru, protože s předaným vlastnictvím uvolní cancel stream v momentě, kdy pozdní zápis může být ještě na cestě. Výjimky vyhozené uvnitř OnCancelRequest se per požadavek pohlcují, takže jeden selhávající transport nemůže zablokovat zbylé zrušení
Jak lifecycle suite dokazuje, že cesta zrušení neuniká?
Lifecycle stress suite PDFium Component procvičuje přerušený download ve stylu sítě v každém míšeném cyklu. Každý cyklus startuje progresivní načítání, jehož úložiště drží jen čtvrtinu bajtů fixture, vyžaduje pdaNotAvailable s neprázdným seznamem hintů, zavolá CancelProgressiveLoad a assertuje, že objekt nehlásí ani ProgressiveLoading, ani Active; pak pustí tutéž streamovací cestu do konce s plnou dostupností, OpenProgressiveDocument, rendrem a zavřením. Defaultní míšený běh pokrývá 100 měřených cyklů s 600 otevřeními, 2300 rendry a 100 progresivními zrušeními a vzorkovaná privátní paměť narostla o 8,21 MiB proti rozpočtu 32 MiB. Suite počítá progresivní zrušení odděleně od zrušení render callbacků, protože přerušený download a render smyčka, která skončí dřív, jsou různé události s různými kritérii přijetí
Kde progresivní cesta přestává pomáhat
Několik limitů stojí za znát, než na tomhle postavíte viewer. Funkce, které potřebují původní bajty souboru, odmítnou nekompletní progresivní zdroj místo hádání: ReadXmpPacket selže explicitně a validace podpisů hlásí Indeterminate, dokud není přítomný celý soubor. Defaultní test dostupnosti předpokládá souvislý prefix, takže transport, který stahuje rozsahy mimo pořadí, je musí dokončit přes RangeRequests nebo odpovídat přes OnDataAvailable, jinak se bude PDFium pořád ptát na bajty, které už držíte. Nelinearizovaný soubor nezíská nic na čase do první stránky, takže pokud záleží na rychlém prvním vykreslení, linearizujte soubor na straně serveru. A CancelProgressiveLoad nezavře vaše sockety sám od sebe; OnCancelRequest je hák, kde se to děje
Pro holou cestu stream adapteru, která načítá kompletní lokální soubor na vyžádání, podívejte se na Streamování obrovských PDF na vyžádání s PDFium v Delphi; pro otevírání PDF, které sedí uvnitř většího bufferu, na načítání přes rozsah bajtů pro vložené PDF. Zrušení pomalého renderu už načtené stránky je samostatný mechanismus, pokrytý v Zrušitelné progresivní renderování PDF v Delphi (PDFium). TPdfProgressiveDocument i jeho range scheduler shipují s PDFium Component pro Delphi a C++Builder