Teknisk artikel

PDFium progressive download og cancel i Delphi (FPDFAvail)

PDFium Component åbner en PDF, der stadig downloader, gennem TPdfProgressiveDocument, en TPdf-subklasse, der wrapper PDFiums FPDFAvail_*-availability-API. BeginProgressiveLoad starter sessionen, CheckDocumentAvailability rapporterer, hvilke byte-ranges PDFium stadig mangler, OpenProgressiveDocument åbner filen, så snart der er nok bytes, og CancelProgressiveLoad opgiver en afbrudt download uden at lække native handles. Det svære er ikke glad-vejen. En viewer på en ustabil forbindelse oplever brugere, der lukker fanen ved 25 procent, ændrer mening og åbner samme link igen, og hver eneste af de afbrudte sessioner har et native availability-handle, to C-callback-records, en stream-adapter og et sæt in-flight range requests, der skal frigives i præcis den rigtige rækkefølge

Hvordan loader TPdfProgressiveDocument en PDF, der stadig downloader?

TPdfProgressiveDocument holder en PDFium availability-provider i live, mens en random-access-stream fyldes, og spørger provideren før hvert parse-trin, om de bytes, den vil have, er til stede. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) tager backing-streamen plus den logiske størrelse af fjernfilen, kobler en IsDataAvail-callback og en AddSegment-callback ind i to records og kalder FPDFAvail_Create. Når PDFium spørger, om et range er til stede, svarer komponenten ja, hvis range'et ligger inden for det kontinuerte prefix, som AvailableByteCount beskriver, eller inden for et range, der allerede er fuldført gennem RangeRequests-scheduleren, og OnDataAvailable-eventet kan overlade kendelsen for sparse stores. Hvert kald til CheckDocumentAvailability returnerer én af tre TPdfDataAvailability-værdier (pdaAvailable, pdaNotAvailable, pdaError) og giver de ranges, PDFium bad om, tilbage som et sorteret, fusioneret TPdfDownloadRanges-array, allerede sat i kø på scheduleren med rrpImmediate-prioritet

// FetchRange er din transport (HTTP Range GET, socket, blob reader):
// den skriver Size bytes ved Offset ind i Store og returnerer, hvor mange kom frem
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;
    // Hintsene er allerede i kø; skriv bytesene først, fuldfør derefter
    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;

To detaljer i den løkke er load-bærende. Runde-loftet betyder noget, fordi et dødt link får CheckDocumentAvailability til at bede om de samme ranges for evigt, og en ubegrænset løkke gør en netværksfejl til en hængende UI. Rækkefølgen betyder noget, fordi scheduleren serialiserer sin egen tilstand med en critical section, men ikke gør noget for TStream.Position på backing store: en transporttråd skal skrive svarbytesene ind i streamen, før den kalder CompleteRequest, for i det øjeblik en fuldførelse publiceres, kan PDFium læse det range, og samtidige writere behøver positioneret I/O eller en lås af deres egen

Availability-løkken i TPdfProgressiveDocument i PDFium Component: BeginProgressiveLoad opretter FPDFAvail-provideren, CheckDocumentAvailability giver sortererede, fusionerede download-hints tilbage, sat i kø med rrpImmediate-prioritet, transporten skriver bytes ind i store, før CompleteRequest publicerer hvert range til PDFium, og løkken er cap'et ved 64 runder, fordi et dødt link bliver ved med at bede om de samme ranges
Skriv bytesene, og fuldfør derefter requestet: i det øjeblik en fuldførelse publiceres, kan PDFium læse det range, og intet beskytter stream-positionen for dig

Hvorfor nægter AvailableByteCount at gå baglæns?

AvailableByteCount vokser kun, og setteren rejser en EPdfError med "Available byte count cannot move backwards", når du forsøger at krympe den. Når IsDataAvail-callbacken har fortalt PDFium, at et range findes, kan parseren allerede have læst og cachet objekter fra det, så at trække de bytes tilbage bagefter ville gøre availability-svarene inkonsistente med det, PDFium allerede har indtaget. Samme setter afviser værdier større end LogicalFileSize og rejser "No progressive load is active" uden for en session, hvilket er derfor, bytes, du allerede holder, før loadet starter, hører hjemme i AInitialAvailableByteCount-argumentet til BeginProgressiveLoad frem for i en egenskabstildeling lavet for tidligt. Fylder din download-store ude af rækkefølge, så forsøg ikke at udtrykke det gennem prefixet: fuldfør ranges gennem scheduleren, eller svar gennem OnDataAvailable

Hvornår kan en delvist downloadet PDF reelt åbnes?

Kun en lineariseret PDF (ISO 32000-1 Annex F, "Fast Web View"-layoutet) åbner, før hele filen er ankommet; en ikke-lineariseret PDF behøver stadig hver byte. OpenProgressiveDocument tjekker Linearization-egenskaben (plnUnknown, plnNotLinearized, plnLinearized) og ruter derefter: en lineariseret fil åbnes gennem FPDFAvail_GetDocument, så snart first-page-sektionen og hint-tabellerne er til stede, mens en ikke-lineariseret fil åbnes gennem FPDF_LoadCustomDocument på samme file-access-record og behandles som læsbar kun som helhed. Routingen findes af en konkret grund. At kalde FPDFAvail_GetDocument på en ikke-lineariseret fil kan returnere et non-null handle, hvis sideantal er nul — et dokument, der ser åbent ud og er tomt. I komponentens egen test-suite når en 51-siders lineariseret fixture pdaAvailable og åbner med sit fulde paginetræ, mens den sparse download-store stadig ikke dækker filen

Hvordan OpenProgressiveDocument ruter en delvis download i PDFium Component: en lineariseret fil åbnes gennem FPDFAvail_GetDocument, så snart first-page-sektionen og hint-tabellerne ankommer, en ikke-lineariseret fil behøver FPDF_LoadCustomDocument og hver byte, og LoadAvailablePage tjekker form-availability med FPDFAvail_IsFormAvail før side-tjekket, hvilket undgår non-null nul-side-handle-fælden
Kun lineariserede filer får et forspring; på alt andet kan FPDFAvail_GetDocument returnere et åbent-seende dokument med nul sider, hvilket er netop det, routingen forhindrer
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 er nu den aktive side
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

LoadAvailablePage tager et 1-baseret sidetal og håndhæver rækkefølgen, PDFium forventer: før det første side-tjek kører den CheckFormAvailability, som wrapper FPDFAvail_IsFormAvail, og først derefter kalder den FPDFAvail_IsPageAvail. Et resultat af pfaNotPresent er det normale svar for et dokument uden AcroForm og blokerer ingenting. Når siden er klar, gør LoadAvailablePage den til den aktive side, så en viewer kan rendere side 1 i en lineariseret brochure, mens de resterende sider stadig er undervejs; FirstAvailablePageNumber fortæller, hvilken side lineariseringsdictionaryen udpeger som den første, allerede konverteret fra PDFiums nul-baserede indeks

Hvad frigiver CancelProgressiveLoad, og i hvilken rækkefølge?

CancelProgressiveLoad river en session ned i fire trin, der ikke kan byttes om: annullér range-scheduleren, luk dokumentet, destrúr availability-handleet med FPDFAvail_Destroy, og disponér derefter callback-recordene og frigiv stream-adapteren. At annullere scheduleren først bumper dens generationstæller, dropper hvert pending- og in-flight-request og fyre OnCancelRequest for hvert in-flight-request, så en transportfuldførelse, der lander senere, bærer den gamle generation, og CompleteRequest returnerer False uden at røre noget. Dokumentet skal lukkes, før availability-handleet og adapteren forsvinder, fordi PDFium kan calle tilbage ind i file-access-provideren, mens det lukker et dokument, og hvis adapteren allerede er væk, læser callback frigjort hukommelse

Den faste teardown-rækkefølge for CancelProgressiveLoad i PDFium Component: annullér range-scheduleren først, så sene fuldførelser rammer den bumpede generationstæller og returnerer False, luk dokumentet, før file-access-adapteren forsvinder, destrúr availability-handleet med FPDFAvail_Destroy, og disponér først derefter callback-recordene og frigiv stream-adapteren
Én idempotent metode rydder op efter en fejlslagen start, en bruger-cancel og destruktoren alike; med en worker-tråd, der skriver i store, forbliver stream-ownership hos dig
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // Scheduleren lever så længe FPdf, så kobl den én gang
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // din kode: luk den socket eller request
end;

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

Metoden er idempotent og er den eneste cleanup-vej for tre situationer: en BeginProgressiveLoad, der fejler halvvejs gennem konstruktionen, en eksplicit bruger-cancel og destruktoren. BeginProgressiveLoad kalder den også, før den starter, så genstart af samme objekt på en ny URL er sikker uden en eksplicit cancel. Én ownership-beslutning er din at få rigtigt: skriver en worker-tråd ind i backing-streamen, så giv AOwnsStream = False og frigiv streamen selv, efter workeren er stoppet, for med ownership overdraget frigør cancel'en streamen, mens en sen skriv måske stadig er på vej. Exceptions rejst inde i OnCancelRequest opsluges pr. request, så én fejlende transport ikke kan blokere de resterende annulleringer

Hvordan beviser lifecycle-suiten, at cancel-vejen ikke lækker?

PDFium Components lifecycle stress-suite øver en afbrudt netværksagtig download på hver blandet cyklus. Hver cyklus starter et progressivt load, hvis store kun holder en fjerdedel af fixture-bytes, kræver pdaNotAvailable med en ikke-tom hint-liste, kalder CancelProgressiveLoad og assert, at objektet hverken rapporterer ProgressiveLoading eller Active; derefter kører den samme streaming-vej til fuldførelse med fuld availability, OpenProgressiveDocument, en rendering og en lukning. Det default blandede run dækker 100 målte cyklusser med 600 åbninger, 2300 renderinger og 100 progressive annulleringer, og samplet privat hukommelse voksede med 8,21 MiB mod et 32 MiB-budget. Suiten tæller progressive annulleringer separat fra render-callback-annulleringer, for en afbrudt download og en render-løkke, der stopper tidligt, er forskellige events med forskellige acceptkriterier

Hvor den progressive vej holder op med at hjælpe

Et par grænser er værd at kende, før du bygger en viewer oven på dette. Features, der behøver de originale file-bytes, afviser en ufuldstændig progressiv kilde i stedet for at gætte: ReadXmpPacket fejler eksplicit, og signaturvalidering rapporterer Indeterminate, til hele filen er til stede. Availability-testen antager et kontinuert prefix, så en transport, der henter ranges ude af rækkefølge, skal fuldføre dem gennem RangeRequests eller svare gennem OnDataAvailable, ellers bliver PDFium ved med at bede om bytes, du allerede holder. En ikke-lineariseret fil vinder ingenting i tid til første side, så hvis hurtig første painting betyder noget, linearisér filen på serversiden. Og CancelProgressiveLoad lukker ikke dine sockets af sig selv; OnCancelRequest er hooken, hvor det sker

For den almindelige stream-adapter-vej, der loader en komplet lokal fil on demand, se streaming large PDFs on demand with PDFium; for at åbne en PDF, der ligger inde i en større buffer, se byte range loading for embedded PDFs. At annullere en langsom rendering af en allerede loadet side er en separat mekanisme, gennemgået i cancellable progressive page rendering. TPdfProgressiveDocument og dets range-scheduler skibes med PDFium Component for Delphi and C++Builder