Műszaki cikk

Egymás melletti PDF összehasonlítás Delphiben PDFium Component-lel

Két dokumentum egyszerre megnyitva, azonos oldalszám, mindegyik a saját görgethető (scrollable) paneljében: ez egy összehasonlító megjelenítő (comparison viewer) magja. A PDFium Component ezt egy egyértelmű objektummodellen keresztül biztosítja, ahol a TPdf birtokolja a fájlt, a TPdfView pedig a megjelenítést (display). Egy dokumentum, egy TPdf, egy TPdfView. Ha három panelt szeretne, akkor három párja van. A nehéz részek nem az API hívások; hanem az elrendezés (layout) aritmetikája az ablak átméretezésekor, és az oldal-szinkronizálási logika, amikor eldönti, melyik nézetnek melyiket kell követnie

Űrlap elrendezése (Form Layout)

A VCL űrlap három TScrollBox tárolót tartalmaz egymás mellett, mindegyikben egy-egy TPdfView található, amely az alClient-re van igazítva, így kitölti a dobozt. Két TSplitter komponens ül a dobozok között, hogy a felhasználó futásidőben (runtime) beállíthassa az oszlopszélességeket. A panelek feletti eszköztár (toolbar) hordozza a megnyitás gombokat, a nagyítás (zoom) vezérlőket, és a kétnézetes / háromnézetes (two-view / three-view) váltót (toggle)

A háromnézetes mód egy boolean, amelyet az űrlap belsőleg (internally) követ nyomon. Amikor átvált, ön újraszámolja a szélességeket, és megjeleníti vagy elrejti a harmadik oszlopot. A legegyszerűbb megközelítés az összes Align tulajdonság törlése, az elválasztók (splitters) elrejtése, majd abszolút pozíciók beállítá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;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  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;

Az Align := alNone beállítása mindhárom dobozon az egész számú (integer) aritmetika előtt elkerüli, hogy a VCL kényszermotor (constraint engine) harcoljon az ön hozzárendeléseivel. Állítsa vissza az elválasztó (splitter) láthatóságát a pozícionálás után, ha a húzással történő átméretezést (drag-to-resize) szeretné használni kétnézetes módban

Minden gördítődoboz (scroll box) magassága a kliens terület (client area) mínusz az eszköztár paneljének magassága. Mivel az eszköztár a tetejére van dokkolva (docked) az alTop-pal, a ClientHeight - PanelButtons.Height adja meg a használható függőleges teret. Rendelje ezt hozzá mindhárom dobozhoz ugyanazon az UpdateLayout híváson belül, így soha nem lesz olyan képkocka (frame), ahol az egyik doboz magasabb lenne a többinél, ami elrendezés-villódzást (layout flicker) okozna

Egy dokumentum megnyitása

Minden panelpárnak szüksége van a saját megnyitási eljárására (open procedure). A minta rövid: inaktiválja a komponenst, állítsa be a fájlnevet, próbálja meg aktiválni, kapja el az EPdfError-t, ha a fájl jelszót igényel. Vegye figyelembe, hogy a TPdfView.Active az, ami a renderelést vezérli, de a TPdf.Active az, ami ténylegesen megnyitja a fájlt; ezek függetlenek. A PdfView.Active := True beállítása, amikor a hozzá kapcsolt TPdf még nem aktív, ártalmatlan, de nem jelenít meg semmit

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 := '';

  try
    PdfComponent.Active := True;
  except
    on E: EPdfError do
    begin
      if InputQuery('Password', 'Enter document password:', Password) then
      begin
        PdfComponent.Password := Password;
        PdfComponent.Active   := True;
      end
      else
        raise;
    end;
  end;

  if PdfComponent.Active then
  begin
    PdfViewComponent.PageNumber := 1;
    SetActivePdfView(PdfViewComponent);
  end;
end;

Mindig ellenőrizze a PdfComponent.Active-ot a hozzárendelés után; egy sérült fájl vagy rossz jelszó miatt a betöltés (load) csendben meghiúsul anélkül, hogy kivételt vetne fel (raising an exception) az alapértelmezett útvonalon. A PdfViewComponent.PageNumber := 1 explicit beállítása egy sikeres megnyitás után elkerüli az előző dokumentumból visszamaradt, elavult (stale) oldalszámot

A fenti jelszókezelő (password handling) kód minden egyéb hibára kivételt dob (raises), kivéve az ismert jelszóüzenetet. Ez szándékos: azt akarja, hogy a sérült vagy nem támogatott fájlok azonnal felszínre kerüljenek, ahelyett, hogy egy csendes üres panelként elnyelődnének (swallowed). A felhasználónak, aki nem lát semmit, fogalma sincs arról, hogy a fájl betöltődött-e és egyszerűen üres, vagy a komponens utasította el (rejected). A kivételdobás (raising) láthatóan tartja a hibát

Aktív panel követése (Active Panel Tracking)

Amikor a felhasználó egy panelen belül kattint, az a panel aktívvá válik. Az űrlap egy privát FActivePdfView: TPdfView mezőt követ nyomon. A vizuális visszajelzés a szegélyszín (border color) megváltozása a tartalmazó TScrollBox-on: állítsa be clHighlight-ra az aktív számára, és clWindow-ra a többinél. Kösse be (wire) ezt minden TPdfView.OnClick-hez és a megnyitási eljáráshoz, így a fókusz az éppen megnyitott dokumentumot követi

Egyes műveletek (operations) az összes látható panelre vonatkoznak, nem csak az aktívra. Az űrlapon lévő logikai (boolean) FAllViewsMode vezérli ezt az ágat (branch). Ha igaz, a nagyítás (zoom) változásai és az oldalnavigáció szétágazik (fan out) minden olyan panelre, amely aktív dokumentummal rendelkezik:

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;

Szinkronizált oldalnavigáció (Synchronized Page Navigation)

A szinkronizált navigáció opcionális, de hasznos olyan dokumentum-revíziós munkafolyamatokhoz (workflows), ahol mindkét fájl ugyanazt az oldaltartományt fedi le. A logika egy olyan eseménykezelőbe (event handler) tartozik, amely azután tüzel (fires), hogy a felhasználó az egyik nézetben navigált. Amikor egy forrásnézet megváltoztatja a PageNumber-ét, a kezelő propagálja ezt a számot a többi nézetre, egyetlen őr (guard) feltétellel: a cél nézetnek (target view) legalább annyi oldallal kell rendelkeznie, különben hagyja ki (skip)

A PageNumber a TPdfView-n és a TPdf-en függetlenek egymástól. A TPdf.PageNumber nyomon követi, hogy a dokumentum komponens melyik oldalt tekinti aktuálisnak; a TPdfView.PageNumber nyomon követi, hogy mi jelenik meg a képernyőn. Navigációs célokra a nézet tulajdonságot (view property) szeretné használni, nem pedig a dokumentum tulajdonságot (document property)

Egy olyan feliratú jelölőnégyzet (checkbox), mint a "Sync pages" (Oldalak szinkronizálása) átadja a vezérlést a felhasználónak. Ha nincs bejelölve, minden panel függetlenül navigál, és a kezelő (handler) azonnal kilép. Ez a függetlenség fontos olyan használati esetekben (use cases), ahol a két dokumentum oldalszáma eltér, vagy ahol a felhasználó a megfelelő szakaszt (passage) szeretné megtalálni egy fordításban, amely egy másik oldalon kezdődik. A szinkronizálás mindig történő kikényszerítése nehezebben használhatóvá tenné az eszközt, mint egy egyszerű kétablakos (two-window) asztali elrendezés

Még egy dolog, amire figyelni kell: a PdfView.PageNumber programozott beállítása a szinkronizálási kezelőn (sync handler) belül maga is kiváltja (trigger) a változási eseményt (change event) azon a nézeten. Védekezzen (guard) a végtelen rekurzió ellen egy logikai (boolean) jelzővel (flag), amelyet a hozzárendelés (assignment) előtt beállít, és közvetlenül utána töröl. A jelző űrlaponkénti (per-form), nem pedig nézetenkénti (per-view), mert mind a három nézet osztozik ugyanazon a kezelőn

Nagyítás (Zoom) panelenként

Minden TPdfView hordozza a saját Zoom tulajdonságát, ami egy Double százalékban kifejezve, ahol a Zoom := 100 a tényleges méretet (100%) jelenti. Ennek beállítása felülbírál bármilyen aktív FitMode-ot (illesztési módot). Egy szélességhez illesztő (fit-to-width) gomb esetén az aktív panelen olvassa ki az illesztési nagyítást a PdfView.PageWidthZoom[PdfView.PageNumber]-ből, és rendelje hozzá. Az oldalhoz illesztéshez (fit-to-page) használja a PageZoom[PageNumber]-t. Mindkettő 1-alapú oldalszámmal indexelt tömbtulajdonság (array properties), így védekezzen (guard) a nulla oldalszám ellen a hozzáférésük előtt

Amikor az aktuális oldalt egy képre exportálja, olvassa be az elforgatást (rotation) a nézetből, de hívja meg a RenderPage-et a TPdf komponensen, ne a nézeten. A TPdf.RenderPage bittérképes (bitmap) formája explicit pixelméreteket, egy TRotation értéket és egy TRenderOptions halmazt (set) vár el. A függvényváltozat (function variant) egy hívó által birtokolt (caller-owned) TBitmap-et ad vissza, amelyet a mentés után önnek magának kell felszabadítania (free):

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 2x-es szorzó (multiplier) a szélességen és a magasságon élesebb kimenetet ad a finom szöveget (fine text) tartalmazó dokumentumok esetében. A bittérkép felszabadítását körülvevő try/finally nem opcionális; egy TSaveDialog megszakítása (cancel) továbbra is eléri a finally blokkot, és ön azt szeretné, ha a bittérkép felszabadulna (released), függetlenül attól, hogy mit csinált a felhasználó

DLL követelmények

A PDFium Component becsomagolja (wraps) a natív pdfium könyvtárat. Egy 32 bites gazdafolyamatnak (host process) a pdfium32.dll-re van szüksége; egy 64 bites gazdának a pdfium64.dll-re. A V8 JavaScript motorral (engine) rendelkező változatok a v8 utótagot (suffix) kapják, és nagyjából 23-27 MB-ot nyomnak, szemben az 5-6 MB-os standard buildekkel (builds). Egy összehasonlító megjelenítőhöz (comparison viewer), amely letiltja az űrlapkitöltést (form filling, Pdf.FormFill := False), a standard, nem V8-as build elegendő, és kisebb méretű elosztást (distribution) tesz lehetővé

Helyezze el a DLL-t ugyanabban a könyvtárban, mint a végrehajtható fájl (executable), vagy a rendszer PATH bármely könyvtárába. A komponens igény szerint (on demand) tölti be, amikor az első TPdf aktiválódik, így a hiányzó DLL ezen a ponton kerül felszínre (surfaces), nem pedig az alkalmazás indításakor. Ha telepítőt (installer) szállít (ship), a legmegbízhatóbb megközelítés a DLL bemásolása az alkalmazás mappájába a telepítés során, ahelyett, hogy egy olyan rendszerkönyvtárra hagyatkozna, amelyet egy rendszergazda (administrator) később esetleg kitakarít

A V8-as buildek elsősorban akkor hasznosak, ha PDF JavaScript műveletekkel (actions) kell interakcióba lépnie, például számítási mezők (calculation fields) vagy beküldési kezelők (submit handlers) kiváltásához (trigger). Egy passzív összehasonlító megjelenítőnek (passive comparison viewer) semmi oka sincs a JavaScript futtatására; a Pdf.FormFill := False beállítása az Active := True előtt teljesen átugorja az űrlapkitöltő (form-fill) környezetet, ami azt is jelenti, hogy semmilyen JS motor nem inicializálódik (initialized), még akkor sem, ha a standard buildet használják. Ez a helyes alapértelmezés (default) egy csak olvasható (read-only) megjelenítő számára, függetlenül attól, hogy melyik DLL változatot (variant) szállítja

A PDFium Component komponensről és a teljes API-járól további részletekért látogasson el a Delphi PDFium Component termékoldalára

A frissített példafolyamat lefedi az űrlap elrendezését, a dokumentum megnyitását, az aktív panel követését, a szinkronizált oldalnavigációt, a panelenkénti nagyítást és a DLL-eket