Technical Article

Визуализатор на PDF за Lazarus и Free Pascal с PDFium

Delphi и Lazarus компилират един и същ Object Pascal и именно това повърхностно сходство прави портирането на визуализатор между тях толкова измамно. Двете вериги от инструменти (toolchains) се разминават на три места, които са от значение за работата с PDF: системният тип string е UTF-16 в Delphi и UTF-8 в LCL приложение; VCL и LCL са различни визуални библиотеки със собствени контроли, диалози и формати за стрийминг на форми; а Delphi бинарният файл е насочен към Windows, докато FPC бинарният файл може да бъде предназначен за Linux или macOS. Нито една от тези разлики не се появява по време на компилация. Визуализатор, изграден с PDFium Component (който предоставя VCL и LCL версии от едно и също кодово дърво), ще се компилира успешно под Lazarus след няколко замени на имена на модули и няколко {$IFDEF FPC} блока. Проблемите възникват по-късно, когато реалните данни и реалното внедряване разкрият предположенията, които Delphi компилацията тихо е правила.

Четири от тези предположения са причина за по-голямата част от изгубеното време: кодирането на текста на границата с потребителския интерфейс, изкушението да се поддържат две копия на формата, начинът, по който се зарежда нативната библиотека на енджина по време на изпълнение, и моментът, в който технологията за преобразуване на текст в реч (text-to-speech) остава без платформа след премахването на SAPI. Справянето с всеки от тези проблеми е лесно, ако знаете за него предварително, и изключително трудно за откриване, ако не го очаквате.

Един и същ Pascal, различни низове

Системният тип string в Delphi е UTF-16 от 2009 г. Lazarus и Free Pascal по подразбиране използват UTF-8 в LCL приложения. Приложните програмни интерфейси (API) за текст на компонента използват UTF-16 чрез типа WString (който при FPC се дефинира като WideString), така че всяка граница, през която преминава текст между вашия LCL интерфейс и PDF енджина, изисква преобразуване.

Преобразуванията се случват автоматично при директни присвоявания и в повечето случаи няма нужда да се мисли за тях. Две добри практики предотвратяват бъгове с кодирането. Предавайте текста директно без манипулации на ниво байт: код, който разделя дума за търсене по байтово отместване, работи в Delphi, където един Char е един UTF-16 елемент, но ще повреди многобайтовия UTF-8 в LCL. Освен това тествайте с данни извън ASCII диапазона още от първото пускане. Немско име на файл, кирилица при търсене или име на автор с диакритични знаци в метаданните на документа: тестовите данни с чист ASCII скриват всеки дефект в кодирането, тъй като ASCII е единственият диапазон, в който UTF-8 и UTF-16 съвпадат байт по байт. Бъгът съществува през цялото време, но ASCII го държи невидим, докато клиент в Мюнхен не отвори файл, който никога не сте тествали.

Един условен блок, а не разклонение за всяка среда за разработка (IDE)

След първите десетина блока с IFDEF, кодът автоматично започва да изглежда като два проекта в едно хранилище и разделянето му за всяко IDE изглежда изкушаващо. Това обаче е грешен ход. Действителните разлики се свеждат до един общ блок от декларации, а разделянето на проекта удвоява разходите за всяко следващо отстраняване на бъг. Дръжте условния слой минимален:

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

Всичко под този блок се компилира идентично и в двете среди за разработка. Работата с документи, навигацията по страниците, извикванията за рендиране â€?TPdf и TPdfView предоставят еднакъв интерфейс във VCL и LCL версиите, така че основната часть от визуализатора не съдържа компилационни условия. Поддържането на тази структура е въпрос на дисциплина, а не на сложни трикове. Споделената логика за PDF живее в модули, които не изискват специфични за съответната библиотека диалогови прозорци или панели. Малкото неща, които действително се различават, като диалози за печат и избор на файлове със съответните им системни конвенции, са скрити зад прост интерфейс, имплементиран по веднъж за всяка библиотека. Блокът IFDEF се превръща в единственото място, където е разрешено да се въвеждат бъдещи системни разлики, вместо да се разпръскват компилационни директиви в десетки различни модули.

Създайте формата в код, а не в два дизайнера

Стриймингът на форми е мястото, където проектите за две среди тихомълком се разпадат. Файловете .dfm и .lfm, които описват една и съща форма, се разминават свойство по свойство с течение на времето, докато двете версии започнат да се държат различно по причини, които никой не може да сравни лесно, тъй като файловете дори не са в един и същ формат. Създаването на визуализатора по време на изпълнение напълно заобикаля този проблем. Има само една последователност на конструктора, управлявана в системата за контрол на версиите като обикновен код, която изглежда по един и същ начин и за двете платформи:

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;

Точният ред на тези присвоявания е по-малко важен от един конкретен ред, който върши основната работа. PdfView.Pdf := Pdf свързва визуалния компонент с компонента на документа и от този момент нататък навигацията по страниците чрез PageNumber и поведението на напасване чрез FitMode работят напълно идентично под VCL и LCL. Една особеност, обща за двете среди, си струва да се знае, преди потребител да я съобщи като бъг: ръчното присвояване на Zoom връща FitMode обратно към pfmNone и в двете библиотеки. Така че, ако лентата с инструменти третира „напасванÐ?по ширинаâ€?като постоянно предпочитание, трябва да зададете отново режима на напасване след всяко програмно мащабиране, в противен случай настройката тихомълком ще спре да работи при първата промяна на мащаба.

Бинарният файл, за който средата за разработка не ви предупреждава

Компонентът обвива енджина PDFium, който се доставя като нативен бинарен файл за съответната платформа, и този файл е причината за почти всяко съобщение от типа „работи в IDE-то, но се проваля при стартиране от прекия пътâ€? Три правила обясняват повечето от тези случаи. Разрядността (bitness) трябва да съвпада точно. 32-битов изпълним файл не може да зареди 64-битова pdfium библиотека, а съобщението, което операционната система връща („module not foundâ€?при някои версии на Windows), често подвежда, тъй като файлът се намира точно до изпълнимия файл. Търсете пътя до библиотеката спрямо изпълнимия файл, а не спрямо работната директория; стартирането от IDE-то и стартирането от конзолата се различават точно по този показател, поради което бъгът остава скрит по време на разработка. И накрая, прихванете неуспешното зареждане преди отварянето на първия документ и го докладвайте с пълно описание на очаквания път и архитектура. Потребителски билет, който гласи „ЛипсвÐ?64-битов бинарен файл на PDFium в <path>â€? се решава за минути. Такъв, който казва само „визуализаторът се срива при стартиранеâ€? води до седмици излишна кореспонденция.

Заедно с това поддържайте версията на библиотеката на енджина съгласувана с изпълнимия файл. PDFium се развива бързо и инсталатор, който актуализира приложението, но оставя стара библиотека на диска, води до сривове, които никой във вашия офис не може да възпроизведе â€?поради простата причина, че всеки компютър във вашия офис разполага със съвпадаща двойка файлове. Третирайте библиотеката като част от компилационния артефакт, със същия инсталатор, същата версия и същия път за възстановяване (rollback) като изпълнимия файл, който я зарежда.

Регистриране на компоненти в Lazarus IDE

Създаването на компонентите по време на изпълнение не изисква никаква регистрация в дизайнера, което е най-чистото решение за визуализатор, който изгражда интерфейса си изцяло в код. Когато обаче искате да имате компонентите в палитрата на Lazarus за работа в дизайнера, инсталирайте пакета и оставете неговия регистрационен модул PDFiumLazReg в Lib/FPC/PDFiumLaz.lpk да се погрижи за това. Този модул е маркиран като предназначен само за средата за разработка (design-time) с конкретна цел: той препраща към интерфейси на редактора на свойства в IDE-то, които никога не трябва да се свързват с крайния изпълним файл.

Ако сгрешите тук, приложението ви неочаквано ще започне да зависи от пакетите на средата за разработка, което ще се прояви като грешка при внедряване на първия клиентски компютър, на който никога не е инсталиран Lazarus.

Гласово четене и екранни четци извън Windows

Преобразуването на текст в реч (text-to-speech) е функцията, при която кросплатформената поддръжка се нарушава, и това се случва на ниво операционна система, а не в компонента. SAPI, обичайната TTS система в Windows, съществува само под Windows. Версия на Lazarus, която все още е насочена към Windows, запазва пълната поддръжка на SAPI и съвместимото с NVDA поведение, което притежава оригиналът на Delphi, така че преходът от Windows към Windows не губи нищо тук и потребител с NVDA не би забелязал разлика между двете версии.

Версия за Linux или macOS обаче е съвсем друг въпрос. Тъй като там няма SAPI система, която да бъде извикана, аудио изходът трябва да бъде пренасочен към съответната вградена гласова услуга на системата, докато API интерфейсите за четене над нея остават непроменени. Това разделение е сериозен аргумент за поставяне на гласовите функции зад интерфейс още от самото начало: анализът на реда на четене и проследяването на думите са независими от платформата и се пренасят без промени, като трябва да се подмени само тънкият слой, който реално възпроизвежда звука. Статията за достъпен четец на PDF разглежда тези механизми в детайли.

Списък за проверка на съответствието преди завършване на портирането

Следният списък за проверка е помагал за откриване на реални регресии, подредени според обичайния ред на тяхното проявление. Отворете документ, чийто път съдържа символи извън ASCII. Потърсете дума с не-ASCII символи и се уверете, че съвпаденията се маркират на правилните места. Изпробвайте превъртането с колелцето на мишката, селекцията с влачене и клавиатурната навигация по страниците за всяка библиотека от компоненти, която разпространявате, тъй като управлението на фокуса и колелцето на мишката са най-зависимите от конкретния LCL компонент детайли. Проверете рендирането при 100%, 150% и 200% мащаби на дисплея. Накрая, стартирайте инсталираната версия, а не тази от IDE-то, на компютър, на който средата за разработка никога не е била инсталирана â€?тъй като това е единственият начин реално да се тества зареждането на нативната библиотека. Всичко останало може да работи, докато тази последна стъпка тихо се проваля.

Скоростта на рендиране се запазва непроменена между двете версии, така че подходът за кеширане от статията за кеш на рендиране и производителност при мащабиране се прилага за LCL визуализатора точно по същия начин, по който е описан за VCL.

Нищо от това не прави LCL версията по-маловажна. Основната функционалност е идентична и от двете страни: TPdf, TPdfView, рендирането, формулярите, извличането на текст и API интерфейсите за достъпност се държат по един и същ начин, независимо от това кое IDE ги е компилирало. Всяка разлика, която си струва да се проследи, е обвързана с платформата, а не с конкретната версия на библиотеката. Четенето със SAPI е само за Windows, диалозите следват конвенциите на всяка библиотека, а бинарният файл трябва да съответства на архитектурата, в която се зарежда. Настройте правилно границите на кодирането, динамичната форма и зареждането на бинарния файл, а останалата част от портирането е просто механична работа, с която компилаторът вече се е справил.

Описаните тук VCL и LCL версии се доставят заедно като част от PDFium Component, с изходен код и идентични публични приложни програмни интерфейси (API) за Delphi, C++Builder и Lazarus/FPC.