Tehnični članak

Čakalna vrsta za izris PDF v ozadju v Delphiju s HotPDF

Razred THPDFBackgroundRenderer v HotPDF je potomec razreda TThread, ki naložene strani PDF izrisuje v bitne slike v delovni niti, zato lahko pregledovalnik Delphi še naprej drsi in se na novo izrisuje, medtem ko se stran v ozadju še rastrira. THPDFBackgroundRenderer.RequestPage doda indeks strani v čakalno vrsto te delovne niti, CancelAll odstrani vse, kar še čaka, GetCachedBitmap pa vrne dokončano bitno sliko, ki jo prevzame klicatelj in jo mora sprostiti. Če dvostranski skenirani dokument s 200 stranmi drsite pri ločljivosti za tiskanje samo v niti uporabniškega vmesnika, vsak prehod strani zaustavi okno, dokler GDI ne konča risanja, in prav to zatikanje odstrani THPDFBackgroundRenderer

Zakaj sploh izrisovati strani PDF v niti v ozadju?

Nit v ozadju upraviči svojo zapletenost, ker je upodabljalnik strani HotPDF pravi tolmač vsebinskega toka, ne poceni kopija bitne slike, ki se vrne, še preden bi kdo opazil: prehodi skozi operatorje PDF, vzdržuje sklad grafičnega stanja ter rasterizira poti, slike in glife prek GDI, istega mehanizma, ki ga uporablja tudi izris naloženih strani PDF v TBitmap. Če to delo izvajate sinhrono znotraj obravnavalnika drsenja ali risanja, se zanka sporočil preneha obdelovati, dokler se klic ne vrne, kar je dejanska zamrznitev okna. Vstavljanje Application.ProcessMessages v klic izrisa tega ne popravi: omogoči praznjenje čakalne vrste sporočil, vendar izris še vedno zaseda klicno nit, zato se okno hitreje izrisuje s staro vsebino, medtem ko se pravo delo ni nikamor premaknilo. Edini način, da pregledovalnik ostane odziven med resnično počasnim izrisom, je izvajanje tega izrisa drugje, zato THPDFBackgroundRenderer obstaja kot podrazred TThread in ne kot povratni klic ali časovnik

Vzpostavitev čakalne vrste zahtev za pregledovalnik z drsenjem

THPDFBackgroundRenderer.Create sprejme naloženi primerek THotPDF in DPI, ki ostane nespremenjen v celotni življenjski dobi tega izrisovalnika, zato se vsaka stran, dodana v čakalno vrsto prek istega primerka, izriše pri isti ločljivosti; pregledovalnik, ki podpira povečavo, potrebuje ob spremembi ravni povečave nov izrisovalnik, ne nove lastnosti DPI. RequestPage doda indeks strani v notranjo čakalno vrsto in se takoj vrne: sam ne izrisuje in se nikoli ne dotakne niti uporabniškega vmesnika. Execute, podedovana vstopna točka TThread, ki jo HotPDF zažene, ko pokličete Start, po en indeks naenkrat vzame z začetka te čakalne vrste, ga izriše prek predpomnilnika strani dokumenta in shrani kopijo, indeksirano po strani, da jo lahko GetCachedBitmap pozneje vrne

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 vrne nil, dokler kopija te strani ni pripravljena, zato zadošča vzorec preverjanja s časovnikom, kot je zgornji; ni ločenega dogodka pripravljenosti, ki bi ga bilo treba povezati, HotPDF to reši s preprostim preverjanjem nil namesto z večjim API-jem za obveščanje. Naslednji razdelek obravnava, kaj CancelAll in klic Free dejansko počneta, saj sta oba pomembna, ko se strani začnejo izrisovati v drugačnem vrstnem redu ali se drsenje zgodi hitreje, kot se lahko čakalna vrsta izprazni

Bližnjica z enim klicem za eno stran

THotPDF.RenderLoadedPageToBitmapAsync obstaja za običajen primer zagona natanko ene strani brez neposrednega dotikanja THPDFBackgroundRenderer: interno ustvari izrisovalnik, enkrat pokliče RequestPage, zažene nit in klicatelju vrne sklic TThread, ki ga klicatelj prevzame in je odgovoren za njegovo sprostitev. Pridobivanje rezultata poteka prek THotPDF.GetLoadedCachedRenderedBitmap in ne prek lastnega GetCachedBitmap izrisovalnika, ker GetLoadedCachedRenderedBitmap bere skupni predpomnilnik dokumenta, ključan po indeksu strani in DPI, isti predpomnilnik, ki ga že polnita RenderLoadedPageToBitmapCached in vgrajeni predhodni izris — stran, ki jo je drug del pregledovalnika že izrisal pri tem DPI, se lahko vrne takoj, še preden operacijski sistem sploh razporedi pravkar zagnano nit v ozadju

// 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;

Ali lahko prekličete stran, ki je že v čakalni vrsti?

CancelAll odstrani samo opravila, ki še čakajo v čakalni vrsti; stran, ki jo je HotPDF že vzel z začetka in predal klicu za izris, se dokonča, ker THPDFBackgroundRenderer nima mehanizma za prekinitev že potekajočega dela. V praksi je to razumen kompromis — en izris strani je redko dovolj dolg, da bi bilo izsiljevanje prednosti vredno dodatne zapletenosti — vendar hitro drsenje, ki ob vsakem dogodku drsenja sproži CancelAll, še vedno plača za tisto stran, ki se je v trenutku vsake prekinitve izrisovala. Uradna referenca je neposredna: že zagnani izris se lahko konča, preden se nit zaključi

Execute ima še eno vedenje, ki ga je lahko spregledati: zanka se konča takoj, ko najde prazno čakalno vrsto, ne pa da bi mirovala in čakala na novo delo. Primerek THPDFBackgroundRenderer je zato paketni delavec za enkratno uporabo, ne trajna storitev v ozadju — dodajte nekaj strani v čakalno vrsto, pokličite Start in osnovna nit operacijskega sistema se po izrisu zadnje čakajoče strani sama konča. Če po tem, ko je Execute že izpraznil čakalno vrsto, na istem primerku znova pokličete RequestPage, se ta ne zažene znova, zato RequestPageWindow zgoraj ob vsakem klicu zamenja primerek izrisovalnika, namesto da bi še naprej polnil en dolgo živeči objekt

Ali je v Delphiju varno dostopati do TBitmap iz niti v ozadju?

Dostop do TBitmap iz niti v ozadju je v zasnovi HotPDF varen, če z danim primerkom bitne slike vedno dela samo ena nit, THPDFBackgroundRenderer pa to mejo uveljavlja, namesto da bi jo prepustil klicatelju. Execute izriše vsako stran znotraj lastne zaklepnice za izris dokumenta, iste kritične sekcije, ki si jo delita vsak klic RenderLoadedPageToBitmapCached in vgrajeni predhodni izris PrefetchLoadedPages, zato se dejansko risanje GDI za dano stran vedno izvaja natanko v eni niti in se nikoli ne prekriva z drugim izrisom istega dokumenta. Nastala bitna slika je objekt v lasti delovne niti, ki ga THPDFBackgroundRenderer klicatelju nikoli ne objavi neposredno

GetCachedBitmap namesto tega ustvari povsem nov TBitmap in ga pod ločeno zaklepnico izrisovalnika napolni s klicem Assign, zato se kopiranje vedno izvede, medtem ko je Execute blokiran in ne more zamenjati tega predpomnilniškega mesta pod njim — klicna nit dobi slikovne podatke, nikoli izvirnega ročaja. Prav ta ločitev je tudi razlog, da se je treba upreti pisanju lastne niti za izris, ki bi neposredno klicala funkcije za izris HotPDF brez uporabe THPDFBackgroundRenderer ali PrefetchLoadedPages: dve izrisovanji, ki tekmujeta za skupne predpomnilnike in graf objektov istega naloženega dokumenta, sta natančno scenarij, ki ga preprečuje notranje zaklepanje HotPDF, razred izrisovalnika v ozadju pa vam to zaklepanje zagotovi brez ponovne izvedbe

Kako se to razlikuje od vgrajenega predhodnega izrisa HotPDF?

PrefetchLoadedPages in THPDFBackgroundRenderer rešujeta sorodni, vendar različni težavi: PrefetchLoadedPages pri danem obsegu strani celotno okolico samodejno izriše v skupni predpomnilnik dokumenta v lastni delovni niti, pri čemer klicatelju ni treba ustvariti ali upravljati objekta čakalne vrste. THPDFBackgroundRenderer to avtomatizacijo zamenja za nadzor — klicatelj natančno določi, kateri indeksi strani so pomembni in v kakšnem vrstnem redu, ter lahko prekliče tiste, ki še čakajo, ne da bi posegel v obseg, ki ga vgrajeni predhodni izris ogreva drugje. Oba uporabljata isto zaklepnico za izris, zato lahko pregledovalnik za običajen primer naslednjih nekaj strani zažene PrefetchLoadedPages, po THPDFBackgroundRenderer pa poseže le, ko se pojavi nekaj zunaj tega vzorca, na primer trak s sličicami, ki skoči neposredno na stran, ki jo je uporabnik pravkar kliknil

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;

Dve podrobnosti življenjskega cikla je vredno prenesti v produkcijsko kodo. Predpomnilnik na ravni dokumenta za RenderLoadedPageToBitmapCached je omejen z RenderCacheCapacity, privzeto na osem strani, in ob zapolnitvi odstrani najdlje neuporabljen vnos, lastni seznam rezultatov primerka THPDFBackgroundRenderer pa nima take omejitve — hrani eno bitno sliko za vsak različen indeks strani, ki je bil kdaj zahtevan prek tega primerka, dokler se primerek ne sprosti, zato lahko izrisovalnik, ki pri visokem DPI ostane živ celotno sejo drsenja, brez težav nabere eno bitno sliko polne ločljivosti za vsako predrsano stran. HotPDF prav tako ne prekliče samodejno izrisovalnika, ki ga ustvari klicatelj, tako kot prekliče lastni predhodni izris, preden se dokument naloži ali uniči, saj primerek THPDFBackgroundRenderer ni nikoli registriran na objektu THotPDF, na katerega kaže — klicajoča koda mora zato preklicati in sprostiti vsak izrisovalnik, ustvarjen za dokument, preden ta dokument znova naloži ali sprosti, po istem vrstnem redu, kot ga HotPDF interno uporablja za PrefetchLoadedPages

THPDFBackgroundRenderer je del fasade naloženega dokumenta za arhitekturo pregledovalnika MVC v HotPDF in se naravno dopolnjuje z delovnimi tokovi na ravni datotek v neposrednem API-ju za velike datoteke PDF, ko je dokument, po katerem drsite, že sam po sebi prevelik za brezskrbno nalaganje. Izris v ozadju, čakalne vrste zahtev in tukaj opisani predpomnilnik izrisa so del standardne komponente HotPDF za Delphi in C++Builder