Articol tehnic

Vizualizator PDF cu defilare continuă în Delphi cu PDFium Component

O singură pagină A4 randată la un nivel de zoom confortabil pentru lectură are dimensiunea a câțiva megaocteți de bitmap pe 32 de biți. Înmulțiți asta cu un contract de 400 de pagini, iar aritmetica nu mai este abstractă: randați fiecare pagină din start și veți solicita sistemului Windows mult peste un gigaoctet de bitmap-uri pe care utilizatorul le va privi pe rând, câte un ecran. Aplicația fie rămâne fără spațiu de adrese într-o versiune pe 32 de biți, fie își petrece primele secunde blocată în timp ce GPU-ul și parser-ul de pagină procesează pagini la care nimeni nu a ajuns încă prin defilare. Un vizualizator cu defilare continuă trebuie să ofere senzația unei benzi înalte de pagini, dar nu le poate păstra pe toate în memorie în același timp

Această tensiune constituie întreaga problemă. PDFium Component o rezolvă în interiorul TPdfView, așa că cea mai mare parte a muncii constă în selectarea modului de afișare corect și înțelegerea a ceea ce face componenta pentru dvs. Părțile pe care nu le face ea în locul dvs., cum ar fi dimensionarea paginilor pentru un flux de citire și menținerea unui ritm fluid la defilarea rapidă, reprezintă momentele în care puțin cod își dovedește utilitatea. Dacă asamblați în continuare restul elementelor vizuale (bara de instrumente, miniaturi, caseta de căutare), ghidul pentru vizualizatorul complet acoperă aceste aspecte; aici subiectul central este defilarea în sine

Aspectul este un mod de afișare, nu un panou de bitmap-uri

Instinctul din VCL este să folosiți o casetă de derulare (scroll box) și să stivuiți controale de imagine în ea, câte unul pentru fiecare pagină. Evitați acest lucru. Acest design vă obligă să gestionați poziționarea paginii, aritmetica defilării și consumul de memorie simultan, iar rezultatul va fi o reinventare ineficientă a acestor mecanisme. TPdfView modelează deja documentul ca pe un șir continuu de pagini și expune aspectul prin proprietatea sa DisplayMode

Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;

PdfView.DisplayMode := dmSingleContinuous;   // lățime de o pagină, defilează vertical

Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
  ShowMessage('Nu s-a putut deschide documentul');

Aceasta este întreaga configurare pentru defilarea continuă. dmSingleContinuous aranjează paginile într-o singură coloană verticală, spațiile dintre ele fiind gestionate intern, iar vizualizatorul parcurge acea coloană ca pe o singură suprafață. Nu există controale per pagină de conectat și nici handleri de defilare de scris pentru navigarea obișnuită. Observați verificarea Pdf.Active după atribuire: deschiderea unui document nu generează niciodată excepții, așa că un fișier deteriorat sau protejat prin parolă lasă valoarea Active setată pe False, fără vreo excepție de capturat, iar un vizualizator care omite această verificare va reda un panou gol

Aceeași proprietate include și modurile tip răspândire (spread). dmTwoPageContinuous plasează paginile una lângă alta, câte două pe rând, pentru lectura în stil carte pe care o solicită unele documente; dmTwoPageContinuousWithCover face același lucru, dar permite primei pagini să stea singură ca o copertă, astfel încât restul paginilor să se alinieze corect pe marginea par-impar. Toate cele trei moduri defilează continuu. Comutarea între ele este o simplă atribuire, ceea ce face ca adăugarea ulterioară a unei casete combinate pentru modurile de afișare să fie extrem de simplă

Doar paginile vizibile sunt rasterizate

Motivul pentru care acest mod se aplică și la un fișier de 400 de pagini este că acea coloană este virtuală. TPdfView cunoaște înălțimea fiecărei pagini din structura de pagini a documentului, astfel încât poate calcula intervalul total de defilare și poziția fiecărei pagini fără a rasteriza nimic. Rasterizarea, pasul costisitor care transformă fluxul de conținut al unei pagini în pixeli, are loc numai pentru paginile care intersectează în mod curent fereastra de vizualizare (viewport), plus o mică marjă pentru ca o pagină să fie pregătită în momentul în care ajunge pe ecran. Pe măsură ce derulați în jos, paginile care intră în fereastra de vizualizare sunt randate, iar paginilor care o părăsesc le sunt eliberate bitmap-urile. Memoria utilizată rămâne proporțională cu ceea ce se potrivește pe ecran, nu cu lungimea documentului

Merită să rețineți acest lucru deoarece modifică modul în care evaluați resursele. Deschiderea unui document de 400 de pagini este rapidă: analizează structura, nu conținutul. Costul este per pagină și este achitat în mod leneș (lazy), exact în momentul în care defilați aproape de o pagină. Un vizualizator care se simte instantaneu la deschidere și fluid la defilare nu depune mai puțin efort per total, ci doar împarte munca pe parcursul real de lectură al utilizatorului și renunță la ceea ce rămâne în urmă. Consecința practică este că nu veți dori aproape niciodată să forțați randarea paginilor înaintea utilizatorului. Lăsați vizualizatorul să decidă ce este vizibil

Dimensionați paginile la lățime, apoi nu modificați zoom-ul

O coloană de citire are nevoie de pagini dimensionate la lățimea panoului, nu blocate la o valoare fixă de zoom. FitMode realizează acest lucru și continuă să facă asta pe măsură ce fereastra își schimbă dimensiunea

PdfView.FitMode := pfmFitWidth;   // fiecare pagină umple lățimea coloanei; înălțimea se adaptează

Cu pfmFitWidth componenta recalculează zoom-ul ori de câte ori vizualizatorul își schimbă dimensiunea, astfel încât coloana să umple întotdeauna lățimea disponibilă, iar înălțimile paginilor și implicit intervalul de defilare se adaptează conform acesteia. Există o capcană des întâlnită: atribuirea directă a valorii Zoom resetează FitMode înapoi la pfmNone. Acest lucru este deliberat, deoarece un zoom manual și o potrivire automată sunt intenții contradictorii, însă înseamnă că o linie rătăcită de tip PdfView.Zoom := 1.0 din codul dvs. va dezactiva în mod silențios potrivirea la lățime, iar următoarea redimensionare nu va mai adapta paginile. Dacă oferiți atât un control de zoom, cât și un buton de potrivire, tratați-le ca pe o comutare de mod: setarea unuia îl anulează pe celălalt, iar dvs. decideți care dintre ele are prioritate

Pentru controalele de zoom absolut, vizualizatorul expune valorile de zoom ca valori pe care le puteți aplica sau afișa: PageWidthZoom[PageNumber] returnează zoom-ul care ar potrivi acea pagină la lățime, iar proprietatea PageZoom potrivește întreaga pagină. Citirea acestora este modul în care populați un meniu de tip „Potrivire lățime” / „Potrivire pagină” fără a introduce manual procente fixe care eșuează pe paginile landscape sau supradimensionate

Menținete defilarea rapidă fluidă prin randarea progresivă

Calea implicită de randare desenează o pagină complet înainte de a returna rezultatul. Pentru o singură pagină, acest lucru este în regulă. În timpul unei defilări rapide printr-un document dens, lucrurile se schimbă: fiecare pagină care trece rapid pe ecran declanșează o rasterizare completă, iar dacă utilizatorul derulează mai rapid decât viteza de randare a paginilor, aceste procese se acumulează, iar panoul începe să se blocheze deoarece se lucrează la pagini care sunt deja în afara ecranului la finalizarea procesului. Soluția constă în a face randarea anulabilă și în abandonarea ei în momentul în care utilizatorul derulează mai departe

Metoda RenderPageProgressive randează în porțiuni și verifică un jeton de anulare la limita fiecărei porțiuni, astfel încât o randare aflată în desfășurare pentru o pagină care tocmai a părăsit ecranul să poată fi abandonată în loc să fie rulată până la capăt

type
  TFormMain = class(TForm)
    // ...
  private
    FRenderCancel: IPdfCancellationTokenSource;
    procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
  end;

procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
  Status: TPdfProgressiveStatus;
begin
  // Anulați orice se randa; vechiul jeton este acum semnalat.
  if Assigned(FRenderCancel) then
    FRenderCancel.Cancel;
  FRenderCancel := TPdfCancellationTokenSource.New;

  Pdf.PageNumber := PageNo;
  Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
    FRenderCancel.Token);

  case Status of
    prsDone:      ;                    // bitmap-ul este complet, desenați-l
    prsCancelled: Exit;                // înlocuit, eliminați acest rezultat
    prsFailed:    ShowMessage('Randarea a eșuat pentru pagina ' + IntToStr(PageNo));
  end;
end;

Elementul cel mai important este valoarea de retur. prsDone înseamnă că bitmap-ul este complet desenat și merită afișat pe ecran; prsCancelled indică faptul că o nouă poziție de defilare a înlocuit această pagină, așa că aruncați rezultatul parțial în loc să-l afișați; prsFailed reprezintă o eroare reală pe acea pagină. Anularea este verificată la limitele porțiunilor, nu preventiv, deci asumați-vă câteva zeci de milisecunde de latență între apelarea Cancel și oprirea efectivă a randării. Aceasta este în continuare mult mai economic decât să lăsați o randare de pagină completă învechită să blocheze coada de așteptare. Transmiterea valorii nil ca jeton randează direct până la finalizare, fiind alegerea corectă pentru o randare unică, cum ar fi o previzualizare la imprimare, unde nu există motive de anulare

Când apelați în schimb forma de funcție a RenderPage, cea care returnează un nou TBitmap, rețineți că apelantul este cel care îl deține și trebuie să apeleze Free pentru el. Într-o buclă de defilare care alocă un bitmap per pagină, omiterea acestui aspect reprezintă o scurgere de memorie care crește cu fiecare pagină parcursă de utilizator, exact tipul de eșec de memorie nelimitată pe care structura continuă trebuia să îl evite. Randați într-un bitmap reutilizat ori de câte ori este posibil

Cu ce rămâneți la final

Vizualizatorul cu defilare continuă este asigurat în mare parte de componentă. Selectați dmSingleContinuous pentru aspect, setați pfmFitWidth astfel încât coloana să se adapteze la dimensiunea ferestrei și verificați Pdf.Active pentru ca un fișier corupt să genereze o eroare clară. Singurul element ce merită scris manual este randarea anulabilă, deoarece un vizualizator este evaluat după modul în care se comportă când cineva trage bara de derulare până la subsolul unui document lung, iar panoul fie ține pasul, fie eșuează. Tot ceea ce urmează după, selecția textului între pagini, evidențierea căutării, un arbore de semne de carte, reprezintă elemente de interfață care se plasează deasupra acestei suprafețe de defilare, nu în interiorul ei

API-urile TPdfView, DisplayMode și RenderPageProgressive prezentate aici fac parte din produsul PDFium Component pentru Delphi și Lazarus