Il PDFium Component apre un PDF che sta ancora scaricando attraverso TPdfProgressiveDocument, una sottoclasse di TPdf che incapsula l'API di disponibilità FPDFAvail_* di PDFium. BeginProgressiveLoad avvia la sessione, CheckDocumentAvailability riferisce quali range di byte PDFium deve ancora avere, OpenProgressiveDocument apre il file una volta che ne esistono abbastanza, e CancelProgressiveLoad abbandona un download interrotto senza perdere handle nativi. La parte difficile non è il percorso felice. Un viewer su una connessione traballante vedrà utenti chiudere la scheda al 25 percento, ricredersi, e riaprire lo stesso link, e ognuna di quelle sessioni abortite ha un handle di disponibilità nativo, due record di callback C, un adattatore di stream e un set di richieste di range in volo che vanno rilasciati nell'ordine esatto giusto
Come carica TPdfProgressiveDocument un PDF che sta ancora scaricando?
TPdfProgressiveDocument tiene vivo un provider di disponibilità PDFium mentre uno stream a accesso casuale si riempie, e interroga quel provider prima di ogni passo di parsing su se i byte che vuole siano presenti. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) prende lo stream di supporto più la dimensione logica del file remoto, collega una callback IsDataAvail e una callback AddSegment a due record, e chiama FPDFAvail_Create. Quando PDFium chiede se un range è presente, il componente risponde sì se il range giace dentro il prefisso contiguo descritto da AvailableByteCount o dentro un range già completato attraverso lo scheduler RangeRequests, e l'evento OnDataAvailable può sovrascrivere il verdetto per store sparsi. Ogni chiamata a CheckDocumentAvailability restituisce uno dei tre valori TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) e rimette i range che PDFium ha chiesto come array TPdfDownloadRanges ordinato e fuso, già in coda sullo scheduler a priorità rrpImmediate
// FetchRange è il vostro trasporto (HTTP Range GET, socket, lettore blob):
// scrive Size byte a Offset dentro Store e restituisce quanti ne arrivano
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;
// Gli hint sono già in coda; scrivete prima i byte, poi completate
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;
Due dettagli in quel loop portano il carico. Il tetto dei round conta perché un link morto fa sì che CheckDocumentAvailability chieda per sempre gli stessi range, e un loop illimitato trasforma un fallimento di rete in una UI appesa. L'ordinamento conta perché lo scheduler serializza il proprio stato con una sezione critica ma non fa nulla per TStream.Position sullo store di supporto: un thread di trasporto deve scrivere i byte di risposta nello stream prima di chiamare CompleteRequest, dato che dal momento in cui un completamento è pubblicato PDFium può leggere quel range, e scrittori concorrenti hanno bisogno di I/O posizionale o di un lock proprio
Perché AvailableByteCount si rifiuta di andare all'indietro?
AvailableByteCount cresce solo, e il setter solleva EPdfError con "Available byte count cannot move backwards" quando provate a restringerlo. Una volta che la callback IsDataAvail ha detto a PDFium che un range esiste, il parser può aver già letto e messo in cache oggetti da esso, quindi ritirare quei byte dopo renderebbe le risposte di disponibilità incoerenti con ciò che PDFium ha già consumato. Lo stesso setter rifiuta valori più grandi di LogicalFileSize e solleva "No progressive load is active" fuori da una sessione, ecco perché i byte che già possedete prima che il caricamento inizi stanno nell'argomento AInitialAvailableByteCount di BeginProgressiveLoad invece che in un assegnamento di proprietà fatto troppo presto. Se il vostro store di download si riempie fuori ordine, non provate a esprimerlo attraverso il prefisso: completate i range tramite lo scheduler o rispondete tramite OnDataAvailable
Quando può aprirsi davvero un PDF scaricato parzialmente?
Solo un PDF linearizzato (ISO 32000-1 Annex F, il layout "Fast Web View") si apre prima che l'intero file sia arrivato; un PDF non linearizzato ha ancora bisogno di ogni byte. OpenProgressiveDocument controlla la proprietà Linearization (plnUnknown, plnNotLinearized, plnLinearized) e instrada di conseguenza: un file linearizzato si apre attraverso FPDFAvail_GetDocument appena la sezione della prima pagina e le tabelle di hint sono presenti, mentre un file non linearizzato si apre attraverso FPDF_LoadCustomDocument sullo stesso record di accesso al file ed è trattato come leggibile solo nel suo complesso. L'instradamento esiste per una ragione concreta. Chiamare FPDFAvail_GetDocument su un file non linearizzato può restituire un handle non nullo il cui conteggio pagine è zero, un documento che sembra aperto ed è vuoto. Nella suite di test del componente una fixture linearizzata di 51 pagine raggiunge pdaAvailable e si apre col suo albero pagine completo mentre lo store di download sparso ancora non copre il file
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 ora è la pagina attiva
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage prende un numero di pagina in base 1 e impone l'ordine che PDFium si aspetta: prima del primo controllo pagina esegue CheckFormAvailability, che incapsula FPDFAvail_IsFormAvail, e solo dopo chiama FPDFAvail_IsPageAvail. Un risultato pfaNotPresent è la risposta normale per un documento senza AcroForm e non blocca nulla. Quando la pagina è pronta, LoadAvailablePage la rende la pagina attiva, così un viewer può renderizzare la pagina 1 di un opuscolo linearizzato mentre le pagine restanti sono ancora in transito; FirstAvailablePageNumber vi dice quale pagina il dizionario di linearizzazione designa come prima, già convertito dall'indice zero-based di PDFium
Che cosa rilascia CancelProgressiveLoad, e in che ordine?
CancelProgressiveLoad smonta una sessione in quattro passi che non si possono riordinare: annullare lo scheduler dei range, chiudere il documento, distruggere l'handle di disponibilità con FPDFAvail_Destroy, poi smaltire i record di callback e liberare l'adattatore di stream. Annullare prima lo scheduler fa avanzare il suo contatore di generazione, butta via ogni richiesta in attesa e in volo, e lancia OnCancelRequest per ciascuna di quelle in volo, così un completamento del trasporto che arriva dopo porta la vecchia generazione e CompleteRequest restituisce False senza toccare nulla. Il documento deve chiudersi prima che l'handle di disponibilità e l'adattatore spariscano perché PDFium può richiamare il provider di accesso al file mentre chiude un documento, e se l'adattatore è già sparito quella callback legge memoria liberata
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// Lo scheduler vive quanto FPdf, quindi collegatelo una volta sola
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // codice vostro: chiudete quel socket o richiesta
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
Il metodo è idempotente ed è l'unico percorso di pulizia per tre situazioni: un BeginProgressiveLoad che fallisce a metà della costruzione, un cancel esplicito dell'utente, e il distruttore. BeginProgressiveLoad lo chiama anche prima di iniziare, quindi riavviare lo stesso oggetto su un nuovo URL è sicuro senza un cancel esplicito. Una decisione di ownership spetta a voi farla giusta: se un thread worker scrive nello stream di supporto, passate AOwnsStream = False e liberate lo stream voi stessi dopo che il worker si è fermato, perché con l'ownership consegnata il cancel libera lo stream mentre una scrittura tardiva può essere ancora in arrivo. Le eccezioni sollevate dentro OnCancelRequest vengono ingoiate per richiesta, così un trasporto che fallisce non può bloccare i restanti annullamenti
Come la suite del lifecycle prova che il percorso di cancel non perde?
La suite di stress del lifecycle del PDFium Component esercita un download interrotto in stile rete a ogni ciclo misto. Ogni ciclo avvia un caricamento progressivo il cui store contiene solo un quarto dei byte della fixture, esige pdaNotAvailable con una lista di hint non vuota, chiama CancelProgressiveLoad, e asserisce che l'oggetto non riferisce né ProgressiveLoading né Active; poi esegue lo stesso percorso di streaming fino alla fine con disponibilità completa, OpenProgressiveDocument, un render e una chiusura. Il run misto predefinito copre 100 cicli misurati con 600 aperture, 2300 render e 100 annullamenti progressivi, e la memoria privata campionata è cresciuta di 8,21 MiB su un budget di 32 MiB. La suite conta gli annullamenti progressivi separatamente dagli annullamenti delle callback di render, perché un download abortito e un loop di render che si ferma presto sono eventi diversi con criteri di accettazione diversi
Dove il percorso progressivo smette di aiutare
Qualche limite vale la pena conoscere prima di costruire un viewer sopra questo. Le funzionalità che hanno bisogno dei byte del file originale rifiutano una sorgente progressiva incompleta anziché indovinare: ReadXmpPacket fallisce esplicitamente e la validazione delle firme riferisce Indeterminate finché l'intero file non è presente. Il test di disponibilità predefinito assume un prefisso contiguo, quindi un trasporto che recupera i range fuori ordine deve completarli tramite RangeRequests o rispondere tramite OnDataAvailable, altrimenti PDFium continuerà a chiedere byte che già possedete. Un file non linearizzato non guadagna nulla nel tempo alla prima pagina, quindi se la prima verniciatura veloce conta, linearizzate il file lato server. E CancelProgressiveLoad non chiude i vostri socket da solo; OnCancelRequest è l'hook dove ciò accade
Per il percorso semplice dell'adattatore di stream che carica un file locale completo su richiesta, vedete streaming di PDF grandi su richiesta con PDFium; per aprire un PDF che sta dentro un buffer più grande, vedete il caricamento per byte range di PDF incorporati. Annullare un render lento di una pagina già caricata è un meccanismo separato, trattato in rendering progressivo di pagine annullabile. TPdfProgressiveDocument e il suo scheduler di range viaggiano con il PDFium Component for Delphi and C++Builder