Technical Article

PDF preglednik za Lazarus i Free Pascal s PDFiumom

Delphi i Lazarus kompajliraju isti Object Pascal, a ta površinska sličnost upravo je ono što portiranje preglednika između njih čini varljivim. Ova se dva lanca alata razilaze na tri mjesta koja su važna za rad s PDF-om: izvorni tip string je UTF-16 u Delphiju i UTF-8 u LCL aplikaciji; VCL i LCL su različiti vizualni okviri sa svojim kontrolama, dijalozima i formatima strujanja formi; a Delphi binarna datoteka cilja Windows dok FPC binarna datoteka može biti namijenjena za Linux ili macOS. Nijedna od tih razlika ne pojavljuje se u vrijeme kompajliranja. Preglednik izgrađen na PDFium komponenti (PDFium Component), koja isporučuje VCL i LCL izdanja iz jednog stabla koda, kompajlirat će se bez problema pod Lazarusom nakon nekoliko zamjena naziva jedinica i nekoliko {$IFDEF FPC} blokova. Neuspjesi dolaze kasnije, kada stvarni podaci i stvarna implementacija razotkriju pretpostavke koje je Delphi verzija prešutno donosila.

Četiri od tih pretpostavki uzrokuju većinu izgubljenog vremena: kodiranje teksta na granici korisničkog sučelja, iskušenje da se održavaju dvije kopije forme, način na koji se binarna datoteka izvornog pokretača rješava u izvođenju i trenutak kada pretvaranje teksta u govor (text-to-speech) ostane bez platforme nakon što SAPI nestane. Svaku od njih je lako riješiti ako znate da dolazi, a skupo je tražiti ako to ne znate.

Isti Pascal, različiti sadržaji nizova

Delphijev izvorni string je UTF-16 od 2009. godine. Lazarus i Free Pascal prema zadanim postavkama koriste UTF-8 u LCL aplikacijama. API-ji komponente koji rukuju tekstom koriste UTF-16 putem tipa WString, koji FPC verzija definira kao alias za WideString, pa je svaka granica na kojoj tekst prelazi između vašeg LCL korisničkog sučelja i PDF pokretača točka pretvorbe.

Pretvorbe se događaju automatski kod jednostavnih dodjeljivanja i većina koda nikada ne mora razmišljati o njima. Dvije navike sprječavaju pogreške kodiranja. Prosljeđujte tekst izravno bez manipulacije na razini bajtova: kod koji reže pojam za pretraživanje prema pomaku bajtova radi u Delphiju, gdje je jedan Char jedna UTF-16 jedinica, ali kvari višebajtni UTF-8 u LCL-u. I testirajte s podacima koji nisu ASCII od prvog pokretanja. Njemački naziv datoteke, ćirilični pojam za pretraživanje, ime autora s dijakritičkim znakovima u metapodacima dokumenta: čisti ASCII testni podaci skrivaju svaku pogrešku kodiranja, jer je ASCII jedino područje u kojem se UTF-8 i UTF-16 podudaraju bajt po bajt za svaki znak. Pogreška je prisutna cijelo vrijeme; ASCII je samo čini nevidljivom sve dok klijent u Münchenu ne otvori datoteku koju nikada niste isprobali.

Jedan uvjetni blok, a ne zasebna grana koda za svaki IDE

Nakon prvih desetak IFDEF-ova, baza koda počinje izgledati kao dva projekta u jednom repozitoriju, pa se račvanje za svaki IDE čini primamljivim. To je pogrešan potez. Stvarne se razlike svode na jedan zajednički deklaracijski blok, a račvanje udvostručuje trošak svakog popravka pogreške od tog trenutka nadalje. Držite uvjetni sloj ovako malim:

{$IFDEF FPC}
uses
  LCLType, Forms, Graphics, Controls;

type
  WString = WideString;   // component text APIs are UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

Sve ispod tog bloka kompajlira se identično u oba IDE-a. Rukovanje dokumentima, navigacija stranicama, pozivi za iscrtavanje: TPdf i TPdfView izlažu istu površinu u VCL i LCL izdanjima, tako da veći dio preglednika nikada ne vidi uvjet kompajlera. Održavanje koda takvim je strukturna disciplina, a ne pametan trik. Zajednička PDF logika živi u jedinicama koje ne uvode dijaloge ili ploče specifične za okvir. Nekolicina stvari koje se doista razlikuju, kao što su dijalozi za ispis i odabir datoteka sa svojim platformskim konvencijama, skrivaju se iza tankog sučelja koje se implementira jednom po okviru. IFDEF blok postaje jedino mjesto na koje se dopušta smještaj budućih razlika među platformama, umjesto curenja direktiva kompajlera kroz četrdeset jedinica.

Izgradite formu u kodu, a ne u dva dizajnera

Strujanje formi (form streaming) je mjesto gdje projekti s dva IDE-a tiho trunu. Datoteke .dfm i .lfm koje tvrde da opisuju istu formu razilaze se svojstvo po svojstvo sve dok se dvije verzije ne počnu ponašati različito iz razloga koje nitko ne može usporediti (diff), jer te dvije datoteke nisu čak ni u istom formatu. Izgradnja preglednika u izvođenju (runtime) zaobilazi cijeli problem. Postoji jedan redoslijed konstruktora, pod kontrolom verzija kao običan kod, i čita se isto na obje platforme:

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  Pdf := TPdf.Create(Self);

  PdfView := TPdfView.Create(Self);
  PdfView.Parent := Self;
  PdfView.Align := alClient;
  PdfView.Pdf := Pdf;
  PdfView.FitMode := pfmFitWidth;

  if ParamCount > 0 then
  begin
    Pdf.FileName := ParamStr(1);
    Pdf.Active := True;   // opens the document; PageCount valid after this
  end;
end;

Točan redoslijed tih dodjeljivanja manje je važan od jedne linije koja obavlja stvarni posao. PdfView.Pdf := Pdf povezuje vizualnu kontrolu s komponentom dokumenta, i od te točke navigacija stranicama kroz PageNumber i ponašanje prilagodbe kroz FitMode reagiraju identično pod VCL-om i LCL-om. Vrijedi znati jednu neobičnost specifičnu za okvire prije nego što je korisnik preglednik prijavi kao pogrešku: ručno dodjeljivanje Zoom vraća FitMode natrag na pfmNone na oba okvira. Dakle, ako vaša alatna traka tretira "prilagodi širini" kao trajnu postavku, morate ponovno dodijeliti način prilagodbe nakon svakog programskog zumiranja, inače postavka tiho prestaje vrijediti čim kod prvi put dotakne razinu zumiranja.

Binarna datoteka na koju vas IDE nikada nije upozorio

Komponenta omotava PDFium pokretač, koji se isporučuje kao izvorna binarna datoteka platforme, a ta binarna datoteka je izvor gotovo svakog izvješća tipa "radi u IDE-u, ne radi s instaliranog prečaca". Tri pravila objašnjavaju većinu njih. Arhitektura (32-bitna/64-bitna) mora se točno podudarati. 32-bitna izvršna datoteka ne može učitati 64-bitnu pdfium knjižnicu, a poruka koju operativni sustav vraća ("modul nije pronađen" na nekim verzijama Windowsa) aktivno dovodi u zabludu, jer datoteka stoji točno tamo pokraj izvršne datoteke. Riješite putanju knjižnice relativno u odnosu na izvršnu datoteku, nikada u odnosu na radni direktorij; pokretanje iz IDE-a i pokretanje iz ljuske razlikuju se upravo u toj točki, zbog čega se pogreška skriva tijekom razvoja. I uhvatite neuspjelo učitavanje prije nego što se otvori prvi dokument, a zatim ga prijavite s jasno navedenom očekivanom putanjom i arhitekturom. Prijava podršci koja glasi "nedostaje PDFium 64-bitna binarna datoteka na putanji <path>" rješava se u nekoliko minuta. Ona koja glasi "preglednik se ruši pri pokretanju" pretvara se u tjedan dana dopisivanja.

Usregodine uskladite verziju binarne datoteke pokretača s izvršnom datotekom. PDFium se brzo razvija, a instalacijski program koji ažurira aplikaciju, ali ostavlja zastarjelu knjižnicu na disku, uzrokuje rušenja koja nitko u vašem uredu ne može reproducirati, iz jednostavnog razloga što svako računalo u vašem uredu ima odgovarajući par. Tretirajte knjižnicu kao dio artefakta izgradnje, s istim instalacijskim programom, istom oznakom verzije i istom putanjom povrata kao i izvršna datoteka koju učitava.

Registracija komponenti u Lazarus IDE-u

Izrada u izvođenju ne zahtijeva nikakvu registraciju u vrijeme dizajna (design-time), što je najčišća postavka za preglednik koji sam gradi svoje sučelje u kodu. Kada ipak želite komponente na Lazarusovoj paleti za rad u vrijeme dizajna, instalirajte paket i prepustite njegovoj namjenskoj registracijskoj jedinici, PDFiumLazReg u Lib/FPC/PDFiumLaz.lpk, da to riješi. Ta je jedinica namjerno označena za vrijeme dizajna: ona upućuje na sučelja urednika svojstava IDE-a koja se nikada ne smiju povezati s vašom izvršnom datotekom za isporuku.

Ako pogriješite u ovome, simptom je aplikacija koja neobjašnjivo ovisi o paketima IDE-a, što se manifestira kao neuspjeh implementacije na prvom računalu korisnika koje nikada nije imalo instaliran Lazarus.

Govor i čitači zaslona izvan Windowsa

Pretvaranje teksta u govor (text-to-speech) je jedina značajka u kojoj se priča o više platformi prekida, a prekida se na operativnom sustavu, a ne na komponenti. SAPI, uobičajena TTS pozadinska podrška na Windowsima, postoji samo na Windowsima. Lazarus verzija koja i dalje cilja Windows zadržava puni SAPI izlaz i isto ponašanje kompatibilno s NVDA čitačem koje je imao Delphi izvornik, tako da prijenos s Windowsa na Windows ovdje ne gubi ništa, a korisnik NVDA čitača ne može razlikovati ove dvije verzije.

Ciljanje Linuxa ili macOS-a je druga stvar. Nema SAPI-ja koji bi se mogao pozvati, pa se audio izlaz mora preusmjeriti na izvornu govornu uslugu dok API-ji za čitanje iznad nje ostaju nepromijenjeni. Taj je podjelak razlog za postavljanje govora iza sučelja od prvog unosa koda (commit): analiza redoslijeda čitanja i kursor za praćenje riječi neutralni su u odnosu na platformu i prenose se netaknuti, a mijenjati se mora samo tanki sloj koji zapravo proizvodi zvuk. Članak o pristupačnom čitatelju detaljno pokriva taj mehanizam čitanja.

Kontrolni popis pariteta prije nego što proglasite prijenos dovršenim

Sljedeća provjera otkrila je stvarne regresije, navedene otprilike redoslijedom kojim se neuspjesi obično pojavljuju. Otvorite dokument čija putanja sadrži znakove koji nisu ASCII. Potražite pojam koji sadrži znakove koji nisu ASCII i potvrdite da su pogoci istaknuti tamo gdje trebaju biti. Isprobajte pomicanje kotačićem miša, povlačenje za odabir i navigaciju stranicama pomoću tipkovnice na svakom skupu widgeta koji isporučujete, jer su rukovanje fokusom i ponašanje kotačića najosjetljiviji dijelovi LCL-a ovisni o skupu widgeta. Provjerite iscrtavanje na 100%, 150% i 200% skaliranja zaslona. Na kraju, pokrenite instaliranu verziju, a ne verziju iz IDE-a, na računalu koje nikada nije imalo instaliran IDE, jer je to jedini test koji pošteno provjerava učitavanje binarne datoteke. Sve ostalo može proći, dok taj dio tiho zakazuje.

Propusnost iscrtavanja prenosi se nepromijenjena između dva izdanja, pa se pristup predmemoriranju iz članka o predmemoriji iscrtavanja i performansama zumiranja primjenjuje na LCL preglednik točno onako kako je napisan za VCL.

Ništa od ovoga ne čini LCL izdanje lošijim. Glavna površina je identična na obje strane: TPdf, TPdfView, iscrtavanje, obrasci, ekstrakcija teksta i API-ji za pristupačnost ponašaju se isto bez obzira na to koji ih je IDE kompajlirao. Svaka razlika vrijedna praćenja vezana je uz platformu, a ne uz izdanje. SAPI govor je dostupan samo na Windowsima, dijalozi prate konvencije pojedinog okvira, a binarna datoteka mora odgovarati arhitekturi u koju se učitava. Ispravno riješite granice kodiranja, formu u izvođenju i učitavanje binarne datoteke, a ostatak prijenosa je mehanički posao koji je kompajler već odradio za vas.

Ovdje opisana VCL i LCL izdanja isporučuju se zajedno kao PDFium komponenta (PDFium Component), s izvornim kodom i identičnim javnim API-jima za Delphi, C++Builder i Lazarus/FPC.