Műszaki cikk

PDF-megjelenítő készítése Delphiben a PDFium Component segítségével

Egy PDF-megjelenítő Delphiben két komponensre és a köztük lévő kapcsolatokra (wiring) vezethető vissza. A TPdf birtokolja a dokumentumot: megnyitja a fájlt, visszafejti (decrypts), és válaszol az oldalszámmal és a metaadatokkal kapcsolatos kérdésekre. A TPdfView az a vizuális vezérlő (visual control), amely az oldalakat a képernyőre festi, és kezeli a görgetést, a nagyítást (zoom), valamint azt az oldalt, amelyet a felhasználó éppen néz. A PDFium Component ugyanazt a renderelő motort csomagolja be, amelyet a Chrome-ban is szállítanak, így a vásznon (canvas) kapott glyphek, élsimítások (anti-aliasing) és színek megegyeznek azzal, amit a felhasználók már látnak a böngészőjükben. A munka nem a renderelésben van. Abban áll, hogy a dokumentum objektumot összekapcsolja a nézettel, sérült vagy jelszóval védett fájl esetén összeomlás nélkül töltse be, és megadja a felhasználónak azt a maroknyi vezérlőt, amelytől egy megjelenítő befejezettnek érződik: lapozás, a nagyítás megváltoztatása, és az oldal ablakhoz igazítása

Ez a cikk abban a sorrendben vezeti végig ezen az összeállításon, ahogyan ténylegesen megépíti azt. Itt minden egyszerre csak egy oldalt renderel, amit a legtöbb dokumentum-munkafolyamat is igényel. Ha az oldalakat egyetlen folyamatosan görgethető oszlopban egymásra rakva szeretné látni, az egy eltérő elrendezési döntés, és nem ez az itt bemutatott út

A TPdf összekötése a TPdfView-val

Dobjon be egy TPdf-et és egy TPdfView-t a formra, majd mondja meg a nézetnek, hogy melyik dokumentumot jelenítse meg. Ez az egyetlen értékadás jelenti a teljes kapcsolatot a nem-vizuális dokumentum és az azt kirajzoló vezérlő között

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

Mielőtt ebből bármi is lefutna, a PDFium natív könyvtárának (native library) rajta kell lennie a gépen. A PDFium Component a célplatformtól függően a pdfium32.dll vagy a pdfium64.dll fájlt hívja meg, és a dokumentum egyszerűen nem hajlandó megnyílni, ha a DLL nem található. Szállítsa a megfelelő DLL-t a végrehajtható fájlja mellett, vagy helyezze oda, ahol a rendszerbetöltő (system loader) megtalálja. A V8-kompatibilis buildek csak azokhoz a PDF-ekhez léteznek, amelyek végrehajtandó JavaScriptet tartalmaznak, egy egyszerű megjelenítő viszont nem tesz ilyet, ezért válassza a standard DLL-t, hacsak nincs konkrét oka arra, hogy ne így tegyen

Egy dokumentum betöltése a bemenet megbízhatóságának feltételezése nélkül

Az ösztönös reakció az, hogy a betöltést egy try/except blokkba csomagoljuk, és a kivételt hibaként kezeljük. Ez az ösztön itt téves, és ha elrontjuk, akkor egy olyan megjelenítőt kapunk, amely jól néz ki, amíg valaki egy sérült fájlt nem ad neki. Az Active := True beállítása nem dob kivételt (raise) betöltési hiba esetén. A PDFium Component elkapja a belső hibát, és az Active tulajdonságot False értéken hagyja, így az egyetlen őszinte módja annak, hogy megtudjuk, megnyílt-e a dokumentum, ha visszaolvassuk a tulajdonságot, miután beállítottuk azt

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

Két dolog érdemel figyelmet. Az első, hogy a PageNumber mindkét objektumon létezik, és a kettő független egymástól. A Pdf.PageNumber a dokumentum aktuális oldalra vonatkozó fogalma; a PdfView.PageNumber pedig az az oldal, amelyet a vezérlő ténylegesen megjelenít, és ezt állítja be, hogy végigvezesse a felhasználót a fájlon. Az egyik beállítása nem mozgatja a másikat, így egy megjelenítő mindig a nézet (view) tulajdonságát vezérli. A második dolog az 1-alapú indexelés: az oldalak 1-től a Pdf.PageCount-ig tartanak, nem pedig 0-tól, ami megtréfálhatja mindazokat, akik hozzászoktak a nulla-alapú tömbökhöz

Titkosított fájl kezelése

A titkosított dokumentumok ugyanebbe a betöltési útvonalba illeszkednek be. Ha a megnyitási jelszó az aktiválás előtt be van állítva, a dokumentum megnyitás közben visszafejtésre kerül; ha rossz vagy hiányzik, az Active False marad pontosan úgy, mint egy korrupt fájl esetében. A helyreállítás (recovery) tehát abból áll, hogy bekérjük a jelszót, és újra megpróbáljuk az aktiválást

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;       // must be set before 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;

Mivel a hiba csendes (silent) mind a rossz jelszó, mind a sérült fájl esetében, a kettőt nem tudja megkülönböztetni pusztán az Active tulajdonság alapján. A gyakorlatban ez elfogadható egy megjelenítő esetében: a felhasználó vagy megadja a helyes jelszót, vagy megtudja, hogy a fájl nem fog megnyílni, és az üzenet mindkét esetben ugyanazt mondja

Lapozás a dokumentumban

A megnyitott dokumentum navigációja aritmetikai művelet a PdfView.PageNumber-ön, amelyet a Pdf.PageCount határol. Az egyetlen igazi munka a korlátozás (clamping), így a gombok soha nem tolják az oldalt a tartományon kívülre, és az első és utolsó gombok letiltva maradnak a fájl végein

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;

// the four navigation buttons reduce to one call each
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;

Egy "Ugrás az N. oldalra" szövegmező ugyanaz a GoToPage hívás, egy elemzett (parsed) egész számból táplálva, a korlátozás (clamp) pedig lefedi azt az esetet, amikor a felhasználó 9999-et gépel be egy tízoldalas fájlba. Tartsa meg az UpdatePageLabel-t, mint azt az egyetlen helyet, amely kiírja a "3 / 12. oldal" szöveget, így a kijelzett érték soha nem csúszik szét (out of sync) azzal, amit a nézet mutat

Nagyítás: explicit százalékok és igazítási módok

A TPdfView-n a nagyítás (zoom) két változatban érkezik, amelyek kölcsönhatásba lépnek egymással, és ezen kölcsönhatás megértése jelenti a különbséget egy jól viselkedő nagyításvezérlő és egy olyan között, amelyik harcol a felhasználóval. A közvetlen út a Zoom tulajdonság, egy százalék, ahol a 100 az eredeti méretet jelenti. A másik út a FitMode, amely arra utasítja a nézetet, hogy számítsa ki önnek a nagyítást, és folyamatosan számolja újra azt, ahogy az ablak mérete változik

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

Itt van az a rész, amiben az emberek elbotlanak. A Zoom közvetlen hozzárendelése visszaállítja a FitMode-ot pfmNone-ra. Ez a helyes viselkedés, nem pedig hiba: abban a pillanatban, amikor a felhasználó egy pontos 150%-ot választ, a nézet már nem tarthatja tiszteletben a "szélességhez igazítást" ("fit to width") is, mert a két kérés ütközik. A felhasználói felület (UI) szempontjából ennek az a következménye, hogy a nagyítás gomb és az oldalhoz igazítás (fit-to-page) gomb kölcsönösen kizárják egymást, az eszköztárnak pedig láthatóvá kell tennie az aktív módot. Amikor a felhasználó az oldalhoz igazításra kattint, állítsa be a FitMode-ot; amikor egy numerikus nagyításra kattint, állítsa be a Zoom-ot, és hagyja, hogy magától törölje az igazítási módot (fit mode)

Ha inkább ön szeretné kiszámítani az igazítási értéket (fit value), talán azért, hogy egy nagyítási csúszkát beállítson az aktuális igazítási százalékkal, az oldalankénti (per-page) segédek megadják önnek a számokat a mód megváltoztatása nélkül. A PageWidthZoom[N], a PageZoom[N] és az ActualSizeZoom[N] azt a százalékot adják vissza, amely az N. oldalt a szélességhez igazítaná, egészben jelenítené meg, vagy tényleges (actual) méretben renderelné

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Mire van valójában szüksége egy kész megjelenítőnek

A fenti megjelenítő néhány tucat sornyi, és már elvégzi azt a munkát, amire egy dokumentum-munkafolyamatnak szüksége van: megnyit egy fájlt, túlél egy hibásat, megjelenít egy oldalt, mozog az oldalak között, és kézzel vagy igazítással megváltoztatja a nagyítást. A PDFium csendben elvégzi a nehéz részeket. A beágyazott betűtípusok feloldódnak, a jegyzetek és az űrlapmezők ott jelennek meg, ahová a dokumentum helyezi őket, és az oldal, amit ön lát, megegyezik azzal, amit egy Chrome-felhasználó is látna, mivel ugyanaz a motor (engine) rajzolja mindkettőt

Ebből az alapból a kiegészítések (additions) inkább növekményesek, mint strukturálisak. A szövegkijelölés és a keresés ugyanabból a szövegrétegből olvas, amit a PDFium már amúgy is felépít; a metaadatok, például a Pdf.Title és a Pdf.Author egyetlen tulajdonság-olvasásra vannak; a forgatás és a szürkeárnyalat renderelési opciók, amiket átad, amikor az oldalt egy bittérképre (bitmap) rajzolja. Ezek egyike sem változtatja meg az itt meglévő gerincet (spine), ami a dokumentum objektum, a nézet és az ezeket összekötő "betöltés majd navigálás" (load-then-navigate) folyamat. Építse fel ezt a gerincet jól, és a többi már csak díszítés

A mindenütt használt TPdf és TPdfView komponensek a Delphihez és C++Builderhez készült PDFium Component részei, amelynek termékoldala a teljes megjelenítő (viewer) referenciát tartalmazza