Articolo tecnico

Creare un visualizzatore PDF interattivo in Delphi con TPDFlibViewer

L'integrazione di un pannello di lettura PDF in un'applicazione aziendale Delphi non richiede la scrittura di un ciclo di rendering, di un motore di scorrimento o di un modello di zoom. La libreria PDF di losLab fornisce TPDFlibViewer, un controllo VCL che apre un documento con una sola chiamata e offre selezione del testo, panoramica (panning), zoom con marquee, snapshot, evidenziazione di aree, evidenziazione dei risultati della ricerca full-text, interazione con le annotazioni e stampa come comportamenti integrati. Questo articolo illustra i cinque passaggi per trasformare un modulo vuoto in un visualizzatore funzionante: inserire il controllo, collegare le azioni pronte all'uso, aggiungere l'evidenziazione della ricerca, gestire i clic sulle annotazioni e stampare

Perché usare un controllo visualizzatore invece di scrivere un ciclo di rendering?

L'argomento relativo alla stratificazione (layering) viene prima di tutto, perché spiega cosa stai effettivamente acquistando. Il rendering di una pagina in una bitmap rappresenta solo il 20 percento più semplice; l'altro 80 percento è tutto ciò che vi sta intorno: una cache delle bitmap di pagina con un budget di rimozione, il layout di scorrimento su pagine di dimensioni diverse, modalità di zoom che si ricalcolano al ridimensionamento, controlli di hit-testing che rimappano un pixel del mouse in coordinate di pagina a qualsiasi zoom e rotazione, e la pianificazione del ridisegno (repaint) che evita lo sfarfallio. TPDFlibViewer gestisce tutto questo e delega il lavoro raster allo stesso motore descritto nell'articolo sul rendering multi-motore, in modo che il livello del controllo e quello del rendering rimangano sostituibili separatamente. Il codice del tuo modulo comunica con un controllo Windows scorrevole, non con un renderer

Come si integra un visualizzatore PDF in un'applicazione Delphi?

TPDFlibViewer.LoadFromFile apre un documento tramite l'istanza interna di TPDFlib del controllo stesso e restituisce 1 in caso di successo, quindi il visualizzatore minimo consiste in una chiamata al costruttore, un'assegnazione di Parent e un caricamento. Se possiedi già un'istanza di TPDFlib altrove nell'applicazione, AttachLibrary mostra invece il suo documento selezionato, mantenendo la proprietà dal tuo lato

uses PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  if FViewer.LoadFromFile('quarterly-report.pdf', '') = 1 then
    FViewer.ViewerMode := vmHand;  // pan by dragging; a plain click still follows links
end;

La proprietà ViewerMode seleziona l'azione del mouse tra cinque valori: vmSelect è lo strumento di selezione del testo predefinito, vmHand esegue la panoramica trascinando con un cursore a mano mentre un semplice clic segue ancora i collegamenti, vmZoom è uno zoom di selezione (marquee zoom) che ingrandisce l'area trascinata per riempire la finestra di visualizzazione, vmSnapshot acquisisce un'area trascinata negli appunti (con un evento OnSnapshot e un'opzione SnapshotToClipboard) e vmHighlight disegna evidenziazioni di aree traslucide persistenti. Lo strumento di evidenziazione merita una nota sulle coordinate: le aree sono memorizzate nello spazio utente della pagina, non nei pixel del client, quindi i contrassegni di AddHighlightRegion sopravvivono allo zoom, alla rotazione della visualizzazione e alla navigazione della pagina senza alcuna gestione da parte tua. In una visualizzazione ruotata, la direzione del trascinamento può persino andare contro gli assi della pagina, il che produce una larghezza o un'altezza negativa dopo la mappatura di rotazione inversa — il controllo normalizza il rettangolo per te

Come si collega una barra degli strumenti PDF in Delphi senza codice di raccordo?

L'unità PDFlibViewerActions elimina completamente il codice di raccordo per la barra degli strumenti. Fornisce venti discendenti di TAction pronti all'uso che coprono la navigazione delle pagine (TPDFlibViewerFirstPage, TPDFlibViewerPriorPage, TPDFlibViewerNextPage, TPDFlibViewerLastPage), lo zoom (TPDFlibViewerZoomIn, TPDFlibViewerZoomOut, TPDFlibViewerFitWidth, TPDFlibViewerFitPage, TPDFlibViewerActualSize), la rotazione della visualizzazione (TPDFlibViewerRotateClockwise, TPDFlibViewerRotateAntiClockwise), la stampa, le finestre di dialogo di apertura e salvataggio file, la copia del testo selezionato e un'azione di commutazione per ciascuno strumento del mouse (TPDFlibViewerSelectMode, TPDFlibViewerHandMode, TPDFlibViewerMarqueeZoomMode, TPDFlibViewerSnapshotMode, TPDFlibViewerHighlightMode). Ciascuna azione punta a un controllo tramite la sua proprietà pubblicata Viewer e mantiene il proprio stato: ogni azione si disabilita quando non ci sono documenti aperti, le azioni di navigazione tracciano la posizione della pagina, la copia rileva se esiste una selezione, e le azioni di modalità e adattamento mantengono il loro stato selezionato in sincrono con lo strumento e la modalità di zoom attivi. In fase di progettazione, si registrano tutte nella categoria losLab PDF dell'editor Action List; a runtime, le stesse classi richiedono solo tre righe ciascuna

uses System.Actions, Vcl.ActnList, PDFlibViewer, PDFlibViewerActions;

procedure TMainForm.BuildToolbar;
var
  NextPage: TPDFlibViewerNextPage;
  FitWidth: TPDFlibViewerFitWidth;
  HandMode: TPDFlibViewerHandMode;
begin
  NextPage := TPDFlibViewerNextPage.Create(Self);
  NextPage.ActionList := ActionList1;
  NextPage.Viewer := FViewer;
  btnNext.Action := NextPage;

  FitWidth := TPDFlibViewerFitWidth.Create(Self);
  FitWidth.ActionList := ActionList1;
  FitWidth.Viewer := FViewer;
  btnFitWidth.Action := FitWidth;

  HandMode := TPDFlibViewerHandMode.Create(Self);
  HandMode.ActionList := ActionList1;
  HandMode.Viewer := FViewer;
  btnHand.Action := HandMode;
end;

C'è una trappola a livello di unità che vale la pena conoscere se si compila la registrazione delle proprie azioni basandosi su questo schema. RegisterActions è dichiarato in System.Actions, non in Vcl.ActnList, e l'errore del compilatore che si ottiene includendo solo Vcl.ActnList nella clausola uses è decisamente fuorviante: il valore letterale dell'array di classi [TAction1, TAction2, ...] viene interpretato come un set, producendo una catena di errori E2010 Incompatible types: 'Integer' and 'class of ...' che sembrano una mancata corrispondenza di tipo nelle classi di azione. La correzione consiste in una singola voce uses, ma la diagnostica punta ovunque tranne che su di essa. Per le barre degli strumenti di zoom, TPDFlibViewer.EffectiveZoomPercent riporta lo zoom effettivamente in vigore — incluso il fattore calcolato nelle modalità di adattamento larghezza e adattamento pagina — in modo che un incremento dello zoom possa basarsi sull'effettivo ingrandimento corrente piuttosto che sull'ultimo valore assegnato

Evidenziazione dei risultati di ricerca e inversione delle coordinate correlata

HighlightSearchHits cerca una query in tutto il documento e disegna una barra traslucida su ogni corrispondenza, restituendo il conteggio dei risultati; ClearSearchHighlights, SearchHighlightCount e la proprietà SearchHighlightColor completano l'interfaccia. Poiché i rettangoli dei risultati sono memorizzati nello spazio della pagina e riproiettati ad ogni disegno, le barre rimangono ancorate al testo mentre l'utente scorre, zooma e ruota la visualizzazione. Se si utilizza il componente complementare TPDFlibSearchPanel, il suo metodo RunSearch disegna automaticamente le stesse barre nel visualizzatore associato tramite la proprietà HighlightInView, abilitata per impostazione predefinita

procedure TMainForm.btnSearchClick(Sender: TObject);
var
  Hits: Integer;
begin
  FViewer.SearchHighlightColor := clYellow;
  Hits := FViewer.HighlightSearchHits(edtQuery.Text, False);  // case-insensitive
  StatusBar1.SimpleText := Format('%d matches', [Hits]);
end;

La parte complessa, e il motivo per cui è preferibile utilizzare il percorso integrato rispetto al crearne uno proprio a partire dai risultati di ricerca non elaborati, is la conversione del sistema di coordinate. Il motore di testo riporta i rettangoli di corrispondenza in coordinate che dipendono dall'impostazione Origin della libreria: con Origin 0 o 3 l'asse Y va dal basso verso l'alto come nello spazio utente PDF nativo, e con Origin 2 o 3 l'asse X viene misurato dal bordo destro. La sovrapposizione del visualizzatore funziona in uno spazio di pagina dall'alto verso il basso e orientato a sinistra, quindi ogni rettangolo del risultato deve essere invertito (Y = PageHeight - Bottom e, se applicabile, X = PageWidth - Right) prima di essere memorizzato — prendere il bordo superiore riportato così com'è con l'origine predefinita disegna l'evidenziazione nella posizione specchiata verticalmente sulla pagina. TPDFlibViewer esegue questa conversione in base all'origine attiva quando registra ciascun risultato, quindi applica la mappatura di rotazione diretta al momento del disegno, motivo per cui le barre si posizionano correttamente a qualsiasi angolazione. La traslucenza stessa utilizza l'API Windows AlphaBlend con un alpha di origine costante, poiché il canvas VCL non ha un compositing alpha nativo. Le API sottostanti per la ricerca del testo e la geometria sono trattate in dettaglio nell'articolo sulla ricerca di testo e l'enumerazione degli elementi di pagina

Clic sulle annotazioni, suggerimenti ed esportazione degli allegati

TPDFlibViewer gestisce le annotazioni come oggetti interattivi di prim'ordine. Facendo clic su un'annotazione che non sia un collegamento si attiva OnAnnotClick con il numero di pagina, l'indice dell'annotazione a base 1 e la stringa del sottotipo; AnnotAtPagePoint espone lo stesso hit test a livello di programmazione e salta le annotazioni Popup associate, in modo da ottenere sempre l'annotazione effettivamente vista dall'utente. Passando il mouse su un'annotazione viene mostrato il testo del suo contenuto come suggerimento del controllo tramite l'opzione AnnotHints, attiva per impostazione predefinita e compatibile con la proprietà standard VCL ShowHint. Per le annotazioni FileAttachment, SaveAnnotAttachmentToFile scrive il file incorporato sul disco e restituisce 1 in caso di successo, completando un flusso di lavoro click-to-save in poche righe

procedure TMainForm.ViewerAnnotClick(Sender: TObject; APage, AnnotIndex: Integer;
  const Subtype: WideString);
begin
  if Subtype = 'FileAttachment' then
    if FViewer.SaveAnnotAttachmentToFile(APage, AnnotIndex,
        'C:\Temp\attached-invoice.xml') = 1 then
      StatusBar1.SimpleText := 'Attachment saved';
end;

La stampa e i limiti da pianificare

La stampa non richiede un'integrazione separata. PrintDoc mostra la finestra di dialogo di stampa standard di Windows — scelta della stampante, intervallo di pagine, copie, fascicolazione — e affida il lavoro al motore di stampa della libreria, mentre PrintDoc(False) invia l'intero documento alla stampante predefinita senza alcuna interfaccia utente. PrintDocRange funziona in modo completamente programmato con un nome di stampante esplicito, pagina iniziale e finale, numero di copie e flag di fascicolazione, rappresentando il punto di accesso corretto per lavori di stampa batch o avviati da server gestiti dalla stessa istanza del visualizzatore

Due limiti meritano di essere menzionati chiaramente. In primo luogo, TPDFlibViewer è un controllo Windows VCL; il livello del visualizzatore è escluso dalle build NOVCL, quindi i servizi console e i target non VCL dovrebbero utilizzare direttamente l'API di rendering TPDFlib sottostante. In secondo luogo, l'uso in modalità headless è volutamente supportato ma limitato: CaptureRegion esegue il rendering di qualsiasi area client — pagine ed evidenziazioni — in una bitmap di proprietà del chiamante e funziona su un controllo senza Parent e senza handle di finestra, poiché il percorso di disegno evita le chiamate VCL che forzerebbero la creazione dell'handle. Ciò rende pratica la verifica a livello di pixel nei test automatizzati, ma qualsiasi operazione che richieda realmente un ciclo di messaggi, come gli strumenti interattivi del mouse, appartiene comunque a una sessione UI reale. Sul fronte delle prestazioni, il controllo mantiene una cache delle pagine in memoria con un budget configurabile tramite SetCacheLimit e può rendere persistenti le pagine renderizzate tra le sessioni; l'articolo sulla cache delle pagine su disco del visualizzatore copre in dettaglio quel livello e il suo comportamento DPI per monitor

Cinque passaggi, quindi: posizionare il controllo e chiamare LoadFromFile, associare le azioni pronte all'uso di PDFlibViewerActions, chiamare HighlightSearchHits per la ricerca all'interno della pagina, gestire OnAnnotClick per i flussi di lavoro delle annotazioni ed esporre PrintDoc. Ogni elemento mostrato qui è incluso nella losLab PDF Library standard per Delphi, dove il controllo visualizzatore, le classi di azione e il motore di rendering si installano insieme