Technical Article

Tvorba PDF prehliadača v Delphi pomocou PDFium VCL

Prehliadač PDF v Delphi sa opiera o dva komponenty a ich vzájomné prepojenie. TPdf spravuje samotný dokument: otvára súbor, dešifruje ho a poskytuje informácie o počte strán a metadátach. TPdfView je vizuálny ovládací prvok, ktorý vykresľuje stránky na obrazovke a zabezpečuje posúvanie, približovanie a sledovanie stránky, ktorú si používateľ práve prezerá. PDFium VCL využíva rovnaké vykresľovacie jadro, aké sa dodáva v prehliadači Chrome, takže výsledné glyfy, vyhladzovanie a farby na plátne presne zodpovedajú tomu, čo vaši používatelia už poznajú. Úloha nespočíva vo vykresľovaní. Spočíva v prepojení objektu dokumentu so zobrazením, v bezpečnom načítaní bez pádov pri poškodenom alebo heslom chránenom súbore a v poskytnutí niekoľkých ovládacích prvkov, ktoré z prehliadača robia hotový produkt: otáčanie stránok, zmena priblíženia a prispôsobenie stránky oknu.

Tento návod vás prevedie touto zostavou v poradí, v akom ju skutočne vytvárate. Všetko tu vykresľuje jednu stránku naraz, čo vyžaduje väčšina procesov spracovania dokumentov. Ak potrebujete stránky zoradené pod sebou v jednom súvislom stĺpci, ide o iné rozhodnutie o rozložení, ktorému sa venuje samostatný článok a nie je predmetom tohto návodu.

Prepojenie TPdf a TPdfView

Umiestnite komponenty TPdf a TPdfView na formulár a potom povedzte zobrazeniu, ktorý dokument má zobraziť. Toto jediné priradenie predstavuje celé prepojenie medzi nevizuálnym dokumentom a ovládacím prvkom, ktorý ho vykresľuje.

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;

Pred spustením kódu musí byť na počítači prítomná natívna knižnica PDFium. PDFium VCL volá súbor pdfium32.dll alebo pdfium64.dll v závislosti od vašej cieľovej platformy a dokument sa jednoducho odmietne otvoriť, ak DLL súbor nemožno nájsť. Distribuujte zodpovedajúci DLL súbor vedľa vášho spustiteľného súboru alebo ho umiestnite tam, kde ho nájde systémový zavádzač. Verzie s povoleným V8 existujú iba pre súbory PDF, ktoré obsahujú JavaScript, ktorý chcete spustiť, čo bežný prehliadač nepotrebuje. Preto siahnite po štandardnej verzii DLL, pokiaľ nemáte konkrétny dôvod urobiť inak.

Načítanie dokumentu bez dôvery voči vstupu

Inštinkt velí zabaliť načítanie do bloku try/except a vyvolanú výnimku považovať za zlyhanie. Tento inštinkt je tu však nesprávny a jeho uplatnenie vedie k prehliadaču, ktorý vyzerá v poriadku, kým mu niekto nepredloží poškodený súbor. Nastavenie Active := True nevyvolá výnimku pri zlyhaní načítania. PDFium VCL zachytí internú chybu a ponechá vlastnosť Active na hodnote False, takže jediným spoľahlivým spôsobom, ako zistiť, či sa dokument otvoril, je prečítať túto vlastnosť po jej nastavení.

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;

Dve veci si zaslúžia pozornosť. Prvou je, že PageNumber existuje na oboch objektoch a oba sú nezávislé. Pdf.PageNumber je predstava dokumentu o aktuálnej stránke; PdfView.PageNumber je stránka, ktorú ovládací prvok skutočne zobrazuje, a je to tá, ktorú nastavujete na presun používateľa v rámci súboru. Nastavenie jednej vlastnosti nepresunie druhú, preto prehliadač vždy riadi vlastnosť zobrazenia. Druhou vecou je indexovanie od 1: stránky prebiehajú od 1 do Pdf.PageCount, nie od 0, čo môže prekvapiť každého, kto je zvyknutý na polia indexované od nuly.

Spracovanie šifrovaného súboru

Šifrované dokumenty sa začleňujú do rovnakej cesty načítania. Ak je heslo na otvorenie nastavené pred aktiváciou, dokument sa pri otvorení dešifruje; ak chýba alebo je nesprávne, vlastnosť Active zostane na hodnote False rovnako ako pri poškodenom súbore. Riešením je preto požiadať o heslo a zopakovať aktiváciu.

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;

Keďže zlyhanie je tiché pri nesprávnom hesle aj pri poškodenom súbore, nemôžete ich od seba odlíšiť iba na záklaste vlastnosti Active. V praxi je to pre prehliadač prijateľné: používateľ buď zadá správne heslo, alebo zistí, že súbor sa nedá otvoriť, pričom správa je v oboch prípadoch rovnaká.

Stránkovanie v dokumente

S otvoreným dokumentom je navigácia aritmetikou nad vlastnosťou PdfView.PageNumber ohraničenou hodnotou Pdf.PageCount. Jedinou reálnou prácou je obmedzenie rozsahu, aby tlačidlá nikdy neposunuli stránku mimo povolený rozsah a tlačidlá pre prechod na začiatok a koniec zostali na koncoch súboru zakázané.

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;

Textové pole pre „prechod na stránku N“ je rovnaké volanie GoToPage napájané z analyzovaného celého čísla, pričom ohraničenie pokrýva prípad, keď používateľ zadá číslo 9999 v desaťstránkovom súbore. Ponechajte UpdatePageLabel ako jediné miesto, ktoré zapisuje text napríklad „Stránka 3 z 12“, aby údaj na paneli nikdy nešiel mimo synchrón s tým, čo zobrazuje zobrazenie.

Lupa: explicitné percentá a režimy prispôsobenia

Priblíženie v komponentu TPdfView prichádza v dvoch variantoch, ktoré sa navzájom ovplyvňujú, a pochopenie tejto interakcie určuje rozdiel medzi ovládacím prvkom priblíženia, ktorý funguje správne, a tým, ktorý s používateľom bojuje. Priamou cestou je vlastnosť Zoom vyjadrujúca percentuálny podiel, kde 100 znamená skutočnú veľkosť. Druhou cestou je FitMode, ktorý hovorí zobrazeniu, aby vypočítalo priblíženie za vás a prepočítavalo ho pri zmene veľkosti okna.

// 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

Tu je časť, ktorá často pletie vývojárov. Priame priradenie hodnoty do Zoom vynuluje FitMode na pfmNone. Je to správne správanie, nie chyba: v momente, keď používateľ zvolí presných 150 %, zobrazenie už nemôže zároveň dodržiavať „prispôsobenie na šírku“, pretože tieto dve požiadavky sú v konflikte. Dôsledkom pre vaše používateľské rozhranie je, že tlačidlo priblíženia a tlačidlo prispôsobenia stránky predstavujú vzájomne sa vylučujúce stavy a na paneli nástrojov by mal byť viditeľný aktívny režim. Keď používateľ klikne na prispôsobenie stránky, nastavte FitMode; keď klikne na číselné priblíženie, nastavte Zoom a nechajte ho, aby režim prispôsobenia sám vyčistil.

Ak by ste radšej vypočítali hodnotu prispôsobenia sami, napríklad na naplnenie posuvníka priblíženia aktuálnym percentom prispôsobenia, pomocné metódy pre jednotlivé stránky vám poskytnú tieto čísla bez zmeny režimu. Metódy PageWidthZoom[N], PageZoom[N] a ActualSizeZoom[N] vracajú percento, ktoré by prispôsobilo stránku N na šírku, na výšku alebo ju vykreslilo v skutočnej veľkosti.

// 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;

Čo hotový prehliadač skutočne potrebuje

Pôvodný názov prácu trochu nadhodnocuje. Vyššie uvedený prehliadač má len niekoľko desiatok riadkov a už robí to, čo spracovanie dokumentov vyžaduje: otvorí súbor, prežije poškodený súbor, zobrazí stránku, presúva sa medzi stránkami a mení zväčšenie ručne alebo prispôsobením. PDFium robí tie zložité časti potichu. Vložené písma sa správne vykreslia, anotácie a polia formulárov sa vykreslia tam, kam ich dokument umiestnil, a stránka, ktorú vidíte, sa zhoduje s tou, ktorú by videl používateľ prehliadača Chrome, pretože obe vykresľuje rovnaké jadro.

Z tohto základu sú ďalšie doplnky skôr prírastkové než štrukturálne. Výber textu a vyhľadávanie čítajú z rovnakej textovej vrstvy, ktorú PDFium už stavia; metadáta ako Pdf.Title and Pdf.Author sú dostupné cez jednoduché čítanie vlastností; rotácia a odtiene sivej sú možnosti vykresľovania, ktoré odovzdávate pri kreslení stránky na bitmapu. Žiadna z týchto vecí nemení základ, ktorý tu máte – a tým je objekt dokumentu, zobrazenie a tok načítania a navigácie, ktorý ich spája. Urobte tento základ správne a zvyšok je už len dekorácia.

Komponenty TPdf a TPdfView použité v tomto návode sú súčasťou knižnice PDFium VCL pre Delphi a C++Builder, ktorá obsahuje kompletnú referenčnú implementáciu prehliadača na svojej produktovej stránke.