Technický článek

Prohlížeč PDF pro Lazarus a Free Pascal s PDFium

Delphi a Lazarus kompilují stejný Object Pascal, a právě tato povrchová podobnost je to, co dělá portování prohlížečky mezi nimi zavádějícím. Tyto dva toolchainy se rozcházejí na třech místech, která jsou pro práci s PDF důležitá: nativní typ string je v Delphi UTF-16 a v aplikaci LCL UTF-8; VCL a LCL jsou různé vizuální frameworky s vlastními ovládacími prvky, dialogy a formáty streamování formulářů; a binárka Delphi cílí na Windows, zatímco binárka FPC může mířit na Linux nebo macOS. Žádný z těchto rozdílů se neprojeví při kompilaci. Prohlížečka postavená na PDFium Component, který dodává edice VCL i LCL z jednoho zdrojového stromu, se pod Lazarem zkompiluje čistě po hrstce výměn názvů jednotek a několika blocích {$IFDEF FPC}. Selhání přijdou později, když skutečná data a skutečné nasazení odhalí předpoklady, které si Delphi build potichu dělal

Čtyři z těchto předpokladů odpovídají za většinu ztraceného času: kódování textu na hranici UI, pokušení udržovat dvě kopie formuláře, způsob, jakým se binárka nativního enginu za běhu překládá na cestu, a moment, kdy převodu textu na řeč dojde platforma ve chvíli, kdy zmizí SAPI. Každý z nich je levné vyřešit, pokud víte, že přichází, a drahé dohledat, pokud ne

Stejný Pascal, jiné payloady řetězců

Nativní string v Delphi je UTF-16 už od roku 2009. Lazarus a Free Pascal ve výchozím stavu používají v aplikacích LCL UTF-8. API komponenty směrem k textu mluví UTF-16 přes typ WString, který FPC build aliasuje na WideString, takže každá hranice, kde text přechází mezi vaším LCL UI a enginem PDF, je bodem konverze

Konverze probíhají automaticky u přímočarých přiřazení, a většina kódu o nich nikdy nemusí přemýšlet. Dva zvyky drží chyby kódování mimo hru. Předávejte text rovnou beze změny bez manipulace na úrovni bajtů: kód, který krájí hledaný výraz podle bajtového offsetu, funguje v Delphi, kde jeden Char je jedna jednotka UTF-16, a poškodí vícebajtové UTF-8 v LCL. A testujte s daty mimo ASCII už od prvního běhu. Německý název souboru, azbukou psaný hledaný výraz, jméno autora s diakritikou v metadatech dokumentu: čistě ASCII testovací data schovají každou vadu kódování, protože ASCII je jediný rozsah, kde se UTF-8 a UTF-16 shodují bajt na znak. Chyba je reálná celou dobu; ASCII ji jen drží neviditelnou, dokud soubor, který jste nikdy nevyzkoušeli, neotevře zákazník v Mnichově

Diagram hranic konverze řetězců UTF-16 a UTF-8 mezi UI prohlížeče LCL a komponentou PDFium v Lazarusu
Textová API komponenty mluví UTF-16 skrze WString, takže UI LCL držící řetězce UTF-8 potká konverzní bod na každé hranici a krájení po bajtových offsetech nebo testovací data jen z ASCII jsou místa, kde se schovají kódovací chyby

Jeden podmíněný blok, ne fork pro každé IDE

Po prvním tuctu IFDEFů začne kódová základna působit jako dva projekty nosící jeden repozitář, a forknout ji podle IDE vypadá lákavě. Je to špatný tah. Skutečné rozdíly se dají sbalit do jednoho sdíleného bloku deklarací, a fork od té chvíle zdvojnásobí cenu každé opravy chyby. Udržte podmíněnou vrstvu takhle malou:

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

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

Vše pod tímto blokem se kompiluje identicky v obou IDE. Práce s dokumentem, navigace stránek, vykreslovací volání: TPdf a TPdfView vystavují stejné rozhraní v edicích VCL i LCL, takže většina prohlížečky nikdy neuvidí kompilátorovou podmínku. Udržet to tak je spíš strukturální disciplína než chytrý trik. Sdílená logika PDF žije v jednotkách, které netahají dovnitř žádné dialogy ani panely specifické pro framework. Hrstka věcí, které se skutečně liší, jako tiskové dialogy a výběry souborů s jejich platformními konvencemi, se schovává za tenkým rozhraním implementovaným jednou pro každý framework. Blok IFDEF se stane jediným místem, kam smí přistát budoucí platformní rozdíly, místo aby direktivy kompilátoru prosakovaly napříč čtyřiceti jednotkami

Sestavte formulář v kódu, ne ve dvou designerech

Streamování formulářů je místo, kde projekty pro dvě IDE potichu hnijí. .dfm a .lfm, které tvrdí, že popisují stejný formulář, se rozcházejí vlastnost po vlastnosti, dokud se oba buildy nezačnou chovat jinak z důvodů, které nikdo nedokáže diffnout, protože ty dva soubory ani nejsou ve stejném formátu. Sestavení prohlížečky za běhu tento celý problém obejde. Existuje jedna konstruktorová sekvence, verzovaná jako obyčejný kód, a čte se stejně na obou platformách:

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;   // otevře dokument; PageCount je platný až poté
  end;
end;

Na přesném pořadí těchto přiřazení nezáleží tolik jako na jednom řádku, který dělá skutečnou práci. PdfView.Pdf := Pdf naváže vizuální ovládací prvek na komponentu dokumentu, a od té chvíle reagují navigace stránek přes PageNumber a chování přizpůsobení přes FitMode identicky pod VCL i LCL. Jedna zvláštnost napříč frameworky stojí za to vědět dřív, než ji uživatel nahlásí jako chybu: ruční přiřazení Zoom vrátí FitMode zpátky na pfmNone na obou frameworcích. Takže pokud váš toolbar zachází s „přizpůsobit šířce" jako s trvalou preferencí, musíte po jakémkoli programovém zoomu režim přizpůsobení znovu přiřadit, jinak preference potichu přestane držet hned při prvním dotyku kódu s úrovní zoomu

Binárka, před kterou vás IDE nikdy nevarovalo

Komponenta obaluje engine PDFium, který se dodává jako nativní platformní binárka, a právě tato binárka je zdrojem téměř každého hlášení typu „funguje v IDE, selže z nainstalované zástupce". Za většinu z nich odpovídají tři pravidla. Bitová šířka se musí přesně shodovat. 32bitový spustitelný soubor nedokáže načíst 64bitovou knihovnu pdfium, a zpráva, kterou vrátí OS („module not found" na některých verzích Windows), aktivně zavádí, protože soubor leží přímo vedle spustitelného souboru. Cestu ke knihovně řešte relativně vůči spustitelnému souboru, nikdy vůči pracovnímu adresáři; spuštění z IDE a spuštění ze shellu se liší přesně v tomto bodě, a proto se chyba během vývoje schová. A zachyťte neúspěšné načtení dřív, než se otevře první dokument, a pak ho nahlaste s vypsanou očekávanou cestou a architekturou. Tiket supportu, který zní „chybí 64bitová binárka PDFium na <path>", se vyřeší za pár minut. Ten, který zní „prohlížečka při startu spadne", se promění v týden dopisování

Zatímco jste u toho, verzujte binárku enginu spolu se spustitelným souborem. PDFium se vyvíjí rychle, a instalátor, který aktualizuje aplikaci, ale na disku nechá zastaralou knihovnu, vyprodukuje pády, které nikdo ve vaší kanceláři nedokáže zreprodukovat, z prostého důvodu, že každý počítač ve vaší kanceláři náhodou drží odpovídající pár. Zacházejte s knihovnou jako se součástí build artefaktu, se stejným instalátorem, stejným razítkem verze a stejnou cestou pro rollback jako se spustitelným souborem, který ji načítá

Diagram tří pravidel načítání nativní binárky PDFium pro spustitelný soubor prohlížeče PDF v Lazarusu nebo Delphi
Tři pravidla pokryjí většinu hlášení funguje-v-IDE-padá-po-instalaci: spustitelný soubor i knihovna PDFium musí sdílet jednu bitnost, cesta knihovny se rozřeší od spustitelného souboru, nikoli od pracovní složky, a nezdařené načtení se chytí s vypsanou očekávanou cestou

Registrace komponent v Lazarus IDE

Sestavení za běhu nepotřebuje vůbec žádnou registraci v design-time, což je nejčistší nastavení pro prohlížečku, která si vlastní UI staví v kódu. Když opravdu chcete mít komponenty na paletě Lazarus pro práci v design-time, nainstalujte balíček a nechte to na jeho vyhrazené registrační jednotce, PDFiumLazReg v Lib/FPC/PDFiumLaz.lpk. Tato jednotka je záměrně označená jako design-time: odkazuje na rozhraní property editorů IDE, která se nikdy nesmí slinkovat do vašeho dodávaného spustitelného souboru

Pokud se to pokazí, příznakem je aplikace, která nevysvětlitelně závisí na balíčcích IDE, což se projeví jako selhání nasazení na prvním zákaznickém počítači, který nikdy neměl nainstalovaný Lazarus

Řeč a čtečky obrazovky mimo Windows

Převod textu na řeč je jediná funkce, kde se mezplatformní příběh rozpadá, a rozpadá se na úrovni operačního systému, ne komponenty. SAPI, obvyklý TTS backend na Windows, existuje jen na Windows. Build v Lazarus, který pořád cílí na Windows, si zachová plný výstup SAPI a stejné chování kompatibilní s NVDA, jaké měl originál v Delphi, takže port z Windows na Windows tu nic neztrácí, a uživatel NVDA ty dva buildy nerozezná

Cíl Linux nebo macOS je jiná záležitost. Není tam žádné SAPI k zavolání, takže zvukový výstup musí být přepojen na nativní řečovou službu, zatímco API pro čtení nad ním zůstávají na místě. Toto rozdělení je argumentem pro to dát řeč za rozhraní už od prvního commitu: analýza pořadí čtení a kurzor sledující slova jsou platformně neutrální a přenášejí se beze změny, a měnit se podle platformy musí jen tenká vrstva, která skutečně produkuje zvuk. Článek o přístupné čtečce tuto mechaniku čtení pokrývá do hloubky

Kontrolní seznam parity, než port prohlásíte za hotový

Následující průchod zachytil skutečné regrese, seřazené zhruba v pořadí, ve kterém mají selhání tendenci vyplouvat na povrch. Otevřete dokument, jehož cesta obsahuje znaky mimo ASCII. Vyhledejte výraz se znaky mimo ASCII a potvrďte, že se výsledky zvýrazní tam, kde mají. Vyzkoušejte scrollování kolečkem myši, tažený výběr a klávesnicovou navigaci stránek na každé sadě widgetů, kterou dodáváte, protože zacházení s fokusem a chování kolečka jsou nejvíc závislé na sadě widgetů kouty LCL. Zkontrolujte vykreslování při škálování displeje 100 %, 150 % a 200 %. Nakonec spusťte nainstalovaný build, ne build z IDE, na počítači, který nikdy neměl IDE, protože to je jediný test, který poctivě prověří rozlišení binárky. Všechno ostatní může projít, zatímco tenhle jeden test potichu selže

Propustnost vykreslování se mezi oběma edicemi přenáší beze změny, takže přístup ke kešování z článku o vykreslovací mezipaměti a výkonu zoomu platí pro prohlížečku LCL přesně tak, jak byl napsaný pro tu VCL

Nic z toho nedělá z edice LCL horší edici. Jádrové rozhraní je na obou stranách identické: TPdf, TPdfView, vykreslování, formuláře, extrakce textu i API přístupnosti se chovají stejně bez ohledu na to, které IDE je zkompilovalo. Každý rozdíl, který stojí za sledování, je vázaný na platformu, ne na edici. Řeč SAPI je jen pro Windows, dialogy se řídí konvencemi každého frameworku a binárka se musí shodovat s architekturou, do které se načítá. Zvládněte hranice kódování, formulář za běhu a rozlišování binárky, a zbytek portu je mechanická práce, kterou už za vás zvládl kompilátor

Diagram PDFium Component štěpení rozhraní řeči přesouvajícího TTS výstup za engine podle platformy v prohlížeči PDF v Lazarusu
Analýza pořadí čtení i kurzor sledující slova zůstávají neutrální k platformě, zatímco jedno tenké řečové rozhraní se rozřeší na SAPI ve Windows a na nativní řečové služby v Linuxu a macOS

Zde popsané edice VCL a LCL se dodávají společně jako PDFium Component, se zdrojovým kódem a identickými veřejnými API pro Delphi, C++Builder a Lazarus/FPC