Useimmat PDF-sivut rasteroidaan muutamassa millisekunnissa, etkä ajattele asiaa lainkaan. Sitten käyttäjä avaa A1-kokoisen teknisen piirustuksen, sivun jossa on kymmeniä tuhansia vektoriviivoja, tai julisteen joka on täynnä läpinäkyvyysryhmiä ja pehmeitä maskeja, ja yksittäinen maalauskutsu kestää kaksi tai kolme sekuntia. Jos kutsu ajetaan käyttöliittymäsäikeessä, ikkuna lakkaa piirtymästä, otsikkopalkki harmaantuu ja käyttöjärjestelmä tarjoaa sovelluksen tappamista. Työ on sinänsä perusteltua. Sivu todella vaatii sen ajan. Vika on siinä, että renderöinti on yksi jakamaton estävä kutsu, jolla ei voi hengähtää välillä eikä jota voi pysäyttää
Tämä artikkeli käsittelee täsmälleen toista näistä kahdesta ongelmasta: pitkän yksittäisen sivun renderöinnin peruuttamista ilman käyttöliittymän jäätymistä. Käyttäjä napsautti seuraavaa sivua, zoomasi tai sulki asiakirjan, ja käynnissä oleva renderöinti on nyt turhaa työtä, jonka pitäisi päättyä seuraavassa mahdollisessa kohdassa eikä jatkua loppuun asti. Vierityksen ja zoomauksen pehmentäminen välimuistiin tallennettujen rasterointitulosten avulla on eri kysymys ja vaatii oman suunnittelunsa; sitä käsitellään lopussa linkitetyssä sisarartikkelissa. Tässä ainoa kysymys on, miten yksi progressiivinen renderöinti saadaan vastaamaan peruutuspyyntöön nopeasti ja siististi
PDFiumin jo tarjoama progressiivinen renderöinti-API
PDFium ennakoi ongelman käyttöliittymää jäädyttävän puolen. Kertakutsuna käytettävän FPDF_RenderPageBitmap-funktion rinnalla se tarjoaa progressiivisen vaihtoehdon, joka jakaa sivun työhön osiin. Kutsut FPDF_RenderPageBitmap_Start-funktiota kerran valmistellaksesi renderöinnin kohdebittikarttaa vasten, ja kutsut sitten FPDF_RenderPage_Continue-funktiota toistuvasti. Jokainen Continue rasteroi rajatun siivun ja palauttaa tilan. FPDF_RENDER_TOBECONTINUED tarkoittaa, että työtä on jäljellä, FPDF_RENDER_DONE tarkoittaa että sivu on valmis, ja FPDF_RENDER_FAILED tarkoittaa että toiminto pysähtyi virheeseen. Kun silmukka päättyy, kutsut FPDF_RenderPage_Close-funktiota vapauttaaksesi sivukohtaisen progressiivisen tilan. Koska hallinta palaa koodillesi siivujen välillä, voit käsitellä viestejä, päivittää etenemisosoitinta tai tarkistaa halutaanko työtä edelleen tehdä
PDFiumin tarjoama mekanismi päättämään milloin pitäisi luovuttaa vuoro takaisin kutsujalle on takaisinsoittorakenne nimeltä IFSDK_PAUSE. Se annetaan sekä Start-funktiolle että jokaiselle Continue-kutsulle. Jokaisen työosuuden jälkeen PDFium kutsuu sen NeedToPauseNow-funktio-osoitinta, ja jos se palauttaa nollasta poikkeavan arvon, nykyinen Continue pysähtyy aikaisin ja palauttaa hallinnan tilalla FPDF_RENDER_TOBECONTINUED. Rakenteessa on myös version-kenttä, jonka arvon on oltava 1, sekä vapaamuotoinen user-osoitin, johon PDFium ei koske ja jonka se vain kuljettaa mukanaan muuttamattomana. Juuri tuo koskemattomana pysyvä osoitin on koko seuraavan suunnittelun sarana
Pause-signaalin käyttäminen cancel-signaalina
NeedToPauseNow-kutsun alkuperäinen tarkoitus on aikaviipalointi. Palauta nollasta poikkeava arvo, kun ruutuaikabudjetti on kulutettu, palauta nolla jos renderöinti saa jatkua, ja PDFium keskeyttää työn jotta voit tehdä jotain muuta ennen saman renderöinnin jatkamista. PDFium Component käyttää samaa signaalia uudelleen eri merkityksessä. Sen sijaan että takaisinsoitto vastaisi kysymykseen "pitäisikö minun pysähtyä jotta voit jatkaa myöhemmin", se vastaa kysymykseen "onko tämä työ peruutettu". Nämä kaksi menevät luontevasti päällekkäin siksi, mitä silmukka tekee nähdessään lipun. Aito pause odottaa myöhempää Continue-kutsua; cancel ei odota. Kun kutsuva silmukka havaitsee tokenin peruuntuneen, se sulkee renderöintikontekstin eikä kutsu enää koskaan Continue-funktiota, joten sama nollasta poikkeava paluuarvo, jonka PDFium tulkitsee merkityksessä "pysäytä tämä siivu", muuttuu käytännössä merkitykseksi "pysäytä lopullisesti"
Peruutus ilmaistaan rajapinnalla IPdfCancellationToken, jonka IsCancelled-ominaisuus vaihtuu arvosta false arvoon true, kun jokin toinen ohjelman osa pyytää renderöinnin pysäyttämistä. Silta Pascal-rajapinnan ja PDFiumin C-tyylisen takaisinsoiton välillä on yksi osoitin. Tokenin rajapintaviite kirjoitetaan kenttään IFSDK_PAUSE.user, ja staattinen cdecl-takaisinsoitto lukee sen sieltä takaisin ja kysyy sen tilan. Tämä on klassinen ongelma, kun C-kirjaston annetaan kutsua takaisin Pascal-koodia: takaisinsoiton on oltava tavallinen C-kutsukäytännön funktio eikä olion metodi, koska PDFium tallentaa ja kutsuu paljasta funktio-osoitinta, joka ei tiedä mitään Pascal-olioista eikä Self-viitteestä
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;
Takaisinsoitto palauttaa tokenin tyypittämästä pThis^.user-osoittimen takaisin rajapintatyypiksi ja lukemalla IsCancelled-ominaisuuden. Se ei varaa muistia, lukitse eikä blokkaa, mikä on tärkeää, koska PDFium kutsuu sitä renderöintisäikeessä jokaisen siivun jälkeen ja kaikki tässä tehty työ lisätään itse renderöinnin kustannukseen. Suojaus nil-rakenteen tai nil-user-kentän varalta tarkoittaa, että sama funktio on turvallinen asentaa myös renderöintiin, jolle ei koskaan annettu oikeaa tokenia
Tokenin pitäminen hengissä silmukan ajan
Rajapintaosoittimen muuntaminen paljaan Pointer-osoittimen kautta ja takaisin on juuri se kohta, jossa elinkaarihin liittyvät bugit syntyvät. Delphissä IInterface on viitelaskettu, ja laskuri muuttuu vain kun kääntäjä näkee rajapintatyyppisen muuttujan saavan uuden arvon. Jos token tallennettaisiin vain paljaana osoittimena kenttään IFSDK_PAUSE.user, se piilotettaisiin kokonaan viitelaskurilta. Jos ainoa muu viite tokeniin menisi näkyvistä sillä aikaa kun Continue-silmukka vielä pyörii, olio vapautettaisiin takaisinsoiton alta ja seuraava siivu dereferoisi roikkuvaa osoitinta
Siksi kuvaaja on tietue, jossa on kaksi asiaa eikä yksi. Pause-kenttä on rakenne jonka PDFium lukee. Token-kenttä on oikea rajapintatyyppinen viite, jonka kääntäjä laskee, ja se on olemassa vain siksi että token pysyy muistissa niin kauan kuin tietue elää. Tietue on renderöintirutiinin pinossa oleva paikallinen muuttuja, joten se pysyy voimassa koko silmukan ajan ja puretaan vasta kun rutiini poistuu. Paljas osoitin kentässä user ja laskettu viite kentässä Token nimeävät saman olion; toinen on sitä mitä PDFium voi lukea, toinen estää oliota keräytymästä pois
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ä miten silmukka päättyy
Jokainen FPDF_RenderPageBitmap_Start-kutsu varaa progressiivisen tilan, jonka PDFium liittää sivuun, ja tuo tila vapautuu vain kutsumalla FPDF_RenderPage_Close. Ajosilmukasta on kolme ulospääsyä. Sivu valmistuu ja viimeinen tila on FPDF_RENDER_DONE. Token laukeaa ja silmukka poistuu aikaisin raportoidakseen peruutuksen. Jokin epäonnistuu ja tila on FPDF_RENDER_FAILED. Kaikkien kolmen täytyy kutsua Close, ja peruutuspolku on helpoin tehdä väärin, koska luonnollinen tapa "näe peruutus, poistu silmukasta" ohittaa helposti siivouksen matkalla ulos. Jos Close jää saavuttamatta, sivukohtainen tila vuotaa, ja katselin joka antaa käyttäjän peruuttaa renderöintejä jatkuvasti kerää vuodon jokaisesta keskeytetystä sivusta
Kestävä rakenne sijoittaa silmukan ja tuloksen luokittelun try-lohkoon ja FPDF_RenderPage_Close-kutsun vastaavaan finally-lohkoon. Kohdebittikartta tuhotaaan samassa lohkossa. Peruutus voi poistua silmukasta aikaisella Exit-käskyllä ja finally suoritetaan silti, joten progressiivisen tilan vapauttamiseen on tasan yksi paikka eikä sitä voi kiertää
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 tokenin ennen jokaista Continue-kutsua eikä vain luota sen sisällä olevaan takaisinsoittoon. Takaisinsoitto lyhentää nykyisen siivun; silmukan tarkistus estää seuraavan siivun käynnistymisen. Yhdessä ne rajaavat peruutuksen vaikutusajan suunnilleen yhden siivun pituuteen
Kolme lopputulosta ja mitä bittikartassa on peruutuksen jälkeen
Julkinen sisäänkäynti on TPdf.RenderPageProgressive, ja se palauttaa TPdfProgressiveStatus-arvon, joka on joko prsDone, prsCancelled tai prsFailed. Arvot vastaavat Pascal-tyylisesti PDFiumin FPDF_RENDER_*-vakioita mutta nostavat peruutuksen ensimmäisen luokan tulokseksi virheen sijaan
Kohta joka hämmentää ihmisiä on se, mitä kohdebittikartta sisältää tilan prsCancelled jälkeen. Se ei ole tyhjä. PDFium renderöi progressiivisesti samaan bittikarttaan siivu siivulta, joten kun peruutus pysäyttää silmukan, bittikartassa on kaikki se mikä siihen mennessä ehdittiin piirtää, eli osittainen kuva: osa kaistoista on valmiina ja loput näyttävät yhä täyttövärin. Onko osittaisesta tuloksesta hyötyä, riippuu kutsujasta. Katselin joka on juuri hylkäämässä bittikartan käyttäjän siirtyessä muualle voi yksinkertaisesti sivuuttaa sen. Katselin joka haluaa näyttää halvan esikatselun voi pitää sen. Sitä ei saa olettaa, että prsCancelled tarkoittaisi tyhjää tai määrittelemätöntä bittikarttaa; se tarkoittaa totuudenmukaista välähdystä 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-token ja haaraton takaisinsoittopolku
Peruutus on valinnainen ominaisuus. Kutsujan, joka haluaa vain progressiivisen renderöinnin viestinkäsittelyn vuoksi ilman aikomusta keskeyttää työtä, pitäisi voida antaa tokeniksi nil. Naiivi tapa tukea tätä on ripotella tarkistuksia "jos token annettiin" sekä takaisinsoittoon että silmukkaan, mikä tarkoittaa haarautumista jokaisella siivulla ja takaisinsoittoa, jonka täytyy käsitellä sekä oikea token että sen puuttuminen
Toteutus välttää tämän korvaamalla puuttuvan tokenin singletonilla. nil-token vaihdetaan arvoon PdfNoCancellationToken, eli rajapintaan jonka IsCancelled on aina false. Sen jälkeen sekä takaisinsoitolla että silmukalla on jokaisessa tapauksessa token jota kysyä, joten kumpikaan ei tarvitse nil-tarkistusta eikä erikoispolkua. Tämä koskaan peruuttamaton token vastaa aina false, takaisinsoitto palauttaa aina nollan ja renderöinti jatkuu loppuun täsmälleen kuten renderöinti, jota ei voi peruuttaa. Valinnainen käyttäytyminen mallinnetaan tokenilla joka ei koskaan laukea, ei tokenin puuttumisella, mikä pitää kuuman polun 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;
Esille nouseva rakenne on pieni mutta toistamisen arvoinen, koska juuri se on uudelleenkäytettävä osa. C-kirjasto, joka tukee takaisinsoittoa, antaa täsmälleen yhden kanavan välittää tilaa takaisinsoittoon: läpinäkymättömän user-osoittimen. Pane tuon osoittimen taakse laskettu Pascal-rajapintaviite, pidä toinen oikea viite elossa rakenteen vieressä jotta olio ei voi kadota kesken kutsun, ja lue rajapinta takaisin ulos staattisen cdecl-funktion sisällä. Kääri koko ajosilmukka try-lohkoon ja vapauta natiivikonteksti finally-lohossa. Sama mallipohja toimii kaikissa progressiivisissa tai takaisinsoittoihin perustuvissa PDFium-operaatioissa, joissa Pascal-koodin täytyy hallita elinkaarta samalla kun C pitää osoitinta käsissään
Peruutus on vain puolet responsiivisesta katselijasta. Toinen puoli on se, ettei jo kerran piirrettyjä sivuja renderöidä uudelleen, vaan zoomaus ja vieritys pidetään sulavina tarjoamalla välimuistiin tallennettuja bittikarttoja, mistä kerrotaan renderöintivälimuistia ja zoomauksen suorituskykyä käsittelevässä artikkelissamme. Jos haluat nähdä, miten tässä kuvattu peruutettava renderöinti sopii kokonaiseen katselijaan navigoinnin, valinnan ja haun rinnalle, katso artikkeli monipuolisen PDF-katselijan rakentamisesta PDFium Componentilla Delphissä. Tässä kuvattu progressiivinen renderöinti toimitetaan osana PDFium Component -tuotetta Delphiä ja Lazarusta varten yhdessä lataus-, renderöinti- ja lomake-APIen kanssa, joita käsitellään muualla tässä blogissa