Articol tehnic

Construiți un vizualizator PDF în Delphi cu PDFium

Un vizualizator PDF în Delphi se reduce la două componente și la legătura dintre ele. TPdf deține documentul: deschide fișierul, îl decriptează și răspunde la întrebări despre numărul de pagini și despre metadate. TPdfView este controlul vizual care desenează paginile pe ecran și se ocupă de derulare, de zoom și de pagina pe care o privește utilizatorul în acel moment. PDFium Component împachetează același motor de randare care se livrează în interiorul Chrome, așa că glifele, antialiasingul și culoarea pe care le obțineți pe pânză corespund cu ce văd deja utilizatorii dumneavoastră în browser. Munca nu stă în randare. Ea stă în conectarea obiectului document la vizualizare, în încărcarea fără prăbușire a unui fișier deteriorat sau protejat cu parolă și în oferirea către utilizator a acelui pumn de comenzi care fac un vizualizator să pară terminat: întoarce pagina, schimbă zoomul, potrivește pagina în fereastră

Textul acesta parcurge asamblarea în ordinea în care o construiți de fapt. Tot ce urmează randează câte o singură pagină, ceea ce vor majoritatea fluxurilor de lucru cu documente. Dacă aveți nevoie de pagini stivuite într-o singură coloană cu derulare continuă, aceea este o altă decizie de aranjare și nu este drumul de aici

Legarea lui TPdf de TPdfView

Puneți un TPdf și un TPdfView pe formular, apoi spuneți-i vizualizării ce document să afișeze. Acea singură atribuire este toată legătura dintre documentul nevizual și controlul care îl desenează

Arhitectura unui vizualizator PDF în Delphi, în care TPdf deține documentul, TPdfView îl desenează, iar o singură atribuire de proprietate le leagă deasupra bibliotecii PDFium
TPdf deține documentul, în timp ce TPdfView îl desenează, iar o singură atribuire le conectează peste motorul PDFium comun
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf și PdfView au fost puse pe formular la momentul proiectării.
  PdfView.Pdf := Pdf;                 // vizualizarea desenează ce conține acest document
  PdfView.FitMode := pfmFitWidth;     // pornește utilizatorul de la un zoom rezonabil
end;

Înainte ca oricare dintre acestea să ruleze, biblioteca nativă PDFium trebuie să se afle pe mașină. PDFium Component apelează în pdfium32.dll sau în pdfium64.dll, în funcție de platforma dumneavoastră țintă, iar documentul pur și simplu refuză să se deschidă dacă DLL-ul nu poate fi găsit. Livrați DLL-ul potrivit lângă executabil sau puneți-l acolo unde îl va găsi încărcătorul sistemului. Compilările cu V8 activat există doar pentru PDF-urile care poartă JavaScript pe care vreți să îl executați, ceea ce un vizualizator obișnuit nu face, așa că apelați la DLL-ul standard dacă nu aveți un motiv concret să nu o faceți

Încărcarea unui document fără a avea încredere în intrare

Instinctul este să încadrați încărcarea într-un try/except și să tratați o excepție aruncată drept eșec. Instinctul acesta este greșit aici, iar greșeala produce un vizualizator care pare în regulă până când cineva îi dă un fișier stricat. Setarea lui Active := True nu ridică excepție la un eșec de încărcare. PDFium Component prinde eroarea internă și lasă Active pe False, așa că singura cale onestă de a ști dacă documentul s-a deschis este să citiți proprietatea înapoi după ce ați setat-o

Fluxul de decizie la încărcare într-un vizualizator Delphi cu PDFium, în care setarea lui Active nu ridică niciodată excepție, un false tăcut înseamnă parolă greșită sau fișier deteriorat, iar apoi urmează o singură reîncercare cu parolă
Activarea nu ridică niciodată excepție la eșec, așa că vizualizatorul citește Active înapoi și răspunde unui false tăcut cu o singură reîncercare de parolă
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // nu ridică excepție; la eșec Active rămâne False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // vizualizarea își urmărește propria pagină curentă
  UpdatePageLabel;
end;

Două lucruri merită atenție. Primul este că PageNumber există pe ambele obiecte, iar cele două sunt independente. Pdf.PageNumber este noțiunea documentului despre pagina curentă; PdfView.PageNumber este pagina pe care o afișează efectiv controlul și este cea pe care o setați pentru a-l muta pe utilizator prin fișier. Setarea uneia nu o mută pe cealaltă, așa că un vizualizator conduce întotdeauna proprietatea vizualizării. Al doilea este indexarea de la 1: paginile merg de la 1 la Pdf.PageCount, nu de la 0, ceea ce îi prinde pe cei obișnuiți cu tablouri indexate de la zero

Tratarea unui fișier criptat

Documentele criptate se pliază pe același drum de încărcare. Dacă parola de deschidere este setată înainte de activare, documentul se decriptează pe măsură ce se deschide; dacă este greșită sau lipsește, Active rămâne False exact ca la un fișier corupt. Așa că recuperarea înseamnă să cereți o parolă și să reîncercați activarea

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // trebuie setată înainte de Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

Pentru că eșecul este tăcut atât la o parolă greșită, cât și la un fișier deteriorat, nu le puteți deosebi doar după Active. În practică asta este acceptabil pentru un vizualizator: utilizatorul fie furnizează parola corectă, fie află că fișierul nu se va deschide, iar mesajul se citește la fel în ambele cazuri

Răsfoirea documentului

Cu documentul deschis, navigarea înseamnă aritmetică pe PdfView.PageNumber, mărginită de Pdf.PageCount. Singura muncă reală este limitarea, ca butoanele să nu împingă niciodată pagina în afara intervalului, iar butoanele de prima și ultima pagină să rămână dezactivate la capetele fișierului

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// cele patru butoane de navigare se reduc la câte un singur apel
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

O casetă de text „mergi la pagina N” este același apel GoToPage, alimentat dintr-un întreg parsat, iar limitarea acoperă cazul în care utilizatorul tastează 9999 într-un fișier de zece pagini. Păstrați UpdatePageLabel drept singurul loc care scrie „Pagina 3 din 12”, ca afișajul să nu se dezalinieze niciodată de ce arată vizualizarea

Zoom: procente explicite și moduri de potrivire

Zoomul pe TPdfView vine în două variante care interacționează, iar înțelegerea interacțiunii este diferența dintre un control de zoom care se poartă frumos și unul care se luptă cu utilizatorul. Calea directă este proprietatea Zoom, un procent în care 100 înseamnă dimensiunea reală. Cealaltă cale este FitMode, care îi spune vizualizării să calculeze ea zoomul în locul dumneavoastră și să îl recalculeze pe măsură ce fereastra se redimensionează

Interacțiunea dintre Zoom și FitMode într-un vizualizator Delphi cu PDFium, în care atribuirea unui Zoom exact șterge FitMode la pfmNone, iar alegerea unui mod de potrivire dă înapoi calculul zoomului către vizualizare
Atribuirea unui zoom exact șterge modul de potrivire, iar alegerea unui mod de potrivire dă calculul zoomului înapoi vizualizării
// măriri fixe
PdfView.Zoom := 100;     // dimensiune reală
PdfView.Zoom := 50;      // jumătate
PdfView.Zoom := 200;     // dublu

// lasă vizualizarea să dimensioneze pagina în fereastră și să o țină așa la redimensionare
PdfView.FitMode := pfmFitWidth;   // lățimea paginii umple controlul
PdfView.FitMode := pfmFitPage;    // pagina întreagă este vizibilă
PdfView.FitMode := pfmActualSize; // 1:1 cu punctele documentului

Iată partea care încurcă lumea. Atribuirea directă a lui Zoom readuce FitMode la pfmNone. Acesta este un comportament corect, nu o eroare: din clipa în care utilizatorul alege un 150% exact, vizualizarea nu mai poate respecta în același timp și „potrivește pe lățime”, pentru că cele două cereri intră în conflict. Consecința pentru interfața dumneavoastră este că un buton de mărire și un buton de potrivire în pagină sunt stări care se exclud reciproc, iar bara de instrumente ar trebui să facă vizibil modul activ. Când utilizatorul apasă potrivirea în pagină, setați FitMode; când apasă un zoom numeric, setați Zoom și lăsați-l să șteargă singur modul de potrivire

Dacă preferați să calculați singur valoarea de potrivire, poate ca să inițializați un cursor de zoom cu procentul curent de potrivire, funcțiile ajutătoare per pagină vă dau numerele fără să schimbe modul. PageWidthZoom[N], PageZoom[N] și ActualSizeZoom[N] returnează procentul care ar potrivi pagina N pe lățime, ar potrivi-o întreagă sau ar randa-o la dimensiunea reală

// inițializează afișajul de zoom din valoarea de potrivire pe lățime a paginii curente
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

De ce are nevoie de fapt un vizualizator terminat

Vizualizatorul de mai sus are câteva zeci de rânduri și face deja ce îi trebuie unui flux de lucru cu documente: deschide un fișier, supraviețuiește unuia stricat, arată o pagină, se mută între pagini și schimbă mărirea manual sau prin potrivire. PDFium face părțile grele în tăcere. Fonturile încorporate se rezolvă, adnotările și câmpurile de formular se desenează acolo unde le pune documentul, iar pagina pe care o vedeți corespunde cu cea pe care ar vedea-o un utilizator de Chrome, pentru că le desenează același motor

De la această bază, adăugirile sunt incrementale, nu structurale. Selecția de text și căutarea citesc din același strat de text pe care PDFium îl construiește deja; metadate precum Pdf.Title și Pdf.Author sunt la o singură citire de proprietate distanță; rotirea și tonurile de gri sunt opțiuni de randare pe care le transmiteți când desenați o pagină într-o imagine bitmap. Niciuna dintre ele nu schimbă coloana vertebrală pe care o aveți aici, adică obiectul document, vizualizarea și fluxul încarcă-apoi-navighează care le leagă. Faceți corect această coloană vertebrală și restul este decor

Componentele TPdf și TPdfView folosite peste tot fac parte din PDFium Component pentru Delphi și C++Builder, care poartă referința completă a vizualizatorului pe pagina lui de produs