Tekninen artikkeli

PDFium progressive download ja cancel Delphissä (FPDFAvail)

PDFium Component avaa vielä latautuessa olevan PDF:n TPdfProgressiveDocumentin kautta, TPdfin aliluokka joka käärii PDFiumin FPDFAvail_*-saatavuus-API:n. BeginProgressiveLoad käynnistää istunnon, CheckDocumentAvailability raportoi mitkä tavualueet PDFium vielä tarvitsee, OpenProgressiveDocument avaa tiedoston kun tarpeeksi tavuja on olemassa, ja CancelProgressiveLoad hylkää keskeytetyn latauksen vuotamatta natiiveja handlexia. Vaikea osa ei ole onnistumisen polku. Huonolla yhteydellä oleva katselin näkee käyttäjät sulkemassa välilehden 25 prosentissa, muuttamassa mielensä ja avaamassa saman linkin uudelleen, ja jokaisella kyseisistä keskeytetyistä istunnoista on natiivi saatavuushandle, kaksi C-callback-tietuetta, stream-sovitin ja joukko lennossa olevia aluepyyntöjä jotka on vapautettava täsmälleen oikeassa järjestyksessä

Miten TPdfProgressiveDocument lataa vielä latautuessa olevan PDF:n?

TPdfProgressiveDocument pitää PDFiumin saatavuustarjoajan hengissä kun satunnaislukustreamia täytetään, ja kysyy kyseiseltä tarjoajalta ennen jokaista jäsennysaskelta ovatko sen haluamat tavut läsnä. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) ottaa taustalle streamin plus etätiedoston loogisen koon, kytkee IsDataAvail-callbackin ja AddSegment-callbackin kahteen tietueeseen ja kutsuu FPDFAvail_Createia. Kun PDFium kysyy onko alue läsnä, komponentti vastaa kyllä jos alue sijaitsee AvailableByteCountin kuvaaman yhtenäisen etuliitteen sisällä tai alueen sisällä joka on jo viimeistelty RangeRequests-ajastimen kautta, ja OnDataAvailable-eventti voi kumota tuomion harvoille varastoille. Jokainen CheckDocumentAvailability-kutsu palauttaa yhden kolmesta TPdfDataAvailability-arvosta (pdaAvailable, pdaNotAvailable, pdaError) ja luovuttaa PDFiumin pyytämät alueet lajiteltuna, yhdistettynä TPdfDownloadRanges-taulukkona, jo jonossa ajastimella rrpImmediate-prioriteetilla

// FetchRange on sinun kuljetuksesi (HTTP Range GET, socket, blob-lukija):
// se kirjoittaa Size tavua Offsettiin Storeen ja palauttaa kuinka monta saapui
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;
    // Vihjeet ovat jo jonossa; kirjoita tavut ensin, viimeistele sitten
    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;

Kaksi yksityiskohtaa kyseisessä silmukassa kantaa kuormaa. Kierroskatto merkitsee koska kuollut linkki saa CheckDocumentAvailabilityin kysymään samoja alueita ikuisesti, ja rajaton silmukka kääntää verkkovirheen jumiutuneeksi käyttöliittymäksi. Järjestys merkitsee koska ajastin serialisoi oman tilansa critical sectionilla mutta ei tee mitään TStream.Positionille taustavarastossa: kuljetussäikeen on kirjoitettava vastauksen tavut streamiin ennen CompleteRequestin kutsumista, sillä sillä hetkellä kun viimeistely julkaistaan PDFium voi lukea kyseisen alueen, ja rinnakkaiset kirjoittajat tarvitsevat positionoitua I/O:a tai oman lukon

TPdfProgressiveDocumentin saatavuussilmukka PDFium Componentissa: BeginProgressiveLoad luo FPDFAvail-tarjoajan, CheckDocumentAvailability luovuttaa lajitellut yhdistetyt latausvihjeet jo rrpImmediate-prioriteetilla jonossa, kuljetus kirjoittaa tavut varastoon ennen kuin CompleteRequest julkaisee jokaisen alueen PDFiumille, ja silmukka on katto arvoon 64 kierrosta koska kuollut linkki kysyy samoja alueita yhä
Kirjoita tavut, viimeistele sitten pyyntö: sillä hetkellä kun viimeistely julkaistaan PDFium voi lukea kyseisen alueen, eikä mikään suojaa streamin positiotasi puolestasi

Miksi AvailableByteCount kieltäytyy liikkumasta taaksepäin?

AvailableByteCount vain kasvaa, ja setteri nostaa EPdfErrorin viestillä "Available byte count cannot move backwards" kun yrität kutistaa sitä. Kun IsDataAvail-callback on kertonut PDFiumille että alue on olemassa, jäsentin voi jo lukea ja välimuistittaa objekteja siitä, joten niiden tavujen peruuttaminen jälkikäteen tekisi saatavusvastauksista epäjohdonmukaisia siihen nähden mitä PDFium on jo kuluttanut. Sama setteri hylkää arvot jotka ovat suurempia kuin LogicalFileSize ja nostaa "No progressive load is active" -viestin istunnon ulkopuolella, mistä syystä tavut jotka jo pidät ennen latauksen alkamista kuuluvat BeginProgressiveLoadin AInitialAvailableByteCount-argumenttiin eikä liian aikaiseen tehtyyn property-sijoitukseen. Jos latausvarastosi täyttyy epäjärjestyksessä, älä yritä ilmaista sitä etuliitteen kautta lainkaan: viimeistele alueet ajastimen kautta tai vastaa OnDataAvailablein kautta

Milloin osittain ladattu PDF oikeasti avautuu?

Vain linearisoitu PDF (ISO 32000-1 Annex F, "Fast Web View" -asettelu) avautuu ennen kuin koko tiedosto on saapunut; ei-linearisoitu PDF tarvitsee yhä jokaisen tavun. OpenProgressiveDocument tarkistaa Linearization-propertyn (plnUnknown, plnNotLinearized, plnLinearized) ja reitittää sen mukaan: linearisoitu tiedosto avautuu FPDFAvail_GetDocumentin kautta heti kun ensimmäisen sivun osio ja vihjetaulukot ovat läsnä, kun taas ei-linearisoitu tiedosto avataan FPDF_LoadCustomDocumentilla saman tiedostokäsittelytietueen yli ja kohdellaan luettavana vain kokonaisuutena. Reititys on olemassa konkreettisesta syystä. FPDFAvail_GetDocumentin kutsuminen ei-linearisolulle tiedostolle voi palauttaa ei-null handlein jonka sivumäärä on nolla, dokumentti joka näyttää avautuneen ja on tyhjä. Komponentin omassa testisarjassa 51-sivuinen linearisoitu fixtuuri saavuttaa pdaAvailablein ja avautuu täydellisellä sivupuullaan kun harva latausvarasto ei yhä kata tiedostoa

Miten OpenProgressiveDocument reitittää osittaisen latauksen PDFium Componentissa: linearisoitu tiedosto avautuu FPDFAvail_GetDocumentilla heti kun ensimmäisen sivun osio ja vihjetaulukot saapuvat, ei-linearisoitu tiedosto tarvitsee FPDF_LoadCustomDocumentin ja jokaisen tavun, ja LoadAvailablePage tarkistaa lomakkeen saatavuuden FPDFAvail_IsFormAvaililla ennen sivutarkistusta välttäen ei-null nollan sivun handle-ansan
Vain linearisoidut tiedostot saavat etumatkan; kaikilla muilla FPDFAvail_GetDocument voi palauttaa avautuneelta näyttävän dokumentin nollalla sivulla, mikä on täsmälleen se minkä reititys estää
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 on nyt aktiivinen sivu
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

LoadAvailablePage ottaa 1-pohjaisen sivunumeron ja valvoo järjestystä jota PDFium odottaa: ennen ensimmäistä sivutarkistusta se ajaa CheckFormAvailabilityin, joka käärii FPDFAvail_IsFormAvailin, ja vasta sen jälkeen se kutsuu FPDFAvail_IsPageAvailia. Tulos pfaNotPresent on normaali vastaus dokumentille ilman AcroFormia eikä estä mitään. Kun sivu on valmis, LoadAvailablePage tekee siitä aktiivisen sivun, joten katselin voi renderöidä linearisoidun esitteen sivun 1 kun taas loput sivut ovat yhä matkalla; FirstAvailablePageNumber kertoo minkä sivun linearisaatiosanakirja nimeää ensimmäiseksi, jo konvertoituna PDFiumin nollapohjaisesta indeksistä

Mitä CancelProgressiveLoad vapauttaa, ja missä järjestyksessä?

CancelProgressiveLoad purkaa istunnon neljässä askeleessa joita ei voi järjestää uudelleen: peruuta alueajastin, sulje dokumentti, tuhoa saatavuushandle funktiolla FPDFAvail_Destroy, sitten hävitä callback-tietueet ja vapauta stream-sovitin. Ajastimen peruuttaminen ensimmäisenä nostaa sen generatiolaskuria, pudottaa jokaisen jonossa olevan ja lennossa olevan pyynnön, ja laukaisee OnCancelRequestin jokaiselle lennossa olevalle, joten myöhemmin laskeutuva kuljetusviimeistely kantaa vanhaa generatiota ja CompleteRequest palauttaa arvon False koskematta mihinkään. Dokumentin on suljettava ennen kuin saatavuushandle ja sovitin katoavat, koska PDFium voi kutsua takaisin tiedostokäsittelytietueeseen sulkiessaan dokumenttia, ja jos sovitin on jo poissa kyseinen callback lukee vapautettua muistia

CancelProgressiveLoadin kiinteä purkujärjestys PDFium Componentissa: peruuta alueajastin ensin joten myöhäiset viimeistelyt osuvat nostettuun generatiolaskuriin ja palauttavat False:n, sulje dokumentti ennen kuin tiedostokäsittelysovitin katoaa, tuhoa saatavuushandle funktiolla FPDFAvail_Destroy, ja vasta sitten hävitä callback-tietueet ja vapauta stream-sovitin
Yksi idempotentti metodi siivoaa epäonnistuneen alun, käyttäjän peruutuksen ja destruktorin samalla lailla; kun työskerrosäie kirjoittaa varastoon, streamin omistajuus jää sinulle
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // Ajastin elää yhtä kauan kuin FPdf, joten kytkä se kerran
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // sinun koodisi: sulje kyseinen socket tai pyyntö
end;

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

Metodi on idempotentti ja on yksi siivouspolku kolmelle tilanteelle: BeginProgressiveLoad joka epäonnistuu rakentamisen puolivälissä, eksplisiittinen käyttäjän peruutus ja destruktori. BeginProgressiveLoad kutsuu sitä myös ennen aloitusta, joten saman objektin uudelleenkäynnistäminen uudelle URLille on turvallista ilman eksplisiittistä peruutusta. Yksi omistajuuspäätös on sinun saatava oikein: jos työskerrosäie kirjoittaa taustalle streamiin, välitä AOwnsStream = False ja vapauta stream itse työskerrosäikeen pysähdyttyä, koska omistajuuden luovuttamisella peruutus vapauttaa streamin kun myöhäinen kirjoitus voi yhä olla matkalla. OnCancelRequestin sisällä nostetut poikkeukset niellään pyyntöä kohden joten yksi epäonnistuva kuljetus ei voi estää loput peruutukset

Miten lifecycle-testisarja todistaa ettei perumispolku vuoda?

PDFium Componentin lifecycle-stressisarja harjoittelee keskeytettyä verkkotyyppistä latausta jokaisella sekoitetulla kierroksella. Jokainen kierros käynnistää progressiivisen latauksen jonka varasto kantaa vain neljäsosan fixtuurin tavuista, vaatii pdaNotAvailablein ei-tyhjällä vihjelistalla, kutsuu CancelProgressiveLoadia ja assertoi että objekti raportoi ei ProgressiveLoadingia eikä Activea; se ajaa sitten saman streamauspolun loppuun täydellisellä saatavuudella, OpenProgressiveDocumentilla, renderöinnillä ja sulkemisella. Oletussekoitettu ajo kattaa 100 mitattua kierrosta 600 avauksella, 2300 renderöinnillä ja 100 progressiivisella peruutuksella, ja näytteistetty yksityinen muisti kasvoi 8,21 MiB 32 MiB:n budjettia vasten. Testisarja laskee progressiiviset peruutukset erikseen render-callback-peruutuksista, koska keskeytetty lataus ja varhain pysähtyvä renderöintisilmukka ovat eri tapahtumia erilaisilla hyväksymiskriteereillä

Missä progressiivinen polku lakkaa auttamasta

Muutama raja kannattaa tietää ennen kuin rakennat katselinta tämän päälle. Ominaisuudet jotka tarvitsevat alkuperäisen tiedoston tavut kieltäytyvät puutteellisesta progressiivisesta lähteestä sen sijaan että arvaisivat: ReadXmpPacket epäonnistuu eksplisiittisesti ja allekirjoitusvalidointi raportoi Indeterminaten kunnes koko tiedosto on läsnä. Oletussaatavuustesti olettaa yhtenäisen etuliitteen, joten kuljetus joka hakee alueita epäjärjestyksessä on viimeisteltävä ne RangeRequestsin kautta tai vastattava OnDataAvailablein kautta, tai PDFium kysyy yhä tavuja jotka jo pidät. Ei-linearisoitu tiedosto ei saa mitään etua ensimmäisen sivun aikaan, joten jos nopea ensimmäinen maalaus merkitsee, linearisoi tiedosto palvelinpuolella. Ja CancelProgressiveLoad ei sulje sockettejasi itse; OnCancelRequest on koukku jossa se tapahtuu

Tavalliselle stream-sovitinpolulle joka lataa kokonaisen paikallisen tiedoston tarpeen mukaan katso artikkeli suurten PDF:ien streamauksesta tarpeen mukaan PDFiumilla; isommassa puskurissa istuvan PDF:n avaamisesta katso artikkeli tavualueiden lataus upotetuille PDF:eille. Jo ladatun sivun hitaan renderöinnin peruuttaminen on erillinen mekanismi, jota käsittelee artikkeli perutettavissa oleva progressiivinen sivun renderöinti. TPdfProgressiveDocument ja sen alueajastin toimitetaan mukana PDFium Component for Delphi and C++Builder -paketissa