De PDFium Component opent een PDF die nog aan het downloaden is via TPdfProgressiveDocument, een TPdf-subklasse die PDFium's FPDFAvail_*-availability-API omwikkelt. BeginProgressiveLoad start de sessie, CheckDocumentAvailability meldt welke bytebereiken PDFium nog nodig heeft, OpenProgressiveDocument opent het bestand zodra er genoeg bytes zijn, en CancelProgressiveLoad laat een afgebroken download vallen zonder native handles te lekken. Het moeilijke zit niet in het gunstige pad. Een viewer op een haperende verbinding ziet gebruikers de tab sluiten op 25 procent, zich bedenken, en dezelfde verbinding opnieuw openen, en elk van die afgebroken sessies heeft een native availability-handle, twee C-callback-records, een streamadapter en een set in-de-lucht-zittende range-verzoeken die in precies de juiste volgorde moet worden vrijgegeven
Hoe laadt TPdfProgressiveDocument een PDF die nog aan het downloaden is?
TPdfProgressiveDocument houdt een PDFium-availability-provider in leven terwijl een random-access-stream wordt gevuld, en vraagt die provider vóór elke parsestap of de bytes die hij wil aanwezig zijn. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) neemt de onderliggende stream plus de logische grootte van het externe bestand, zet een IsDataAvail-callback en een AddSegment-callback in twee records vast, en roept FPDFAvail_Create aan. Vraagt PDFium of een bereik aanwezig is, dan antwoordt de component ja als het bereik binnen de aaneengesloten prefix ligt die AvailableByteCount beschrijft of binnen een bereik dat al via de RangeRequests-scheduler is voltooid, en de OnDataAvailable-event kan het oordeel voor versnipperde opslagen overrulen. Elke aanroep van CheckDocumentAvailability geeft een van drie TPdfDataAvailability-waarden terug (pdaAvailable, pdaNotAvailable, pdaError) en overhandigt de bereiken die PDFium vroeg als een gesorteerde, samengevoegde TPdfDownloadRanges-array, al op de scheduler gequeud met rrpImmediate-prioriteit
// FetchRange is uw transport (HTTP Range GET, socket, blob-lezer):
// hij schrijft Size bytes op Offset in Store en geeft terug hoeveel aankwamen
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;
// De hints staan al in de wachtrij; schrijf eerst de bytes, voltooi daarna
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;
Twee details in die lus dragen lading. De rondelimiet is belangrijk omdat een dode verbinding CheckDocumentAvailability voor altijd dezelfde bereiken laat vragen, en een onbegrensde lus een netwerkfaling in een hangende UI verandert. De volgorde is belangrijk omdat de scheduler zijn eigen staat met een critical section serialiseert maar niets doet voor TStream.Position op de onderliggende opslag: een transportthread moet de antwoordbytes in de stream schrijven vóórdat hij CompleteRequest aanroept, want op het moment dat een voltooiing wordt gepubliceerd mag PDFium dat bereik lezen, en gelijktijdige schrijvers hebben gepositioneerde I/O of een eigen slot nodig
Waarom weigert AvailableByteCount achteruit te gaan?
AvailableByteCount groeit alleen, en de setter werpt EPdfError op met "Available byte count cannot move backwards" wanneer u probeert hem te verkleinen. Zodra de IsDataAvail-callback PDFium heeft verteld dat een bereik bestaat, heeft de parser er mogelijk al objecten uit gelezen en gecached, dus die bytes daarna intrekken zou de availability-antwoorden incongruent maken met wat PDFium al heeft geconsumeerd. Dezelfde setter wijst waarden groter dan LogicalFileSize af en werpt "No progressive load is active" op buiten een sessie, wat verklaart waarom bytes die u al heeft voordat de lading begint thuishoren in het AInitialAvailableByteCount-argument van BeginProgressiveLoad in plaats van in een te vroeg gedaan property-toewijzing. Vult uw downloadopslag zich uit de volgorde, probeer dat dan helemaal niet via de prefix uit te drukken: voltooi de bereiken via de scheduler of antwoord via OnDataAvailable
Wanneer opent een gedeeltelijk gedownloade PDF werkelijk?
Alleen een gelinieariseerde PDF (Annex F van ISO 32000-1, de "Fast Web View"-indeling) opent voordat het hele bestand is aangekomen; een niet-gelinieariseerde PDF heeft nog steeds elke byte nodig. OpenProgressiveDocument controleert de Linearization-eigenschap (plnUnknown, plnNotLinearized, plnLinearized) en routeert dienovereenkomstig: een gelinieariseerd bestand opent via FPDFAvail_GetDocument zodra de first-page-sectie en de hinttabellen aanwezig zijn, terwijl een niet-gelinieariseerd bestand via FPDF_LoadCustomDocument op hetzelfde file-access-record wordt geopend en alleen als geheel als leesbaar wordt behandeld. De routering bestaat om een concrete reden. FPDFAvail_GetDocument aanroepen op een niet-gelinieariseerd bestand kan een niet-null-handle opleveren waarvan het paginanaantal nul is, een document dat er open uitziet en leeg is. In de eigen testsuite van de component haalt een gelinieariseerde fixture van 51 pagina's pdaAvailable en opent met zijn volledige paginaboom terwijl de versnipperde downloadopslag het bestand nog niet eens dekt
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 is nu de actieve pagina
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage neemt een 1-based paginanummer en dwingt de volgorde af die PDFium verwacht: vóór de eerste paginacontrole draait hij CheckFormAvailability, dat FPDFAvail_IsFormAvail omwikkelt, en pas daarna roept hij FPDFAvail_IsPageAvail aan. Een uitslag van pfaNotPresent is het normale antwoord voor een document zonder AcroForm en blokkeert niets. Is de pagina klaar, dan maakt LoadAvailablePage haar de actieve pagina, dus een viewer kan pagina 1 van een gelinieariseerde brochure renderen terwijl de resterende pagina's nog onderweg zijn; FirstAvailablePageNumber vertelt u welke pagina de linearisatie-dictionary als eerste aanwijst, al omgezet vanuit de zero-based index van PDFium
Wat geeft CancelProgressiveLoad vrij, en in welke volgorde?
CancelProgressiveLoad sloopt een sessie in vier stappen die niet herordend kunnen worden: annuleer de range-scheduler, sluit het document, vernietig de availability-handle met FPDFAvail_Destroy, geef dan de callback-records prijs en maak de streamadapter vrij. De scheduler eerst annuleren verhoogt zijn generatieteller, laat elk wachtend en in-de-lucht-zittend verzoek vallen, en vuurt OnCancelRequest af voor elk in de lucht zittend, dus een transportvoltooiing die later landt draagt de oude generatie en CompleteRequest geeft False terug zonder iets aan te raken. Het document moet sluiten voordat de availability-handle en de adapter verdwijnen, omdat PDFium tijdens het sluiten van een document terug kan bellen naar de file-access-provider, en is de adapter al weg dan leest die callback vrijgegeven geheugen
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// De scheduler leeft zo lang als FPdf, dus sluit hem een keer aan
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // uw code: sluit die socket of dat verzoek
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
De methode is idempotent en is het enige opruimpad voor drie situaties: een BeginProgressiveLoad die halverwege de bouw faalt, een expliciete gebruikersannulering, en de destructor. BeginProgressiveLoad roept hem ook aan voordat hij start, dus hetzelfde object op een nieuwe URL herstarten is veilig zonder expliciete annulering. Eén eigendomsbeslissing moet u zelf goed doen: schrijft een workerthread in de onderliggende stream, geef dan AOwnsStream = False door en geef de stream zelf vrij nadat de worker is gestopt, want met overgedragen eigendom geeft de annulering de stream vrij terwijl er mogelijk nog een late schrijfactie onderweg is. Exceptions die binnen OnCancelRequest worden geworpen worden per verzoek ingeslikt, zodat één falend transport de resterende annuleringen niet kan blokkeren
Hoe bewijst de lifecycle-suite dat het annuleerpad niet lekt?
De lifecycle-stresstest van de PDFium Component onderwerpt elke gemengde cyclus aan een onderbroken download in netwerkstijl. Elke cyclus start een progressieve lading waarvan de opslag maar een kwart van de fixturebytes bevat, eist pdaNotAvailable met een niet-lege hintlijst, roept CancelProgressiveLoad aan, en eist dat het object noch ProgressiveLoading noch Active meldt; daarna draait hij hetzelfde streamingpad af tot voltooiing met volledige availability, OpenProgressiveDocument, een render en een sluiting. De standaard gemengde run omvat 100 gemeten cycli met 600 opens, 2300 renders en 100 progressieve annuleringen, en bemonsterd privé-geheugen groeide met 8,21 MiB tegen een budget van 32 MiB. De suite telt progressieve annuleringen apart van render-callback-annuleringen, want een afgebroken download en een renderlus die vroeg stopt zijn verschillende gebeurtenissen met verschillende acceptatiecriteria
Waar het progressieve pad ophoudt te helpen
Enkele grenzen zijn het weten waard voordat u er een viewer bovenop bouwt. Functies die de originele bestandsbytes nodig hebben weigeren een incompleten progressieve bron in plaats van te gokken: ReadXmpPacket faalt expliciet en handtekeningvalidatie meldt Indeterminate totdat het hele bestand aanwezig is. De standaard availability-toets veronderstelt een aaneengesloten prefix, dus een transport dat bereiken uit de volgorde ophaalt moet ze via RangeRequests voltooien of via OnDataAvailable antwoorden, anders blijft PDFium vragen om bytes die u al heeft. Een niet-gelinieariseerd bestand wint niets in tijd tot eerste pagina, dus als een snelle eerste opmaak ertoe doet, linieariseer het bestand dan aan de serverkant. En CancelProgressiveLoad sluit uw sockets niet vanzelf; OnCancelRequest is de haak waar dat gebeurt
Voor het gewone streamadapter-pad dat een compleet lokaal bestand op aanvraag laadt, zie grote PDF's op aanvraag streamen met PDFium; voor het openen van een PDF die in een grotere buffer zit, zie byte-bereik-lading voor embedded PDF's. Een trage render van een al geladen pagina annuleren is een apart mechanisme, behandeld in annuleerbare progressieve paginarendering. TPdfProgressiveDocument en zijn range-scheduler verschepen met de PDFium Component for Delphi and C++Builder