Technický článek

Progresivní download a zrušení v Delphi s PDFium (FPDFAvail)

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

Availability smyčka TPdfProgressiveDocument v PDFium Component: BeginProgressiveLoad vytvoří providera FPDFAvail, CheckDocumentAvailability podá setříděné sloučené download hinty zaražené do fronty s prioritou rrpImmediate, transport zapíše bajty do úložiště dřív, než CompleteRequest publikuje každý rozsah PDFium, a smyčka má strop 64 kol, protože mrtvý odkaz se pořád ptá na tytéž rozsahy
Zapište bajty, pak dokončete požadavek: v momentě, kdy se dokončení publikuje, si tenhle rozsah může PDFium přečíst a nic za vás nechrání pozici streamu

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

Jak směruje OpenProgressiveDocument částečný download v PDFium Component: linearizovaný soubor se otevře přes FPDFAvail_GetDocument, jakmile dorazí sekce první stránky a hint tabulky, nelinearizovaný soubor potřebuje FPDF_LoadCustomDocument a každý bajt a LoadAvailablePage kontroluje dostupnost formuláře přes FPDFAvail_IsFormAvail před kontrolou stránky, čímž obchází pastku nenulového handle s nulou stránek
Náskok dostávají jen linearizované soubory; u všeho ostatního může FPDFAvail_GetDocument vrátit dokument vypadající otevřeně s nulou stránek a přesně tomu směrování brání
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ěť

Pevné pořadí rozebírání v CancelProgressiveLoad v PDFium Component: nejdřív zrušit range scheduler, aby pozdní dokončení narazila na zvýšené počítadlo generace a vrátila False, zavřít dokument dřív, než zmizí adapter file access, zničit availability handle přes FPDFAvail_Destroy a teprve pak uvolnit callback záznamy a stream adapter
Jedna idempotentní metoda uklidí stejně neúspěšný start, zrušení uživatelem i destruktor; s worker vláknem zapisujícím do úložiště zůstává vlastnictví streamu na vás
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