Tehnički članak

Red zahteva za pozadinsko iscrtavanje PDF stranica u Delphiju uz HotPDF

Klasa THPDFBackgroundRenderer u HotPDF-u naslednica je klase TThread i iscrtava učitane PDF stranice u bitmape na radnoj niti, pa Delphi pregledač može da nastavi sa pomeranjem i ponovnim iscrtavanjem dok se stranica i dalje rasterizuje u pozadini. THPDFBackgroundRenderer.RequestPage stavlja indeks stranice u red za tu radnu nit, CancelAll uklanja sve što još čeka, a GetCachedBitmap vraća gotovu bitmapu čiji je vlasnik pozivalac i koji mora da je oslobodi. Ako skrolujete kroz skenirani ugovor od dve stotine stranica u rezoluciji za štampu samo na UI niti, svaki prelazak na novu stranicu zaustavlja prozor dok GDI ne završi iscrtavanje, upravo ono zastajkivanje koje THPDFBackgroundRenderer uklanja

Zašto uopšte iscrtavati PDF stranice u pozadinskoj niti?

Pozadinska nit opravdava svoju složenost zato što je HotPDF-ov iscrtavač stranica pravi interpreter sadržajnog toka, a ne jeftina kopija bitmape koja se završi pre nego što iko primeti: prolazi kroz PDF operatore, održava stek stanja grafike i rasterizuje putanje, slike i glifove kroz GDI, isti mehanizam opisan u tekstu o iscrtavanju učitanih PDF stranica u TBitmap. Ako taj posao pokrenete sinhrono u rukovaocu pomeranja ili iscrtavanja, petlja poruka prestaje da obrađuje događaje dok se poziv ne završi, što zapravo predstavlja zamrznut prozor. Umetanje Application.ProcessMessages u poziv za iscrtavanje ne rešava problem: red poruka se prazni, ali sam posao iscrtavanja i dalje zauzima nit pozivaoca, pa se prozor brže ponovo iscrtava starim sadržajem dok se stvarni posao nije pomerio ni korak. Jedini način da pregledač ostane odzivan tokom zaista sporog iscrtavanja jeste da taj posao pokrenete negde drugde, zbog čega THPDFBackgroundRenderer postoji kao podklasa klase TThread, a ne kao povratni poziv ili tajmer

Postavljanje reda zahteva za pregledač sa pomeranjem

THPDFBackgroundRenderer.Create prima učitanu instancu THotPDF i DPI koji ostaje nepromenjen tokom celog životnog veka tog iscrtavača, pa se svaka stranica stavljena u red kroz jednu instancu iscrtava u istoj rezoluciji; pregledaču koji podržava zumiranje potrebna je nova instanca iscrtavača, a ne nova vrednost DPI-ja, kad god se nivo uvećanja promeni. RequestPage dodaje indeks stranice u internu listu i odmah se vraća: sam ne iscrtava ništa i nikada ne dodiruje UI nit. Execute, nasleđena ulazna tačka klase TThread koju HotPDF pokreće kada pozovete Start, uzima jedan indeks sa početka reda, iscrtava ga kroz keš stranica dokumenta i čuva kopiju indeksiranu po stranici kako bi je GetCachedBitmap kasnije vratio

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 vraća nil dok kopija te stranice ne bude spremna, pa je obrazac provere pomoću tajmera, poput prethodnog primera, dovoljan; nije potrebno povezivati poseban događaj spremnosti, jer HotPDF ovo rešava običnom proverom vrednosti nil umesto većeg API-ja za obaveštavanje. Sledeći odeljak objašnjava šta CancelAll i poziv Free zapravo rade, jer su oba važna kada stranice počnu da se iscrtavaju van redosleda ili se pomeranje odvija brže nego što red može da se isprazni

Prečica sa jednim pozivom za jednu stranicu

THotPDF.RenderLoadedPageToBitmapAsync postoji za uobičajen slučaj pokretanja iscrtavanja tačno jedne stranice bez direktnog korišćenja THPDFBackgroundRenderer: interno konstruiše iscrtavač, jednom poziva RequestPage, pokreće nit i vraća referencu na TThread čiji je vlasnik pozivalac i koji je odgovoran za njeno oslobađanje. Rezultat se preuzima preko THotPDF.GetLoadedCachedRenderedBitmap, a ne preko sopstvene metode GetCachedBitmap iscrtavača, zato što GetLoadedCachedRenderedBitmap čita zajednički keš dokumenta indeksiran indeksom stranice i DPI-jem, isti keš koji već pune RenderLoadedPageToBitmapCached i ugrađeni prethodni učitavač — stranica koju je neki drugi deo pregledača već iscrtavao pri tom DPI-ju može da se vrati odmah, pre nego što operativni sistem uopšte zakaže upravo pokrenutu pozadinsku nit

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

Može li se otkazati stranica koja je već u redu?

CancelAll uklanja samo poslove koji još čekaju u redu; stranica koju je HotPDF već uzeo sa početka reda i predao pozivu za iscrtavanje nastavlja do kraja, jer THPDFBackgroundRenderer nema mehanizam za prekid već započetog posla. To je razumna praktična zamena — iscrtavanje jedne stranice retko traje dovoljno dugo da bi prekid opravdao dodatnu složenost — ali brzo pomeranje koje poziva CancelAll pri svakom događaju pomeranja i dalje plaća cenu one stranice koja se u trenutku svakog otkazivanja već iscrtavala. Zvanična referenca je jasna: iscrtavanje koje je već u toku može da se završi pre nego što se nit prekine

Execute ima i drugo ponašanje koje se lako previda: petlja se završava čim utvrdi da je red prazan, ne miruje čekajući da stignu novi poslovi. Instanca THPDFBackgroundRenderer zato je jednokratni radnik za paketnu obradu, a ne trajna pozadinska usluga — stavite nekoliko stranica u red, pozovite Start i, kada se iscrtavanje poslednje stranice završi, osnovna nit operativnog sistema sama se završava. Ponovni poziv RequestPage na istoj instanci nakon što je Execute već ispraznio red ne pokreće je ponovo, upravo zato što RequestPageWindow iz prethodnog primera pri svakom pozivu zamenjuje instancu iscrtavača umesto da pokušava da dopunjuje jedan dugovečni objekat

Da li je u Delphiju bezbedno pristupiti objektu TBitmap iz pozadinske niti?

Pristup objektu TBitmap iz pozadinske niti bezbedan je u HotPDF-ovom dizajnu sve dok samo jedna nit u datom trenutku radi sa konkretnom instancom bitmape, a THPDFBackgroundRenderer nameće tu granicu umesto da je prepušta pozivaocu. Execute iscrtava svaku stranicu unutar sopstvene blokade za iscrtavanje dokumenta, istog kritičnog odseka koji dele svaki poziv RenderLoadedPageToBitmapCached i ugrađeni prethodni učitavač PrefetchLoadedPages, pa se stvarno GDI iscrtavanje određene stranice uvek odvija tačno na jednoj niti i nikada se ne preklapa sa drugim iscrtavanjem tog dokumenta. Dobijeni objekat bitmape pripada radnoj niti i THPDFBackgroundRenderer ga nikada ne objavljuje direktno pozivaocu

GetCachedBitmap umesto toga alocira potpuno novu instancu TBitmap i poziva Assign nad njom pod zasebnom blokadom iscrtavača, pa se kopiranje uvek odvija dok Execute ne može da zameni taj slot keša ispod nje — pozivajuća nit dobija podatke o pikselima, a ne originalni handle. To razdvajanje je i razlog da ne pravite prilagođenu nit za iscrtavanje koja direktno poziva HotPDF funkcije za iscrtavanje bez THPDFBackgroundRenderer ili PrefetchLoadedPages: dva konkurentna iscrtavanja protiv zajedničkih kešova i grafa objekata istog učitanog dokumenta upravo su scenario koji HotPDF-ovo interno zaključavanje sprečava, a klasa za pozadinsko iscrtavanje daje vam to zaključavanje bez potrebe da ga ponovo implementirate

Po čemu se ovo razlikuje od HotPDF-ovog ugrađenog prethodnog učitavanja stranica?

PrefetchLoadedPages i THPDFBackgroundRenderer rešavaju srodne, ali različite probleme: PrefetchLoadedPages, kada dobije opseg stranica, automatski iscrtava celo okolno područje u zajednički keš dokumenta na sopstvenoj radnoj niti, bez objekta reda koji bi pozivalac morao da kreira ili održava. THPDFBackgroundRenderer tu automatizaciju menja kontrolom — pozivalac tačno određuje koji su indeksi stranica važni i kojim redom, pa može da otkaže one koji još čekaju bez uticaja na opseg koji ugrađeni prethodni učitavač zagreva na drugom mestu. Oba koriste istu blokadu za iscrtavanje, pa pregledač može da koristi PrefetchLoadedPages za uobičajen slučaj nekoliko sledećih stranica, a da posegne za THPDFBackgroundRenderer samo kada se pojavi nešto izvan tog obrasca, na primer traka sličica koja skače pravo na stranicu koju je korisnik upravo izabrao

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;

Dva detalja životnog ciklusa vredi preneti u produkcioni kod. Keš na nivou dokumenta iza RenderLoadedPageToBitmapCached ograničen je vrednošću RenderCacheCapacity, podrazumevano na osam stranica, i kada se napuni izbacuje stavku koja je najduže bila nekorišćena, ali sopstvena lista rezultata instance THPDFBackgroundRenderer nema takvo ograničenje — čuva po jednu bitmapu za svaki različiti indeks stranice ikada zatražen kroz tu instancu sve dok se sama instanca ne oslobodi, pa će iscrtavač koji ostane živ tokom cele sesije pomeranja pri visokom DPI-ju rado nagomilati po jednu bitmapu pune rezolucije za svaku stranicu pored koje se prošlo. HotPDF takođe ne otkazuje automatski iscrtavač koji je napravio pozivalac na način na koji otkazuje sopstveni prethodni učitavač pre učitavanja ili uništavanja dokumenta, jer instanca THPDFBackgroundRenderer nikada nije registrovana na objektu THotPDF na koji pokazuje — zato pozivajući kod mora da otkaže i oslobodi svaki iscrtavač napravljen za dokument pre ponovnog učitavanja ili oslobađanja tog dokumenta, po istom redosledu koji HotPDF interno primenjuje na PrefetchLoadedPages

THPDFBackgroundRenderer je deo fasade učitanog dokumenta iza HotPDF-ove MVC arhitekture pregledača i prirodno se nadovezuje na tokove rada na nivou datoteke u Direct File API-ju za velike PDF-ove kada je dokument kroz koji se pomerate sam po sebi prevelik za bezbrižno učitavanje. Pozadinsko iscrtavanje, redovi zahteva i keš iscrtavanja opisani ovde deo su standardne HotPDF Component komponente za Delphi i C++Builder