Műszaki cikk

Háttérbeli PDF-renderelési várólista Delphiben a HotPDF-fel

A HotPDF THPDFBackgroundRenderer osztálya egy TThread-leszármazott, amely a betöltött PDF-oldalakat bitképekké rendereli egy munkásszálon, így egy Delphi megjelenítő tovább görgethet és rajzolhat, miközben egy oldal még raszterizálás alatt áll a háttérben. A THPDFBackgroundRenderer.RequestPage egy oldalindexet állít sorba ennek a munkásszálnak, a CancelAll eldob mindent, ami még várakozik, a GetCachedBitmap pedig visszaad egy kész bitképet, amelyet a hívó birtokol, és amelyet fel kell szabadítania. Görgess végig egy kétszáz oldalas beszkennelt szerződést nyomtatási felbontásban kizárólag a UI-szálon, és minden lapozás megállítja az ablakot, amíg a GDI befejezi a rajzolást, ez pontosan az a döccenés, amelynek megszüntetésére a THPDFBackgroundRenderer létezik

Miért érdemes egyáltalán háttérszálon renderelni a PDF-oldalakat?

Egy háttérszál azért éri meg a bonyolultságát, mert a HotPDF oldal-renderelője valódi tartalomfolyam-értelmező, nem pedig egy olcsó bitkép-másolat, amely azelőtt visszatér, hogy bárki észrevenné: PDF-operátorokat jár be, egy grafikai-állapot vermet tart fenn, és útvonalakat, képeket és glyph-eket raszterizál GDI-n keresztül, ugyanazzal a motorral, amelyet a betöltött PDF-oldalak TBitmap-be renderelése tárgyal. Futtasd ezt a munkát szinkronban egy görgetési vagy rajzolási kezelőn belül, és az üzenethurok abbahagyja a pumpálást, amíg a hívás vissza nem tér, és pontosan ez az, ami egy lefagyott ablak. Az Application.ProcessMessages beillesztése a renderelő hívásba nem oldja meg ezt: hagyja, hogy az üzenetsor kiürüljön, de maga a renderelés még mindig a hívó szálat birtokolja, így az ablak gyorsabban rajzolja újra az elavult tartalmat, miközben a valódi munka semerre nem haladt. Az egyetlen mód, hogy egy megjelenítő fürge maradjon egy valóban lassú renderelés alatt, az, hogy azt a renderelést valahol máshol futtatjuk, és ezért létezik a THPDFBackgroundRenderer TThread-alosztályként, nem pedig callback-ként vagy időzítőként

Kérésvárólista beállítása egy görgethető megjelenítőhöz

A THPDFBackgroundRenderer.Create átveszi a betöltött THotPDF-példányt és egy DPI-t, amely fixen marad ennek a renderelőnek a teljes élettartama alatt, így minden oldal, amely egy példányon keresztül kerül sorba, egyetlen felbontáson rendereli; egy megjelenítőnek, amely támogatja a nagyítást, egy friss renderelőre van szüksége, nem egy friss DPI-tulajdonságra, valahányszor a nagyítási szint megváltozik. A RequestPage hozzáfűz egy oldalindexet egy belső várólistához, és azonnal visszatér: maga nem végez renderelést, és soha nem nyúl a UI-szálhoz. Az Execute, az örökölt TThread belépési pont, amelyet a HotPDF egyszer futtat, amint meghívod a Start-ot, egyszerre egy indexet húz le a várólista elejéről, rendereli azt a dokumentum oldal-gyorsítótárán keresztül, és tárol egy másolatot oldal szerint indexelve, hogy a GetCachedBitmap később vissza tudja adni

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;

A GetCachedBitmap nil-t ad vissza, amíg az adott oldal másolata nem áll készen, így egy időzítőre épülő lekérdezési minta, mint a fenti, elegendő; nincs külön "kész" esemény bekötendő, a HotPDF ezt egy egyszerű nil-ellenőrzéssel oldja meg egy nagyobb értesítési API helyett. A következő szakasz tárgyalja, mit is csinál valójában a CancelAll és az a Free hívás, mert mindkettő számít, amint az oldalak sorrenden kívül kezdenek renderelni, vagy egy görgetés gyorsabban történik, mint ahogy a várólista ki tudna ürülni

Az egyhívásos gyorsút egyetlen oldalhoz

A THotPDF.RenderLoadedPageToBitmapAsync azért létezik, hogy a gyakori esetet lefedje, amikor pontosan egy oldalt indítunk el anélkül, hogy közvetlenül nyúlnánk a THPDFBackgroundRenderer-hez: belsőleg felépíti a renderelőt, egyszer meghívja a RequestPage-et, elindítja a szálat, és visszaadja a TThread-referenciát a hívónak, aki birtokolja azt, és felelős a felszabadításáért. Az eredmény lekérése a THotPDF.GetLoadedCachedRenderedBitmap-en keresztül történik, nem a renderelő saját GetCachedBitmap-ján, mert a GetLoadedCachedRenderedBitmap a dokumentum megosztott, oldalindex és DPI szerint kulcsolt gyorsítótárát olvassa, ugyanazt a gyorsítótárat, amelyet a RenderLoadedPageToBitmapCached és a beépített előtöltő már feltölt — egy oldal, amelyet a megjelenítő egy másik része már renderelt azon a DPI-n, azonnal visszajöhet, még mielőtt az imént épp csak elindított háttérszálat egyáltalán ütemezte volna az operációs rendszer

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

Megszakítható egy oldal, amely már sorba került?

A CancelAll csak azokat a feladatokat távolítja el, amelyek még a várólistában ülnek; egy oldal, amelyet a HotPDF már lehúzott az elejéről, és átadott a renderelési hívásának, folytatódik a befejezésig, mert a THPDFBackgroundRenderer-nek nincs mechanizmusa a már folyamatban lévő munka megszakítására. Ez a gyakorlatban ésszerű kompromisszum — egyetlen oldal renderelése ritkán elég hosszú ahhoz, hogy a megelőzés (preemption) megérje a hozzáadott bonyolultságot —, de egy gyors görgetés, amely minden görgetési eseménynél kilövi a CancelAll-t, még mindig megfizet azért az egy oldalért, amely éppen renderelés közben volt minden egyes megszakítás pillanatában. A hivatalos dokumentáció egyértelmű ebben: a már futó renderelés befejeződhet, mielőtt a szál leáll

Az Execute-nak van egy második, könnyen kihagyható viselkedése: a ciklus kilép, amint üresnek találja a várólistát, nem tétlenkedik, és nem várja meg, hogy több munka érkezzen. Egy THPDFBackgroundRenderer-példány tehát egyszeri kötegelt munkás, nem tartós háttérszolgáltatás — állíts sorba néhány oldalt, hívd meg a Start-ot, és amint az utolsó sorba állított oldal renderelődött, az alatta lévő operációs rendszer-szál magától véget ér. A RequestPage ismételt hívása ugyanazon a példányon, miután az Execute már kiürítette a várólistát, nem indítja újra azt, és pontosan ez az oka annak, hogy a fenti RequestPageWindow minden hívásnál lecseréli a renderelő-példányt, ahelyett hogy megpróbálná tovább táplálni ugyanazt a hosszú életű objektumot

Biztonságos-e Delphiben egy TBitmap-hez nyúlni egy háttérszálról?

Egy TBitmap-hez való nyúlás egy háttérszálból biztonságos a HotPDF felépítésében, amíg egy adott bitkép-példányon mindig csak egy szál dolgozik, és a THPDFBackgroundRenderer betartatja ezt a határt ahelyett, hogy a hívóra hagyná. Az Execute minden oldalt a dokumentum saját renderelési zárján belül rendereli, ugyanabban a kritikus szakaszban, amelyet minden RenderLoadedPageToBitmapCached hívás és a beépített PrefetchLoadedPages előtöltő már megoszt, így a tényleges GDI-rajzolás egy adott oldalhoz mindig pontosan egy szálon történik, és soha nem fedi át annak a dokumentumnak egy másik renderelését. Az eredményül kapott bitkép egy munkásszál tulajdonában lévő objektum, amelyet a THPDFBackgroundRenderer soha nem tesz közzé közvetlenül a hívó felé

A GetCachedBitmap ehelyett egy vadonatúj TBitmap-et allokál, és Assign-t hív rá a renderelő saját, külön zárja alatt, így a másolás mindig akkor történik, amikor az Execute épp nem cserélheti le azt a gyorsítótár-helyet alatta — a hívó szál pixeladatot kap, soha nem az eredeti handle-t. Ez az elkülönítés az oka annak is, hogy miért érdemes tartózkodni attól, hogy valaki saját renderelő szálat gördítsen, amely közvetlenül hívja a HotPDF renderelő-függvényeit anélkül, hogy a THPDFBackgroundRenderer-en vagy a PrefetchLoadedPages-en keresztül menne: két renderelés, amely versenyben van ugyanannak a betöltött dokumentumnak a megosztott gyorsítótáraiért és objektumgráfjáért, pontosan az a forgatókönyv, amelynek megelőzésére a HotPDF belső zárolása létezik, és a háttér-renderelő osztály ingyen adja meg ezt a zárolást ahelyett, hogy újra kellene azt valósítani

Miben különbözik ez a HotPDF beépített oldal-előtöltésétől?

A PrefetchLoadedPages és a THPDFBackgroundRenderer rokon, de eltérő problémákat old meg: a PrefetchLoadedPages, egy oldaltartományt kapva, automatikusan renderálja azt az egész szomszédságot a megosztott dokumentum-gyorsítótárba a saját munkásszálán, anélkül hogy a hívónak kellene létrehoznia vagy kezelnie egy várólista-objektumot. A THPDFBackgroundRenderer ezt az automatizálást vezérlésre cseréli — a hívó dönti el pontosan, mely oldalindexek számítanak és milyen sorrendben, és megszakíthatja azokat, amelyek még sorban vannak, anélkül hogy hozzányúlna ahhoz a tartományhoz, amelyet a beépített előtöltő máshol éppen melegít. Mindkettő ugyanazon a renderelési zárolóin keresztül halad, így egy megjelenítő futtathatja a PrefetchLoadedPages-t a szokásos következő-néhány-oldal esetre, és csak akkor nyúlhat a THPDFBackgroundRenderer-hez, amikor valami ezen a mintán kívüli dolog merül fel, például egy miniatűrsáv, amely egyenesen egy olyan oldalra ugrik, amelyre a felhasználó éppen kattintott

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;

Két életciklus-részletet érdemes átvinni a termelési kódba. A RenderLoadedPageToBitmapCached mögötti dokumentum-szintű gyorsítótár korlátos a RenderCacheCapacity által, alapértelmezetten nyolc oldal, és kilöki a legrégebben használt bejegyzést, amint megtelik, de egy THPDFBackgroundRenderer-példány saját eredménylistájának nincs ilyen korlátja — egyetlen bitképet tart meg minden egyes, azon a példányon keresztül valaha kért, különálló oldalindexhez, amíg maga a példány fel nem szabadul, így egy renderelő, amelyet egy teljes görgetési munkamenet alatt életben tartunk nagy DPI-n, szívesen felhalmoz egy teljes felbontású bitképet minden elgörgetett oldalhoz. A HotPDF nem szakítja meg automatikusan a hívó által létrehozott renderelőt sem úgy, ahogyan a saját előtöltőjét szakítja meg egy dokumentum betöltése előtt, vagy önmagát semmisíti meg, mivel egy THPDFBackgroundRenderer-példány soha nincs regisztrálva azon a THotPDF-objektumon, amelyre mutat — így a hívó kódnak minden, egy dokumentum ellen épített renderelőt meg kell szakítania és fel kell szabadítania, mielőtt azt a dokumentumot újratöltené vagy felszabadítaná, ugyanaz a sorrendi fegyelem, amelyet a HotPDF belsőleg alkalmaz a PrefetchLoadedPages-re

A THPDFBackgroundRenderer egy darabja a HotPDF MVC megjelenítő-architektúrája mögötti betöltött-dokumentum homlokzatnak, és természetesen párosul a nagy PDF-ekhez készült Direct File API fájl-szintű munkafolyamataival, amikor a görgetett dokumentum önmagában is túl nagy ahhoz, hogy eleve könnyedén betöltsük. Az itt leírt háttérrenderelés, kérésvárólisták és renderelési gyorsítótár mind a Delphihez és C++Builderhez készült szabványos HotPDF komponens részei