Tehnični članak

Pregledovalnik PDF za Lazarus in Free Pascal s PDFium

Delphi in Lazarus prevajata isti Object Pascal, in prav ta zunanja podobnost je razlog, da je prenos pregledovalnika med njima varljiv. Orodni verigi se razhajata na treh mestih, pomembnih za delo s PDF-ji: privzeta vrsta niza (string) je v Delphi UTF-16, v aplikacijah LCL pa UTF-8; VCL in LCL sta različna vizualna okvira z lastnimi kontrolami, pogovornimi okni in formati za shranjevanje obrazcev; binarna datoteka Delphi pa je namenjena sistemu Windows, medtem ko je FPC binarna datoteka lahko namenjena sistemom Linux ali macOS. Nobena od teh razlik se ne pokaže ob prevajanju. Pregledovalnik, zgrajen s komponento PDFium Component, ki ponuja izdaji VCL in LCL iz enega samega izvornega drevesa, se bo v okolju Lazarus prevedel brez napak po nekaj zamenjavah imen enot in nekaj blokih {$IFDEF FPC}. Težave pa se pojavijo pozneje, ko resnični podatki in namestitev razkrijejo predpostavke, ki jih je različica za Delphi tiho sprejela

Štiri od teh predpostavk povzročijo večino izgubljenega časa: kodiranje besedila na meji uporabniškega vmesnika, skušnjava po vzdrževanju dveh kopij obrazca, način razreševanja binarne datoteke pogona ob zagonu in trenutek, ko sinteza govora ostane brez platforme po odstranitvi vmesnika SAPI. Vsako izmed teh težav je enostavno rešiti, če nanjo računamo vnaprej, in izjemno drago odpravljati, če nas preseneti

Enak Pascal, različna vsebina nizov

Delphijev privzeti tip niza string je od različice 2009 UTF-16. Lazarus in Free Pascal v aplikacijah LCL privzeto uporabljata UTF-8. Vmesniki API komponente, namenjeni delu z besedilom, uporabljajo UTF-16 prek tipa WString (ki ga FPC prevajalnik preslika v WideString), zato je vsaka meja, kjer besedilo prehaja med vašim LCL uporabniškim vmesnikom in pogonom PDF, točka pretvorbe

Pretvorbe se pri preprostih dodelitvah izvedejo samodejno in v večini kode o njih ni treba razmišljati. Dve navadi pomagata preprečiti napake pri kodiranju. Besedilo prenašajte neposredno brez spreminjanja na ravni bajtov: koda, ki reže iskalni niz glede na bajtni odmik, deluje v okolju Delphi, kjer je en znak (Char) enaka eni enoti UTF-16, v LCL pa poškoduje večbajtni UTF-8. Prav tako že od začetka testirajte s podatki, ki niso v formatu ASCII. Nemško ime datoteke, iskalni izraz v cirilici, ime avtorja s šumniki v metapodatkih dokumenta: testni podatki, ki vsebujejo samo ASCII znake, prikrijejo vsako napako pri kodiranju, saj je ASCII edino območje, kjer se UTF-8 in UTF-16 ujemata znak za znakom. Hrošč je ves čas prisoten, ASCII pa ga le skriva, dokler stranka v Münchnu ne odpre datoteke, ki je niste nikoli preizkusili

En pogojni blok namesto vejanja za vsak IDE

Po prvih ducat navodilih IFDEF lahko koda začne delovati kot dva projekta v istem repozitoriju, zato se zdi ločevanje kode za vsak IDE privlačna možnost. Vendar je to napačna poteza. Dejanske razlike se zmanjšajo na en skupni deklaracijski blok, ločevanje pa bi podvojilo stroške vsakega popravka napak v prihodnje. Ohranite pogojno plast čim manjšo:

{$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}

Vse pod tem blokom se prevaja enako v obeh okoljih IDE. Delo z dokumenti, navigacija po straneh, klici za izris: razreda TPdf in TPdfView ponujata enak vmesnik v izdajah VCL in LCL, zato večji del pregledovalnika nikoli ne naleti na prevajalske pogoje. Ohranjanje takšne strukture je stvar discipline in ne le pametna zvijača. Skupna logika PDF se nahaja v enotah, ki ne vsebujejo pogovornih oken ali plošč, specifičnih za posamezen okvir. Nekaj stvari, ki se resnično razlikujejo (kot so tiskalna pogovorna okna in izbira datotek z njihovimi platformnimi posebnostmi), je skritih za tankim vmesnikom, implementiranim enkrat za vsak okvir. Blok IFDEF tako postane edino mesto, kjer je dovoljeno uvajanje platformnih razlik, namesto da bi prevajalske direktive puščale skozi štirideset različnih enot

Zgradite obrazec v kodi, ne v dveh oblikovalnikih

Spreminjanje obrazcev je točka, kjer projekti z dvema okoljema IDE tiho propadajo. Datoteki .dfm in .lfm, ki naj bi opisovali isti obrazec, se začneta razlikovati pri posameznih lastnostih, dokler se obe različici ne začneta obnašati drugače zaradi razlogov, ki jih nihče ne more primerjati z orodji za primerjavo (diff), saj datoteki sploh nista v enakem formatu. Izgradnja pregledovalnika med delovanjem (runtime) se temu problemu popolnoma izogne. Na voljo je eno zaporedje konstruktorja, shranjeno v kodi, ki se bere enako na obeh platformah:

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;

Natančen vrstni red teh dodelitev je manj pomemben od tiste vrstice, ki opravi glavno delo. PdfView.Pdf := Pdf poveže vizualno kontrolo s komponento dokumenta, od te točke naprej pa krmarjenje po straneh prek PageNumber in prilagajanje prek FitMode delujeta enako pod VCL in LCL. Preden uporabnik prijavi napako, je dobro poznati eno posebnost med okviri: ročna nastavitev Zoom ponastavi FitMode nazaj na pfmNone v obeh sistemih. Če torej vaša orodna vrstica obravnava »prilagodi širini« kot trajno nastavitev, morate ponovno dodeliti način prilagajanja po vsaki programski spremembi povečave, sihronizacija pa preprosto neha veljati ob prvem premiku povečave

Binarna datoteka, na katero vas IDE nikoli ni opozoril

Komponenta ovija pogon PDFium, ki je na voljo kot domača binarna datoteka za platformo, ta datoteka pa je vir skoraj vseh poročil o napakah tipa »v IDE deluje, iz bližnjice pa ne«. Tri pravila pojasnjujejo večino teh težav. Bitnost se mora natančno ujemati. 32-bitna izvršljiva datoteka ne more naložiti 64-bitne knjižnice pdfium, operacijski sistem pa ob tem vrne sporočilo (npr. »modula ni mogoče najti« v nekaterih različicah Windows), ki je zavajajoče, saj se datoteka nahaja tik ob izvršljivi datoteki. Razrešite pot knjižnice glede na izvršljivo datoteko in nikoli glede na delovni imenik; zagon iz IDE in zagon iz lupine se razlikujeta prav v tej točki, zato se napaka med razvojem skrije. Prav tako ulovite neuspešno nalaganje, preden se odpre prvi dokument, in javite napako z izpisano pričakovano potjo in arhitekturo. Podporni zahtevek z vsebino »manjka 64-bitna binarna datoteka PDFium na poti <pot>« se reši v nekaj minutah. Tisti, ki pravi le »pregledovalnik se sesuje ob zagonu«, pa pomeni en teden dopisovanja

Ob tem posodabljajte različico binarne datoteke pogona skupaj z izvršljivo datoteko. PDFium se hitro razvija in namestitveni program, ki posodobi aplikacijo, a pusti zastarelo knjižnico na disku, povzroči sesutja, ki jih nihče v vaši pisarni ne more reproducirati – preprosto zato, ker ima vsak računalnik v vaši pisarni ujemajoč se par. Knjižnico obravnavajte kot del gradbenega artefakta z enakim namestitvenim programom, različico in možnostjo povrnitve kot izvršljivo datoteko, ki jo nalaga

Registracija komponent v okolju Lazarus IDE

Gradnja med delovanjem ne potrebuje nobene registracije med načrtovanjem, kar je najbolj čista rešitev za pregledovalnik, ki gradi lasten uporabniški vmesnik v kodi. Če pa želite imeti komponente na paleti Lazarus za delo med načrtovanjem, namestite paket in prepustite registracijo namenski registracijski enoti PDFiumLazReg v datoteki Lib/FPC/PDFiumLaz.lpk. Ta enota je namenoma označena za načrtovanje (design-time): sklicuje se na vmesnike urejevalnika lastnosti IDE, ki se nikoli ne smejo povezati z vašo končno izvršljivo datoteko

Če se tukaj zmotite, bo aplikacija nepojasnjeno odvisna od paketov IDE, registracija pa se bo pokazala kot napaka pri namestitvi na prvem računalniku stranke, ki nima nameščenega okolja Lazarus

Govor in bralniki zaslona izven sistema Windows

Sinteza govora (text-to-speech) je edina funkcija, kjer se večplatformna zgodba ustavi, in sicer na ravni operacijskega sistema, ne komponente. SAPI, običajni govorni pogon v sistemu Windows, obstaja le v tem sistemu. Različica Lazarus, ki še vedno cilja na Windows, ohranja poln izhod SAPI in enako združljivost z bralnikom NVDA kot original v Delphi, zato prenos med sistemoma Windows tukaj ne izgubi ničesar in uporabnik bralnika NVDA ne bo opazil razlike

Pri sistemih Linux ali macOS pa je situacija drugačna. Ker tam ni storitve SAPI, je treba zvočni izhod preusmeriti na domačo govorno storitev platforme, medtem ko API-ji za branje nad njo ostanejo nespremenjeni. Ta razdelitev govori v prid temu, da govor že od prvega commit-a postavite za vmesnik: analiza vrstnega reda branja in kazalec za sledenje besedam sta neodvisna od platforme in se preneseta brez sprememb, zamenjati pa je treba le tanko plast, ki dejansko proizvaja zvok. Članek o dostopnem bralniku podrobno opisuje te mehanizme

Kontrolni seznam za primerjavo, preden zaključite prenos

Naslednji kontrolni seznam je razkril resnične regresije, navedene v vrstnem redu, v katerem se napake običajno pojavijo. Odprite dokument, katerega pot vsebuje ne-ASCII znake. Poiščite izraz z ne-ASCII znaki in potrdite, da so zadetki označeni na pravih mestih. Preizkusite pomikanje s kolescem miške, vlečenje izbora in krmarjenje po straneh s tipkovnico na vsakem naboru gradnikov, ki ga ponujate, saj sta obravnava fokusa in obnašanje kolesca najbolj odvisna od nabora gradnikov v LCL. Preverite izris pri 100 %, 150 % in 200 % sistemski povečavi zaslona. Na koncu zaženite nameščeno različico (ne tiste iz IDE) na računalniku, ki nikoli ni imel nameščenega IDE – to je edini test, ki iskreno preveri razreševanje binarne datoteke. Vse ostalo lahko deluje, ta korak pa tiho spodleti

Hitrost izrisa se med izdajama ne spremeni, zato se pristop s predpomnjenjem iz članka o predpomnilniku izrisa in zmogljivosti povečave uporablja za pregledovalnik LCL natanko tako, kot je zapisano za različico VCL

Nič od tega ne pomeni, da je različica LCL slabša. Jedro vmesnika je na obeh straneh enako: TPdf, TPdfView, izris, obrazci, ekstrakcija besedila in dostopnostni API-ji delujejo enako, ne glede na to, kateri IDE jih je prevedel. Vsaka razlika, ki jo je vredno spremljati, je vezana na platformo in ne na izdajo komponente. Govor SAPI je omejen na Windows, pogovorna okna sledijo konvencijam posameznega okvira, binarna datoteka pa se mora ujemati z arhitekturo, v katero se nalaga. Ko pravilno uredite meje kodiranja, zagon obrazca in razreševanje binarnih datotek, je preostanek prenosa le še mehansko delo, ki ga je prevajalnik že opravil namesto vas

Izdaji VCL in LCL, opisani tukaj, sta na voljo skupaj kot del paketa PDFium Component z izvorno kodo in enakimi javnimi API-ji za Delphi, C++Builder in Lazarus/FPC