Technical Article

Izrada preglednika PDF-a u Delphiju pomoću PDFium VCL-a

Preglednik PDF-a u Delphiju svodi se na dvije komponente i njihovo međusobno povezivanje. TPdf upravlja dokumentom: otvara datoteku, dešifrira je i odgovara na upite o broju stranica i metapodacima. TPdfView je vizualna kontrola koja iscrtava stranice na zaslonu te upravlja pomicanjem (scrolling), zumiranjem i stranicom koju korisnik trenutno gleda. PDFium VCL koristi isti mehanizam za iscrtavanje (rendering engine) koji se isporučuje s preglednikom Chrome, tako da glifovi, zaglađivanje (anti-aliasing) i boje na platnu odgovaraju onome što vaši korisnici već vide u svom pregledniku. Glavni zadatak nije u samom renderiranju. On se sastoji od povezivanja objekta dokumenta s prikazom, učitavanja bez rušenja aplikacije na oštećenoj datoteci ili datoteci zaštićenoj lozinkom, te pružanja korisniku onih nekoliko osnovnih kontrola koje preglednik čine potpunim: okretanje stranica, promjena zumiranja i prilagodba stranice prozoru.

Ovaj vodič prolazi kroz taj postupak redoslijedom kojim ga stvarno gradite. Sve što je ovdje opisano prikazuje jednu po jednu stranicu, što je i najčešći zahtjev u obradi dokumenata. Ako trebate stranice naslagane u jednoj neprekidnoj koloni koja se pomiče, to je drugačija odluka o izgledu koja je pokrivena u zasebnom članku i nije dio ovog puta.

Povezivanje TPdf i TPdfView

Postavite TPdf and TPdfView na formu, a zatim recite prikazu koji dokument treba prikazati. Ta jednostavna dodjela predstavlja cjelokupnu vezu između nevizualnog dokumenta i kontrole koja ga iscrtava.

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;

Prije nego što se išta od ovoga pokrene, nativna PDFium knjižnica mora biti na računalu. PDFium VCL poziva pdfium32.dll ili pdfium64.dll ovisno o vašoj ciljnoj platformi, a dokument se jednostavno neće otvoriti ako se DLL ne može pronaći. Isporučite odgovarajući DLL uz svoju izvršnu datoteku ili ga smjestite tamo gdje će ga sustav sam pronaći. Verzije s omogućenim V8 mehanizmom postoje samo za PDF-ove koji sadrže JavaScript koji želite izvršiti, što običan preglednik ne zahtijeva, pa koristite standardni DLL osim ako nemate konkretan razlog protiv toga.

Učitavanje dokumenta bez povjerenja u ulazne podatke

Instinkt nas navodi da učitavanje omotamo u try/except blok i tretiramo bačenu iznimku kao neuspjeh. Taj je instinkt ovdje pogrešan, a njegovo slijeđenje stvara preglednik koji izgleda dobro sve dok vam netko ne prosljedi oštećenu datoteku. Postavljanje Active := True ne podiže iznimku u slučaju neuspjeha učitavanja. PDFium VCL hvata internu pogrešku i ostavlja svojstvo Active na vrijednosti False, pa je jedini točan način da saznate je li se dokument otvorio čitanje tog svojstva nakon što ga postavite.

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;

Dvije stvari zaslužuju pažnju. Prva je da PageNumber postoji na oba objekta i da su oni neovisni. Pdf.PageNumber je dokumentov koncept trenutne stranice; PdfView.PageNumber je stranica koju kontrola zapravo prikazuje i to je ona koju postavljate kako biste korisnika pomicali kroz datoteku. Postavljanje jednog ne pomiče drugo, tako da preglednik uvijek upravlja svojstvom prikaza. Druga stvar je indeksiranje koje počinje od 1: stranice idu od 1 do Pdf.PageCount, a ne od 0, što može zbuniti svakoga tko je navikao na polja s početnim indeksom nula.

Rukovanje šifriranom datotekom

Šifrirani dokumenti uklapaju se u isti tijek učitavanja. Ako se lozinka za otvaranje postavi prije aktivacije, dokument se dešifrira prilikom otvaranja; ako je pogrešna ili nedostaje, Active ostaje na False, baš kao i kod oštećene datoteke. Stoga se rješenje sastoji u traženju lozinke od korisnika i ponovnom pokušaju aktivacije.

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;

Budući da je neuspjeh tih i za pogrešnu lozinku i za oštećenu datoteku, ne možete ih razlikovati samo na temelju svojstva Active. U praksi je to prihvatljivo za preglednik: korisnik ili unese ispravnu lozinku ili sazna da se datoteka ne može otvoriti, a poruka je ista u oba slučaja.

Kretanje kroz stranice dokumenta

Nakon što je dokument otvoren, navigacija je aritmetika nad svojstvom PdfView.PageNumber ograničena s Pdf.PageCount. Jedini stvarni posao je ograničavanje vrijednosti (clamping), tako da gumbi nikada ne pomaknu stranicu izvan raspona, a gumbi za prvu i zadnju stranicu ostaju onemogućeni na krajevima datoteke.

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;

Polje za unos teksta "idi na stranicu N" poziva istu metodu GoToPage s analiziranim cijelim brojem, a ograničavanje pokriva slučaj kada korisnik utipka 9999 u datoteku od deset stranica. Neka UpdatePageLabel bude jedino mjesto koje ispisuje poruku poput "Stranica 3 od 12", tako da prikazani broj nikada ne odudara od onoga što se stvarno prikazuje.

Zumiranje: točni postoci i načini prilagodbe

Zumiranje na TPdfView dolazi u dva oblika koji međusobno djeluju, a razumijevanje te interakcije čini razliku između kontrole zumiranja koja se ponaša kako treba i one koja se bori protiv korisnika. Izravni put je svojstvo Zoom, postotak u kojem 100 označava stvarnu veličinu. Drugi put je FitMode, koji govori prikazu da sam izračuna zumiranje i da ga ponovno izračunava prilikom svake promjene veličine prozora.

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

Evo dijela na kojem se programeri često spotaknu. Izravno dodjeljivanje vrijednosti svojstvu Zoom resetira FitMode na pfmNone. To je ispravno ponašanje, a ne bug: onog trenutka kada korisnik odabere točno 150%, prikaz više ne može poštovati opciju "prilagodi širini" jer su ta dva zahtjeva u konfliktu. Posljedica za vaše korisničko sučelje je da su gumb za zumiranje i gumb za prilagodbu stranice međusobno isključiva stanja, a alatna traka trebala bi jasno prikazati aktivni način rada. Kada korisnik klikne na prilagodbu stranice, postavite FitMode; kada klikne na numeričko zumiranje, postavite Zoom i pustite da se način prilagodbe sam poništi.

Ako radije sami želite izračunati vrijednost prilagodbe, primjerice kako biste klizač za zumiranje inicijalizirali s trenutnim postotkom prilagodbe, pomoćne funkcije po stranici pružaju vam te brojeve bez promjene načina rada. PageWidthZoom[N], PageZoom[N] i ActualSizeZoom[N] vraćaju postotak koji bi stranicu N prilagodio širini, prilagodio cijelu ili je prikazao u stvarnoj veličini.

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

Što dovršeni preglednik zapravo treba

Izvorni naslov preuveličava količinu posla. Gore opisani preglednik ima svega nekoliko desetaka redaka, a već obavlja posao koji je potreban za rad s dokumentima: otvara datoteku, preživljava oštećenu, prikazuje stranicu, kreće se među stranicama i mijenja povećanje ručno ili prilagodbom. PDFium tiho odrađuje teže dijelove. Ugrađeni fontovi se razrješavaju, bilješke (annotations) i polja obrazaca iscrtavaju se tamo gdje ih dokument postavi, a stranica koju vidite odgovara onoj koju bi vidio korisnik preglednika Chrome, jer ih iscrtava isti mehanizam.

Od ove osnove, dodaci su inkrementalni, a ne strukturalni. Odabir teksta i pretraga čitaju iz istog tekstualnog sloja koji PDFium već izgrađuje; metapodaci kao što su Pdf.Title and Pdf.Author udaljeni su samo jedno čitanje svojstva; rotacija i crno-bijeli prikaz su opcije iscrtavanja koje prosljeđujete kada stranicu crtate u bitmapu. Ništa od toga ne mijenja kralježnicu sustava koju ovdje imate, a to su objekt dokumenta, prikaz i tijek učitavanja i navigacije koji ih povezuje. Uspostavite tu kralježnicu kako treba, a sve ostalo je samo ukras.

Komponente TPdf i TPdfView korištene u ovom članku dio su paketa PDFium VCL za Delphi i C++Builder, koji sadrži cjelovitu dokumentaciju preglednika na svojoj stranici proizvoda.