Műszaki cikk

Egymás melletti PDF-összehasonlítás Delphiben PDFiummal

Két dokumentum nyílik meg egyszerre, ugyanazon az oldalszámon, mindegyik a maga görgethető paneljében: ez egy összehasonlító megjelenítő lényege. A PDFium Component ezt egyszerű objektummodellen keresztül adja, ahol a TPdf birtokolja a fájlt, a TPdfView pedig a megjelenítést. Egy dokumentum, egy TPdf, egy TPdfView. Ha három panelt akarunk, három párunk lesz. A nehéz részek nem az API-hívások; hanem az elrendezés számtana az ablak átméretezésekor, és az oldal-összehangolás logikája, amikor eldöntjük, melyik nézet kövesse melyiket

Az űrlap elrendezése

A VCL űrlap három TScrollBox tárolót tart egymás mellett, mindegyikben egy TPdfView vezérlővel, amely alClient igazítással kitölti a dobozt. A dobozok között két TSplitter komponens ül, hogy a felhasználó futásidőben állíthassa az oszlopszélességeket. A panelek fölötti eszköztár viszi a megnyitógombokat, a nagyítási vezérlőket és a két- illetve háromnézetes kapcsolót

A háromnézetes mód logikai érték, amelyet az űrlap belül tart nyilván. Amikor átbillen, újraszámoljuk a szélességeket, és megjelenítjük vagy elrejtjük a harmadik oszlopot. A legegyszerűbb megközelítés az, ha töröljük az összes Align tulajdonságot, elrejtjük az elválasztókat, majd abszolút pozíciókat állítunk be:

Egy Delphiben, PDFium Component eszközzel épített egymás melletti PDF-összehasonlító megjelenítő űrlapelrendezésének ábrája: eszköztár, három görgetődoboz TPdfView panelekkel, valamint elválasztók két- és háromnézetes módban
Minden panel egy görgetődoboz, benne egy TPdfView vezérlővel, a két- és háromnézetes mód közötti váltás pedig csupán a szélességek másféle kiosztása
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;
    // Ugyanazt a (ClientHeight - eszköztármagasság) értéket adjuk mindhárom Height tulajdonságnak
  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;

Ha az egészszámos számtan előtt mindhárom dobozon beállítjuk az Align := alNone értéket, azzal elkerüljük, hogy a VCL kényszermotorja az értékadásainkkal küzdjön. A pozicionálás után állítsuk vissza az elválasztók láthatóságát, ha kétnézetes módban húzással átméretezhető oszlopokat szeretnénk

Az egyes görgetődobozok magassága a kliensterület mínusz az eszköztárpanel magassága. Mivel az eszköztár alTop igazítással a tetejére van dokkolva, a ClientHeight - PanelButtons.Height adja a használható függőleges helyet. Ezt ugyanazon az UpdateLayout híváson belül rendeljük mindhárom dobozhoz, hogy soha ne legyen olyan képkocka, amelyben az egyik doboz magasabb a többinél, és elrendezésvillanást okoz

Dokumentum megnyitása

Minden panelpárnak saját megnyitó eljárás kell. A minta rövid: kapcsoljuk ki a komponenst, állítsuk be a fájlnevet, kapcsoljuk be, majd ellenőrizzük az Active tulajdonságot; ha False maradt, kérjünk jelszót, és próbáljuk újra. Jegyezzük meg, hogy a megjelenítést a TPdfView.Active vezérli, a fájlt viszont a TPdf.Active nyitja meg; a kettő független. A PdfView.Active := True beállítása akkor, amikor a hozzá kötött TPdf még nem aktív, ártalmatlan, de semmit nem jelenít meg

Egy PDF-dokumentum megnyitásának folyamatábrája PDFium Component eszközzel Delphiben: néma Active ellenőrzés, egyetlen jelszavas újrapróbálkozás és hibaüzenet sérült vagy jelszóval védett fájlokra
A sikertelen betöltés kivétel nélkül hagyja False értéken az Active tulajdonságot, ezért a folyamat ellenőrzi, egyszer újrapróbálja jelszóval, végül jelenti a gondot ahelyett, hogy üres panelt mutatna
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;

  // A betöltési hibák némák: az Active False marad, kivétel nélkül.
  if not PdfComponent.Active then
  begin
    // Nagy valószínűséggel jelszóval védett fájl; adjunk egy újrapróbálkozást.
    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;

Az értékadás után mindig ellenőrizzük a PdfComponent.Active tulajdonságot; sérült fájl vagy rossz jelszó esetén a betöltés némán bukik el, az alapértelmezett úton kivétel kiváltása nélkül. Ha sikeres megnyitás után kifejezetten beállítjuk a PdfViewComponent.PageNumber := 1 értéket, azzal elkerüljük, hogy az előző dokumentum elavult oldalszáma maradjon érvényben

A végén álló üzenetablak szándékos: azt akarjuk, hogy a sérült vagy nem támogatott fájlok azonnal felszínre kerüljenek, ne pedig csendes üres panelként nyelődjenek el. Az a felhasználó, aki semmit nem lát, nem tudja, hogy a fájl betöltődött-e és egyszerűen üres, vagy a komponens utasította el. A hiba jelentése láthatóan tartja a problémát

Az aktív panel nyilvántartása

Amikor a felhasználó egy panelen belülre kattint, az a panel válik aktívvá. Az űrlap egy privát FActivePdfView: TPdfView mezőt tart nyilván. A vizuális visszajelzés a befoglaló TScrollBox keretszínének változása: az aktívnál clHighlight, a többinél clWindow. Kössük ezt minden TPdfView.OnClick eseményhez és a megnyitó eljáráshoz is, hogy a fókusz kövesse az imént megnyitott dokumentumot

Néhány művelet nem csak az aktív, hanem minden látható panelre vonatkozik. Ezt az ágat az űrlapon lévő FAllViewsMode logikai érték vezérli. Ha igaz, a nagyítási változások és az oldalléptetés minden olyan panelre kiterjednek, amelyben aktív dokumentum van:

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;

Összehangolt oldalléptetés

Az összehangolt léptetés választható, de hasznos olyan dokumentumverziózási munkafolyamatokban, ahol mindkét fájl ugyanazt az oldaltartományt fedi le. A logika abba az eseménykezelőbe tartozik, amely azután fut le, hogy a felhasználó az egyik nézetben lépett. Amikor egy forrásnézet megváltoztatja a PageNumber értékét, a kezelő továbbadja ezt a számot a többi nézetnek, egyetlen feltétellel: a célnézetnek legalább ennyi oldalt kell tartalmaznia, különben kihagyjuk

A TPdfView és a TPdf PageNumber tulajdonsága független. A TPdf.PageNumber azt tartja nyilván, melyik oldalt tekinti aktuálisnak a dokumentumkomponens; a TPdfView.PageNumber azt, mi látszik a képernyőn. Léptetéshez a nézet tulajdonsága kell, nem a dokumentumé

Egy „Oldalak összehangolása” feliratú jelölőnégyzet adja a felhasználónak az irányítást. Ha nincs bejelölve, minden panel önállóan lépked, és a kezelő azonnal kilép. Ez a függetlenség fontos azoknál a felhasználási eseteknél, ahol a két dokumentum oldalszáma eltér, vagy ahol a felhasználó egy fordításban keresi a megfelelő szakaszt, amely más oldalon kezdődik. Ha mindig kikényszerítenénk az összehangolást, az eszköz nehezebben lenne használható, mint két egyszerű ablak az asztalon

Egy dologra figyeljünk: ha a PdfView.PageNumber értéket programból állítjuk be az összehangoló kezelőn belül, az maga is kiváltja az adott nézet változáseseményét. Védekezzünk a végtelen rekurzió ellen egy logikai jelzővel, amelyet az értékadás előtt beállítunk, és közvetlenül utána törlünk. A jelző űrlaponkénti, nem nézetenkénti, mert mind a három nézet ugyanazon a kezelőn osztozik

Az összehangolt oldalléptetés ábrája egy Delphi PDF-összehasonlító megjelenítőben PDFium Component eszközzel: az összehangolás jelölőnégyzete, célnézetenkénti oldalszám-ellenőrzés és rekurzióvédő jelző
Az oldalszám csak akkor jut el a forrásnézettől az összes többi nézethez, ha az összehangolás be van kapcsolva, és a célnézet valóban tartalmazza azt az oldalt

Panelenkénti nagyítás

Minden TPdfView saját Zoom tulajdonságot hordoz, egy Double értéket százalékban, ahol a Zoom := 100 a tényleges méretet (100%) jelenti. A beállítása felülír minden aktív FitMode értéket. Az aktív panelen működő szélességhez igazító gombhoz olvassuk ki az igazítási nagyítást a PdfView.PageWidthZoom[PdfView.PageNumber] tulajdonságból, és adjuk értékül. Oldalhoz igazításnál használjuk a PageZoom[PageNumber] tulajdonságot. Mindkettő 1-alapú oldalszámmal indexelt tömbtulajdonság, ezért elérés előtt védekezzünk a nulla oldalszám ellen

Amikor az aktuális oldalt képbe exportáljuk, az elforgatást a nézetből olvassuk ki, a RenderPage hívást viszont a TPdf komponensen adjuk ki, ne a nézeten. A TPdf.RenderPage bitképes alakja kifejezett képpontméreteket, egy TRotation értéket és egy TRenderOptions halmazt vesz át. A függvényváltozat a hívó tulajdonába kerülő TBitmap objektumot ad vissza, amelyet mentés után magunknak kell felszabadítanunk:

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;

A szélességre és magasságra alkalmazott 2x szorzó élesebb kimenetet ad az apró betűs dokumentumoknál. A bitkép felszabadítása köré tett try/finally nem elhagyható; egy TSaveDialog megszakítása is eljut a finally blokkig, és azt akarjuk, hogy a bitkép felszabaduljon, bármit tett is a felhasználó

DLL-követelmények

A PDFium Component a natív pdfium könyvtárat burkolja. Egy 32 bites gazdafolyamathoz pdfium32.dll kell; egy 64 biteshez pdfium64.dll. A V8 JavaScript-motort tartalmazó változatok a v8 utótagot kapják, és nagyjából 23-27 MB méretűek a szokásos buildek 5-6 MB méretéhez képest. Egy olyan összehasonlító megjelenítőhöz, amely kikapcsolja az űrlapkitöltést (Pdf.FormFill := False), a szokásos, V8 nélküli build elegendő, és kisebben tartja a terjesztést

Tegyük a DLL-t a végrehajtható állománnyal azonos könyvtárba, vagy a rendszer PATH változójában szereplő bármely könyvtárba. A komponens igény szerint tölti be, amikor az első TPdf aktiválódik, ezért a hiányzó DLL ott derül ki, nem az alkalmazás indulásakor. Ha telepítőt szállítunk, a legmegbízhatóbb megoldás az, ha a DLL-t telepítéskor az alkalmazás mappájába másoljuk, ahelyett hogy olyan rendszerkönyvtárra hagyatkoznánk, amelyet egy rendszergazda később kitakaríthat

A V8 buildek elsősorban akkor hasznosak, ha PDF-beli JavaScript-műveletekkel kell kapcsolatba lépnünk, például számított mezők vagy beküldési kezelők kiváltásához. Egy passzív összehasonlító megjelenítőnek nincs oka JavaScriptet futtatni; ha az Active := True előtt beállítjuk a Pdf.FormFill := False értéket, azzal teljesen kihagyjuk az űrlapkitöltési környezetet, ami azt is jelenti, hogy JS-motor akkor sem inicializálódik, ha a szokásos buildet használjuk. Ez a helyes alapértelmezés egy csak olvasható megjelenítőnél, függetlenül attól, melyik DLL-változatot szállítjuk

A PDFium Component eszközről és teljes API-járól további részletekért látogassuk meg a Delphi PDFium Component termékoldalát