Le PDFium Component ouvre un PDF encore en cours de téléchargement via TPdfProgressiveDocument, une sous-classe de TPdf qui enveloppe l'API de disponibilité FPDFAvail_* de PDFium. BeginProgressiveLoad démarre la session, CheckDocumentAvailability rapporte quelles plages d'octets PDFium demande encore, OpenProgressiveDocument ouvre le fichier dès qu'il y a assez d'octets, et CancelProgressiveLoad abandonne un téléchargement interrompu sans fuir de handles natifs. La partie dure, ce n'est pas le chemin heureux. Une visionneuse sur une connexion capricieuse verra des utilisateurs fermer l'onglet à 25 pour cent, changer d'avis, et rouvrir le même lien, et chacune de ces sessions avortées possède un handle de disponibilité natif, deux enregistrements de callbacks C, un adaptateur de flux et un ensemble de requêtes de plages en vol qui doivent être libérés dans exactement le bon ordre
Comment TPdfProgressiveDocument charge-t-il un PDF encore en téléchargement ?
TPdfProgressiveDocument garde un fournisseur de disponibilité PDFium en vie pendant qu'un flux à accès aléatoire se remplit, et demande à ce fournisseur, avant chaque étape d'analyse, si les octets qu'il veut sont présents. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) prend le flux de support plus la taille logique du fichier distant, câble un callback IsDataAvail et un callback AddSegment dans deux enregistrements, et appelle FPDFAvail_Create. Quand PDFium demande si une plage est présente, le composant répond oui si la plage se trouve dans le préfixe contigu décrit par AvailableByteCount ou dans une plage déjà complétée par l'ordonnanceur RangeRequests, et l'événement OnDataAvailable peut passer outre le verdict pour des magasins clairsemés. Chaque appel à CheckDocumentAvailability renvoie l'une des trois valeurs TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) et rend les plages que PDFium a demandées sous forme d'un tableau TPdfDownloadRanges trié et fusionné, déjà mis en file sur l'ordonnanceur à la priorité rrpImmediate
// FetchRange est votre transport (HTTP Range GET, socket, lecteur de blob) :
// il écrit Size octets à Offset dans Store et renvoie combien sont arrivés
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;
// Les indications sont déjà en file ; écrivez d'abord les octets, puis complétez
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;
Deux détails de cette boucle portent la charge. Le plafond de tours compte parce qu'un lien mort fait demander à CheckDocumentAvailability les mêmes plages pour toujours, et une boucle non bornée transforme un échec réseau en interface figée. L'ordre compte parce que l'ordonnanceur sérialise son propre état avec une section critique mais ne fait rien pour TStream.Position sur le magasin de support : un thread de transport doit écrire les octets de réponse dans le flux avant d'appeler CompleteRequest, puisque dès qu'une complétion est publiée, PDFium peut lire cette plage, et des écrivains concurrents ont besoin d'E/S positionnées ou de leur propre verrou
Pourquoi AvailableByteCount refuse-t-il de reculer ?
AvailableByteCount ne fait que croître, et le setter lève EPdfError avec « le nombre d'octets disponibles ne peut pas reculer » quand vous essayez de le réduire. Une fois que le callback IsDataAvail a dit à PDFium qu'une plage existe, l'analyseur a peut-être déjà lu et mis en cache des objets depuis elle, donc retirer ces octets ensuite rendrait les réponses de disponibilité incohérentes avec ce que PDFium a déjà consommé. Le même setter rejette les valeurs plus grandes que LogicalFileSize et lève « aucun chargement progressif actif » hors session, voilà pourquoi les octets que vous détenez déjà avant le démarrage du chargement appartiennent à l'argument AInitialAvailableByteCount de BeginProgressiveLoad plutôt qu'à une assignation de propriété faite trop tôt. Si votre magasin de téléchargement se remplit dans le désordre, n'essayez pas de l'exprimer par le préfixe du tout : complétez les plages via l'ordonnanceur ou répondez via OnDataAvailable
Quand un PDF partiellement téléchargé peut-il réellement s'ouvrir ?
Seul un PDF linéarisé (Annexe F d'ISO 32000-1, la disposition « Fast Web View ») s'ouvre avant que tout le fichier ne soit arrivé ; un PDF non linéarisé a encore besoin de chaque octet. OpenProgressiveDocument vérifie la propriété Linearization (plnUnknown, plnNotLinearized, plnLinearized) et route en conséquence : un fichier linéarisé s'ouvre via FPDFAvail_GetDocument dès que la section de première page et les tables d'indications sont présentes, tandis qu'un fichier non linéarisé est ouvert via FPDF_LoadCustomDocument sur le même enregistrement d'accès fichier et traité comme lisible seulement en entier. Le routage existe pour une raison concrète. Appeler FPDFAvail_GetDocument sur un fichier non linéarisé peut renvoyer un handle non nul dont le nombre de pages est zéro, un document qui semble ouvert et est vide. Dans la suite de tests du composant lui-même, une fixture linéarisée de 51 pages atteint pdaAvailable et s'ouvre avec son arbre de pages complet tandis que le magasin de téléchargement clairsemé ne couvre toujours pas le fichier
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 est désormais la page active
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage prend un numéro de page en base 1 et impose l'ordre qu'attend PDFium : avant le premier contrôle de page, il exécute CheckFormAvailability, qui enveloppe FPDFAvail_IsFormAvail, et ce n'est qu'après qu'il appelle FPDFAvail_IsPageAvail. Un résultat pfaNotPresent est la réponse normale pour un document sans AcroForm et ne bloque rien. Quand la page est prête, LoadAvailablePage en fait la page active, si bien qu'une visionneuse peut rendre la page 1 d'une brochure linéarisée pendant que les pages restantes sont encore en transit ; FirstAvailablePageNumber vous dit quelle page le dictionnaire de linéarisation désigne comme première, déjà convertie depuis l'index en base 0 de PDFium
Que libère CancelProgressiveLoad, et dans quel ordre ?
CancelProgressiveLoad démonte une session en quatre étapes qui ne peuvent pas être réordonnées : annuler l'ordonnanceur de plages, fermer le document, détruire le handle de disponibilité avec FPDFAvail_Destroy, puis disposer les enregistrements de callbacks et libérer l'adaptateur de flux. Annuler l'ordonnanceur en premier incrémente son compteur de génération, jette chaque requête en attente et en vol, et déclenche OnCancelRequest pour chacune de celles en vol, si bien qu'une complétion de transport qui atterrit plus tard porte l'ancienne génération et CompleteRequest renvoie False sans rien toucher. Le document doit se fermer avant que le handle de disponibilité et l'adaptateur disparaissent, parce que PDFium peut rappeler le fournisseur d'accès fichier pendant qu'il ferme un document, et si l'adaptateur est déjà parti, ce callback lit de la mémoire libérée
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// L'ordonnanceur vit autant que FPdf, donc câblez-le une fois
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // votre code : fermez ce socket ou cette requête
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
La méthode est idempotente et est l'unique chemin de nettoyage pour trois situations : un BeginProgressiveLoad qui échoue à mi-construction, une annulation explicite de l'utilisateur, et le destructeur. BeginProgressiveLoad l'appelle aussi avant de démarrer, si bien que redémarrer le même objet sur une nouvelle URL est sûr sans annulation explicite. Une décision de propriété est à prendre correctement de votre côté : si un thread de travail écrit dans le flux de support, passez AOwnsStream = False et libérez le flux vous-même après que le travailleur s'est arrêté, parce qu'avec la propriété cédée, l'annulation libère le flux alors qu'une écriture tardive peut encore être en route. Les exceptions levées dans OnCancelRequest sont avalées par requête pour qu'un transport en échec ne puisse pas bloquer les annulations restantes
Comment la suite de cycle de vie prouve-t-elle que le chemin d'annulation ne fuit pas ?
La suite de stress de cycle de vie du PDFium Component exerce un téléchargement interrompu façon réseau à chaque cycle mixte. Chaque cycle démarre un chargement progressif dont le magasin ne détient qu'un quart des octets de la fixture, exige pdaNotAvailable avec une liste d'indications non vide, appelle CancelProgressiveLoad, et asserte que l'objet ne rapporte ni ProgressiveLoading ni Active ; il fait ensuite tourner le même chemin de streaming jusqu'au bout avec la disponibilité complète, OpenProgressiveDocument, un rendu et une fermeture. La course mixte par défaut couvre 100 cycles mesurés avec 600 ouvertures, 2300 rendus et 100 annulations progressives, et la mémoire privée échantillonnée a grandi de 8,21 Mio contre un budget de 32 Mio. La suite compte les annulations progressives séparément des annulations de callbacks de rendu, parce qu'un téléchargement avorté et une boucle de rendu qui s'arrête tôt sont des événements différents avec des critères d'acceptation différents
Là où le chemin progressif cesse d'aider
Quelques limites valent la peine d'être connues avant de construire une visionneuse là-dessus. Les fonctionnalités qui ont besoin des octets du fichier d'origine refusent une source progressive incomplète plutôt que de deviner : ReadXmpPacket échoue explicitement et la validation de signature rapporte Indeterminate tant que tout le fichier n'est pas présent. Le test de disponibilité par défaut suppose un préfixe contigu, si bien qu'un transport qui va chercher les plages dans le désordre doit les compléter via RangeRequests ou répondre via OnDataAvailable, sinon PDFium continuera de demander des octets que vous détenez déjà. Un fichier non linéarisé ne gagne rien en délai de première page, donc si le premier rendu rapide compte, linéarisez le fichier côté serveur. Et CancelProgressiveLoad ne ferme pas vos sockets tout seul ; OnCancelRequest est le crochet où cela se passe
Pour le chemin simple d'adaptateur de flux qui charge un fichier local complet à la demande, voyez le streaming de grands PDF à la demande avec PDFium ; pour ouvrir un PDF logé dans un tampon plus grand, voyez le chargement par plages d'octets pour les PDF embarqués. Annuler un rendu lent d'une page déjà chargée est un mécanisme séparé, couvert dans le rendu progressif de pages annulable. TPdfProgressiveDocument et son ordonnanceur de plages sont livrés avec le PDFium Component pour Delphi et C++Builder