Articol tehnic

Descărcare progresivă și anulare PDFium în Delphi

PDFium Component deschide un PDF aflat încă în plină descărcare prin TPdfProgressiveDocument, o subclasă TPdf care îmbracă API-ul de disponibilitate FPDFAvail_* din PDFium. BeginProgressiveLoad pornește sesiunea, CheckDocumentAvailability raportează ce intervale de byte-i mai are nevoie PDFium, OpenProgressiveDocument deschide fișierul de îndată ce există destui byte-i, iar CancelProgressiveLoad abandonează o descărcare întreruptă fără să scuipe handle-uri native. Partea grea nu e calea fericită. Un viewer pe o conexiune șovăielnică va vedea utilizatori care închid tab-ul la 25 la sută, se răzgândesc și redeschid același link, iar fiecare sesiune abandonată din acestea are un handle nativ de disponibilitate, două înregistrări de callback C, un adaptor de stream și un set de cereri de intervale în zbor care trebuie eliberate exact în ordinea corectă

Cum încarcă TPdfProgressiveDocument un PDF aflat încă în descărcare?

TPdfProgressiveDocument ține în viață un furnizor de disponibilitate PDFium cât timp un stream cu acces aleator se umple, și îl întreabă pe furnizor înainte de fiecare pas de parsare dacă byte-ii de care are nevoie sunt prezenți. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) ia stream-ul de suport plus mărimea logică a fișierului la distanță, leagă un callback IsDataAvail și un callback AddSegment în două înregistrări și apelează FPDFAvail_Create. Când PDFium întreabă dacă un interval e prezent, componenta răspunde da dacă intervalul se află în interiorul prefixului contiguu descris de AvailableByteCount sau în interiorul unui interval deja completat prin planificatorul RangeRequests, iar evenimentul OnDataAvailable poate suprascrie verdictul pentru magazine sparse. Fiecare apel al lui CheckDocumentAvailability întoarce una dintre cele trei valori TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) și pasează înapoi intervalele cerute de PDFium ca un tablou TPdfDownloadRanges sortat și îmbinat, deja pus în coadă pe planificator la prioritatea rrpImmediate

// FetchRange este transportul dumneavoastră (HTTP Range GET, socket, cititor blob):
// scrie Size byte-i la Offset în Store și întoarce câți au ajuns
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;
    // Indiciile sunt deja în coadă; scrieți mai întâi byte-i, apoi completați
    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;

Două detalii din bucla aceea țin structura în picioare. Plafonul de runde contează pentru că un link mort face CheckDocumentAvailability să ceară aceleași intervale la infinit, iar o buclă nelimitată transformă o defecțiune de rețea într-o interfață atârnată. Ordinea contează pentru că planificatorul își serializează propria stare cu o secțiune critică, dar nu face nimic pentru TStream.Position pe magazinele de suport: un thread de transport trebuie să scrie byte-i de răspuns în stream înainte să apeleze CompleteRequest, pentru că în clipa în care o completare e publicată PDFium poate citi intervalul acela, iar scriitorii concurenți au nevoie de I/O poziționat sau de un lacăt al lor

Bucla de disponibilitate a lui TPdfProgressiveDocument în PDFium Component: BeginProgressiveLoad creează furnizorul FPDFAvail, CheckDocumentAvailability pasează înapoi indicii de descărcare sortați și îmbinați puși în coadă la prioritatea rrpImmediate, transportul scrie byte-i în magazin înainte ca CompleteRequest să publice fiecare interval către PDFium, iar bucla e limitată la 64 de runde pentru că un link mort tot cere aceleași intervale
Scrieți byte-i, apoi completați cererea: în clipa în care o completare e publicată PDFium poate citi intervalul acela, și nimic nu vă protejează poziția în stream

De ce AvailableByteCount refuză să meargă înapoi?

AvailableByteCount doar crește, iar setter-ul ridică EPdfError cu „numărul de byte-i disponibili nu poate merge înapoi" când încercați să-l micșorați. Odată ce callback-ul IsDataAvail i-a spus lui PDFium că un interval există, parserul poate fi citit și cache-uit deja obiecte din el, deci retragerea byte-ilor aceia după aceea ar face răspunsurile de disponibilitate inconsecvente cu ce a consumat deja PDFium. Același setter respinge valorile mai mari decât LogicalFileSize și ridică „nicio încărcare progresivă nu e activă" în afara unei sesiuni, motiv pentru care byte-ii pe care îi dețineți deja înainte de pornirea încărcării aparțin argumentului AInitialAvailableByteCount al lui BeginProgressiveLoad, nu unei asignări de proprietate făcute prea devreme. Dacă magazinul de descărcare se umple dezordonat, nu încercați deloc să exprimați asta prin prefix: completați intervalele prin planificator sau răspundeți prin OnDataAvailable

Când se poate deschide efectiv un PDF descărcat parțial?

Doar un PDF liniarizat (Anexa F a ISO 32000-1, aranjamentul „Fast Web View") se deschide înainte ca tot fișierul să fi sosit; un PDF neliniarizat mai are nevoie de fiecare byte. OpenProgressiveDocument verifică proprietatea Linearization (plnUnknown, plnNotLinearized, plnLinearized) și o direcționează în consecință: un fișier liniarizat se deschide prin FPDFAvail_GetDocument de îndată ce secțiunea primei pagini și tabelele de indicii sunt prezente, în timp ce un fișier neliniarizat se deschide prin FPDF_LoadCustomDocument pe aceeași înregistrare de acces la fișier și e tratat ca lizibil doar ca tot. Direcționarea există dintr-un motiv concret. Apelarea lui FPDFAvail_GetDocument pe un fișier neliniarizat poate întoarce un handle non-null al cărui număr de pagini e zero, un document care pare deschis și e gol. În suita proprie de teste a componentei, o fixtură liniarizată de 51 de pagini ajunge la pdaAvailable și se deschide cu întregul ei arbore de pagini în timp ce magazinul de descărcare sparse tot nu acoperă fișierul

Cum direcționează OpenProgressiveDocument o descărcare parțială în PDFium Component: un fișier liniarizat se deschide prin FPDFAvail_GetDocument odată ce sosesc secțiunea primei pagini și tabelele de indicii, un fișier neliniarizat are nevoie de FPDF_LoadCustomDocument și de fiecare byte, iar LoadAvailablePage verifică disponibilitatea formularului cu FPDFAvail_IsFormAvail înainte de verificarea paginii, evitând capcana handle-ului non-null cu zero pagini
Doar fișierele liniarizate câștigă un avans; pe orice altceva FPDFAvail_GetDocument poate întoarce un document cu aspect deschis și zero pagini, exact ceea ce direcționarea previne
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 e acum pagina activă
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

LoadAvailablePage ia un număr de pagină cu bază 1 și impune ordinea la care PDFium se așteaptă: înainte de prima verificare de pagină rulează CheckFormAvailability, care îmbracă FPDFAvail_IsFormAvail, și abia apoi apelează FPDFAvail_IsPageAvail. Un rezultat pfaNotPresent e răspunsul normal pentru un document fără AcroForm și nu blochează nimic. Când pagina e gata, LoadAvailablePage o face pagina activă, deci un viewer poate randa pagina 1 a unei broșuri liniarizate în timp ce restul paginilor sunt încă pe drum; FirstAvailablePageNumber vă spune ce pagină desemnează dicționarul de liniarizare drept prima, deja convertită din indexul cu bază zero al lui PDFium

Ce eliberează CancelProgressiveLoad și în ce ordine?

CancelProgressiveLoad demontează o sesiune în patru pași care nu pot fi reordonați: anulați planificatorul de intervale, închideți documentul, distrugeți handle-ul de disponibilitate cu FPDFAvail_Destroy, apoi distrugeți înregistrările de callback și eliberați adaptorul de stream. Anularea planificatorului primul îi mărește contorul de generație, renunță la fiecare cerere în așteptare și în zbor și declanșează OnCancelRequest pentru fiecare dintre cele în zbor, deci o completare de transport care aterizează mai târziu poartă generația veche, iar CompleteRequest întoarce False fără să atingă nimic. Documentul trebuie închis înainte să dispară handle-ul de disponibilitate și adaptorul, pentru că PDFium poate apela înapoi în furnizorul de acces la fișier cât timp închide un document, iar dacă adaptorul a dispărut deja, callback-ul acela citește memorie eliberată

Ordinea fixă de demontare a lui CancelProgressiveLoad în PDFium Component: anulați mai întâi planificatorul de intervale astfel încât completările târzii să lovească contorul de generație mărit și să întoarcă False, închideți documentul înainte să dispară adaptorul de acces la fișier, distrugeți handle-ul de disponibilitate cu FPDFAvail_Destroy, și abia apoi distrugeți înregistrările de callback și eliberați adaptorul de stream
O singură metodă idempotentă curăță la fel un start eșuat, o anulare din partea utilizatorului și destructorul; cu un thread de lucru scriind în magazin, ownership-ul stream-ului rămâne la dumneavoastră
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // Planificatorul trăiește cât FPdf, deci legați-l o singură dată
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // codul dumneavoastră: închideți acel socket sau cerere
end;

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

Metoda e idempotentă și e singura cale de curățare pentru trei situații: un BeginProgressiveLoad care pică pe jumătatea construcției, o anulare explicită din partea utilizatorului și destructorul. BeginProgressiveLoad o apelează și înainte să pornească, deci repornirea aceluiași obiect pe un URL nou e sigură fără o anulare explicită. O decizie de ownership vă aparține să o rezolvați corect: dacă un thread de lucru scrie în stream-ul de suport, pasați AOwnsStream = False și eliberați stream-ul singuri după ce worker-ul s-a oprit, pentru că cu ownership-ul predat anularea eliberează stream-ul în timp ce o scriere târzie poate fi încă pe drum. Excepțiile ridicate în interiorul lui OnCancelRequest sunt înghițite per cerere, astfel încât un transport care pică nu poate bloca anulările rămase

Cum dovedește suita de ciclu de viață că calea de anulare nu scurge memorie?

Suita de stres de ciclu de viață a PDFium Component exercită o descărcare întreruptă în stil rețea la fiecare ciclu mixt. Fiecare ciclu pornește o încărcare progresivă al cărei magazin deține doar un sfert din byte-i fixturii, cere pdaNotAvailable cu o listă de indicii nevidă, apelează CancelProgressiveLoad și afirmă că obiectul nu raportează nici ProgressiveLoading, nici Active; apoi rulează aceeași cale de streaming până la capăt cu disponibilitate completă, OpenProgressiveDocument, o randare și o închidere. Rularea mixtă implicită acoperă 100 de cicluri măsurate cu 600 de deschideri, 2300 de randări și 100 de anulări progresive, iar memoria privată eșantionată a crescut cu 8,21 MiB contra unui buget de 32 MiB. Suita numără anulările progresive separat de anulările callback-ului de randare, pentru că o descărcare abandonată și o buclă de randare care se oprește devreme sunt evenimente diferite cu criterii de acceptare diferite

Unde calea progresivă încetează să mai ajute

Câteva limite merită cunoscute înainte să construiți un viewer peste asta. Funcțiile care au nevoie de byte-i originali ai fișierului refuză o sursă progresivă incompletă în loc să ghicească: ReadXmpPacket pică explicit, iar validarea semnăturilor raportează Indeterminate până e prezent tot fișierul. Testul de disponibilitate implicit presupune un prefix contiguu, deci un transport care aduce intervale dezordonat trebuie să le completeze prin RangeRequests sau să răspundă prin OnDataAvailable, altfel PDFium va tot cere byte-i pe care deja îi dețineți. Un fișier neliniarizat nu câștigă nimic la timpul până la prima pagină, deci dacă contează prima pictură rapidă, liniarizați fișierul pe partea de server. Iar CancelProgressiveLoad nu închide singur socket-urile dumneavoastră; OnCancelRequest e cârligul acolo unde asta se întâmplă

Pentru calea simplă cu adaptor de stream care încarcă la cerere un fișier local complet, vedeți streaming de PDF-uri mari la cerere cu PDFium; pentru deschiderea unui PDF aflat în interiorul unui buffer mai mare, vedeți încărcarea pe intervale de byte pentru PDF-uri înglobate. Anularea unei randări lente a unei pagini deja încărcate e un mecanism separat, acoperit în randare progresivă de pagini anulabilă. TPdfProgressiveDocument și planificatorul său de intervale sosesc cu PDFium Component pentru Delphi și C++Builder