Teknisk artikel

Progressiv nedladdning och avbrott i Delphi (FPDFAvail)

PDFium Component öppnar en PDF som fortfarande laddas ner genom TPdfProgressiveDocument, en TPdf-subklass som slår in PDFium:s FPDFAvail_*-tillgänglighets-API. BeginProgressiveLoad startar sessionen, CheckDocumentAvailability rapporterar vilka byteintervall PDFium fortfarande behöver, OpenProgressiveDocument öppnar filen när tillräckligt många byte finns, och CancelProgressiveLoad överger en avbruten nerladdning utan att läcka nativa handtag. Det svåra är inte lyckovägen. En viewer på en opålitlig förbindelse kommer att se användare stänga fliken vid 25 procent, ändra sig och öppna samma länk igen, och varenda en av de avbrutna sessionerna har ett nativt tillgänglighetshandtag, två C-callback-poster, en strömadapter och en uppsättning pågående intervallbegäranden som måste frigöras i exakt rätt ordning

Hur läser TPdfProgressiveDocument in en PDF som fortfarande laddas ner?

TPdfProgressiveDocument håller en PDFium-tillgänglighetsleverantör vid liv medan en slumpåtkomstström fylls, och frågar den leverantören före varje tolkningssteg om de byte den vill ha finns. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) tar backströmmen plus den logiska storleken på fjärrfilen, kopplar en IsDataAvail-callback och en AddSegment-callback till två poster och anropar FPDFAvail_Create. När PDFium frågar om ett intervall finns svarar komponenten ja om intervallet ligger inom det sammanhängande prefix som AvailableByteCount beskriver eller inom ett intervall som redan fullbordats genom RangeRequests-schemaläggaren, och eventen OnDataAvailable kan överträffa domen för glesa lager. Varje anrop av CheckDocumentAvailability returnerar ett av tre TPdfDataAvailability-värden (pdaAvailable, pdaNotAvailable, pdaError) och lämnar tillbaka de intervall PDFium bad om som en sorterad, sammanslagen TPdfDownloadRanges-array, redan köad på schemaläggaren med rrpImmediate-prioritet

// FetchRange är din transport (HTTP Range GET, socket, blob-läsare):
// den skriver Size byte vid Offset i Store och returnerar hur många som kom fram
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;
    // Hintarna är redan köade; skriv bytena först, fullborda sedan
    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;

Två detaljer i den loopen bär lasten. Varvtaket spelar roll för att en död länk får CheckDocumentAvailability att be om samma intervall för alltid, och en obegränsad loop gör ett nätverksfel till ett hängt UI. Ordningen spelar roll för att schemaläggaren serialiserar sitt eget tillstånd med ett kritiskt avsnitt men inte gör något för TStream.Position på backlagret: en transporttråd måste skriva svarsbytena i strömmen innan den anropar CompleteRequest, eftersom PDFium i samma stund som en fullbordan publiceras kan läsa det intervallet, och samtidiga skrivare behöver positionerad I/O eller ett eget lås

Tillgänglighetsloopen hos TPdfProgressiveDocument i PDFium Component: BeginProgressiveLoad skapar FPDFAvail-leverantören, CheckDocumentAvailability lämnar tillbaka sorterade sammanslagna nerladdningshintar köade med rrpImmediate-prioritet, transporten skriver byte i lagret innan CompleteRequest publicerar varje intervall till PDFium, och loopen är takad vid 64 varv eftersom en död länk fortsätter be om samma intervall
Skriv bytena, fullborda sedan begäran: i samma stund som en fullbordan publiceras kan PDFium läsa det intervallet, och ingenting skyddar strömpositionen åt dig

Varför vägrar AvailableByteCount att röra sig bakåt?

AvailableByteCount växer bara, och settern kastar EPdfError med "Antalet tillgängliga byte kan inte röra sig bakåt" när du försöker krympa den. När IsDataAvail-callbacken en gång talat om för PDFium att ett intervall finns, kan tolkaren redan ha läst och cachat objekt från det, så att återkalla de bytena efteråt skulle göra tillgänglighetssvaren inkonsekventa med vad PDFium redan konsumerat. Samma setter avvisar värden större än LogicalFileSize och kastar "Ingen progressiv inläsning är aktiv" utanför en session, vilket är därför byte du redan har innan inläsningen startar hör hemma i argumentet AInitialAvailableByteCount hos BeginProgressiveLoad i stället för i en egenskapstilldelning gjord för tidigt. Fylls ditt nerladdningslager i oordning, försök alls inte uttrycka det genom prefixet: fullborda intervallen genom schemaläggaren eller svara genom OnDataAvailable

När kan en delvis nerladdad PDF faktiskt öppnas?

Bara en lineariserad PDF (ISO 32000-1 Annex F, "Fast Web View"-layouten) öppnas innan hela filen anlänt; en icke-lineariserad PDF behöver fortfarande varje byte. OpenProgressiveDocument kontrollerar egenskapen Linearization (plnUnknown, plnNotLinearized, plnLinearized) och dirigerar därefter: en lineariserad fil öppnas genom FPDFAvail_GetDocument så snart förstasidesavsnittet och hinttabellerna finns, medan en icke-lineariserad fil öppnas genom FPDF_LoadCustomDocument på samma filåtkomstpost och behandlas som läsbar bara som helhet. Dirigeringen finns av ett konkret skäl. Att anropa FPDFAvail_GetDocument på en icke-lineariserad fil kan returnera ett icke-null-handtag vars sidantal är noll, ett dokument som ser öppet ut och är tomt. I komponentens egen testsvit når en 51-sidig lineariserad fixture pdaAvailable och öppnas med sitt fullständiga sidträd medan det glesa nerladdningslagret fortfarande inte täcker filen

Hur OpenProgressiveDocument dirigerar en partiell nerladdning i PDFium Component: en lineariserad fil öppnas genom FPDFAvail_GetDocument så snart förstasidesavsnittet och hinttabellerna anlänt, en icke-lineariserad fil behöver FPDF_LoadCustomDocument och varje byte, och LoadAvailablePage kontrollerar formulärtillgänglighet med FPDFAvail_IsFormAvail före sidkontrollen, vilket undviker fällan med icke-null-handtag och noll sidor
Bara lineariserade filer får försprånget; på allt annat kan FPDFAvail_GetDocument returnera ett öppet-seende dokument med noll sidor, vilket är exakt det dirigeringen förhindrar
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 är nu den aktiva sidan
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

LoadAvailablePage tar ett 1-baserat sidnummer och driver igenom den ordning PDFium förväntar sig: före den första sidkontrollen kör den CheckFormAvailability, som slår in FPDFAvail_IsFormAvail, och först därefter anropar den FPDFAvail_IsPageAvail. Ett resultat av pfaNotPresent är det normala svaret för ett dokument utan AcroForm och blockerar ingenting. När sidan är redo gör LoadAvailablePage den till aktiv sida, så att en viewer kan rita sida 1 av en lineariserad broschyr medan de återstående sidorna fortfarande är på väg; FirstAvailablePageNumber talar om vilken sida lineariseringsordboken utser till den första, redan omvandlad från PDFium:s nollbaserade index

Vad frigör CancelProgressiveLoad, och i vilken ordning?

CancelProgressiveLoad river en session i fyra steg som inte kan ordnas om: avbryt intervallschemaläggaren, stäng dokumentet, förstör tillgänglighetshandtaget med FPDFAvail_Destroy, och frigör sedan callback-posterna och strömadaptern. Att avbryta schemaläggaren först stegar dess generationsräknare, kastar varje väntande och pågående begäran och avfyrar OnCancelRequest för vart och ett av de pågående, så att en transportfullbordan som landar senare bär den gamla generationen och CompleteRequest returnerar False utan att röra något. Dokumentet måste stängas innan tillgänglighetshandtaget och adaptern försvinner, för PDFium kan anropa tillbaka in i filåtkomstleverantören medan det stänger ett dokument, och om adaptern redan är borta läser den callbacken frigjort minne

Den fasta rivningsordningen hos CancelProgressiveLoad i PDFium Component: avbryt intervallschemaläggaren först så att sena fullbordor träffar den stegda generationsräknaren och returnerar False, stäng dokumentet innan filåtkomstadaptern försvinner, förstör tillgänglighetshandtaget med FPDFAvail_Destroy, och först sedan frigör callback-posterna och strömadaptern
En enda idempotent metod städar upp en misslyckad start, ett användaravbrott och destruktorn lika; med en arbetstråd som skriver i lagret stannar strömägarskapet hos dig
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // Schemaläggaren lever lika länge som FPdf, så koppla den en gång
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // din kod: stäng den socketen eller begäran
end;

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

Metoden är idempotent och är den enda städvägen för tre situationer: en BeginProgressiveLoad som faller mitt i uppbyggnaden, ett explicit användaravbrott, och destruktorn. BeginProgressiveLoad anropar den också innan den startar, så att en omstart av samma objekt på en ny URL är säker utan explicit avbrott. Ett ägarskapsbeslut är ditt att få rätt: skriver en arbetstråd i backströmmen, skicka AOwnsStream = False och frigör strömmen själv efter att arbetaren stannat, för med ägarskapet överlämnat frigör avbrottet strömmen medan en sen skrivning fortfarande kan vara på väg. Undantag kastade inne i OnCancelRequest sväljs per begäran så att en fallerande transport inte kan blockera de återstående avbrotten

Hur bevisar livscykelsviten att avbrottsvägen inte läcker?

PDFium Components livscykelstressvit prövar en avbruten nätverksliknande nerladdning i varje blandat varv. Varje varv startar en progressiv inläsning vars lager håller bara en fjärdedel av fixture-bytena, kräver pdaNotAvailable med en icke-tom hintlista, anropar CancelProgressiveLoad och hävdar att objektet rapporterar vare sig ProgressiveLoading eller Active; därefter kör det samma strömningsväg till fullbordan med full tillgänglighet, OpenProgressiveDocument, en rendering och en stängning. Det blandade standardvarvet täcker 100 uppmätta varv med 600 öppningar, 2300 renderingar och 100 progressiva avbrott, och det sampade privata minnet växte med 8,21 MiB mot en budget på 32 MiB. Sviten räknar progressiva avbrott separat från render-callback-avbrott, för en avbruten nerladdning och en renderloop som stannar tidigt är olika händelser med olika godkännandekriterier

Där den progressiva vägen slutar hjälpa

Några gränser är värda att känna till innan du bygger en viewer ovanpå detta. Funktioner som behöver originalfilens byte avvisar en ofullständig progressiv källa i stället för att gissa: ReadXmpPacket misslyckas explicit och signaturvalideringen rapporterar Indeterminate tills hela filen finns. Standardtillgänglighetstestet antar ett sammanhängande prefix, så en transport som hämtar intervall i oordning måste fullborda dem genom RangeRequests eller svara genom OnDataAvailable, annars fortsätter PDFium fråga efter byte du redan har. En icke-lineariserad fil vinner ingenting i tid till första sidan, så om snabb första målning spelar roll, linearisera filen på serversidan. Och CancelProgressiveLoad stänger inte dina sockets av sig självt; OnCancelRequest är kroken där det sker

För den vanliga strömadaptervägen som läser in en komplett lokal fil på begäran, se strömning av stora PDF:er på begäran med PDFium; för att öppna en PDF som ligger inuti en större buffert, se byte-intervallinläsning för inbäddade PDF:er. Att avbryta en långsam rendering av en redan inläst sida är en separat mekanism, som tas upp i avbrytbar progressiv sidrendering. TPdfProgressiveDocument och dess intervallschemaläggare skeppas med PDFium Component för Delphi och C++Builder