Useimmat PDF-sivut rasteroituvat muutamassa millisekunnissa, etkä koskaan ajattele asiaa. Sitten käyttäjä avaa A1-kokoisen teknisen piirustuksen, kymmenillä tuhansilla vektorivedoilla pakatun sivun tai julisteen, joka on täynnä läpinäkyvyysryhmiä ja pehmeitä maskeja (soft masks), ja se yksi kutsu, joka piirtää sen, vie kaksi tai kolme sekuntia. Jos tuo kutsu ajetaan käyttöliittymäsäikeessä, ikkuna lakkaa piirtämästä itseään uudelleen, otsikkopalkki harmaantuu, ja käyttöjärjestelmä tarjoaa sovelluksen tappamista. Työ on oikeutettua. Sivu todella tarvitsee niin kauan. Vika on siinä, että renderöinti on yksi jakamaton estävä kutsu (blocking call), jolla ei ole mitään tapaa haukata happea eikä mitään tapaa pysähtyä
Tämä artikkeli käsittelee täsmälleen toista näistä kahdesta ongelmasta: pitkän yksisivuisen renderöinnin peruuttamista ilman käyttöliittymän jäätymistä. Käyttäjä napsautti seuraavaa sivua, tai zoomasi, tai sulki asiakirjan, ja käynnissä oleva renderöinti on nyt hukkaan heitettyä työtä, jonka tulisi päättyä heti seuraavassa mahdollisessa tilanteessa sen sijaan, että se ajettaisiin loppuun asti. Vierityksen ja zoomauksen pehmentäminen välimuistittamalla se, mikä on jo rasteroitu, on erillinen huolenaihe omalla suunnittelullaan, jota käsitellään lopussa linkitetyssä sisarartikkelissa. Tässä ainoa kysymys on, kuinka saada yksi progressiivinen renderöinti vastaamaan peruutuspyyntöön nopeasti ja siististi
Progressiivisen renderöinnin ohjelmointirajapinta, joka PDFiumin mukana jo toimitetaan
PDFium ennakoi ongelman jäätymispuoliskon. Kertalaakista tapahtuvan FPDF_RenderPageBitmap-kutsun rinnalla se tarjoaa progressiivisen muunnelman, joka jakaa sivun työpalasiin (chunks). Kutsut FPDF_RenderPageBitmap_Start-funktiota kerran määrittääksesi renderöinnin kohdebittikarttaa vasten, ja sen jälkeen kutsut FPDF_RenderPage_Continue-funktiota toistuvasti. Jokainen Continue rasteroi rajatun siivun ja palauttaa tilan. FPDF_RENDER_TOBECONTINUED tarkoittaa, että tekemistä on vielä, FPDF_RENDER_DONE tarkoittaa, että sivu on valmis, ja FPDF_RENDER_FAILED tarkoittaa, että se pysähtyi virheeseen. Kun silmukka päättyy, kutsut FPDF_RenderPage_Close-funktiota vapauttaaksesi sivukohtaisen progressiivisen tilan. Koska hallinta palautuu koodillesi siivujen välissä, voit pumpata viestejä, päivittää edistymisilmaisinta tai tarkistaa, halutaanko työtä edelleen
Mekanismi, jonka PDFium tarjoaa sen päättämiseen, milloin luovuttaa vuoro (yield), on IFSDK_PAUSE-niminen takaisinkutsutietue (callback struct). Ojennet sen Start-kutsulle ja jokaiselle Continue-kutsulle. Jokaisen palasen jälkeen PDFium kutsuu sen NeedToPauseNow-funktio-osoitinta, ja jos se palauttaa nollasta poikkeavan arvon, nykyinen Continue pysähtyy aikaisin ja antaa hallinnan takaisin tilalla FPDF_RENDER_TOBECONTINUED. Tietue kantaa myös version-kenttää, joka on asetettava arvoon 1, sekä vapaamuotoista user-osoitinta, johon PDFium ei koskaan koske ja jonka se päästää läpi koskemattomana. Tämä koskematon osoitin on koko seuraavan suunnittelun sarana
Tauon uudelleenkäyttö peruutuksena
NeedToPauseNow-kutsun alkuperäinen tarkoitus on aikasiivutus (time-slicing). Palauta nollasta poikkeava arvo, kun ruutubudjettisi on käytetty, palauta nolla jatkaaksesi renderöintiä, jolloin PDFium pitää tauon, jotta voit tehdä jotain muuta ennen saman renderöinnin jatkamista. PDFium-komponentti käyttää uudelleen tätä samaa signaalia eri verbille. Sen sijaan, että se vastaisi "pitäisikö minun pitää tauko ja antaa sinun jatkaa", takaisinkutsu vastaa "onko tämä työ peruutettu". Nämä kaksi mapittuvat toisiinsa siististi sen ansiosta, mitä silmukka tekee, kun se näkee lipun. Aito tauko odottaa myöhempää Continue-kutsua; peruutus ei. Kun kutsuva silmukka havaitsee, että tunniste on peruutettu, se sulkee renderöintikontekstin eikä koskaan enää kutsu Continue-funktiota, joten se sama nollasta poikkeava paluuarvo, jonka PDFium lukee merkityksessä "pysäytä tämä palanen", muuttuu käytännössä merkitykseksi "pysähdy lopullisesti"
Peruutus ilmaistaan rajapinnan, IPdfCancellationToken, kautta, jonka ominaisuus IsCancelled kääntyy falsesta trueksi, kun jokin toinen ohjelman osa pyytää renderöintiä pysähtymään. Silta tuon Pascal-rajapinnan ja PDFiumin C-takaisinkutsun välillä on yksittäinen osoitin. Tunnisteen rajapintaviittaus kirjoitetaan kohteeseen IFSDK_PAUSE.user, ja staattinen cdecl-takaisinkutsu lukee sen takaisin ulos ja tekee siihen kyselyn. Tämä on klassinen ongelma annettaessa C-kirjaston tehdä takaisinkutsu Pascaliin: takaisinkutsun on oltava tavallinen funktio C-kutsukäytännöllä, ei metodi, koska PDFium tallentaa ja kutsuu paljasta funktio-osoitinta, joka ei tiedä mitään Pascal-objekteista tai Self-osoittimesta
type
TPdfProgressivePause = record
Pause: IFSDK_PAUSE; // PDFium reads this; .user holds the token
Token: IPdfCancellationToken; // strong ref keeps the token alive
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; // non-zero: PDFium stops this chunk
end;
Takaisinkutsu palauttaa tunnisteen (token) muuntamalla (cast) arvon pThis^.user takaisin rajapintatyypiksi ja lukee ominaisuuden IsCancelled. Mikään siinä ei varaa muistia, lukitse tai estä (block), millä on merkitystä, koska PDFium kutsuu sitä renderöintisäikeessä jokaisen palasen jälkeen, ja kaikki täällä tehty työ lisätään itse renderöinnin hintaan. Suojus (guard) nilliä tietuetta tai nilliä user-kenttää vastaan tarkoittaa, että sama funktio on turvallinen asentaa jopa renderöintiin, jolle ei koskaan annettu aitoa tunnistetta
Tunnisteen pitäminen elossa silmukan yli
Rajapintaosoittimen muuntaminen (casting) raa'an Pointer-tyypin kautta ja takaisin on se paikka, missä elinkaaribugit syntyvät. IInterface on Delphissä viitelaskettu (reference counted), ja laskuri liikkuu vain, kun kääntäjä näkee rajapintatyyppisen muuttujan saavan arvon. Tunnisteen tallentaminen pelkästään paljaana osoittimena kohteeseen IFSDK_PAUSE.user piilottaisi sen viitelaskurilta kokonaan. Jos ainoa muu viittaus tuohon tunnisteeseen poistuisi näkyvyysalueelta (goes out of scope) sillä aikaa, kun Continue-silmukka on yhä käynnissä, objekti vapautettaisiin takaisinkutsun alta, ja seuraava palanen dereferoisi roikkuvan osoittimen (dangling pointer)
Tästä syystä kuvaaja (descriptor) on tietue, joka sisältää kaksi asiaa, ei yhtä. Pause-kenttä on tietue (struct), jonka PDFium lukee. Token-kenttä on aito rajapintatyyppinen viittaus, jonka kääntäjä laskee, ja se on olemassa ainoastaan kiinnittääkseen (pin) tunnisteen muistiin niin kauan kuin tietue elää. Tietue on paikallinen muuttuja renderöintirutiinin pinossa (stack), joten se pysyy validina silmukan koko keston ajan ja puretaan vasta, kun rutiini poistuu. Paljas osoitin kentässä user ja laskettu viittaus kentässä Token nimeävät saman objektin; toinen on se, minkä PDFium pystyy lukemaan, toinen on se, mikä estää kyseistä objektia joutumasta roskienkeruuseen (collected)
var
Pause: TPdfProgressivePause;
EffectiveToken: IPdfCancellationToken;
begin
// ... choose EffectiveToken ...
// Strong ref first, then publish the same object to PDFium via .user.
Pause.Token := EffectiveToken;
Pause.Pause.version := 1;
Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
Pause.Pause.user := Pointer(EffectiveToken);
Renderöintikontekstin sulkeminen riippumatta siitä, kuinka silmukka päättyy
Jokainen kutsu FPDF_RenderPageBitmap_Start-funktiolle varaa progressiivisen tilan, jonka PDFium yhdistää sivuun, ja tuo tila vapautetaan vain kutsulla FPDF_RenderPage_Close. Ohjaussilmukasta on kolme poistumistietä. Sivu valmistuu ja viimeinen tila on FPDF_RENDER_DONE. Tunniste laukeaa ja silmukka poistuu aikaisin raportoiden peruutuksen. Jokin epäonnistuu ja tila on FPDF_RENDER_FAILED. Kaikkien kolmen on kutsuttava Close-funktiota, ja peruutuspolku on helpoin saada väärin, koska luonnollinen muoto "näe peruutus, murtaudu ulos" pyrkii ohittamaan siivouksen matkallaan uloskäynnille. Jos Close jää saavuttamatta, sivukohtainen tila vuotaa, ja katseluohjelma, joka antaa käyttäjän peruuttaa renderöinnin toisensa jälkeen, keräisi tuota vuotoa jokaisella keskeytetyllä sivulla
Vankka rakenne sijoittaa silmukan ja tulosten luokittelun try-lohkoon ja FPDF_RenderPage_Close-kutsun sitä vastaavaan finally-lohkoon. Kohdebittikartta tuhotaan samassa lohkossa. Peruutus voi poistua silmukasta varhaisen Exit-kutsun kautta ja finally suoritetaan silti, joten on tasan yksi paikka, joka vapauttaa progressiivisen tilan, eikä sitä voida ohittaa
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
// Frees the progressive state Start allocated; mandatory on every path.
FPDF_RenderPage_Close(FPage);
FPDFBitmap_Destroy(PdfBmp);
end;
Silmukka tarkistaa tunnisteen ennen jokaista Continue-kutsua sen lisäksi, että se luottaa sen sisällä olevaan takaisinkutsuun. Takaisinkutsu lyhentää nykyistä palasta; silmukkatarkistus estää seuraavan alkamisen. Yhdessä ne rajaavat peruutuksen voimaantuloajan suurin piirtein yhden palasen kestoon
Kolme lopputulosta, ja mitä bittikartta sisältää peruutuksen jälkeen
Julkinen aloituspiste on TPdf.RenderPageProgressive, ja se palauttaa tyypin TPdfProgressiveStatus, joka on joko prsDone, prsCancelled tai prsFailed. Arvot peilaavat PDFiumin FPDF_RENDER_*-vakioita Pascal-idiomiin mukautettuina, mutta sisällyttävät peruutustapauksen ensimmäisen luokan tuloksena pikemminkin kuin virheenä
Seikka, joka yllättää ihmiset, on se, mitä kohdebittikartta sisältää prsCancelled-tilan jälkeen. Se ei ole tyhjä. PDFium renderöi progressiivisesti samaan bittikarttaan palanen palaselta, joten kun peruutus pysäyttää silmukan, bittikartta sisältää kaiken, mitä siihen hetkeen mennessä oli maalattu, mikä on osittainen kuva: joitakin raitoja valmiina, ja loput näyttävät yhä täyttöväriä. Onko tuo osittainen tulos hyödyllinen, riippuu kutsujasta. Katseluohjelma, joka on aikeissa heittää bittikartan pois, koska käyttäjä siirtyi muualle, voi yksinkertaisesti jättää sen huomiotta. Katseluohjelma, joka haluaa näyttää edullisen esikatselun, voi pitää sen. Mitä et saa tehdä, on olettaa, että prsCancelled viittaa tyhjään tai määrittelemättömään bittikarttaan; se viittaa totuudenmukaiseen tilannekuvaan keskeneräisestä renderöinnistä
var
Bmp: TBitmap;
Token: IPdfCancellationToken;
Status: TPdfProgressiveStatus;
begin
Bmp := TBitmap.Create;
try
// Token starts un-cancelled; flip Token.IsCancelled from elsewhere
// (a UI action, a navigation event) to abort the render in flight.
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
case Status of
prsDone: Image1.Picture.Assign(Bmp); // fully rendered
prsCancelled: ; // partial bitmap, usually discarded
prsFailed: ShowMessage('Render failed');
end;
finally
Bmp.Free;
end;
end;
Nil-tunniste ja haarautumaton takaisinkutsupolku
Peruutus on vapaaehtoinen (opt-in). Kutsujan, joka haluaa vain progressiivisen renderöinnin viestien pumppauksen hyödyn vuoksi ilman aikomustakaan keskeyttää, tulisi pystyä antamaan tunnisteeksi nil. Naiivi tapa tukea tätä on ripotella "jos tunniste toimitettiin" -tarkistuksia takaisinkutsuun ja silmukkaan. Tämä tarkoittaa haarautumista (branch) jokaisella palasella ja takaisinkutsua, jonka on käsiteltävä sekä aito tunniste että sen puuttuminen
Toteutus välttää tämän korvaamalla sen singletonilla, kun kutsuja ei anna mitään. nil-tunniste vaihdetaan PdfNoCancellationToken-rajapintaan, jonka IsCancelled on aina false. Tästä pisteestä eteenpäin takaisinkutsulla ja silmukalla on tunniste, jolta kysyä jokaisessa tapauksessa, joten kumpikaan ei tarvitse nil-tarkistusta eikä kumpikaan tarvitse erityistä polkua. Koskaan-ei-peruuteta -tunniste yksinkertaisesti vastaa aina false, takaisinkutsu palauttaa aina nollan, ja renderöinti ajetaan loppuun täsmälleen niin kuin peruuttamatonkin ajettaisiin. Valinnainen käyttäytyminen on mallinnettu tunnisteena, joka ei koskaan laukea, pikemminkin kuin tunnisteen puuttumisena, mikä pitää kuuman polun (hot path) yhtenäisenä
// nil -> never-cancel singleton, so the callback path is identical
// whether or not the caller opted into cancellation.
if AToken <> nil then
EffectiveToken := AToken
else
EffectiveToken := PdfNoCancellationToken;
Muodostuva rakenne on pieni ja arvoinen toistaa, koska se on se uudelleenkäytettävä osa. C-kirjasto, joka tukee takaisinkutsua, antaa sinulle tasan yhden kanavan tilan välittämiseen kyseiseen takaisinkutsuun: läpinäkymättömän käyttäjäosoittimen (opaque user pointer). Laita laskettu Pascal-rajapintaviittaus tuon osoittimen taakse, pidä toinen aito viittaus elossa tietueen (struct) vieressä, jotta objektia ei voida kerätä (collect) kesken kutsun, ja lue rajapinta takaisin ulos staattisen cdecl-funktion sisällä. Kääri koko ohjaussilmukka try-lohkoon ja vapauta natiivi konteksti finally-lohkossa. Sama malli siirtyy mihin tahansa progressiiviseen tai takaisinkutsuohjattuun PDFium-operaatioon, jossa Pascal-koodin on pysyttävä hallinnassa elinkaaresta silloin kun C pitää osoitinta hallussaan
Peruutus on vain yksi puolisko responsiivisesta katseluohjelmasta. Toinen puoli on se, ettei jo piirrettyjä sivuja renderöidä uudelleen, ja että zoomaus ja vieritys pidetään pehmeinä tarjoamalla välimuistitettuja bittikarttoja, mikä on käsitelty artikkelissa renderöintivälimuisti ja zoomaussuorituskyky. Katso kuinka peruutettava renderöinti sopii täydelliseen katseluohjelmaan navigoinnin, valinnan ja haun rinnalla artikkelista monipuolisen PDF-katseluohjelman rakentaminen PDFium VCL -komponentin avulla. Tässä kuvattu progressiivinen renderöinti toimitetaan osana PDFium-komponenttia Delphille ja Lazarukselle yhdessä lataus-, renderöinti- ja lomakeohjelmointirajapintojen kanssa, joita käsitellään muualla tässä blogissa