Műszaki cikk

PDFium progresszív letöltés és megszakítás Delphiben

A PDFium Component megnyit egy PDF-et, amely még töltődik, a TPdfProgressiveDocument-ön át, egy olyan TPdf alosztályon, amely becsomagolja a PDFium FPDFAvail_* elérhetőségi API-ját. A BeginProgressiveLoad elindítja a szekciót, a CheckDocumentAvailability jelenti, mely bájttartományokra van még szüksége a PDFiumnak, az OpenProgressiveDocument megnyitja a fájlt, amint elég bájt létezik, a CancelProgressiveLoad pedig felad egy megszakított letöltést natív handelek szivárgása nélkül. A nehéz rész nem a boldog út. Egy billegő kapcsolat nézőjében a felhasználók bezárják a fület 25 százaléknál, meggondolják magukat, és újra megnyitják ugyanazt a linket, és mindegyik megszakított szekcióhoz tartozik egy natív elérhetőségi handle, két C callback rekord, egy stream adapter és egy repülésben lévő tartománykérés-halmaz, amelyeket pontosan a helyes sorrendben kell elengedni

Hogyan tölt be egy még töltődő PDF-et a TPdfProgressiveDocument?

A TPdfProgressiveDocument életben tart egy PDFium elérhetőségi szolgáltatót, miközben egy véletlen hozzáférésű stream telik, és minden elemzési lépés előtt megkérdezi tőle, hogy a kívánt bájtok megvannak-e. A BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) megkapja a támogató streamet plusz a távoli fájl logikai méretét, két rekordba beköti az IsDataAvail callbacket és az AddSegment callbacket, majd meghívja a FPDFAvail_Create-t. Amikor a PDFium megkérdezi, hogy egy tartomány megvan-e, a komponens akkor felel igennel, ha a tartomány az AvailableByteCount által leírt folytonos előtagban vagy a RangeRequests scheduléren át már befejezett tartományban fekszik, az OnDataAvailable esemény pedig ritka tárolóknál felülírhatja az ítéletet. Minden CheckDocumentAvailability hívás a három TPdfDataAvailability érték egyikét adja (pdaAvailable, pdaNotAvailable, pdaError), és visszaadja azokat a tartományokat, amelyeket a PDFium kért, rendezett, összevont TPdfDownloadRanges tömbként, már a schedulérre sorakoztatva rrpImmediate prioritással

// A FetchRange az Ön transzportja (HTTP Range GET, socket, blob-olvasó):
// az Offset pozícióra Size bájtot ír a Store-ba, és visszaadja, hány érkezett
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;
    // A hintek már sorban állnak; előbb írja a bájtokat, aztán fejezze be
    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;

Az a hurok két részlete teherbíró. A körplafon azért számít, mert egy halott hivatkozás miatt a CheckDocumentAvailability örökké ugyanazokat a tartományokat kéri, és a korlátlan hurok hálózati hibából lógó UI-t farag. A sorrend azért számít, mert a schedulér a saját állapotát kritikus szekcióval szerializálja, de a támogató tároló TStream.Position-jére semmit sem tesz: egy transzport szálnak a CompleteRequest hívása előtt kell a válaszbájtokat a streambe írnia, hiszen abban a pillanatban, amikor egy befejezés publikálódik, a PDFium olvashatja azt a tartományt, a párhuzamos íróknak pedig pozicionált I/O vagy saját zár kell

A TPdfProgressiveDocument elérhetőségi huroka a PDFium Componentben: a BeginProgressiveLoad létrehozza az FPDFAvail szolgáltatót, a CheckDocumentAvailability rendezett, összevont letöltés-hinteket ad vissza, rrpImmediate prioritással sorakoztatva, a transzport a CompleteRequest előtt írja a bájtokat a tárolóba, mielőtt minden tartományt publikálna a PDFiumnak, a hurok pedig 64 körön plafonozott, mert egy halott hivatkozás örökké ugyanazokat a tartományokat kéri
Írja a bájtokat, aztán fejezze be a kérést: abban a pillanatban, amikor egy befejezés publikálódik, a PDFium olvashatja azt a tartományt, és semmi sem védi meg Ön helyett a stream pozícióját

Miért tagadja meg az AvailableByteCount a visszafelé lépést?

Az AvailableByteCount csak nő, és a setter EPdfError-t dob „Available byte count cannot move backwards” üzenettel, ha zsugorítani próbálja. Miután az IsDataAvail callback azt mondta a PDFiumnak, hogy egy tartomány létezik, az elemző már olvashatta és cache-elhette onnan az objektumokat, így azoknak a bájtoknak az utólagos visszavonása a elérhetőségi válaszokat ellentmondóvá tenné azzal, amit a PDFium már elfogyasztott. Ugyanez a setter a LogicalFileSize-nál nagyobb értéket is elutasít, és szekción kívül „No progressive load is active”-t dob, ezért a betöltés indulása előtt már birtokolt bájtok a BeginProgressiveLoad AInitialAvailableByteCount argumentumába tartoznak, nem túl korai tulajdonság-hozzárendelésbe. Ha a letöltési tárolója sorrenden kívül telik, ne akarja azt az előtagon át kifejezni: fejezze be a tartományokat a scheduléren át, vagy válaszoljon az OnDataAvailable-on

Mikor nyílhat meg valójában egy részlegesen letöltött PDF?

Csak egy linearizált PDF (ISO 32000-1 F melléklet, a „Fast Web View” elrendezés) nyílik meg, mielőtt az egész fájl megérkezett; egy nem linearizált PDF-nek még minden bájtra szüksége van. Az OpenProgressiveDocument megnézi a Linearization tulajdonságot (plnUnknown, plnNotLinearized, plnLinearized), és ennek megfelelően irányít: egy linearizált fájl a FPDFAvail_GetDocument-ön át nyílik, amint az elsőoldal-szekció és a hint táblák megvannak, egy nem linearizált pedig a FPDF_LoadCustomDocument-ön át nyílik ugyanazon fájl-hozzáférési rekordon, és csak egészében kezelhető olvashatóként. Az irányítás konkrét okból létezik. A FPDFAvail_GetDocument hívása nem linearizált fájlon visszaadhat nem null handelt, amelynek oldalszáma nulla — olyan dokumentumot, amely nyitottnak néz ki, és üres. A komponens saját tesztcsomagjában egy 51 oldalas linearizált tesztállomány eléri a pdaAvailable-t, és teljes oldalafával nyílik meg, miközben a ritka letöltési tároló még mindig nem fedi le a fájlt

Hogyan irányítja az OpenProgressiveDocument a részleges letöltést a PDFium Componentben: egy linearizált fájl a FPDFAvail_GetDocument-ön át nyílik, amint az elsőoldal-szekció és a hint táblák megérkeznek, egy nem linearizáltnak FPDF_LoadCustomDocument kell és minden bájt, a LoadAvailablePage pedig FPDFAvail_IsFormAvail-lel ellenőrzi az űrlap elérhetőségét az oldal-ellenőrzés előtt, elkerülve a nem null, nulla oldalas handle csapdáját
Csak a linearizált fájlok szereznek előnyt; bármi másnál a FPDFAvail_GetDocument nyitottnak tűnő, nulla oldalas dokumentumot adhat vissza, pontosan ezt előzi meg az irányítás
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);   // a PageNumber immár az aktív oldal
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

A LoadAvailablePage 1-alapú oldalszámot kap, és kikényszeríti a PDFium által várt sorrendet: az első oldal-ellenőrzés előtt lefuttatja a CheckFormAvailability-t, amely becsomagolja a FPDFAvail_IsFormAvail-t, és csak ezután hívja a FPDFAvail_IsPageAvail-t. A pfaNotPresent eredmény a normál válasz egy AcroForm nélküli dokumentumra, és semmit sem blokkol. Amikor az oldal kész, a LoadAvailablePage aktív oldallá teszi, így egy néző megrajzolhatja egy linearizált brosúra 1. oldalát, miközben a többi oldal még úton van; a FirstAvailablePageNumber megmondja, melyik oldalt nevezi elsőnek a linearizációs szótár, a PDFium nullaalapú indexéből már átszámolva

Mit enged el a CancelProgressiveLoad, és milyen sorrendben?

A CancelProgressiveLoad négy lépésben bont le egy szekciót, amelyek nem cserélhetők fel: leállítja a tartomány-schedulért, bezárja a dokumentumot, a FPDFAvail_Destroy-szel megsemmisíti az elérhetőségi handelt, aztán elengedi a callback rekordokat, és felszabadítja a stream adaptert. A schedulér legelőbbi leállítása növeli a generációs számlálóját, eldobja minden függő és repülésben lévő kérését, mindegyik repülőre OnCancelRequest-et tüzel, így a később beérkező transzport-befejezés a régi generációt hordozza, és a CompleteRequest semmihez nem nyúlva False-ot ad. A dokumentumnak az elérhetőségi handle és az adapter eltűnése előtt kell bezáródnia, mert a PDFium dokumentumzárás közben visszahívhatja a fájl-hozzáférési szolgáltatót, és ha az adapter már nincs meg, az a callback felszabadított memóriába olvas

A CancelProgressiveLoad rögzített bontási sorrendje a PDFium Componentben: előbb állítsa le a tartomány-schedulért, hogy a késői befejezések a növelt generációs számlálóba ütközzenek és False-t adjanak, zárja be a dokumentumot, mielőtt a fájl-hozzáférési adapter eltűnik, semmisítse meg az elérhetőségi handelt a FPDFAvail_Destroy-szel, és csak azután engedje el a callback rekordokat, szabadítsa fel a stream adaptert
Egyetlen idempotens metódus tisztít fel egy félbeszakadt indítást, egy felhasználói leállítást és a destruktort egyaránt; ha munkaszál ír a tárolóba, a stream tulajdonlása Önnél marad
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // A schedulér addig él, amíg az FPdf, ezért egyszer kösse be
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // az Ön kódja: zárja be azt a socketet vagy kérést
end;

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

A metódus idempotens, és az egyetlen takarítási út három helyzethez: egy BeginProgressiveLoad-hoz, amely az építés felénél bukik el, egy explicit felhasználói leállításhoz és a destruktorhoz. A BeginProgressiveLoad indulás előtt maga is meghívja, így ugyanazon objektum újraindítása új URL-en explicit leállítás nélkül is biztonságos. Egy tulajdonlási döntést Önnek kell jól eltalálnia: ha munkaszál ír a támogató streambe, adjon át AOwnsStream = False-ot, és Ön szabadítsa fel a streamet, miután a munkaszál leállt, mert átadott tulajdonlással a leállítás felszabadítja a streamet, miközben egy késői írás még úton lehet. Az OnCancelRequest-en belül dobott kivételek kérésenként elnyelődnek, így egy elbukó transzport nem blokkolhatja a maradék leállításokat

Hogyan bizonyítja a lifecycle csomag, hogy a leállítási út nem szivárog?

A PDFium Component lifecycle stresszcsomagja minden vegyes cikluson gyakoroltat egy megszakított, hálózatjellegű letöltést. Minden ciklus elindít egy progresszív betöltést, amelynek tárolója a tesztállomány bájtjainak csak negyedét tartja, megköveteli a pdaNotAvailable-t nem üres hint listával, meghívja a CancelProgressiveLoad-ot, és azt állítja, hogy az objektum sem ProgressiveLoading-et, sem Active-ot nem jelent; majd ugyanazt a streamelési utat futtatja a végigs, teljes elérhetőséggel, OpenProgressiveDocument-tel, egy rajzolással és egy zárással. Az alapértelmezett vegyes futás 100 mért ciklust fed le 600 nyitással, 2300 rajzolással és 100 progresszív leállítással, a mintavételezett privát memória 32 MiB-es kerettel szemben 8,21 MiB-vel nőtt. A csomag a progresszív leállításokat elkülönítve számolja a render-callback leállításoktól, mert egy megszakított letöltés és egy korán leálló rajzolási hurok különböző események, eltérő elfogadási kritériumokkal

Ahol a progresszív út már nem segít

Pár korlátot érdemes ismerni, mielőtt erre nézőt épít. Azok a funkciók, amelyeknek az eredeti fájlbájtok kellenek, hiányos progresszív forrást utasítanak el találgatás helyett: a ReadXmpPacket explicit elbukik, az alágírás-ellenőrzés pedig Indeterminate-et jelent, amíg az egész fájl meg nem van. Az alapértelmezett elérhetőségi teszt folytonos előtagot feltételez, tehát egy sorrenden kívül töltő transzportnak a tartományokat a RangeRequests-en át kell befejeznie vagy az OnDataAvailable-on át kell válaszolnia, különben a PDFium tovább kéri a már birtokolt bájtokat. Egy nem linearizált fájl az első oldal idejében semmit sem nyer, tehát ha a gyors első rajzolás számít, linearizálja a fájlt szerveroldalon. A CancelProgressiveLoad pedig nem zárja be magától a socketjeit; az OnCancelRequest az a horog, ahol ez történik

A sima stream-adapteres úthoz, amely igény szerint tölt be teljes helyi fájlt, lásd a nagy PDF-ek igény szerinti streamelését PDFiummal; a nagyobb pufferen belül ülő PDF megnyitásához lásd a beágyazott PDF-ek bájttartomány-betöltését. Egy már betöltött oldal lassú rajzolásának leállítása külön mechanizmus, amelyet a megszakítható progresszív oldalrajzolás tárgyal. A TPdfProgressiveDocument és a tartomány-schedulérja a Delphihez és C++Builderhez készült PDFium Component része