Tehnički članak

Izradite PDF preglednik u Delphiju pomoću PDFium Component-a

PDF preglednik u Delphiju svodi se na dvije komponente i ožičenje (wiring) između njih. TPdf posjeduje dokument: otvara datoteku, dešifrira je te odgovara na pitanja o broju stranica i metapodacima. TPdfView je vizualna kontrola koja crta stranice na zaslonu i rješava pomicanje (scrolling), zumiranje i stranicu koju korisnik trenutno gleda. PDFium Component omotava (wraps) isti pogon za renderiranje koji se isporučuje unutar Chromea, tako da glifovi, izglađivanje rubova (anti-aliasing) i boja koju dobijete na platnu odgovaraju onome što vaši korisnici već vide u svom pregledniku. Posao nije u renderiranju. On se sastoji od povezivanja objekta dokumenta s pogledom, učitavanja bez pada na oštećenoj datoteci ili datoteci zaštićenoj lozinkom, te davanja korisniku pregršt kontrola zbog kojih se preglednik osjeća dovršenim: okretanje stranice, mijenjanje zumiranja, prilagođavanje (fit) stranice prozoru

Oovo prolazi kroz taj sklop (assembly) onim redoslijedom kojim ga zapravo gradite. Sve se ovdje renderira po jednu stranicu odjednom, što je ono što većina radnih tokova (workflows) s dokumentima želi. Ako trebate stranice naslagane u jednom stupcu za kontinuirano pomicanje, to je drugačija odluka o izgledu i to nije put ovdje

Povezivanje TPdf-a s TPdfView-om

Ispustite TPdf i TPdfView na obrazac (form), a zatim recite pogledu koji dokument da prikaže. Ta jedna dodjela cijela je poveznica između nevizualnog dokumenta i kontrole koja ga oslikava

Arhitektura Delphi PDF preglednika gdje TPdf posjeduje dokument, TPdfView ga boji, a jedna dodjela svojstva povezuje ih na vrhu PDFium DLL-a
TPdf dokument posjeduje dok TPdfView naslikava, i jedna dodjela dvoje povezuje preko zajedničkog PDFium enginea
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf i PdfView postavljeni su u vrijeme dizajniranja.
  PdfView.Pdf := Pdf;                 // pogled crta sve što ovaj dokument sadrži
  PdfView.FitMode := pfmFitWidth;     // postavi korisnika na razuman početni zum
end;

Prije nego što se išta od ovoga pokrene, nativna (native) biblioteka PDFium-a mora biti na stroju. PDFium Component poziva u pdfium32.dll ili pdfium64.dll ovisno o vašoj ciljanoj platformi, a dokument se jednostavno odbija otvoriti ako se DLL ne može pronaći. Isporučite odgovarajući DLL pored svoje izvršne datoteke ili ga smjestite tamo gdje će ga učitavač (loader) sustava pronaći. Verzije (builds) s omogućenim V8-om postoje samo za PDF-ove koji nose JavaScript koji želite izvršiti, što običan preglednik ne radi, stoga posegnite za standardnim DLL-om osim ako nemate konkretan razlog da to ne učinite

Učitavanje dokumenta bez povjerenja u unos

Instinkt je zamotati učitavanje u try/except i tretirati izbačenu iznimku (thrown exception) kao neuspjeh. Taj instinkt je ovdje pogrešan, a pogreška proizvodi preglednik koji izgleda u redu sve dok mu netko ne preda pokvarenu (broken) datoteku. Postavljanje Active := True ne uzrokuje iznimku pri pogrešci učitavanja. PDFium Component hvata (catches) internu grešku i ostavlja Active postavljenim na False, tako da je jedini pošten (honest) način da saznate je li se dokument otvorio taj da pročitate svojstvo nakon što ste ga postavili

Dijagram odluke učitavanja za Delphi PDFium preglednik gdje postavljanje Active nikad ne podiže iznimku, tih false znači krivu lozinku ili oštećenu datoteku, a slijedi jedan ponovni pokušaj lozinke
Aktivacija pri kvaru nikad ne baca iznimku, pa viewer Active očita natrag i na tihi false odgovara jednim ponovnim pokušajem lozinke
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // nikad ne baca iznimku; neuspjeh ostavlja Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // pogled prati vlastitu trenutnu stranicu
  UpdatePageLabel;
end;

Dvije stvari zaslužuju pažnju. Prva je da PageNumber postoji na oba objekta i da su neovisna. Pdf.PageNumber je pojam dokumenta o trenutnoj stranici; PdfView.PageNumber je stranica koju kontrola zapravo prikazuje i ona je ta koju postavljate da biste korisnika premještali (move) kroz datoteku. Postavljanje jednog ne pomiče (move) drugo, tako da preglednik (viewer) uvijek pokreće (drives) svojstvo pogleda (view). Drugo je indeksiranje temeljeno na 1 (1-based indexing): stranice idu od 1 do Pdf.PageCount, a ne od 0, što hvata (catches) sve one koji su navikli na polja koja počinju s nulom

Rukovanje šifriranom datotekom

Šifrirani (encrypted) dokumenti preklapaju se (fold) u istu putanju (path) učitavanja. Ako je lozinka za otvaranje postavljena prije aktivacije, dokument se dešifrira (decrypts) dok se otvara; ako je pogrešna ili nedostaje, Active ostaje False točno kao i za oštećenu datoteku. Stoga je oporavak traženje lozinke (prompt for a password) i ponovni pokušaj 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;       // mora biti postavljeno prije 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 (failure) tih i za lošu lozinku i za oštećenu datoteku, ne možete ih razlikovati samo prema Active. U praksi je to prihvatljivo za preglednik: korisnik ili da ispravnu lozinku ili sazna da se datoteka neće otvoriti, a poruka se čita isto na oba načina

Listanje kroz dokument

Kada je dokument otvoren, navigacija je aritmetika na PdfView.PageNumber ograničena sa Pdf.PageCount. Jedini pravi posao je stezanje (clamping), tako da gumbi (buttons) nikada ne gurnu (push) stranicu izvan raspona, a prvi i zadnji gumb 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;

// četiri navigacijska gumba svode se na po jedan poziv
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;

Tekstualni okvir "idi na stranicu N (go to page N)" isti je GoToPage poziv koji se dobiva iz parsiranog cijelog broja, a stezaljka (clamp) pokriva slučaj u kojem korisnik upisuje 9999 u datoteku od deset stranica. Zadržite UpdatePageLabel kao jedino mjesto koje piše "Stranica 3 od 12" tako da ispis (readout) nikada ne izađe iz sinkronizacije (drifts out of sync) s onim što pogled prikazuje

Zumiranje: eksplicitni postoci i načini prilagođavanja

Zumiranje na TPdfView dolazi u dva okusa (flavors) koja komuniciraju, a razumijevanje interakcije razlika je između kontrole zumiranja koja se ponaša kako treba i one koja se bori (fights) s korisnikom. Izravan (direct) put je svojstvo Zoom, postotak u kojem 100 znači stvarnu (actual) veličinu. Drugi je put FitMode, koji pogledu (view) kaže da umjesto vas izračuna (compute) zumiranje i nastavi ga iznova izračunavati (recomputing) kako se prozor mijenja (resizes)

Interakcija Zoom i FitMode u PDFium Delphi pregledniku gdje dodjela točnog Zoom-a postavlja FitMode na pfmNone, a odabir načina prilagbe vraća zumiranje prikazu
Dodjela točnog zuma fit način briše, a biranje fit načina računanje zuma vraća viewu
// fiksna povećanja
PdfView.Zoom := 100;     // stvarna veličina
PdfView.Zoom := 50;      // pola
PdfView.Zoom := 200;     // dvostruko

// pusti pogled da prilagodi stranicu prozoru i drži je prilagođenom pri promjeni veličine
PdfView.FitMode := pfmFitWidth;   // širina stranice ispunjava kontrolu
PdfView.FitMode := pfmFitPage;    // cijela stranica vidljiva
PdfView.FitMode := pfmActualSize; // 1:1 s točkama dokumenta

Ovdje je dio koji sapliće ljude (trips people up). Izravno dodjeljivanje (assigning) Zoom-a resetira FitMode na pfmNone. To je ispravno ponašanje, a ne greška (bug): u trenutku kada korisnik odabere točnih 150%, pogled više ne može uvažavati "prilagođavanje širini (fit to width)" jer su ta dva zahtjeva u sukobu (conflict). Posljedica za vaše korisničko sučelje je ta da su gumb (button) za povećanje (zoom-in) i gumb za prilagođavanje stranici (fit-to-page) međusobno isključiva stanja, a alatna traka (toolbar) trebala bi aktivni način učiniti vidljivim. Kada korisnik klikne prilagodi stranici, postavite FitMode; kada kliknu numeričko (numeric) zumiranje, postavite Zoom i pustite ga da sam očisti način prilagođavanja

Ako biste radije sami izračunali vrijednost prilagođavanja (fit value), možda da biste posijali (seed) klizač zumiranja s trenutnim postotkom prilagođavanja (fit percentage), pomoćnici po stranici (per-page helpers) daju vam brojke bez mijenjanja načina. PageWidthZoom[N], PageZoom[N] i ActualSizeZoom[N] vraćaju postotak koji bi stranicu N prilagodio širini, prilagodio cijelu ili bi je renderirao u njezinoj stvarnoj (actual) veličini

// posij prikaz zuma iz vrijednosti prilagodbe širini trenutne stranice
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Što je dovršenom pregledniku zapravo potrebno

Gore navedeni preglednik (viewer) je nekoliko desetaka linija, a on već obavlja (does) posao (job) koji tijek rada (workflow) dokumenta treba: otvara datoteku, preživljava (survive) onu lošu, prikazuje stranicu, kreće se između stranica i mijenja povećanje (magnification) ručno ili prilagođavanjem (fit). PDFium radi teške (hard) dijelove tiho. Ugrađeni (embedded) fontovi se razrješavaju (resolve), bilješke (annotations) i polja obrasca (form fields) crtaju (paint) tamo gdje ih dokument postavlja, a stranica koju vidite odgovara (matches) onoj koju bi Chromeov korisnik vidio, jer ih crta (drawing) isti motor (engine)

Iz ove su baze dodaci (additions) inkrementalni (incremental), a ne strukturni. Odabir teksta i pretraživanje (search) čitaju s istog sloja (layer) teksta koji PDFium već gradi; metapodaci kao što su Pdf.Title i Pdf.Author udaljeni su (away) samo jedno čitanje (read) svojstva; rotacija i sivi tonovi (grayscale) su render opcije (options) koje prolazite (pass) kada crtate (draw) stranicu na bitmapu. Ništa od toga ne mijenja kralježnicu (spine) koju ovdje imate, a to su objekt dokumenta, pogled (view) i tok učitaj-zatim-navigiraj (load-then-navigate flow) koji ih povezuje (connecting). Ispravite (get right) tu kralježnicu i ostatak (rest) je ukras (decoration)

Komponente TPdf i TPdfView koje se ovdje koriste dio su PDFium Component-a za Delphi i C++Builder, koji nosi punu referencu preglednika na stranici svog proizvoda