Techninis straipsnis

PDFium progressive download ir cancel Delphi (FPDFAvail)

PDFium Component atveria PDF, kuris vis dar atsisiunčiamas, per TPdfProgressiveDocument — TPdf palikuonį, apvelkantį PDFium FPDFAvail_* prieinamumo API. BeginProgressiveLoad pradeda seansą, CheckDocumentAvailability praneša, kokių baitų diapazonų PDFium dar reikia, OpenProgressiveDocument atveria failą, vos tik pakanka baitų, o CancelProgressiveLoad palieka nutrūkusį parsisiuntimą neišliejęs natyviųjų handle. Sunku nėra sėkmingasis kelias. Peržiūryklė svyravančio ryšio sąlygomis pamatys vartotojus užveriant kortelę ties 25 procentais, persigalvojant ir atveriant tą patį saitą iš naujo, ir kiekvienas toks nutrūkęs seansas turi natyvųjį prieinamumo handle, du C callback įrašus, srauto adapterį ir pakeliui esančių diapazono užklausų rinkinį, kurį reikia atlaisvinti lygiai teisinga tvarka

Kaip TPdfProgressiveDocument įkelia PDF, kuris vis dar atsisiunčiamas?

TPdfProgressiveDocument laiko PDFium prieinamumo tiekėją gyvą, kol atsitiktinės prieigos srautas pildomas, ir prieš kiekvieną išskaidymo žingsnį klausia to tiekėjo, ar jo norimi baitai yra. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) ima pagrindinį srautą plus nuotolinio failo loginį dydį, suveda IsDataAvail callback ir AddSegment callback į du įrašus ir kviečia FPDFAvail_Create. Kai PDFium klausia, ar diapazonas yra, komponentas atsako taip, jei diapazonas glūdi nuoseklioje AvailableByteCount apibrėžtoje priešdėlio dalyje arba diapazone, jau užbaigtame per RangeRequests planuotoją, o OnDataAvailable įvykis gali perrašyti verdiktą išretintoms saugykloms. Kiekvienas CheckDocumentAvailability kvietimas grąžina vieną iš trijų TPdfDataAvailability reikšmių (pdaAvailable, pdaNotAvailable, pdaError) ir perduoda diapazonus, kurių prašė PDFium, kaip surikiuotą, sujungtą TPdfDownloadRanges masyvą, jau eileje planuotoją ties rrpImmediate prioritetu

// FetchRange yra jūsų transportas (HTTP Range GET, socket, blob skaitytojas):
// įrašo Size baitus ties Offset į Store ir grąžina, kiek jų atkeliavo
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;
    // Užuominos jau eilėje; pirmiau parašykite baitus, tada užbaikite
    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;

Du dalykai tame cikle laiko visą konstrukciją. Rato riba svarbi, nes negyva nuoroda daro, jog CheckDocumentAvailability amžinai prašytų tų pačių diapazonų, o neribotas ciklas tinklo nesėkmę paverstų pakibusia naudotojo sąsaja. Tvarka svarbi, nes planuotojas savo būseną serijuoja critical section, bet už TStream.Position pagrindinėje saugykloje nedaro nieko: transporto gija privalo parašyti atsakymo baitus į srautą dar prieš kviesdama CompleteRequest, nes akimirksniu, kai užbaigimas paskelbtas, PDFium gali tą diapazoną skaityti, o lygiagretėms rašytojoms reikia pozicionuotos I/O ar savos spynos

TPdfProgressiveDocument prieinamumo ciklas PDFium Component: BeginProgressiveLoad sukuria FPDFAvail tiekėją, CheckDocumentAvailability perduoda surikiuotas sujungtas parsisiuntimo užuominas, eile statomas ties rrpImmediate prioritetu, transportas įrašo baitus į saugyklą dar prieš CompleteRequest paskelbdamas kiekvieną diapazoną PDFium, o ciklas apribotas 64 ratais, nes negyva nuoroda amžinai prašytų tų pačių diapazonų
Parašykite baitus, tada užbaikite užklausą: akimirksniu, kai užbaigimas paskelbtas, PDFium gali tą diapazoną skaityti, ir niekas už jus srauto pozicijos nesaugoja

Kodėl AvailableByteCount atsisako judėti atgal?

AvailableByteCount tik auga, o setteris kelia EPdfError su „Available byte count cannot move backwards“, kai bandote jį sumažinti. Kai IsDataAvail callback jau pasakė PDFium, jog diapazonas egzistuoja, parseris gali būti jau perskaitęs ir podėlyje laikantis iš jo objektus, tad tų baitų atsiėmimas vėliau padarytų prieinamumo atsakymus nesuderinamus su tuo, ką PDFium jau suvartojo. Tas pats setteris atmeta reikšmes, didesnes už LogicalFileSize, ir už seanso ribų kelia „No progressive load is active“ — todėl baitai, kuriuos jau laikote dar prieš įkėlimo startą, priklauso BeginProgressiveLoad argumentui AInitialAvailableByteCount, o ne per anksti atliekamam savybės priskirimui. Jei jūsų parsisiuntimo saugykla pildosi ne tvarka, visai nebandykite to reikšti per priešdėlį: užbaikite diapazonus per planuotoją ar atsakykite per OnDataAvailable

Kada iš dalies atsisiųstas PDF realiai gali atsiverti?

Tik linearizuotas PDF (ISO 32000-1 Annex F, „Fast Web View“ išdėstymas) atsiveria, kol neatkeliavęs visas failas; nelinerizuotam PDF vis dar reikia kiekvieno baito. OpenProgressiveDocument tikrina Linearization savybę (plnUnknown, plnNotLinearized, plnLinearized) ir nukreipia atitinkamai: linearizuotas failas atsiveria per FPDFAvail_GetDocument, vos tik yra pirmojo puslapio sekcija ir užuominų lentelės, o nelinerizuotas atveriamas per FPDF_LoadCustomDocument tame pačiame failo prieigos įraše ir laikomas skaitomu tik kaip visuma. Maršrutizavimas egzistuoja dėl konkrečios priežasties. FPDFAvail_GetDocument kvietimas ant nelinerizuoto failo gali grąžinti ne nulinį handle, kurio puslapių skaičius nulinis — dokumentą, atrodantį atvertą ir tuščią. Komponento paties testų rinkinyje 51 puslapio linearizuotas fixture pasiekia pdaAvailable ir atsiveria su pilnu puslapių medžiu, kol išretinta parsisiuntimo saugykla vis dar nedengia failo

Kaip OpenProgressiveDocument nukreipia iš dalies atsisiųstą PDF PDFium Component: linearizuotas failas atsiveria per FPDFAvail_GetDocument, kai atkeliavo pirmojo puslapio sekcija ir užuominų lentelės, nelinerizuotam reikia FPDF_LoadCustomDocument ir kiekvieno baito, o LoadAvailablePage formos prieinamumą tikrina su FPDFAvail_IsFormAvail dar prieš puslapio tikra, išvengdama ne nulinio nulinio puslapių skaičiaus handle spąstų
Iš ankstokės tik linearizuoti failai; bet kuo kita FPDFAvail_GetDocument gali grąžinti atversto atrodantį dokumentą su nuliu puslapių, ir būtent tai maršrutizavimas draudžia
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 dabar yra aktyvusis puslapis
      pdaError:
        Exit(False);
      pdaNotAvailable:
        while Pdf.RangeRequests.TryDequeue(Request) do
          Pdf.RangeRequests.CompleteRequest(Request,
            FetchRange(Store, Request.Offset, Request.Size));
    end;
end;

LoadAvailablePage ima puslapio numerį nuo 1 ir priverčia tvarką, kurios tikisi PDFium: prieš pirmąjį puslapio tikrą jis paleidžia CheckFormAvailability, apvelkantį FPDFAvail_IsFormAvail, ir tik po to kviečia FPDFAvail_IsPageAvail. pfaNotPresent rezultatas — normalus atsakymas dokumentui be AcroForm ir nieko neužstoja. Kai puslapis paruoštas, LoadAvailablePage padaro jį aktyviuoju, tad peržiūryklė gali atvaizduoti linearizuoto buklelio 1 puslapį, kol likusieji vis dar pakeliui; FirstAvailablePageNumber pasako, kurį puslapį linearizacijos žodynas skelbia pirmuoju, jau pavertus iš PDFium numeravimo nuo nulio

Ką CancelProgressiveLoad atlaisvina ir kokia tvarka?

CancelProgressiveLoad seansą išardo keturiais žingsniais, kurių permainti negalima: atšaukti diapazonų planuotoją, užverti dokumentą, sunaikinti prieinamumo handle su FPDFAvail_Destroy, paskui sudėti callback įrašus ir atlaisvinti srauto adapterį. Planuotojo atšaukimas pirmiausia pakelia jo kartos skaitliuką, numeta kiekvieną laukiančią ir pakeliui esančią užklausą ir kiekvienai pakeliui esančiai sužadina OnCancelRequest, tad transporto užbaigimas, atkeliavęs vėliau, neša senąją kartą, ir CompleteRequest grąžina False nieko neliečianti. Dokumentas privalo užsidaryti dar gyviems prieinamumo handle ir adapteriui, nes PDFium gali kviestis atgal į failo prieigos tiekėją užverdama dokumentą, ir, jei adapterio jau nebėra, tas callback skaito atlaisvintą atmintį

Fiksuota CancelProgressiveLoad išardymo tvarka PDFium Component: pirmiausia atšaukiamas diapazonų planuotojas, tad vėlieji užbaigimai krenta ant pakeltos kartos skaitliuko ir grąžina False, dokumentas užveriamas dar gyvam failo prieigos adapteriui, prieinamumo handle sunaikinamas su FPDFAvail_Destroy, ir tik tada sudedami callback įrašai bei atlaisvinamas srauto adapteris
Vienas idempotentiškas metodas sutvarko ir nesėkmingą startą, ir vartotojo atšaukimą, ir destruktorių; o su darbo gija, rašančia į saugyklą, srauto nuosavybė lieka jūsų
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // Scheduleris gyvuoja tiek, kiek FPdf, tad sujunkite jį vieną kartą
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // jūsų kodas: užverkite tą socket arba užklausą
end;

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

Metodas idempotentiškas ir yra vienintelis sutvarkymo kelias trims situacijoms: BeginProgressiveLoad, žlungančiam statybos viduryje, aiškiam vartotojo atšaukimui ir destruktoriui. BeginProgressiveLoad jį kviečia ir prieš startą, tad tą patį objektą paleisti iš naujo su nauju URL saugu be atskiros atšaukimo komandos. Vienas nuosavybės sprendimas — jūsų atsakomybė: jei darbo gija rašo į pagrindinį srautą, perduokite AOwnsStream = False ir srautą atlaisvinkite patys, po to, kai darbuotoja sustojo, nes su perduota nuosavybe cancel atlaisvina srautą, kol vėlyvas rašymas gali vis dar būti kelyje. OnCancelRequest viduje pakeltos išimtys prarandamos kiekvienai užklausai atskirai, tad viena gendanti transporto dalis negali užstoti likusių atšaukimų

Kaip lifecycle rinkinys įrodo, jog cancel kelias nenuotėkia?

PDFium Component lifecycle streso rinkinys kiekviename mišriame cikle išbando nutrūkusį tinklo stiliaus parsisiuntimą. Kiekvienas ciklas pradeda progressive įkėlimą, kurio saugykla laiko tik ketvirtadalį fixture baitų, reikalauja pdaNotAvailable su netuščiu užuominų sąrašu, kviečia CancelProgressiveLoad ir teigia, jog objektas nepraneša nei ProgressiveLoading, nei Active; paskui tas pats srautinimo kelias paleidžiamas iki galo su pilnu prieinamumu, OpenProgressiveDocument, atvaizdavimu ir užvėrimu. Numatytasis mišrus pravažiavimas dengia 100 išmatuotų ciklų su 600 atvėrimais, 2300 atvaizdavimais ir 100 progressive atšaukimų, ir išimtinės atminties įminiai augo 8.21 MiB prieš 32 MiB biudžetą. Rinkinys progressive atšaukimus skaičiuoja atskirai nuo atvaizdavimo callback atšaukimų, nes nutrūkęs parsisiuntimas ir anksti sustojanti atvaizdavimo grandinė — skirtingi įvykiai su skirtingais priėmimo kriterijais

Kur progressive kelias nustoja padėti

Kelios ribos vertos žinoti, dar prieš statant ant to peržiūryklę. Ypatybės, kurioms reikia originalo failo baitų, atsisako nepilnos progressive įvesties vietoj spėliojimų: ReadXmpPacket aiškiai nepavyksta, o parašų validacija praneša Indeterminate, kol visas failas nepatekęs. Numatytasis prieinamumo testas tiki nuosekliu priešdėliu, tad transportas, siunčiantis diapazonus ne tvarka, privalo juos užbaigti per RangeRequests ar atsakyti per OnDataAvailable, kitaip PDFium toliau prašys baitų, kuriuos jau laikote. Nelinerizuotas failas laimės nė nieko iki pirmojo puslapio, tad jei greita pirmoji piešimas svarbi, linearizuokite failą serveryje. O CancelProgressiveLoad pats savo socket neužveria; OnCancelRequest — ta vieta, kur tai daroma

Paprastajam srauto adapterio keliui, kuris paklaustas įkelia pilną vietinį failą, žr. kaip srautinti didelius PDF pagal pareikalavimą su PDFium; PDF atvėrimui, sėdinčiam didesniame buferyje, žr. baitų diapazonų įkėlimą įmontuotiems PDF. Lėto jau įkelto puslapio atvaizdavimo nutraukimas — atskiras mechanizmas, aprašytas atšaukiamame progressive puslapių atvaizdavime. TPdfProgressiveDocument ir jo diapazonų planuotojas keliauja kartu su PDFium Component for Delphi and C++Builder