Articol tehnic

Comparare PDF alăturată în Delphi cu PDFium Component

Două documente deschise în același timp, același număr de pagină, fiecare în panoul lui derulabil: acesta este miezul unui vizualizator de comparare. PDFium Component îl oferă printr-un model de obiecte direct, în care TPdf deține fișierul, iar TPdfView deține afișarea. Un document, un TPdf, un TPdfView. Vreți trei panouri, aveți trei perechi. Părțile grele nu sunt apelurile de API; sunt aritmetica de aranjare când fereastra se redimensionează și logica de sincronizare a paginilor când decideți ce vizualizare pe cine urmează

Aranjarea formularului

Formularul VCL ține trei containere TScrollBox alăturate, fiecare cu un TPdfView înăuntru, aliniat la alClient ca să umple caseta. Două componente TSplitter stau între casete, ca utilizatorul să poată ajusta lățimile coloanelor la execuție. O bară de instrumente deasupra panourilor poartă butoanele de deschidere, comenzile de zoom și comutatorul între două și trei vizualizări

Modul cu trei vizualizări este un boolean pe care formularul îl urmărește intern. Când se schimbă, recalculați lățimile și afișați sau ascundeți a treia coloană. Cea mai simplă abordare este să goliți toate proprietățile Align, să ascundeți separatoarele, apoi să setați poziții absolute:

Diagramă a aranjării formularului unui vizualizator de comparare PDF alăturată în Delphi, construit cu PDFium Component, cu o bară de instrumente, trei casete de derulare cu panouri TPdfView și separatoare, în modurile cu două și cu trei vizualizări
Fiecare panou este o casetă de derulare cu un TPdfView înăuntru, iar trecerea între două și trei vizualizări înseamnă doar un alt set de atribuiri de lățime
procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // Aplică aceeași valoare (ClientHeight - înălțimea barei) la toate cele trei Height
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

Setarea lui Align := alNone pe toate cele trei casete înaintea aritmeticii cu întregi împiedică motorul de constrângeri al VCL să se lupte cu atribuirile dumneavoastră. Restabiliți vizibilitatea separatoarelor după poziționare, dacă vreți redimensionare prin tragere în modul cu două vizualizări

Înălțimea fiecărei casete de derulare este aria client minus înălțimea panoului cu bara de instrumente. Pentru că bara este andocată sus, cu alTop, ClientHeight - PanelButtons.Height vă dă spațiul vertical utilizabil. Atribuiți-l tuturor celor trei casete în același apel UpdateLayout, ca să nu existe niciodată un cadru în care o casetă este mai înaltă decât celelalte și provoacă o pâlpâire de aranjare

Deschiderea unui document

Fiecare pereche de panouri are nevoie de propria procedură de deschidere. Tiparul este scurt: dezactivați componenta, setați numele fișierului, activați, apoi verificați Active; dacă a rămas False, cereți o parolă și reîncercați. Rețineți că TPdfView.Active este cel care controlează randarea, dar TPdf.Active este cel care deschide efectiv fișierul; sunt independente. Setarea lui PdfView.Active := True când TPdf-ul legat nu este încă activ este inofensivă, dar nu afișează nimic

Diagramă de flux a deschiderii unui document PDF cu PDFium Component în Delphi, arătând verificarea tăcută a lui Active, o singură reîncercare cu parolă și o casetă de eroare pentru fișiere deteriorate sau protejate cu parolă
O încărcare eșuată lasă Active pe False fără să ridice excepție, așa că fluxul îl verifică, reîncearcă o dată cu o parolă și în final raportează problema, în loc să arate un panou gol
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // Eșecurile de încărcare sunt tăcute: Active rămâne False în loc să ridice excepție.
  if not PdfComponent.Active then
  begin
    // Cel mai probabil un fișier protejat cu parolă; dă-i utilizatorului o reîncercare.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

Verificați întotdeauna PdfComponent.Active după atribuire; un fișier deteriorat sau o parolă greșită fac ca încărcarea să eșueze în tăcere, fără să ridice o excepție pe drumul implicit. Setarea explicită a lui PdfViewComponent.PageNumber := 1 după o deschidere reușită evită un număr de pagină rămas de la documentul anterior

Caseta de mesaj de la final este intenționată: vreți ca fișierele corupte sau nesuportate să iasă imediat la suprafață, nu să fie înghițite ca un panou gol și tăcut. Un utilizator care nu vede nimic nu are cum să știe dacă fișierul s-a încărcat și este pur și simplu gol sau dacă l-a respins componenta. Raportarea eșecului ține eroarea vizibilă

Urmărirea panoului activ

Când utilizatorul dă clic într-un panou, acel panou devine activ. Formularul urmărește un câmp privat FActivePdfView: TPdfView. Feedbackul vizual este o schimbare a culorii de chenar pe TScrollBox-ul care îl conține: setați-o pe clHighlight pentru cel activ și pe clWindow pentru celelalte. Legați asta la fiecare TPdfView.OnClick și la procedura de deschidere, ca focalizarea să urmeze documentul tocmai deschis

Unele operații se aplică tuturor panourilor vizibile, nu doar celui activ. Un boolean FAllViewsMode de pe formular conduce acea ramură. Când este adevărat, schimbările de zoom și navigarea între pagini se răsfrâng asupra fiecărui panou care are un document activ:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

Navigare sincronizată între pagini

Navigarea sincronizată este opțională, dar utilă pentru fluxurile de revizuire a documentelor, în care ambele fișiere acoperă același interval de pagini. Logica îi revine unui handler de eveniment care se declanșează după ce utilizatorul navighează într-o vizualizare. Când o vizualizare sursă își schimbă PageNumber, handlerul propagă acel număr celorlalte vizualizări, cu o singură pază: vizualizarea țintă trebuie să aibă cel puțin atâtea pagini, altfel se sare peste ea

Proprietățile PageNumber de pe TPdfView și de pe TPdf sunt independente. TPdf.PageNumber urmărește ce pagină consideră curentă componenta document; TPdfView.PageNumber urmărește ce se afișează pe ecran. Pentru navigare vreți proprietatea vizualizării, nu pe cea a documentului

O casetă de bifat cu o etichetă de tipul „Sync pages” îi dă utilizatorului controlul. Când este debifată, fiecare panou navighează independent, iar handlerul iese imediat. Acea independență este importantă pentru cazurile în care cele două documente au numere de pagini diferite sau în care utilizatorul vrea să găsească pasajul echivalent dintr-o traducere care începe pe altă pagină. Sincronizarea forțată permanent ar face instrumentul mai greu de folosit decât o simplă aranjare cu două ferestre pe ecran

Un lucru de urmărit: setarea programatică a lui PdfView.PageNumber în interiorul handlerului de sincronizare va declanșa ea însăși evenimentul de schimbare pe acea vizualizare. Păziți-vă de recursivitatea infinită cu un indicator boolean pe care îl setați înainte de atribuire și îl ștergeți imediat după. Indicatorul este per formular, nu per vizualizare, pentru că toate cele trei vizualizări împart același handler

Diagramă a navigării sincronizate între pagini într-un vizualizator Delphi de comparare PDF cu PDFium Component, cu caseta de bifat pentru sincronizare, o pază pe numărul de pagini al fiecărei vizualizări țintă și un indicator de protecție împotriva recursivității
Numărul de pagină călătorește de la vizualizarea sursă spre fiecare altă vizualizare doar când sincronizarea este activată și când fiecare vizualizare țintă conține într-adevăr acea pagină

Zoom per panou

Fiecare TPdfView poartă propria proprietate Zoom, un Double în procente, unde Zoom := 100 înseamnă dimensiunea reală (100%). Setarea ei suprascrie orice FitMode activ. Pentru un buton de potrivire pe lățime aplicat panoului activ, citiți zoomul de potrivire din PdfView.PageWidthZoom[PdfView.PageNumber] și atribuiți-l. Pentru potrivirea în pagină, folosiți PageZoom[PageNumber]. Amândouă sunt proprietăți de tip tablou indexate după numărul de pagină de la 1, așa că păziți-vă de un număr de pagină zero înainte să le accesați

Când exportați pagina curentă într-o imagine, citiți rotirea din vizualizare, dar apelați RenderPage pe componenta TPdf, nu pe vizualizare. Forma cu imagine bitmap a lui TPdf.RenderPage primește dimensiuni explicite în pixeli, plus o valoare TRotation și un set TRenderOptions. Varianta de tip funcție returnează un TBitmap deținut de apelant, pe care îl eliberați singur după salvare:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

Multiplicatorul de 2x pe lățime și înălțime dă un rezultat mai clar pentru documentele cu text fin. Blocul try/finally din jurul eliberării imaginii nu este opțional; o anulare a lui TSaveDialog tot ajunge în blocul finally, iar dumneavoastră vreți ca imaginea să fie eliberată indiferent ce a făcut utilizatorul

Cerințe de DLL

PDFium Component împachetează biblioteca nativă pdfium. Un proces gazdă pe 32 de biți are nevoie de pdfium32.dll; unul pe 64 de biți are nevoie de pdfium64.dll. Variantele cu motorul JavaScript V8 adaugă sufixul v8 și cântăresc cam 23-27 MB, față de compilările standard de 5-6 MB. Pentru un vizualizator de comparare care dezactivează completarea formularelor (Pdf.FormFill := False), compilarea standard, fără V8, este suficientă și menține distribuția mai mică

Puneți DLL-ul în același director cu executabilul sau în orice director aflat pe PATH-ul sistemului. Componenta îl încarcă la cerere, când primul TPdf este activat, așa că un DLL lipsă iese la suprafață în acel moment, nu la pornirea aplicației. Dacă livrați un instalator, cea mai sigură abordare este să copiați DLL-ul în folderul aplicației în timpul instalării, în loc să vă bazați pe un director de sistem pe care un administrator îl poate curăța mai târziu

Compilările cu V8 sunt utile în principal când trebuie să interacționați cu acțiuni JavaScript din PDF, de exemplu pentru a declanșa câmpuri de calcul sau handlere de trimitere. Un vizualizator pasiv de comparare nu are niciun motiv să ruleze JavaScript; setarea lui Pdf.FormFill := False înainte de Active := True sare complet peste mediul de completare a formularelor, ceea ce înseamnă și că niciun motor JS nu este inițializat, chiar dacă se folosește compilarea standard. Aceasta este valoarea implicită corectă pentru un vizualizator doar-citire, indiferent ce variantă de DLL livrați

Pentru mai multe detalii despre PDFium Component și despre API-ul lui complet, vizitați pagina de produs Delphi PDFium Component