Techninis straipsnis

Atšaukiamas laipsniškas PDF atvaizdavimas Delphi (PDFium)

Dauguma PDF puslapių rasterizuojami per kelias milisekundes, ir jūs apie tai net nesusimąstote. Tada naudotojas atveria A1 formato inžinerinį brėžinį, puslapį, prigrūstą dešimčių tūkstančių vektorinių brūkšnių, arba plakatą, apkrautą skaidrumo grupėmis ir minkštomis kaukėmis, ir vienas jį piešiantis iškvietimas užtrunka dvi ar tris sekundes. Jei tas iškvietimas vyksta sąsajos gijoje, langas nustoja persipiešti, antraštės juosta pilkėja, o operacinė sistema pasiūlo aplikaciją nutraukti. Darbas yra teisėtas. Puslapiui tikrai reikia tiek laiko. Defektas yra tas, kad atvaizdavimas yra vienas nedalomas blokuojantis iškvietimas be galimybės išnirti kvėptelėti ir be galimybės sustoti

Šis straipsnis yra būtent apie vieną iš tų dviejų problemų: kaip atšaukti ilgą vieno puslapio atvaizdavimą neužšaldant sąsajos. Naudotojas spustelėjo kitą puslapį, pakeitė mastelį arba užvėrė dokumentą, ir vykstantis atvaizdavimas dabar yra iššvaistytas darbas, kuris turėtų baigtis pirma proga, o ne bėgti iki pabaigos. Slinkties ir mastelio glotninimas podėliuojant tai, kas jau rasterizuota, yra atskiras rūpestis su savo sumanymu, aprašytas giminingame straipsnyje, į kurį nurodoma pabaigoje. Čia vienintelis klausimas yra, kaip priversti vieną laipsnišką atvaizdavimą greitai ir švariai atsakyti į atšaukimo prašymą

Laipsniško atvaizdavimo API, kurį PDFium jau turi

PDFium numatė užšalimo problemos pusę. Greta vienkartinio FPDF_RenderPageBitmap jis atskleidžia laipsnišką variantą, kuris puslapį suskaido į darbo gabalus. Vieną kartą iškviečiate FPDF_RenderPageBitmap_Start, kad paruoštumėte atvaizdavimą į paskirties rastrą, o tada pakartotinai kviečiate FPDF_RenderPage_Continue. Kiekvienas Continue rasterizuoja ribotą atkarpą ir grąžina būseną. FPDF_RENDER_TOBECONTINUED reiškia, kad dar liko darbo, FPDF_RENDER_DONE reiškia, kad puslapis baigtas, o FPDF_RENDER_FAILED reiškia, kad jis sustojo dėl klaidos. Ciklui pasibaigus, kviečiate FPDF_RenderPage_Close, kad atlaisvintumėte puslapio laipsniško atvaizdavimo būseną. Kadangi tarp atkarpų valdymas grįžta į jūsų kodą, galite pumpuoti pranešimus, atnaujinti eigos rodiklį arba patikrinti, ar darbas vis dar reikalingas

PDFium laipsniško atvaizdavimo ciklo diagrama su FPDF_RenderPageBitmap_Start, pakartotiniais FPDF_RenderPage_Continue iškvietimais ir privalomu FPDF_RenderPage_Close Delphi aplinkoje
Kiekvienas Continue rasterizuoja vieną ribotą atkarpą ir grąžina būseną, todėl tarp atkarpų valdymas grįžta į Delphi kodą, o Close atlaisvina laipsnišką būseną kiekviename išėjimo kelyje

Mechanizmas, kurį PDFium siūlo sprendžiant, kada nusileisti, yra atgalinio kvietimo struktūra pavadinimu IFSDK_PAUSE. Ją paduodate į Start ir į kiekvieną Continue. Po kiekvieno gabalo PDFium iškviečia jos NeedToPauseNow funkcijos rodyklę, ir jei ši grąžina ne nulinę reikšmę, dabartinis Continue sustoja anksčiau ir grąžina valdymą su FPDF_RENDER_TOBECONTINUED. Struktūra taip pat neša lauką version, kuris turi būti nustatytas į 1, ir laisvos formos rodyklę user, kurios PDFium niekada neliečia ir perduoda nepakeistą. Būtent ta nepaliesta rodyklė yra visas toliau aprašomo sumanymo vyris

Pauzės pritaikymas atšaukimui

Pirminė NeedToPauseNow paskirtis yra laiko dalijimas. Grąžinkite ne nulį, kai jūsų kadro biudžetas išnaudotas, grąžinkite nulį, kad atvaizdavimas tęstųsi, ir PDFium pristabdys, kad prieš tęsdami tą patį atvaizdavimą galėtumėte nuveikti ką kita. PDFium Component tą patį signalą panaudoja kitam veiksmažodžiui. Užuot atsakęs „ar turėčiau pristabdyti ir leisti tau tęsti“, atgalinis kvietimas atsako „ar šis darbas buvo atšauktas“. Abu dalykai gražiai susiveda dėl to, ką ciklas daro pamatęs vėliavėlę. Tikra pauzė tikisi vėlesnio Continue; atšaukimas — ne. Kai iškviečiantis ciklas pamato, kad žetonas atšauktas, jis užveria atvaizdavimo kontekstą ir Continue daugiau nebekviečia, todėl ta pati ne nulinė grąža, kurią PDFium skaito kaip „nutrauk šį gabalą“, virsta „nutrauk visam laikui“

Atšaukimas išreiškiamas per sąsają IPdfCancellationToken, kurios savybė IsCancelled iš false virsta true, kai kuri nors kita programos dalis paprašo atvaizdavimą stabdyti. Tiltas tarp tos Pascal sąsajos ir PDFium C atgalinio kvietimo yra viena rodyklė. Žetono sąsajos nuoroda įrašoma į IFSDK_PAUSE.user, o statinis cdecl atgalinis kvietimas ją nuskaito atgal ir apklausia. Tai klasikinė problema, kai C bibliotekai leidžiama kviesti atgal į Pascal: atgalinis kvietimas turi būti paprasta funkcija su C iškvietimo konvencija, o ne metodas, nes PDFium saugo ir iškviečia grynąją funkcijos rodyklę, kuri nieko nežino nei apie Pascal objektus, nei apie Self

IFSDK_PAUSE tilto diagrama, leidžianti Delphi atšaukimo žetonui atsakyti į PDFium NeedToPauseNow atgalinį kvietimą per nepaliestą user rodyklę
Įrašas laiko ir grynąją user rodyklę, kurią skaito PDFium, ir skaičiuojamą sąsajos nuorodą, kuri palaiko žetoną gyvą, todėl ne nulinė grąža leidžia ciklui užverti atvaizdavimą ir sustoti visam laikui
type
  TPdfProgressivePause = record
    Pause: IFSDK_PAUSE;            // tai skaito PDFium; .user laiko žetoną
    Token: IPdfCancellationToken; // stipri nuoroda palaiko žetoną gyvą
  end;

function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
  Token: IPdfCancellationToken;
begin
  Result := 0;
  if (pThis = nil) or (pThis^.user = nil) then
    Exit;
  Token := IPdfCancellationToken(pThis^.user);
  if Token.IsCancelled then
    Result := 1; // ne nulis: PDFium nutraukia šį gabalą
end;

Atgalinis kvietimas žetoną atgauna keisdamas pThis^.user tipą atgal į sąsajos tipą ir nuskaito IsCancelled. Niekas jame neišskirsto atminties, nerakina ir neblokuoja, o tai svarbu, nes PDFium jį kviečia atvaizdavimo gijoje po kiekvieno gabalo ir bet koks čia atliktas darbas prisideda prie paties atvaizdavimo kainos. Apsauga nuo tuščios struktūros ar tuščio user lauko reiškia, kad tą pačią funkciją saugu įdiegti net ir atvaizdavimui, kuriam tikras žetonas niekada nebuvo duotas

Žetono palaikymas gyvo per visą ciklą

Sąsajos rodyklės keitimas per grynąjį Pointer ir atgal yra ta vieta, kur gimsta gyvavimo trukmės klaidos. IInterface Delphi aplinkoje turi nuorodų skaitiklį, o skaitiklis juda tik tada, kai kompiliatorius mato, kad priskiriamas sąsajos tipo kintamasis. Laikant žetoną vien kaip grynąją rodyklę IFSDK_PAUSE.user viduje, jis nuo nuorodų skaitiklio būtų visiškai paslėptas. Jei vienintelė kita nuoroda į tą žetoną išeitų iš srities dar veikiant Continue ciklui, objektas būtų atlaisvintas po atgalinio kvietimo kojomis, ir kitas gabalas kreiptųsi į pakibusią rodyklę

Būtent todėl aprašas yra įrašas, laikantis du dalykus, o ne vieną. Laukas Pause yra struktūra, kurią skaito PDFium. Laukas Token yra tikra sąsajos tipo nuoroda, kurią skaičiuoja kompiliatorius, ir ji egzistuoja vien tam, kad prismeigtų žetoną atmintyje tol, kol gyvuoja įrašas. Įrašas yra vietinis kintamasis atvaizdavimo procedūros dėkle, todėl jis lieka galiojantis visą ciklo laiką ir sunaikinamas tik procedūrai išeinant. Gryna rodyklė user lauke ir skaičiuojama nuoroda Token lauke įvardija tą patį objektą; viena yra tai, ką gali skaityti PDFium, kita yra tai, kas neleidžia tam objektui būti surinktam

var
  Pause: TPdfProgressivePause;
  EffectiveToken: IPdfCancellationToken;
begin
  // ... pasirinkti EffectiveToken ...

  // Pirma stipri nuoroda, tada tas pats objektas paskelbiamas PDFium per .user.
  Pause.Token := EffectiveToken;
  Pause.Pause.version := 1;
  Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
  Pause.Pause.user := Pointer(EffectiveToken);

Atvaizdavimo konteksto užvėrimas nesvarbu, kaip ciklas baigtųsi

Kiekvienas FPDF_RenderPageBitmap_Start iškvietimas išskiria laipsnišką būseną, kurią PDFium susieja su puslapiu, ir ta būsena atlaisvinama tik per FPDF_RenderPage_Close. Iš valdančiojo ciklo yra trys išėjimai. Puslapis baigiamas ir paskutinė būsena yra FPDF_RENDER_DONE. Žetonas suveikia ir ciklas baigiasi anksti pranešdamas apie atšaukimą. Kas nors sugenda ir būsena yra FPDF_RENDER_FAILED. Visi trys privalo iškviesti Close, o atšaukimo kelyje suklysti lengviausia, nes natūrali forma „pamatei atšaukimą, iššok“ pakeliui į išėjimą linkusi praleisti valymą. Nepasiektas Close palieka nutekėjusią puslapio būseną, o peržiūros programa, leidžianti naudotojui atšaukti atvaizdavimą vieną po kito, tokį nutekėjimą kauptų kiekviename nutrauktame puslapyje

Tvirta forma ciklą ir rezultato klasifikavimą deda į try, o FPDF_RenderPage_Close — į atitinkamą finally. Paskirties rastras sunaikinamas tame pačiame bloke. Atšaukimas gali palikti ciklą per ankstyvą Exit, o finally vis tiek įvykdomas, todėl yra lygiai viena vieta, kuri atlaisvina laipsnišką būseną, ir jos apeiti neįmanoma

Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
  Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
  while Status = FPDF_RENDER_TOBECONTINUED do
  begin
    if EffectiveToken.IsCancelled then
    begin
      Result := prsCancelled;
      Exit;
    end;
    Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
  end;

  if EffectiveToken.IsCancelled then
    Result := prsCancelled
  else if Status = FPDF_RENDER_DONE then
    Result := prsDone
  else
    Result := prsFailed;
finally
  // Atlaisvina laipsnišką būseną, kurią išskyrė Start; privaloma kiekviename kelyje.
  FPDF_RenderPage_Close(FPage);
  FPDFBitmap_Destroy(PdfBmp);
end;

Ciklas žetoną tikrina prieš kiekvieną Continue, o ne vien pasikliauja jo viduje esančiu atgaliniu kvietimu. Atgalinis kvietimas sutrumpina dabartinį gabalą; ciklo patikra neleidžia prasidėti kitam. Kartu jie apriboja atšaukimo įsigaliojimo laiką maždaug iki vieno gabalo trukmės

Trys baigtys ir ką rastras laiko po atšaukimo

Viešasis įėjimo taškas yra TPdf.RenderPageProgressive, ir jis grąžina TPdfProgressiveStatus, kuris yra vienas iš prsDone, prsCancelled arba prsFailed. Šios reikšmės Pascal maniera atspindi PDFium FPDF_RENDER_* konstantas, tačiau atšaukimo atvejį įtraukia kaip pilnateisį rezultatą, o ne kaip klaidą

Žmones parklupdo tai, ką paskirties rastras turi po prsCancelled. Jis nėra tuščias. PDFium laipsniškai piešia į tą patį rastrą gabalas po gabalo, todėl kai atšaukimas sustabdo ciklą, rastre lieka viskas, kas iki tos akimirkos buvo nupiešta — dalinis vaizdas: kai kurios juostos baigtos, likusios vis dar rodo užpildo spalvą. Ar tas dalinis rezultatas naudingas, priklauso nuo iškviečiančiojo. Peržiūros programa, kuri rastrą vis tiek ruošiasi išmesti, nes naudotojas nuėjo kitur, gali jį tiesiog ignoruoti. Peržiūros programa, norinti parodyti pigią peržiūrą, gali jį pasilikti. Ko daryti negalima, tai laikyti, kad prsCancelled reiškia tuščią ar neapibrėžtą rastrą; jis reiškia teisingą nebaigto atvaizdavimo momentinę nuotrauką

Delphi laipsniško PDF atvaizdavimo prsDone, prsCancelled ir prsFailed baigčių diagrama ir dalinis rastras, kurį atšaukimas palieka paskirtyje
Atšauktas atvaizdavimas palieka teisingą dalinę nuotrauką su kai kuriomis nupieštomis juostomis, o likusios vis dar rodo užpildo spalvą, ir vienas finally blokas užveria būseną kiekviename kelyje
var
  Bmp: TBitmap;
  Token: IPdfCancellationToken;
  Status: TPdfProgressiveStatus;
begin
  Bmp := TBitmap.Create;
  try
    // Žetonas pradžioje neatšauktas; Token.IsCancelled perjunkite iš kitur
    // (sąsajos veiksmo, navigacijos įvykio), kad nutrauktumėte vykstantį atvaizdavimą.
    Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
    case Status of
      prsDone:      Image1.Picture.Assign(Bmp);  // visiškai atvaizduota
      prsCancelled: ;                            // dalinis rastras, paprastai išmetamas
      prsFailed:    ShowMessage('Render failed');
    end;
  finally
    Bmp.Free;
  end;
end;

Tuščias žetonas ir atgalinio kvietimo kelias be šakojimų

Atšaukimas yra pasirenkamas. Iškviečiantysis, kuriam laipsniškas atvaizdavimas reikalingas tik dėl pranešimų pumpavimo naudos ir kuris nieko nutraukti neketina, turėtų galėti vietoj žetono paduoti nil. Naivus būdas tai palaikyti yra išbarstyti patikras „ar žetonas buvo paduotas“ po atgalinį kvietimą ir ciklą, o tai reiškia šakojimą kiekviename gabale ir atgalinį kvietimą, kuris turi tvarkyti ir tikrą žetoną, ir jo nebuvimą

Realizacija to išvengia pakeisdama nepaduotą žetoną singletonu. nil žetonas keičiamas į PdfNoCancellationToken — sąsają, kurios IsCancelled visada yra false. Nuo tos vietos atgalinis kvietimas ir ciklas kiekvienu atveju turi ką apklausti, todėl nei vienam, nei kitam nereikia tuščios reikšmės patikros ir nereikia atskiro kelio. Niekada neatšaukiantis žetonas tiesiog visada atsako false, atgalinis kvietimas visada grąžina nulį, o atvaizdavimas nubėga iki galo lygiai taip, kaip neatšaukiamas. Pasirenkama elgsena modeliuojama kaip niekada nesuveikiantis žetonas, o ne kaip žetono nebuvimas, ir tai išlaiko karštąjį kelią vienodą

// nil -> niekada neatšaukiantis singletonas, todėl atgalinio kvietimo kelias
// vienodas nepriklausomai nuo to, ar iškviečiantysis pasirinko atšaukimą.
if AToken <> nil then
  EffectiveToken := AToken
else
  EffectiveToken := PdfNoCancellationToken;

Susiklosčiusi forma yra nedidelė ir verta pakartojimo, nes būtent ji yra pakartotinai panaudojama dalis. C biblioteka, palaikanti atgalinį kvietimą, duoda jums lygiai vieną kanalą būsenai į tą kvietimą perduoti — nepermatomą user rodyklę. Padėkite už tos rodyklės skaičiuojamą Pascal sąsajos nuorodą, greta struktūros laikykite antrą tikrą nuorodą gyvą, kad objektas nebūtų surinktas iškvietimo viduryje, ir sąsają nuskaitykite atgal statinėje cdecl funkcijoje. Visą valdantįjį ciklą apvyniokite try, o gimtąjį kontekstą atlaisvinkite finally bloke. Tas pats šablonas persikelia į bet kurią laipsnišką ar atgaliniais kvietimais valdomą PDFium operaciją, kur Pascal kodas turi išlaikyti gyvavimo trukmės kontrolę, o C laiko rodyklę

Atšaukimas yra tik pusė žvalios peržiūros programos. Kita pusė yra jau nupieštų puslapių neatvaizduoti iš naujo ir išlaikyti mastelį bei slinktį glotnius pateikiant podėliuotus rastrus, o tai aprašo mūsų straipsnis apie atvaizdavimo podėlį ir mastelio našumą. Kaip atšaukiamas atvaizdavimas įsilieja į pilną peržiūros programą greta navigacijos, žymėjimo ir paieškos, žiūrėkite funkcionalios PDF peržiūros programos kūrimą su PDFium Component. Čia aprašytas laipsniškas atvaizdavimas pristatomas kaip PDFium Component, skirto Delphi ir Lazarus, dalis greta įkėlimo, atvaizdavimo ir formų API, aprašytų kitur šiame tinklaraštyje