Delphi și Lazarus compilează același Object Pascal, iar această similitudine de suprafață este exact ceea ce face ca portarea unui vizualizator între ele să fie înșelătoare. Cele două lanțuri de instrumente diverg în trei puncte care contează pentru lucrul cu PDF: tipul nativ string este UTF-16 în Delphi și UTF-8 într-o aplicație LCL; VCL și LCL sunt cadre vizuale diferite, cu propriile controale, dialoguri și formate de serializare a formularelor; iar un binar Delphi vizează Windows, în timp ce un binar FPC poate fi destinat Linux sau macOS. Niciuna dintre aceste diferențe nu apare la compilare. Un vizualizator construit pe PDFium Component, care livrează edițiile VCL și LCL dintr-un singur arbore sursă, se va compila fără erori sub Lazarus după câteva schimbări de nume de unități și câteva blocuri {$IFDEF FPC}. Eșecurile apar mai târziu, când datele reale și o desfășurare reală expun presupunerile pe care compilarea Delphi le făcea în tăcere
Patru dintre aceste presupuneri explică majoritatea timpului pierdut: codificarea textului la granița interfeței, tentația de a menține două copii ale formularului, modul în care se rezolvă la runtime un binar nativ al motorului și momentul în care conversia text-vorbire rămâne fără platformă odată ce SAPI dispare. Fiecare este ieftin de tratat dacă știți că vine și costisitor de urmărit dacă nu știți
Același Pascal, sarcini utile de tip string diferite
Tipul nativ string din Delphi este UTF-16 din 2009. Lazarus și Free Pascal folosesc implicit UTF-8 în aplicațiile LCL. API-urile componentei orientate spre text vorbesc UTF-16 prin tipul WString, pe care compilarea FPC îl aliasează la WideString, așa că fiecare graniță pe unde trece textul între interfața dvs. LCL și motorul PDF este un punct de conversie
Conversiile au loc automat în atribuirile simple, iar majoritatea codului nu trebuie niciodată să se gândească la ele. Două obiceiuri țin erorile de codificare la distanță. Treceți textul mai departe fără manipulare la nivel de octet: codul care taie un termen de căutare după decalajul în octeți funcționează în Delphi, unde un Char este o unitate UTF-16, și corupe UTF-8 pe mai mulți octeți în LCL. Și testați cu date non-ASCII încă de la prima rulare. Un nume de fișier german, un termen de căutare chirilic, un nume de autor cu diacritice în metadatele documentului: datele de test pur ASCII ascund fiecare defect de codificare, deoarece ASCII este singurul interval în care UTF-8 și UTF-16 coincid octet cu caracter. Eroarea este reală tot timpul; ASCII doar o menține invizibilă până când un client din München deschide un fișier pe care nu l-ați testat niciodată
Un singur bloc condițional, nu o bifurcare pentru fiecare IDE
După primele douăsprezece IFDEF-uri, codul începe să pară două proiecte îmbrăcate într-un singur depozit, iar bifurcarea lui pentru fiecare IDE pare tentantă. Este mișcarea greșită. Diferențele reale se restrâng la un singur bloc de declarații comun, iar o bifurcare dublează costul fiecărei corecții de eroare de atunci încolo. Păstrați stratul condițional la această dimensiune redusă:
{$IFDEF FPC}
uses
LCLType, Forms, Graphics, Controls;
type
WString = WideString; // API-urile de text ale componentei sunt UTF-16
TBytes = array of Byte;
{$ELSE}
uses
Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}
Tot ce se află sub acel bloc se compilează identic în ambele IDE-uri. Gestionarea documentelor, navigarea între pagini, apelurile de randare: TPdf și TPdfView expun aceeași suprafață în edițiile VCL și LCL, așa că cea mai mare parte a vizualizatorului nu vede niciodată o condiție de compilator. Menținerea acestei stări este o disciplină structurală, nu un truc ingenios. Logica PDF partajată trăiește în unități care nu importă niciun dialog sau panou specific unui cadru. Puținele lucruri care diferă cu adevărat, precum dialogurile de tipărire și selectoarele de fișiere cu convențiile lor specifice platformei, se ascund în spatele unei interfețe subțiri implementate o singură dată pentru fiecare cadru. Blocul IFDEF devine singurul loc în care divergența viitoare de platformă are voie să aterizeze, în loc să lase directive de compilator să se scurgă în patruzeci de unități
Construiți formularul din cod, nu în două designere
Serializarea formularelor este locul unde proiectele cu IDE dual putrezesc în tăcere. Un .dfm și un .lfm care pretind că descriu același formular se îndepărtează unul de celălalt proprietate cu proprietate, până când cele două compilări se comportă diferit din motive pe care nimeni nu le poate diferenția, pentru că cele două fișiere nici măcar nu au același format. Construirea vizualizatorului la runtime ocolește întreaga problemă. Există o singură secvență de constructor, aflată sub control de versiuni ca și cod obișnuit, care se citește identic pe ambele 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; // deschide documentul; PageCount este valid după acest apel
end;
end;
Ordinea exactă a acestor atribuiri contează mai puțin decât linia care face treaba reală. PdfView.Pdf := Pdf leagă controlul vizual de componenta document, iar din acel moment navigarea între pagini prin PageNumber și comportamentul de încadrare prin FitMode răspund identic sub VCL și LCL. Există o particularitate între cadre care merită cunoscută înainte ca un utilizator să o raporteze drept eroare: atribuirea manuală a Zoom readuce FitMode la pfmNone pe ambele cadre. Așa că, dacă bara dvs. de instrumente tratează „încadrare pe lățime” ca pe o preferință persistentă, trebuie să reatribuiți modul de încadrare după orice zoom programatic, altfel preferința încetează în tăcere să mai rămână activă de prima dată când codul atinge nivelul de zoom
Binarul despre care IDE-ul nu v-a avertizat niciodată
Componenta încapsulează motorul PDFium, care se livrează ca binar nativ de platformă, iar acel binar este sursa aproape a fiecărui raport de tipul „funcționează în IDE, eșuează din scurtătura instalată”. Trei reguli explică majoritatea lor. Arhitectura pe biți trebuie să se potrivească exact. Un executabil pe 32 de biți nu poate încărca o bibliotecă pdfium pe 64 de biți, iar mesajul pe care îl returnează sistemul de operare („modul negăsit” pe unele versiuni de Windows) induce activ în eroare, deoarece fișierul se află chiar acolo, lângă executabil. Rezolvați calea bibliotecii relativ la executabil, niciodată la directorul de lucru; o lansare din IDE și o lansare din shell diferă exact în acest punct, motiv pentru care eroarea se ascunde în timpul dezvoltării. Și prindeți o încărcare eșuată înainte de deschiderea primului document, apoi raportați-o cu calea și arhitectura așteptate explicit menționate. Un tichet de suport care spune „lipsește binarul PDFium pe 64 de biți la <path>” se închide în câteva minute. Unul care spune „vizualizatorul se blochează la pornire” se transformă într-o săptămână de tur-retur
Versionați binarul motorului odată cu executabilul, fiindcă tot sunteți la asta. PDFium evoluează rapid, iar un program de instalare care actualizează aplicația dar lasă pe disc o bibliotecă învechită produce blocări pe care nimeni din biroul dvs. nu le poate reproduce, din simplul motiv că fiecare mașină din birou se întâmplă să dețină perechea potrivită. Tratați biblioteca drept parte a artefactului de build, cu același program de instalare, aceeași marcă de versiune și aceeași cale de revenire ca și executabilul care o încarcă
Înregistrarea componentelor în IDE-ul Lazarus
Construcția la runtime nu necesită deloc înregistrare la design-time, ceea ce reprezintă configurația cea mai curată pentru un vizualizator care își construiește propria interfață în cod. Când chiar doriți componentele pe paleta Lazarus pentru lucrul la design-time, instalați pachetul și lăsați unitatea sa dedicată de înregistrare, PDFiumLazReg din Lib/FPC/PDFiumLaz.lpk, să se ocupe de asta. Acea unitate este marcată intenționat drept design-time: face referire la interfețe de editor de proprietăți din IDE care nu trebuie niciodată să se lege în executabilul dvs. livrat
Greșiți acest lucru, iar simptomul este o aplicație care depinde inexplicabil de pachete IDE, ceea ce se manifestă ca un eșec de desfășurare pe prima mașină a unui client care nu a avut niciodată Lazarus instalat
Vorbire și cititoare de ecran în afara Windows
Conversia text-vorbire este singura funcționalitate în care povestea cross-platform se rupe, iar ruptura se produce la nivelul sistemului de operare, nu al componentei. SAPI, motorul TTS obișnuit pe Windows, există doar pe Windows. O compilare Lazarus care vizează în continuare Windows păstrează ieșirea SAPI completă și același comportament compatibil cu NVDA pe care îl avea originalul Delphi, așa că o portare Windows-către-Windows nu pierde nimic aici, iar un utilizator NVDA nu poate distinge cele două compilări
O țintă Linux sau macOS este cu totul altă poveste. Nu există niciun SAPI de apelat, așa că ieșirea audio trebuie recablată către un serviciu de vorbire nativ, în timp ce API-urile de citire de deasupra rămân neschimbate. Această separare este argumentul pentru a pune vorbirea în spatele unei interfețe încă de la primul commit: analiza ordinii de citire și cursorul care urmărește cuvintele sunt neutre față de platformă și se transferă neschimbate, iar doar stratul subțire care produce efectiv sunetul trebuie să se schimbe per platformă. Articolul despre cititorul accesibil tratează în profunzime acel mecanism de citire
O listă de verificare a parității înainte de a declara portarea încheiată
Următoarea trecere a prins regresii reale, enumerate aproximativ în ordinea în care tind să apară eșecurile. Deschideți un document a cărui cale conține caractere non-ASCII. Căutați un termen cu caractere non-ASCII și confirmați că rezultatele sunt evidențiate unde ar trebui. Exersați derularea cu rotița mouse-ului, selecția prin tragere și navigarea între pagini cu tastatura pe fiecare set de widget-uri pe care îl livrați, deoarece gestionarea focalizării și comportamentul rotiței sunt colțurile cele mai dependente de setul de widget-uri din LCL. Verificați randarea la scalări de afișare de 100%, 150% și 200%. În cele din urmă, rulați compilarea instalată, nu compilarea din IDE, pe o mașină care nu a avut niciodată IDE-ul instalat, deoarece acesta este singurul test care exersează onest rezolvarea binarului. Orice altceva poate trece în timp ce acesta eșuează în tăcere
Debitul de randare se transferă neschimbat între cele două ediții, așa că abordarea de cache prezentată în articolul despre cache-ul de randare și performanța la zoom se aplică vizualizatorului LCL exact așa cum a fost scrisă pentru cel VCL
Nimic din toate acestea nu face din ediția LCL una inferioară. Suprafața de bază este identică în ambele părți: TPdf, TPdfView, randarea, formularele, extragerea de text și API-urile de accesibilitate se comportă la fel indiferent de IDE-ul care le-a compilat. Fiecare diferență care merită urmărită ține de platformă, nu de ediție. Vorbirea SAPI este exclusiv pentru Windows, dialogurile urmează convențiile fiecărui cadru, iar binarul trebuie să corespundă arhitecturii în care este încărcat. Rezolvați corect granițele de codificare, formularul la runtime și rezolvarea binarului, iar restul portării este munca mecanică pe care compilatorul a făcut-o deja pentru dvs
Edițiile VCL și LCL descrise aici se livrează împreună ca PDFium Component, cu cod sursă și API-uri publice identice pentru Delphi, C++Builder și Lazarus/FPC