PDFium Component otwiera PDF, który wciąż się pobiera, przez TPdfProgressiveDocument, podklasę TPdf owijającą API dostępności FPDFAvail_* z PDFium. BeginProgressiveLoad startuje sesję, CheckDocumentAvailability raportuje, jakich zakresów bajtów PDFium jeszcze potrzebuje, OpenProgressiveDocument otwiera plik, gdy tylko jest dość bajtów, a CancelProgressiveLoad porzuca przerwane pobieranie bez wycieku natywnych uchwytów. Trudna nie jest ścieżka szczęśliwa. Przeglądarka na chwiejnym łączu zobaczy użytkowników zamykających kartę na 25 procentach, zmieniających zdanie i otwierających ten sam link ponownie, a każda z tych przerwanych sesji ma natywny uchwyt dostępności, dwa rekordy callbacków C, adapter strumienia i zestaw żądań zakresów w locie, które trzeba zwolnić w dokładnie właściwej kolejności
Jak TPdfProgressiveDocument ładuje PDF, który wciąż się pobiera?
TPdfProgressiveDocument trzyma dostawcę dostępności PDFium przy życiu, dopóki strumień o dostępie swobodnym się zapełnia, i pyta tego dostawcę przed każdym krokiem parsowania, czy bajty, których potrzebuje, są obecne. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) bierze strumień nośny plus logiczny rozmiar zdalnego pliku, wpina callback IsDataAvail i callback AddSegment w dwa rekordy i woła FPDFAvail_Create. Gdy PDFium pyta, czy zakres jest obecny, komponent odpowiada tak, jeśli zakres leży w ciągłym prefiksie opisanym przez AvailableByteCount albo w zakresie już dokończonym przez scheduler RangeRequests, a zdarzenie OnDataAvailable może zawetować werdykt dla rozproszonych magazynów. Każde wywołanie CheckDocumentAvailability zwraca jedną z trzech wartości TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) i oddaje zakresy, o które pytał PDFium, jako posortowaną, scaloną tablicę TPdfDownloadRanges, już wkolejkowaną na schedulerze z priorytetem rrpImmediate
// FetchRange to twój transport (HTTP Range GET, gniazdo, czytnik blobów):
// zapisuje Size bajtów w Offset do Store i zwraca, ile dotarło
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;
// Podpowiedzi są już wkolejkowane; najpierw zapisz bajty, potem dokończ
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;
Dwa detale w tej pętli dźwigają całość. Limit rund ma znaczenie, bo martwy link każe CheckDocumentAvailability pytać o te same zakresy w nieskończoność, a nieograniczona pętla zamienia awarię sieci w zawieszony UI. Kolejność ma znaczenie, bo scheduler serializuje swój stan sekcją krytyczną, ale nic nie robi dla TStream.Position na magazynie nośnym: wątek transportowy musi zapisać bajty odpowiedzi do strumienia przed wołaniem CompleteRequest, bo w chwili opublikowania dokończenia PDFium może ten zakres przeczytać, a współbieżni pisarze potrzebują I/O pozycjonowanego albo własnej blokady
Dlaczego AvailableByteCount odmawia ruchu do tyłu?
AvailableByteCount tylko rośnie, a setter podnosi EPdfError z komunikatem "Available byte count cannot move backwards", gdy próbujesz go zmniejszyć. Skoro callback IsDataAvail powiedział już PDFium, że zakres istnieje, parser mógł już z niego przeczytać i scachować obiekty, więc wycofanie tych bajtów później uczyniłoby odpowiedzi dostępności niespójnymi z tym, co PDFium już skonsumował. Ten sam setter odrzuca wartości większe niż LogicalFileSize i podnosi "No progressive load is active" poza sesją, dlatego bajty, które już masz, zanim ładowanie się zacznie, należą do argumentu AInitialAvailableByteCount w BeginProgressiveLoad, a nie do przypisania właściwości zrobionego za wcześnie. Jeśli twój magazyn pobierania zapełnia się nie po kolei, nie próbuj tego w ogóle wyrażać przez prefiks: dokańczaj zakresy przez scheduler albo odpowiadaj przez OnDataAvailable
Kiedy częściowo pobrany PDF faktycznie się otworzy?
Tylko zlinearyzowany PDF (Aneks F ISO 32000-1, układ "Fast Web View") otwiera się, zanim przybędzie cały plik; niezlinearyzowany PDF nadal potrzebuje każdego bajtu. OpenProgressiveDocument sprawdza właściwość Linearization (plnUnknown, plnNotLinearized, plnLinearized) i rozdziela ruch stosownie: zlinearyzowany plik otwiera przez FPDFAvail_GetDocument, jak tylko sekcja pierwszej strony i tabele podpowiedzi są obecne, a niezlinearyzowany jest otwierany przez FPDF_LoadCustomDocument na tym samym rekordzie dostępu do pliku i traktowany jako czytelny tylko w całości. Ten rozdział ruchu istnieje z konkretnego powodu. Wołanie FPDFAvail_GetDocument na niezlinearyzowanym pliku może zwrócić niepusty uchwyt, którego liczba stron wynosi zero — dokument, który wygląda na otwarty i jest pusty. We własnym zestawie testów komponentu zlinearyzowana fixtura o 51 stronach dochodzi do pdaAvailable i otwiera się z pełnym drzewem stron, podczas gdy rozproszony magazyn pobierania wciąż nie pokrywa pliku
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 jest teraz aktywną stroną
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage bierze numer strony liczony od 1 i wymusza kolejność, jakiej oczekuje PDFium: przed pierwszą kontrolą strony biegnie CheckFormAvailability, które owija FPDFAvail_IsFormAvail, i dopiero potem woła FPDFAvail_IsPageAvail. Wynik pfaNotPresent to normalna odpowiedź dla dokumentu bez AcroForm i niczego nie blokuje. Gdy strona jest gotowa, LoadAvailablePage czyni ją aktywną stroną, więc przeglądarka może renderować stronę 1 zlinearyzowanej broszury, podczas gdy pozostałe strony są jeszcze w drodze; FirstAvailablePageNumber mówi, którą stronę słownik linearyzacji desygnuje jako pierwszą, już przeliczone z indeksu PDFium liczonego od zera
Co zwalnia CancelProgressiveLoad i w jakiej kolejności?
CancelProgressiveLoad rozbiera sesję w czterech krokach, których nie da się przestawić: anuluj scheduler zakresów, zamknij dokument, zniszcz uchwyt dostępności przez FPDFAvail_Destroy, potem zutylizuj rekordy callbacków i zwolnij adapter strumienia. Anulowanie schedulera najpierw podbija jego licznik generacji, zrzuca każde oczekujące i lecące żądanie i odpala OnCancelRequest dla każdego lecącego, więc dokończenie transportowe, które wyląduje później, niesie starą generację, a CompleteRequest zwraca False, niczego nie dotykając. Dokument musi się zamknąć, zanim odejdą uchwyt dostępności i adapter, bo PDFium może w trakcie zamykania dokumentu cofać się do dostawcy dostępu do pliku, a jeśli adapter już odszedł, ten callback czyta uwolnioną pamięć
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// Scheduler żyje tak długo jak FPdf, więc wpnij go raz
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // twój kod: zamknij to gniazdo albo żądanie
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
Metoda jest idempotentna i jest jedyną ścieżką sprzątania dla trzech sytuacji: BeginProgressiveLoad, które pada w połowie konstrukcji, jawnego anulowania przez użytkownika i destruktora. BeginProgressiveLoad woła ją też przed startem, więc ponowny start tego samego obiektu na nowym URL-u jest bezpieczny bez jawnego anulowania. Jedna decyzja o własności jest po tobie: jeśli wątek roboczy pisze do strumienia nośnego, podaj AOwnsStream = False i zwolnij strumień sam po zatrzymaniu workera, bo przy przekazanej własności cancel zwalnia strumień, podczas gdy spóźniony zapis może jeszcze być w drodze. Wyjątki podniesione wewnątrz OnCancelRequest są połykane per żądanie, żeby jedna padająca warstwa transportowa nie zablokowała pozostałych anulowań
Jak zestaw lifecycle dowodzi, że ścieżka cancelu nie cieka?
Zestaw przeciążeniowy lifecycle PDFium Component ćwiczy przerwane pobieranie w stylu sieciowym w każdym cyklu mieszanym. Każdy cykl startuje ładowanie przyrostowe, którego magazyn trzyma tylko kwartę bajtów fixtury, wymaga pdaNotAvailable z niepustą listą podpowiedzi, woła CancelProgressiveLoad i asercjonuje, że obiekt nie zgłasza ani ProgressiveLoading, ani Active; potem biegnie tę samą ścieżkę strumieniowania do końca z pełną dostępnością, OpenProgressiveDocument, renderem i zamknięciem. Domyślny bieg mieszany pokrywa 100 mierzonych cykli z 600 otwarciami, 2300 renderami i 100 anulowaniami przyrostowymi, a próbkowana pamięć prywatna urosła o 8,21 MiB przy budżecie 32 MiB. Zestaw liczy anulowania przyrostowe osobno od anulowań callbacków renderujących, bo przerwane pobieranie i pętla renderująca kończąca się wcześnie to różne zdarzenia z różnymi kryteriami akceptacji
Gdzie ścieżka przyrostowa przestaje pomagać
Kilka limitów warto znać, zanim zbudujesz na tym przeglądarkę. Funkcje potrzebujące oryginalnych bajtów pliku odmawiają niekompletnego źródła przyrostowego, zamiast zgadywać: ReadXmpPacket pada jawnie, a walidacja podpisów raportuje Indeterminate, dopóki cały plik nie jest obecny. Domyślny test dostępności zakłada ciągły prefiks, więc transport pobierający zakresy nie po kolei musi je dokańczać przez RangeRequests albo odpowiadać przez OnDataAvailable, inaczej PDFium będzie dalej pytał o bajty, które już masz. Niezlinearyzowany plik nie zyskuje nic w czasie do pierwszej strony, więc jeśli szybki pierwszy kadr się liczy, linearyzuj plik po stronie serwera. A CancelProgressiveLoad nie zamyka twoich gniazd sam z siebie; OnCancelRequest to haczyk, gdzie to się dzieje
Zwykłą ścieżką adaptera strumienia ładującą kompletny lokalny plik na żądanie opisuje strumieniowanie dużych PDF-ów na żądanie z PDFium; otwieranie PDF-a siedzącego w większym buforze opisuje ładowanie zakresów bajtów dla osadzonych PDF-ów. Anulowanie powolnego renderu strony już załadowanej to osobny mechanizm, opisany w anulowalnym przyrostowym renderowaniu stron. TPdfProgressiveDocument i jego scheduler zakresów płyną z PDFium Component dla Delphi i C++Buildera