Articol tehnic

Coadă de randare PDF în fundal în Delphi cu HotPDF

Clasa THPDFBackgroundRenderer a HotPDF este un descendent de TThread care randează paginile PDF încărcate în bitmap-uri pe un thread worker, astfel încât un vizualizator Delphi poate continua să deruleze și să se redeseneze în timp ce o pagină este încă rasterizată în fundal. THPDFBackgroundRenderer.RequestPage pune în coadă un index de pagină pentru acel thread worker, CancelAll elimină tot ce mai așteaptă, iar GetCachedBitmap returnează un bitmap finalizat pe care apelantul îl deține și trebuie să îl elibereze. Derulați un contract scanat de două sute de pagini la rezoluție de tipar doar pe thread-ul UI, și fiecare întoarcere de pagină pune fereastra pe pauză până când GDI termină de desenat-o, exact bâlbâiala pe care THPDFBackgroundRenderer există pentru a o elimina

De ce randăm deloc paginile PDF pe un thread în fundal?

Un thread în fundal își merită complexitatea pentru că renderer-ul de pagini al HotPDF este un interpretor de flux de conținut autentic, nu o copiere ieftină de bitmap care revine înainte ca cineva să observe: parcurge operatori PDF, păstrează o stivă de stare grafică și rasterizează trasee, imagini și glife prin GDI, același motor acoperit în randarea paginilor PDF încărcate într-un TBitmap. Rulați această muncă sincron în interiorul unui handler de derulare sau de desenare, și bucla de mesaje se oprește din pompare până când apelul revine, ceea ce este exact ce înseamnă o fereastră înghețată. Introducerea Application.ProcessMessages în interiorul apelului de randare nu rezolvă asta: permite golirea cozii de mesaje, dar randarea în sine tot deține thread-ul apelant, așa că fereastra redesenă mai rapid conținutul perimat, în timp ce munca reală nu a avansat nicăieri. Singurul mod de a păstra un vizualizator receptiv în timpul unei randări cu adevărat lente este să rulați acea randare în altă parte, motiv pentru care THPDFBackgroundRenderer există ca o subclasă de TThread, în loc de un callback sau un timer

Configurarea unei cozi de cereri pentru un vizualizator cu derulare

THPDFBackgroundRenderer.Create preia instanța THotPDF încărcată și un DPI care rămâne fix pe toată durata de viață a acelui renderer, astfel încât fiecare pagină pusă în coadă printr-o instanță se randează la o singură rezoluție; un vizualizator care suportă zoom are nevoie de un renderer nou, nu de o proprietate DPI nouă, ori de câte ori nivelul de zoom se schimbă. RequestPage adaugă un index de pagină la o coadă internă și revine imediat: nu face nicio randare el însuși și nu atinge niciodată thread-ul UI. Execute, punctul de intrare moștenit de TThread pe care HotPDF îl rulează odată ce apelați Start, extrage câte un index de la începutul acelei cozi pe rând, îl randează prin cache-ul de pagini al documentului și stochează o copie indexată după pagină, astfel încât GetCachedBitmap o poate returna mai târziu

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 returnează nil până când copia acelei pagini este gata, așa că un tipar de interogare periodică pe un timer, precum cel de mai sus, este suficient; nu există niciun eveniment separat de „gata” de conectat, HotPDF rezolvă asta printr-o simplă verificare nil, în loc de un API de notificare mai mare. Secțiunea următoare acoperă ce fac de fapt CancelAll și acel apel Free, pentru că ambele contează odată ce paginile încep să se randeze în afara ordinii sau o derulare se întâmplă mai repede decât poate coada să se golească

Scurtătura cu un singur apel pentru o singură pagină

THotPDF.RenderLoadedPageToBitmapAsync există pentru cazul comun de a lansa exact o singură pagină fără a atinge direct THPDFBackgroundRenderer: construiește renderer-ul intern, apelează RequestPage o singură dată, pornește thread-ul și returnează referința TThread apelantului, care o deține și este responsabil de eliberarea ei. Recuperarea rezultatului trece prin THotPDF.GetLoadedCachedRenderedBitmap, nu prin propriul GetCachedBitmap al renderer-ului, pentru că GetLoadedCachedRenderedBitmap citește cache-ul partajat al documentului, indexat după indexul de pagină și DPI, același cache pe care RenderLoadedPageToBitmapCached și prefetcher-ul încorporat îl populează deja — o pagină pe care o altă parte a vizualizatorului a randat-o deja la acel DPI poate reveni imediat, înainte ca thread-ul din fundal abia pornit să fi fost măcar programat de sistemul de operare

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

Puteți anula o pagină deja pusă în coadă?

CancelAll elimină doar sarcinile care încă stau în coadă; o pagină pe care HotPDF a extras-o deja de la început și a predat-o apelului său de randare continuă până la finalizare, pentru că THPDFBackgroundRenderer nu are niciun mecanism de a întrerupe munca deja în curs. Acesta este un compromis rezonabil în practică — o singură randare de pagină este rareori suficient de lungă încât să merite complexitatea suplimentară a preempțiunii — dar o derulare rapidă care declanșează CancelAll la fiecare eveniment de derulare tot plătește pentru orice pagină era la jumătatea randării în momentul fiecărei anulări. Referința oficială este directă în privința asta: randarea deja în curs se poate termina înainte ca thread-ul să se închidă

Execute are un al doilea comportament, ușor de ratat: bucla iese de îndată ce găsește coada goală, nu stă inactivă și nu așteaptă să sosească mai multă muncă. O instanță THPDFBackgroundRenderer este deci un worker de lot cu o singură rundă, nu un serviciu persistent de fundal — puneți în coadă câteva pagini, apelați Start, iar odată ce ultima pagină din coadă s-a randat, thread-ul de sistem de operare de dedesubt se termină singur. Apelarea din nou a RequestPage pe aceeași instanță, după ce Execute a golit deja coada, nu o repornește, motiv exact pentru care RequestPageWindow de mai sus înlocuiește instanța renderer-ului la fiecare apel, în loc să încerce să continue să hrănească un obiect de viață lungă

Este sigur să atingeți un TBitmap dintr-un thread în fundal în Delphi?

Atingerea unui TBitmap dintr-un thread în fundal este sigură în designul HotPDF, atâta timp cât un singur thread operează vreodată pe o instanță de bitmap dată la un moment dat, iar THPDFBackgroundRenderer impune acea limită, în loc să o lase în seama apelantului. Execute randează fiecare pagină în interiorul propriei blocări de randare a documentului, aceeași secțiune critică pe care fiecare apel RenderLoadedPageToBitmapCached și prefetcher-ul încorporat PrefetchLoadedPages o partajează deja, astfel încât desenarea GDI efectivă pentru o pagină dată se întâmplă pe exact un singur thread la un moment dat și nu se suprapune niciodată cu o altă randare a acelui document. Bitmap-ul rezultat este un obiect deținut de thread-ul worker, pe care THPDFBackgroundRenderer nu îl publică niciodată direct unui apelant

GetCachedBitmap alocă în schimb un TBitmap complet nou și apelează Assign pe el sub propria blocare separată a renderer-ului, astfel încât copierea se întâmplă întotdeauna în timp ce Execute este blocat de la înlocuirea acelui slot de cache de dedesubt — thread-ul apelant primește date de pixeli, niciodată handle-ul original. Această separare este de asemenea motivul pentru a rezista tentației de a construi un thread de randare personalizat care apelează funcțiile de randare ale HotPDF direct, fără a trece prin THPDFBackgroundRenderer sau PrefetchLoadedPages: două randări concurente pe cache-urile partajate și graful de obiecte ale aceluiași document încărcat este exact scenariul pentru care există blocarea internă a HotPDF, iar clasa renderer-ului în fundal vă oferă acea blocare gratuit, în loc să o reimplementați

Cum diferă asta de preîncărcarea încorporată de pagini a HotPDF?

PrefetchLoadedPages și THPDFBackgroundRenderer rezolvă probleme înrudite, dar diferite: PrefetchLoadedPages, dat un interval de pagini, randează întreaga acea vecinătate în cache-ul partajat al documentului automat, pe propriul său thread worker, fără niciun obiect de coadă pe care apelantul trebuie să îl creeze sau gestioneze. THPDFBackgroundRenderer schimbă acea automatizare pentru control — apelantul decide exact ce indexuri de pagină contează și în ce ordine, și poate anula pe cele încă puse în coadă fără a atinge orice interval pe care prefetcher-ul încorporat îl încălzește în altă parte. Ambele se canalizează prin aceeași blocare de randare, astfel încât un vizualizator poate rula PrefetchLoadedPages pentru cazul obișnuit al următoarelor câteva pagini și poate apela la THPDFBackgroundRenderer doar când apare ceva în afara acelui tipar, cum ar fi o bandă de miniaturi care sare direct la o pagină pe care utilizatorul tocmai a dat clic

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;

Merită dus mai departe în codul de producție două detalii de ciclu de viață. Cache-ul la nivel de document din spatele RenderLoadedPageToBitmapCached este limitat de RenderCacheCapacity, opt pagini în mod implicit, și evacuează intrarea cel mai puțin recent folosită odată plin, dar propria listă de rezultate a unei instanțe THPDFBackgroundRenderer nu are o astfel de limită — păstrează câte un bitmap pentru fiecare index de pagină distinct cerut vreodată prin acea instanță, până când instanța însăși este eliberată, așa că un renderer păstrat viu pe durata întregii sesiuni de derulare la DPI mare va acumula fericit câte un bitmap la rezoluție completă pentru fiecare pagină derulată. HotPDF de asemenea nu anulează automat un renderer creat de apelant așa cum își anulează propriul prefetcher înainte ca un document să se încarce sau să se distrugă, pentru că o instanță THPDFBackgroundRenderer nu este niciodată înregistrată pe obiectul THotPDF către care indică — așa că codul apelant trebuie să anuleze și să elibereze fiecare renderer construit pentru un document înainte de a reîncărca sau elibera acel document, aceeași disciplină de ordonare pe care HotPDF o aplică intern lui PrefetchLoadedPages

THPDFBackgroundRenderer este o piesă a fațadei de document încărcat din spatele arhitecturii de vizualizator MVC a HotPDF, și se combină natural cu fluxurile de lucru la nivel de fișier din Direct File API pentru PDF-uri mari, atunci când documentul derulat este el însuși prea mare pentru a fi încărcat ocazional în primul rând. Randarea în fundal, cozile de cereri și cache-ul de randare descrise aici fac toate parte din componenta HotPDF standard pentru Delphi și C++Builder