Articolo tecnico

Viewer PDF per Lazarus e Free Pascal con PDFium

Delphi e Lazarus compilano lo stesso Object Pascal, ed è proprio quella somiglianza di superficie a rendere ingannevole il porting di un viewer fra i due. Le due toolchain divergono in tre punti che contano per il lavoro sui PDF: il tipo string nativo è UTF-16 in Delphi e UTF-8 in una applicazione LCL; VCL e LCL sono framework visuali diversi, con controlli, finestre di dialogo e formati di streaming dei form propri; e un binario Delphi punta a Windows mentre un binario FPC può essere diretto a Linux o macOS. Nessuna di quelle differenze si manifesta in fase di compilazione. Un viewer costruito su PDFium Component, che distribuisce le edizioni VCL e LCL da un unico albero sorgente, compilerà pulito sotto Lazarus dopo una manciata di scambi di nomi di unit e qualche blocco {$IFDEF FPC}. I guasti arrivano dopo, quando dati veri e un deployment vero mettono a nudo le assunzioni che la build Delphi faceva in silenzio

Quattro di quelle assunzioni spiegano la maggior parte del tempo perso: la codifica del testo al confine con la UI, la tentazione di mantenere due copie del form, il modo in cui un binario di motore nativo si risolve a runtime e il momento in cui alla sintesi vocale finisce la piattaforma sotto i piedi una volta che SAPI non c'è più. Ciascuna costa poco da gestire se sapete che sta arrivando e costa cara da rincorrere se non lo sapete

Stesso Pascal, carichi di stringa diversi

Il tipo string nativo di Delphi è UTF-16 dal 2009. Lazarus e Free Pascal usano UTF-8 come impostazione predefinita nelle applicazioni LCL. Le API del componente che trattano testo parlano UTF-16 attraverso il tipo WString, che la build FPC mappa su WideString, quindi ogni confine in cui il testo passa fra la vostra UI LCL e il motore PDF è un punto di conversione

Le conversioni avvengono automaticamente nelle assegnazioni dirette, e la maggior parte del codice non deve mai pensarci. Due abitudini tengono fuori i bug di codifica. Passate il testo così com'è senza manipolarlo a livello di byte: il codice che affetta un termine di ricerca per scostamento di byte funziona in Delphi, dove un Char è una unità UTF-16, e corrompe l'UTF-8 multibyte nella LCL. E collaudate con dati non ASCII fin dalla prima esecuzione. Un nome di file tedesco, un termine di ricerca cirillico, un nome di autore accentato nei metadati del documento: i dati di prova di solo ASCII nascondono ogni difetto di codifica, perché ASCII è l'unico intervallo in cui UTF-8 e UTF-16 coincidono byte per carattere. Il bug è reale per tutto il tempo; l'ASCII si limita a tenerlo invisibile finché un cliente di Monaco non apre un file che non avete mai provato

Diagramma dei confini di conversione fra stringhe UTF-16 e UTF-8 fra una UI di viewer LCL e il componente PDFium in Lazarus
Le API di testo del componente parlano UTF-16 tramite WString, quindi una UI LCL che tiene stringhe UTF-8 incontra un punto di conversione a ogni confine, e affettare per scostamento di byte o collaudare solo con ASCII è dove si nascondono i bug di codifica

Un solo blocco condizionale, non un fork per IDE

Dopo la prima dozzina di IFDEF il codice inizia a sembrare due progetti che indossano un solo repository, e forkarlo per IDE appare allettante. È la mossa sbagliata. Le differenze autentiche si riducono a un unico blocco di dichiarazioni condiviso, e un fork raddoppia da lì in poi il costo di ogni correzione di bug. Tenete lo strato condizionale piccolo così:

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

type
  WString = WideString;   // le API di testo del componente sono UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

Tutto ciò che sta sotto quel blocco compila in modo identico nei due IDE. Gestione dei documenti, navigazione fra pagine, chiamate di rendering: TPdf e TPdfView espongono la stessa superficie nelle edizioni VCL e LCL, quindi il grosso del viewer non vede mai una condizione di compilazione. Mantenerlo così è una disciplina strutturale più che un trucco astuto. La logica PDF condivisa vive in unit che non tirano dentro finestre di dialogo o pannelli specifici di un framework. La manciata di cose che differiscono davvero, come le finestre di stampa e i selettori di file con le loro convenzioni di piattaforma, si nasconde dietro una interfaccia sottile implementata una volta per framework. Il blocco IFDEF diventa l'unico posto in cui è ammesso che la futura divergenza di piattaforma atterri, invece di lasciar colare direttive di compilazione attraverso quaranta unit

Costruite il form nel codice, non in due designer

Lo streaming dei form è il punto in cui i progetti a doppio IDE marciscono in silenzio. Un .dfm e un .lfm che sostengono di descrivere lo stesso form divergono proprietà dopo proprietà finché le due build non si comportano diversamente per motivi che nessuno riesce a diffare, perché i due file non sono nemmeno nello stesso formato. Costruire il viewer a runtime aggira tutto il problema. C'è una sola sequenza di costruzione, versionata come normale codice, e si legge allo stesso modo su entrambe le piattaforme:

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;   // apre il documento; poi PageCount è valido
  end;
end;

L'ordine esatto di quelle assegnazioni conta meno dell'unica riga che fa il lavoro vero. PdfView.Pdf := Pdf lega il controllo visuale al componente documento, e da quel momento la navigazione fra pagine tramite PageNumber e il comportamento di adattamento tramite FitMode rispondono in modo identico sotto VCL e LCL. Vale la pena conoscere una stranezza comune ai due framework prima che un utente la segnali come bug: assegnare Zoom a mano riporta di scatto FitMode a pfmNone su entrambi i framework. Quindi se la vostra barra degli strumenti tratta "adatta alla larghezza" come una preferenza persistente, dovete riassegnare la modalità di adattamento dopo ogni zoom via codice, altrimenti la preferenza smette silenziosamente di reggere la prima volta che il codice tocca il livello di zoom

Il binario di cui l'IDE non vi ha mai avvertito

Il componente avvolge il motore PDFium, che viene distribuito come binario nativo di piattaforma, e quel binario è la causa di quasi tutte le segnalazioni del tipo "funziona nell'IDE, fallisce dal collegamento installato". Tre regole ne spiegano la maggior parte. Il numero di bit deve corrispondere esattamente. Un eseguibile a 32 bit non può caricare una libreria pdfium a 64 bit, e il messaggio che il sistema operativo restituisce ("module not found" su alcune versioni di Windows) è attivamente fuorviante, perché il file sta proprio lì accanto all'eseguibile. Risolvete il percorso della libreria rispetto all'eseguibile, mai rispetto alla directory di lavoro; un avvio dall'IDE e un avvio da shell differiscono esattamente su quel punto, ed è per questo che il bug si nasconde durante lo sviluppo. E intercettate un caricamento fallito prima che si apra il primo documento, poi segnalatelo con percorso atteso e architettura scritti per esteso. Un ticket di assistenza che dice "Binario PDFium a 64 bit mancante in <path>" si chiude in pochi minuti. Uno che dice "il viewer va in crash allo avvio" si trasforma in una settimana di botta e risposta

Già che ci siete, versionate il binario del motore insieme all'eseguibile. PDFium si muove in fretta, e un installer che aggiorna l'applicazione ma lascia una libreria vecchia sul disco produce crash che nessuno nel vostro ufficio riesce a riprodurre, per il semplice motivo che ogni macchina del vostro ufficio guarda caso possiede la coppia corrispondente. Trattate la libreria come parte dell'artefatto di build, con lo stesso installer, la stessa marcatura di versione e lo stesso percorso di rollback dell'eseguibile che la carica

Diagramma delle tre regole di caricamento del binario nativo PDFium per un eseguibile di viewer PDF in Lazarus o Delphi
Tre regole coprono la maggior parte delle segnalazioni del tipo funziona-nell-IDE-fallisce-installato: eseguibile e libreria PDFium devono condividere lo stesso numero di bit, il percorso della libreria si risolve dall-eseguibile anziché dalla directory di lavoro, e un caricamento fallito viene intercettato con il percorso atteso scritto per esteso

Registrare i componenti nell IDE Lazarus

La costruzione a runtime non ha bisogno di alcuna registrazione in fase di progettazione, il che è la configurazione più pulita per un viewer che costruisce la propria UI nel codice. Quando invece volete i componenti sulla tavolozza di Lazarus per il lavoro in fase di progettazione, installate il package e lasciate che se ne occupi la sua unit di registrazione dedicata, PDFiumLazReg in Lib/FPC/PDFiumLaz.lpk. Quella unit è marcata come design-time di proposito: fa riferimento a interfacce di editor di proprietà dell IDE che non devono mai finire collegate nel vostro eseguibile distribuito

Sbagliate questo e il sintomo è una applicazione che dipende inspiegabilmente da package dell IDE, cosa che si manifesta come guasto di deployment sulla prima macchina cliente su cui Lazarus non è mai stato installato

Voce e screen reader fuori da Windows

La sintesi vocale è l'unica funzione in cui la storia multipiattaforma si rompe, e si rompe a livello di sistema operativo, non di componente. SAPI, il backend TTS abituale su Windows, esiste solo su Windows. Una build Lazarus che punta ancora a Windows conserva l'output SAPI completo e lo stesso comportamento compatibile con NVDA che aveva l'originale Delphi, quindi un porting da Windows a Windows non perde nulla qui, e un utente NVDA non riesce a distinguere le due build

Un target Linux o macOS è un altro paio di maniche. Non c'è alcun SAPI da chiamare, quindi l'uscita audio va ricablata verso un servizio vocale nativo mentre le API di lettura che le stanno sopra restano dove sono. Quella separazione è l'argomento per mettere la voce dietro una interfaccia fin dal primo commit: l'analisi dell'ordine di lettura e il cursore che segue le parole sono neutri rispetto alla piattaforma e si trasferiscono intatti, e solo lo strato sottile che produce davvero il suono deve cambiare per piattaforma. L'articolo sul lettore accessibile tratta in profondità quel macchinario di lettura

Diagramma di PDFium Component della separazione dell interfaccia vocale che sposta l uscita TTS dietro un motore per piattaforma in un viewer PDF Lazarus
L analisi dell ordine di lettura e il cursore che segue le parole restano neutri rispetto alla piattaforma mentre una sola interfaccia vocale sottile si risolve in SAPI su Windows e nei servizi vocali nativi su Linux e macOS

Una lista di controllo di parità prima di dichiarare finito il porting

Il passaggio che segue ha intercettato regressioni vere, elencate grosso modo nell'ordine in cui i guasti tendono a emergere. Aprite un documento il cui percorso contiene caratteri non ASCII. Cercate un termine con caratteri non ASCII e confermate che le occorrenze si evidenzino dove devono. Esercitate lo scorrimento con la rotellina, la selezione per trascinamento e la navigazione fra pagine da tastiera su ogni widget set che distribuite, perché la gestione del focus e il comportamento della rotellina sono gli angoli della LCL che più dipendono dal widget set. Controllate il rendering al 100%, al 150% e al 200% di scalatura del display. Per ultimo, eseguite la build installata, non quella dell'IDE, su una macchina che non ha mai avuto l'IDE, perché è l'unico collaudo che mette davvero alla prova la risoluzione del binario. Tutto il resto può passare mentre quello fallisce in silenzio

La resa del rendering si trasferisce invariata fra le due edizioni, quindi l'approccio di caching dell'articolo sulla cache di rendering e le prestazioni dello zoom si applica al viewer LCL esattamente come è scritto per quello VCL

Niente di tutto questo fa dell'edizione LCL una edizione minore. La superficie principale è identica dalle due parti: TPdf, TPdfView, rendering, moduli, estrazione del testo e le API di accessibilità si comportano allo stesso modo indipendentemente da quale IDE le abbia compilate. Ogni differenza che vale la pena tracciare è legata alla piattaforma, non all'edizione. La voce SAPI è solo per Windows, le finestre di dialogo seguono le convenzioni di ciascun framework e il binario deve corrispondere all'architettura in cui viene caricato. Sistemate i confini di codifica, il form a runtime e la risoluzione del binario, e il resto del porting è quel lavoro meccanico che il compilatore ha già svolto per voi

Le edizioni VCL e LCL descritte qui vengono distribuite insieme come PDFium Component, con codice sorgente e API pubbliche identiche per Delphi, C++Builder e Lazarus/FPC