Tekninen artikkeli

Jatkuvasti vierivä PDF-katseluohjelma Delphissä PDFium-komponentilla

Yksittäinen mukavalla lukukoolla renderöity A4-sivu vie muutaman megatavun verran 32-bittistä bittikarttamuistia. Kerro se 400-sivuisella sopimuksella, niin laskutoimitus lakkaa olemasta teoreettinen: renderöi jokainen sivu etukäteen, ja pyydät Windowsilta reilusti yli gigatavun verran bittikarttoja, joita käyttäjä katsoo vain ruudullinen kerrallaan. Sovellus joko kuluttaa loppuun osoiteavaruutensa 32-bittisessä kokoonpanossa tai viettää ensimmäiset sekuntinsa jäätyneenä, kun GPU ja sivujäsennin käyvät läpi sivuja, joita kukaan ei ole vielä edes vierittänyt näkyviin. Jatkuvasti vierivän lukijan on tunnutettava yhdeltä pitkältä sivunauhalta, mutta se ei voi pitää niitä kaikkia muistissa kerralla

Tämä ristiriita on koko ongelman ydin. PDFium-komponentti ratkaisee sen TPdfView-luokan sisällä, joten suurin osa työstä on oikean näyttötilan valitsemista ja sen ymmärtämistä, mitä komponentti tekee puolestasi. Osuudet, joita se ei tee puolestasi – kuten sivujen koon sovittaminen lukuvirtaan ja nopean vierityksen pitäminen sujuvana – ovat paikkoja, joissa pieni määrä koodia maksaa itsensä takaisin. Jos olet vielä kokoamassa ympäröiviä käyttöliittymäelementtejä (työkalupalkki, pikkukuvat, hakukenttä), monipuolisen katseluohjelman opas kattaa sen alueen; tässä aiheena on itse vieritys

Asettelu on näyttötila, ei paneeli bittikarttoja

VCL-lomakkeita rakentaessa ensireaktio on tarttua scroll box -komponenttiin ja pinota sen sisään kuvakomponentteja, yksi per sivu. Vastusta sitä. Tuo ratkaisu pakottaa sinut hallitsemaan sivujen asettelua, vieritysmatematiikkaa ja muistikysymystä samanaikaisesti, ja päädyt toteuttamaan ne kaikki itse huonosti. TPdfView mallintaa asiakirjan jo valmiiksi jatkuvana sivujonona ja tuo asettelun näkyville DisplayMode-propertyn kautta:

Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;

PdfView.DisplayMode := dmSingleContinuous;   // one page wide, scrolls vertically

Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
  ShowMessage('Could not open the document');

Tämä on koko jatkuvan vierityksen asennus. dmSingleContinuous asettaa sivut yhteen pystysuoraan sarakkeeseen, jossa sivujen välit käsitellään sisäisesti, ja näkymä vierii tuon sarakkeen läpi yhtenä pintana. Tavallista navigointia varten ei tarvitse kytkeä sivukohtaisia säätimiä tai kirjoittaa vierityskäsittelijää. Huomaa tarkistus Pdf.Active-muuttujasta sijoituksen jälkeen: asiakirjan avaaminen ei koskaan heitä poikkeusta, joten vioittunut tai salasanalla suojattu tiedosto jättää Active-arvon tilaan False ilman poikkeusta. Katseluohjelma, joka ohittaa tämän tarkistuksen, renderöi vain tyhjän ruudun ja jättää kehittäjän ihmettelemään syytä

Sama property kantaa myös aukeamatilat. dmTwoPageContinuous asettaa sivut rinnakkain, kaksi rivilleen, jolloin saavutetaan kirjamainen lukukokemus, jota jotkin asiakirjat vaativat; dmTwoPageContinuousWithCover tekee saman, mutta antaa ensimmäisen sivun olla yksinään kansilehtenä, jolloin loput aukeamat asettuvat luonnollisille parillinen-pariton-rajoille. Kaikki kolme tilaa vierivät jatkuvasti. Vaihto niiden välillä on yksi sijoitus, mikä tekee näyttötilan valintavalikosta helpon toteuttaa myöhemmin

Vain näkyvät sivut rasteroidaan

Syy siihen, miksi tämä skaalautuu 400-sivuiseen tiedostoon, on se, että sarake on virtuaalinen. TPdfView tietää jokaisen sivun korkeuden asiakirjan sivupuustesta, joten se voi laskea kokonaisvierityksen laajuuden ja kunkin sivun sijainnin renderöimättä mitään. Rasterointi, joka on kallis vaihe ja muuttaa sivun sisältövirran pikseleiksi, tapahtuu vain sivuille, jotka kulloinkin leikkaavat näkymäalueen (viewport), sekä pienelle marginaalialueelle, jotta sivu on valmis silloin, kun se vierii näkyviin. Kun vierität alaspäin, näkymäalueelle saapuvat sivut renderöidään ja siitä poistuvien sivujen bittikartat vapautetaan. Muistinkulutus pysyy suhteessa siihen, mikä mahtuu ruudulle, eikä asiakirjan pituuteen

Tämä on tärkeä sisäistää, koska se muuttaa tapaa, jolla arvioit työn hintaa. 400-sivuisen asiakirjan avaaminen on halpaa: se jäsentää rakenteen, ei sisältöä. Kustannus maksetaan sivukohtaisesti ja laiskasti (lazy) vasta siinä vaiheessa, kun sivu vieritetään lähelle. Katseluohjelma, joka tuntuu heti valmiilta avattaessa ja sulavalta vieritettäessä, ei tee vähemmän työtä kokonaisuudessaan, vaan se jakaa työn käyttäjän todelliselle lukupolulle ja heittää taakse jäävän pois. Käytännön seurauksena on se, ettet melkein koskaan halua pakottaa sivuja renderöitymään etukäteen ennen kuin käyttäjä ehtii niihin. Anna näkymän päättää, mikä on näkyvissä

Mitoita sivut leveyden mukaan, jätä sitten zoomaus rauhaan

Lukusarake vaatii sivujen koon sopeuttamista paneelin leveyteen, ei kiinnittämistä absoluuttiseen zoomaukseen. FitMode tekee tämän ja jatkaa sen tekemistä myös ikkunan kokoa muutettaessa:

PdfView.FitMode := pfmFitWidth;   // each page fills the column width; height follows

Asetuksella pfmFitWidth komponentti laskee zoomauksen uudelleen aina näkymän koon muuttuessa, joten sarake täyttää aina käytettävissä olevan leveyden ja sivujen korkeudet (ja sitä kautta vieritysalueen pituus) seuraavat tästä. Tässä on yksi sudenkuoppa, johon monet lankeavat: suora sijoitus Zoom-propertylle palauttaa FitMode-tilan arvoon pfmNone. Tämä on tarkoituksellista, koska manuaalinen zoomaus ja automaattinen sovitus ovat ristiriitaisia aikomuksia, mutta se tarkoittaa, että koodissasi oleva satunnainen PdfView.Zoom := 1.0 kytkee hiljaisesti sovituksen pois päältä ja seuraava koon muutos lakkaa reflow-käsittelystä. Jos tarjoat sekä zoomaussäätimen että sovituspainikkeen, käsittele niitä tilavaihtoehtoina: toisen asettaminen nollaa toisen, ja päätät kumpi voittaa

Jotta absoluuttiset zoomaussäätimet toimisivat luontevasti, näkymä tuo sovituszoomaukset arvoina, joita voit soveltaa tai näyttää: PageWidthZoom[PageNumber] palauttaa zoomauksen, joka sovittaisi kyseisen sivun leveyteen, ja sitä vastaava PageZoom sovittaa koko sivun. Näiden arvojen lukeminen on tapa täyttää "Sovita leveys" / "Sovita sivu" -valikot ilman kovakoodattuja taikaprosentteja, jotka menevät vigaan vaakasivujen tai ylikokoisten sivujen kohdalla

Pidä nopea vieritys responsiivisena progressiivisella renderöinnillä

Oletusarvoinen renderöintipolku piirtää sivun valmiiksi ennen paluuta. Yhdelle sivulle tämä on hienoa. Tiheän asiakirjan nopean vierityksen aikana näin ei ole: jokainen ohi vilahtava sivu käynnistää täyden rasteroinnin, ja jos käyttäjä vierittää nopeammin kuin sivut ehtivät renderöityä, nämä renderöinnit kasaantuvat ja paneeli alkaa nykiä, koska työtä tehdään sivuille, jotka ovat jo poistuneet ruudulta työn valmistuttua. Korjaus on tehdä renderöinnistä peruutettava ja hylätä se heti, kun käyttäjä siirtyy eteenpäin

RenderPageProgressive renderöi lohkoissa ja tarkistaa peruutuslipun kunkin lohkon rajalla, joten juuri näytöltä poistuneen sivun käynnissä oleva renderöinti voidaan keskeyttää sen sijaan, että se ajettaisiin loppuun asti:

type
  TFormMain = class(TForm)
    // ...
  private
    FRenderCancel: IPdfCancellationTokenSource;
    procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
  end;

procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
  Status: TPdfProgressiveStatus;
begin
  // Cancel whatever was rendering; the old token is now signaled.
  if Assigned(FRenderCancel) then
    FRenderCancel.Cancel;
  FRenderCancel := TPdfCancellationTokenSource.New;

  Pdf.PageNumber := PageNo;
  Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
    FRenderCancel.Token);

  case Status of
    prsDone:      ;                    // bitmap is complete, paint it
    prsCancelled: Exit;                // superseded, discard this result
    prsFailed:    ShowMessage('Render failed for page ' + IntToStr(PageNo));
  end;
end;

Merkitsevä muoto on paluuarvo. prsDone tarkoittaa, että bittikartta on maalattu kokonaan ja se voidaan piirtää ruudulle; prsCancelled tarkoittaa, että uudempi vieritysasema korvasi tämän sivun, joten heität osittaisen tuloksen pois sen näyttämisen sijaan; prsFailed on aito virhe kyseisellä sivulla. Peruutusta kysellään lohkorajoilla ennakoivan keskeytyksen sijaan, joten varaudu muutaman kymmenen millisekunnin viiveeseen Cancel-kutsun ja renderöinnin todellisen pysähtymisen välillä. Tämä on silti huomattavasti halvempaa kuin antaa vanhentuneen kokosivun renderöinnin tukkia jonoa. Arvon nil välittäminen peruutuslippuna renderöi suoraan valmiiksi asti, mikä on oikea valinta kertaluonteiselle renderöinnille, kuten tulostuksen esikatselulle, jossa ei ole mitään peruutettavaa

Kun kutsut sen sijaan RenderPage-metodin funktioversiota, joka palauttaa uuden TBitmap-olion, muusta että kutsuja omistaa sen ja sen on vapautettava se (Free). Vierityssilmukassa, joka varaa bittikartan sivua kohden, tämän unohtaminen on vuoto, joka kasvaa jokaisen käyttäjän ohittaman sivun myötä. Se on juuri se rajoittamattoman muistin vikatila, joka jatkuvalla asettelulla oli tarkoitus välttää. Renderöi mahdollisuuksien mukaan uudelleenkäytettävään bittikarttaan

Mitä käteesi jää

Jatkuvasti vierivä lukija on pääosin komponentin itsensä tarjoama. Valitset asetteluksi dmSingleContinuous, asetat pfmFitWidth-tilan jotta sarake sopeutuu ikkunaan ja tarkistat Pdf.Active-arvon, jotta virheellinen tiedosto epäonnistuu kuuluvasti. Ainoa osa, joka kannattaa kirjoittaa itse, on peruutettava renderöinti, sillä katseluohjelmaa arvioidaan sen mukaan, miten se käyttäytyy, kun joku vetää vierityspalkin pitkän asiakirjan pohjaan ja ruutu joko pysyy perässä tai ei. Kaikki tämän jälkeen – tekstin valinta sivujen yli, haun korostus, kirjanmerkkipuu – on käyttöliittymätyötä, joka istuu tämän vierityspinnan päällä eikä sen sisällä

Tässä näytetyt TPdfView-, DisplayMode- ja RenderPageProgressive-API:t ovat osa Delphille ja Lazarukselle tarkoitettua PDFium-komponenttia