Tekninen artikkeli

Taustalla tapahtuva PDF-renderöintijono Delphissä HotPDF:llä

HotPDF:n THPDFBackgroundRenderer-luokka on TThread-jälkeläinen, joka renderöi ladatut PDF-sivut bittikartoiksi työsäikeessä, jotta Delphi-katseluohjelma voi jatkaa vierittämistä ja uudelleenpiirtoa, samalla kun sivua yhä rasteroidaan taustalla. THPDFBackgroundRenderer.RequestPage jonottaa sivuindeksin tuolle työsäikeelle, CancelAll pudottaa kaiken vielä odottavan, ja GetCachedBitmap antaa takaisin valmiin bittikartan, jonka kutsuja omistaa ja jonka se on vapautettava. Vieritä kaksisataasivuinen skannattu sopimus tulostustarkkuudella pelkällä käyttöliittymäsäikeellä, ja jokainen sivunvaihto pysäyttää ikkunan, kunnes GDI on saanut sen piirretyksi valmiiksi — juuri tuon nykimisen THPDFBackgroundRenderer on olemassa poistaakseen

Miksi PDF-sivuja pitää ylipäätään renderöidä taustasäikeessä?

Taustasäie ansaitsee monimutkaisuutensa, koska HotPDF:n sivurenderöijä on aito sisältövirran tulkki, ei halpa bittikartan kopiointi, joka palautuu ennen kuin kukaan huomaa: se käy läpi PDF-operaattoreita, pitää graafisen tilan pinoa ja rasteroi polkuja, kuvia ja glyfejä GDI:n kautta, samaa moottoria, joka käsitellään artikkelissa ladattujen PDF-sivujen renderöinti TBitmap-olioksi. Aja tuo työ synkronisesti vieritys- tai piirtokäsittelijän sisällä, ja viestisilmukka lakkaa pumppaamasta, kunnes kutsu palautuu, mikä on juuri sitä, mitä jäätynyt ikkuna tarkoittaa. Application.ProcessMessages-kutsun pudottaminen renderöintikutsun sisään ei korjaa tätä: se antaa viestijonon tyhjentyä, mutta itse renderöinti omistaa yhä kutsuvan säikeen, joten ikkuna piirtää vanhentunutta sisältöä nopeammin, samalla kun todellinen työ ei ole edennyt minnekään. Ainoa tapa pitää katseluohjelma responsiivisena aidosti hitaan renderöinnin aikana on ajaa tuo renderöinti jossain muualla, minkä vuoksi THPDFBackgroundRenderer on olemassa TThread-aliluokkana takaisinkutsun tai ajastimen sijaan

Pyyntöjonon asettaminen vierittyvälle katseluohjelmalle

THPDFBackgroundRenderer.Create ottaa ladatun THotPDF-instanssin ja DPI:n, joka pysyy kiinteänä koko tuon renderöijän eliniän ajan, joten jokainen yhden instanssin kautta jonotettu sivu renderöityy yhdellä resoluutiolla; katseluohjelma, joka tukee zoomausta, tarvitsee tuoreen renderöijän, ei tuoretta DPI-ominaisuutta, aina kun zoomaustaso muuttuu. RequestPage lisää sivuindeksin sisäiseen jonoon ja palautuu välittömästi: se ei itse tee mitään renderöintiä eikä koskaan kosketa käyttöliittymäsäiettä. Execute, peritty TThread-aloituspiste, jonka HotPDF ajaa heti kun kutsut Start-metodia, ottaa yhden indeksin kerrallaan jonon etupäästä, renderöi sen asiakirjan sivuvälimuistin kautta ja tallentaa kopion sivun mukaan indeksoituna, jotta GetCachedBitmap voi antaa sen takaisin myöhemmin

type
  TViewerForm = class(TForm)
    RenderPollTimer: TTimer;
    procedure RenderPollTimerTimer(Sender: TObject);
  private
    FDoc: THotPDF;
    FRenderer: THPDFBackgroundRenderer;
    FPendingPage: Integer;
    procedure RequestPageWindow(CenterPage: Integer);
  end;

procedure TViewerForm.RequestPageWindow(CenterPage: Integer);
var
  I: Integer;
begin
  if FRenderer <> nil then
  begin
    FRenderer.CancelAll;
    FRenderer.Free;
  end;
  FRenderer := THPDFBackgroundRenderer.Create(FDoc, 150);
  for I := CenterPage - 1 to CenterPage + 1 do
    if (I >= 0) and (I < FDoc.LoadedPageCount) then
      FRenderer.RequestPage(I);
  FPendingPage := CenterPage;
  FRenderer.Start;
end;

procedure TViewerForm.RenderPollTimerTimer(Sender: TObject);
var
  Bmp: TBitmap;
begin
  if FRenderer = nil then Exit;
  Bmp := FRenderer.GetCachedBitmap(FPendingPage);
  if Bmp <> nil then
  begin
    PageImage.Picture.Bitmap.Assign(Bmp);
    Bmp.Free;
  end;
end;

GetCachedBitmap palauttaa nil-arvon, kunnes kyseisen sivun kopio on valmis, joten yllä olevan kaltainen ajastimella kyseleminen riittää; erillistä valmis-tapahtumaa ei tarvitse kytkeä, HotPDF ratkaisee tämän tavallisella nil-tarkistuksella suuremman ilmoitus-API:n sijaan. Seuraava osio käsittelee, mitä CancelAll ja tuo Free-kutsu todella tekevät, koska molemmat ovat merkityksellisiä heti, kun sivut alkavat renderöityä väärässä järjestyksessä tai vieritys tapahtuu nopeammin kuin jono ehtii tyhjentyä

Yhden kutsun oikotie yksittäiselle sivulle

THotPDF.RenderLoadedPageToBitmapAsync on olemassa yleistä tapausta varten, jossa käynnistetään täsmälleen yksi sivu koskematta suoraan THPDFBackgroundRenderer-luokkaan: se rakentaa renderöijän sisäisesti, kutsuu RequestPage-metodia kerran, käynnistää säikeen ja palauttaa TThread-viittauksen kutsujalle, joka omistaa sen ja on vastuussa sen vapauttamisesta. Tuloksen hakeminen kulkee funktion THotPDF.GetLoadedCachedRenderedBitmap kautta renderöijän oman GetCachedBitmap-metodin sijaan, koska GetLoadedCachedRenderedBitmap lukee asiakirjan jaettua välimuistia, joka on avaimistettu sivuindeksin ja DPI:n mukaan, samaa välimuistia, jota RenderLoadedPageToBitmapCached ja sisäänrakennettu esilataaja jo täyttävät — sivu, jonka katseluohjelman jokin muu osa on jo renderöinyt tuolla DPI:llä, voi palautua välittömästi, ennen kuin käyttöjärjestelmä on edes ehtinyt ajoittaa juuri käynnistetyn taustasäikeen

// A simpler alternative to the queue above, for one page at a time.
procedure TViewerForm.RequestSinglePage(PageIndex: Integer);
begin
  if FAsyncWorker <> nil then
    FAsyncWorker.Free; // waits if a prior page is still rendering
  FAsyncWorker := Pdf.RenderLoadedPageToBitmapAsync(PageIndex, 150);
  FPendingPage := PageIndex;
end;

procedure TViewerForm.AsyncPollTimerTimer(Sender: TObject);
var
  Bmp: TBitmap;
begin
  Bmp := Pdf.GetLoadedCachedRenderedBitmap(FPendingPage, 150);
  if Bmp <> nil then
  begin
    PageImage.Picture.Bitmap.Assign(Bmp);
    Bmp.Free;
  end;
end;

Voiko jo jonotetun sivun peruuttaa?

CancelAll poistaa vain jonossa vielä odottavat työt; sivu, jonka HotPDF on jo ottanut jonon etupäästä ja antanut renderöintikutsulleen, jatkuu loppuun asti, koska THPDFBackgroundRenderer-luokalla ei ole mekanismia keskeyttää jo käynnissä olevaa työtä. Tämä on käytännössä järkevä kompromissi — yksittäisen sivun renderöinti on harvoin niin pitkä, että keskeytys kannattaisi lisäkomplleksisuuden hinnalla — mutta nopea vieritys, joka laukaisee CancelAll-kutsun jokaisella vieritystapahtumalla, maksaa silti sen yhden sivun hinnan, joka oli kesken renderöinnin kunkin peruutuksen hetkellä. Virallinen dokumentaatio on tästä suora: jo käynnissä oleva renderöinti saattaa valmistua ennen kuin säie päättyy

Execute-metodilla on toinen, helposti huomaamatta jäävä käytös: silmukka päättyy heti, kun se havaitsee jonon tyhjäksi, se ei jää jouten odottamaan lisätyötä. THPDFBackgroundRenderer-instanssi on siis kertaluonteinen eräajotyöläinen, ei pysyvä taustapalvelu — jonota kourallinen sivuja, kutsu Start-metodia, ja heti kun viimeinen jonotettu sivu on renderöity, taustalla oleva käyttöjärjestelmäsäie päättyy itsestään. RequestPage-metodin kutsuminen uudelleen samalla instanssilla sen jälkeen, kun Execute on jo tyhjentänyt jonon, ei käynnistä sitä uudelleen, minkä vuoksi yllä oleva RequestPageWindow korvaa renderöijäinstanssin jokaisella kutsulla sen sijaan, että yrittäisi jatkaa yhden pitkäikäisen olion ruokkimista

Onko TBitmap-olion koskettaminen taustasäikeestä turvallista Delphissä?

TBitmap-olion koskettaminen taustasäikeestä on turvallista HotPDF:n suunnittelussa, kunhan vain yksi säie koskaan käsittelee tiettyä bittikarttainstanssia kerrallaan, ja THPDFBackgroundRenderer valvoo tuota rajaa sen sijaan, että jättäisi sen kutsujan vastuulle. Execute renderöi jokaisen sivun asiakirjan omassa renderöintilukossa, samassa kriittisessä osiossa, jota jokainen RenderLoadedPageToBitmapCached-kutsu ja sisäänrakennettu PrefetchLoadedPages-esilataaja jo jakavat, joten varsinainen GDI-piirto tietylle sivulle tapahtuu täsmälleen yhdessä säikeessä kerrallaan eikä koskaan mene päällekkäin saman asiakirjan toisen renderöinnin kanssa. Tuloksena syntyvä bittikartta on työsäikeen omistama olio, jota THPDFBackgroundRenderer ei koskaan julkaise suoraan kutsujalle

GetCachedBitmap sen sijaan varaa täysin uuden TBitmap-olion ja kutsuu siihen Assign-metodia renderöijän omassa erillisessä lukossa, joten kopiointi tapahtuu aina samalla kun Execute on estetty korvaamasta tuota välimuistipaikkaa sen alta — kutsuva säie saa pikselidataa, ei koskaan alkuperäistä kahvaa. Tuo erottelu on myös syy vastustaa omaa mukautettua renderöintisäiettä, joka kutsuisi HotPDF:n renderöintifunktioita suoraan kulkematta THPDFBackgroundRenderer- tai PrefetchLoadedPages-luokan kautta: kaksi renderöintiä, jotka kilpailevat saman ladatun asiakirjan jaetuista välimuisteista ja olio-kaaviosta, on juuri se skenaario, jota HotPDF:n sisäinen lukitus on olemassa estämään, ja taustarenderöijäluokka antaa sinulle tuon lukituksen ilmaiseksi sen uudelleentoteuttamisen sijaan

Miten tämä eroaa HotPDF:n sisäänrakennetusta sivujen esilatauksesta?

PrefetchLoadedPages ja THPDFBackgroundRenderer ratkaisevat toisiinsa liittyviä mutta erilaisia ongelmia: PrefetchLoadedPages renderöi annetun sivualueen koko naapuruston jaettuun asiakirjavälimuistiin automaattisesti omalla työsäikeellään, ilman että kutsujan tarvitsee luoda tai hallita jono-oliota. THPDFBackgroundRenderer vaihtaa tuon automaation hallintaan — kutsuja päättää tarkalleen, mitkä sivuindeksit ovat merkityksellisiä ja missä järjestyksessä, ja voi peruuttaa yhä jonossa olevat koskematta siihen sivualueeseen, jota sisäänrakennettu esilataaja mahdollisesti lämmittelee muualla. Molemmat kulkevat saman renderöintilukon kautta, joten katseluohjelma voi ajaa PrefetchLoadedPages-toimintoa tavalliselle "seuraavat muutama sivu" -tapaukselle ja turvautua THPDFBackgroundRenderer-luokkaan vain, kun jotain tuon mallin ulkopuolista ilmenee, kuten pienoiskuvarivi, joka hyppää suoraan sivulle, jota käyttäjä juuri klikkasi

begin
  // PrefetchLoadedPages takes a 1-based "start-end" range string, while
  // RequestPage below stays 0-based like every other loaded-page index.
  Pdf.PrefetchLoadedPages(Format('%d-%d', [CenterPage + 1, CenterPage + 5]), 150);

  // Reach for THPDFBackgroundRenderer only for a page outside that
  // window, such as a thumbnail the user just clicked.
  FRenderer := THPDFBackgroundRenderer.Create(Pdf, 150);
  FRenderer.RequestPage(ClickedThumbnailPage);
  FRenderer.Start;
end;

Kaksi elinkaareen liittyvää yksityiskohtaa kannattaa viedä tuotantokoodiin. RenderLoadedPageToBitmapCached-metodin takana oleva koko asiakirjan kattava välimuisti on rajattu RenderCacheCapacity-arvolla, oletuksena kahdeksaan sivuun, ja häätää vähiten äskettäin käytetyn merkinnän täytyttyään, mutta THPDFBackgroundRenderer-instanssin omalla tuloslistalla ei ole tällaista rajaa — se pitää yhden bittikartan per erillinen sivuindeksi, jota tuon instanssin kautta on koskaan pyydetty, kunnes instanssi itse vapautetaan, joten koko vieritysistunnon ajan korkealla DPI:llä hengissä pidetty renderöijä kerää mielellään yhden täystarkkuuksisen bittikartan jokaista ohi vieritettyä sivua kohden. HotPDF ei myöskään peruuta kutsujan luomaa renderöijää automaattisesti samalla tavalla kuin se peruuttaa oman esilataajansa ennen asiakirjan lataamista tai tuhoamista, koska THPDFBackgroundRenderer-instanssia ei koskaan rekisteröidä sen osoittamaan THotPDF-olioon — joten kutsuvan koodin on peruutettava ja vapautettava jokainen asiakirjaa vasten rakennettu renderöijä ennen kyseisen asiakirjan uudelleenlatausta tai vapauttamista, sama järjestyskuri, jota HotPDF soveltaa sisäisesti PrefetchLoadedPages-toimintoon

THPDFBackgroundRenderer on yksi pala HotPDF:n MVC-katseluohjelma-arkkitehtuurin takana olevasta ladatun asiakirjan julkisivusta, ja se yhdistyy luontevasti tiedostotason työnkulkuihin artikkelissa Direct File API suurille PDF-tiedostoille, kun vieritettävä asiakirja on itsessään liian suuri ladattavaksi vapaasti alun perinkään. Taustarenderöinti, pyyntöjonot ja tässä kuvattu renderöintivälimuisti ovat kaikki osa vakiomuotoista HotPDF-komponenttia Delphille ja C++Builderille