Articolo tecnico

Confronto PDF affiancato in Delphi con PDFium Component

Due documenti aperti nello stesso momento, stesso numero di pagina, ciascuno nel proprio pannello scorrevole: questo è il cuore di un viewer di confronto. PDFium Component lo realizza attraverso un modello a oggetti lineare in cui TPdf possiede il file e TPdfView possiede la visualizzazione. Un documento, un TPdf, un TPdfView. Volete tre pannelli, avete tre coppie. Le parti difficili non sono le chiamate API; sono l'aritmetica del layout quando la finestra viene ridimensionata e la logica di sincronizzazione delle pagine quando decidete quale vista debba seguire quale

Layout del form

Il form VCL contiene tre contenitori TScrollBox affiancati, ciascuno con un TPdfView dentro e allineato ad alClient in modo da riempire il box. Due componenti TSplitter stanno fra i box così che l'utente possa regolare le larghezze delle colonne a runtime. Una barra degli strumenti sopra i pannelli porta i pulsanti di apertura, i controlli di zoom e il selettore fra due e tre viste

La modalità a tre viste è un booleano che il form tiene internamente. Quando cambia, ricalcolate le larghezze e mostrate o nascondete la terza colonna. L'approccio più semplice è azzerare tutte le proprietà Align, nascondere gli splitter e poi impostare posizioni assolute:

Diagramma del layout del form di un viewer di confronto PDF affiancato in Delphi costruito con PDFium Component, che mostra una barra degli strumenti, tre scroll box con pannelli TPdfView e splitter nelle modalità a due e a tre viste
Ogni pannello è uno scroll box con un TPdfView dentro, e passare da due a tre viste è solo un diverso insieme di assegnazioni di larghezza
procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // Applicate lo stesso (ClientHeight - altezza toolbar) ai tre valori Height
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

Impostare Align := alNone su tutti e tre i box prima dell'aritmetica sugli interi evita che il motore dei vincoli della VCL combatta contro le vostre assegnazioni. Ripristinate la visibilità degli splitter dopo il posizionamento se volete il ridimensionamento a trascinamento in modalità a due viste

L'altezza di ogni scroll box è l'area client meno l'altezza del pannello della barra degli strumenti. Poiché la barra è agganciata in alto con alTop, ClientHeight - PanelButtons.Height vi dà lo spazio verticale utilizzabile. Assegnate questo valore a tutti e tre i box dentro la stessa chiamata a UpdateLayout così non esiste mai un fotogramma in cui un box è più alto degli altri e provoca uno sfarfallio del layout

Aprire un documento

Ogni coppia di pannelli ha bisogno della propria procedura di apertura. Lo schema è breve: disattivate il componente, impostate il nome del file, attivate, poi controllate Active; se è rimasto False, chiedete una password e ritentate. Notate che TPdfView.Active è ciò che controlla il rendering, mentre TPdf.Active è ciò che apre davvero il file; sono indipendenti. Impostare PdfView.Active := True quando il TPdf collegato non è ancora attivo è innocuo ma non mostra nulla

Diagramma di flusso dell'apertura di un documento PDF con PDFium Component in Delphi, che mostra il controllo silenzioso di Active, un solo ritentativo con password e una finestra di errore per i file danneggiati o protetti da password
Un caricamento fallito lascia Active a False senza sollevare eccezioni, quindi il flusso lo controlla, ritenta una volta con una password e infine segnala il problema invece di mostrare un pannello vuoto
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // I fallimenti di caricamento sono silenziosi: Active resta False.
  if not PdfComponent.Active then
  begin
    // Molto probabilmente un file protetto da password; un solo ritentativo.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

Controllate sempre PdfComponent.Active dopo l'assegnazione; un file danneggiato o una password sbagliata fanno fallire il caricamento in silenzio, senza sollevare un'eccezione nel percorso predefinito. Impostare esplicitamente PdfViewComponent.PageNumber := 1 dopo un'apertura riuscita evita un numero di pagina residuo dal documento precedente

La finestra di messaggio alla fine è intenzionale: volete che i file corrotti o non supportati emergano subito invece di essere inghiottiti come un pannello vuoto e silenzioso. Un utente che non vede nulla non ha modo di sapere se il file si è caricato ed è semplicemente vuoto, oppure se il componente lo ha rifiutato. Segnalare il fallimento tiene visibile l'errore

Tracciare il pannello attivo

Quando l'utente clicca dentro un pannello, quel pannello diventa attivo. Il form tiene un campo privato FActivePdfView: TPdfView. Il riscontro visivo è un cambio di colore del bordo sul TScrollBox contenitore: impostatelo a clHighlight per quello attivo e a clWindow per gli altri. Collegate tutto questo a ogni TPdfView.OnClick e alla procedura di apertura, così il focus segue il documento appena aperto

Alcune operazioni si applicano a tutti i pannelli visibili anziché al solo pannello attivo. Un booleano FAllViewsMode sul form guida quel ramo. Quando è vero, le variazioni di zoom e la navigazione fra le pagine si diramano verso ogni pannello che ha un documento attivo:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

Navigazione di pagina sincronizzata

La navigazione sincronizzata è facoltativa ma utile nei flussi di revisione dei documenti in cui entrambi i file coprono lo stesso intervallo di pagine. La logica appartiene a un gestore di evento che scatta dopo che l'utente ha navigato una vista. Quando una vista sorgente cambia il proprio PageNumber, il gestore propaga quel numero alle altre viste, con una sola guardia: la vista di destinazione deve avere almeno quel numero di pagine, altrimenti si salta

Il PageNumber di TPdfView e quello di TPdf sono indipendenti. TPdf.PageNumber traccia quale pagina il componente documento considera corrente; TPdfView.PageNumber traccia ciò che è mostrato sullo schermo. Per la navigazione vi serve la proprietà della vista, non quella del documento

Una casella di spunta intitolata qualcosa come Sync pages dà il controllo all'utente. Quando non è spuntata, ogni pannello naviga in modo indipendente e il gestore esce subito. Quella indipendenza è importante nei casi in cui i due documenti hanno un numero di pagine diverso, o in cui l'utente vuole trovare il passaggio equivalente in una traduzione che inizia a una pagina diversa. Forzare sempre la sincronizzazione renderebbe lo strumento più scomodo di una semplice disposizione a due finestre sul desktop

Una cosa a cui fare attenzione: impostare PdfView.PageNumber programmaticamente dentro il gestore di sincronizzazione farà scattare a sua volta l'evento di cambio su quella vista. Proteggetevi dalla ricorsione infinita con un flag booleano che impostate prima dell'assegnazione e azzerate subito dopo. Il flag è per form, non per vista, perché tutte e tre le viste condividono lo stesso gestore

Diagramma della navigazione di pagina sincronizzata in un viewer di confronto PDF Delphi che usa PDFium Component, con la casella di sincronizzazione, una guardia sul numero di pagine per ogni vista di destinazione e un flag di protezione dalla ricorsione
Il numero di pagina viaggia dalla vista sorgente a ogni altra vista solo quando la sincronizzazione è attiva e ciascuna vista di destinazione contiene davvero quella pagina

Zoom per pannello

Ogni TPdfView porta con sé la propria proprietà Zoom, un Double in percentuale dove Zoom := 100 significa dimensione reale (100%). Impostarla scavalca qualsiasi FitMode attivo. Per un pulsante di adattamento alla larghezza sul pannello attivo, leggete lo zoom di adattamento da PdfView.PageWidthZoom[PdfView.PageNumber] e assegnatelo. Per l'adattamento alla pagina, usate PageZoom[PageNumber]. Entrambe sono proprietà array indicizzate per numero di pagina in base 1, quindi proteggetevi da un numero di pagina pari a zero prima di accedervi

Quando esportate la pagina corrente come immagine, leggete la rotazione dalla vista ma chiamate RenderPage sul componente TPdf, non sulla vista. La forma bitmap di TPdf.RenderPage accetta dimensioni in pixel esplicite più un valore TRotation e un insieme TRenderOptions. La variante a funzione restituisce una TBitmap di proprietà del chiamante che liberate voi stessi dopo il salvataggio:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

Il moltiplicatore 2x su larghezza e altezza dà un risultato più nitido per i documenti con testo minuto. Il try/finally attorno alla liberazione della bitmap non è facoltativo; un annullamento della TSaveDialog passa comunque dal blocco finally, e volete che la bitmap venga rilasciata a prescindere da ciò che l'utente ha fatto

Requisiti della DLL

PDFium Component avvolge la libreria nativa pdfium. Un processo host a 32 bit ha bisogno di pdfium32.dll; uno a 64 bit ha bisogno di pdfium64.dll. Le varianti con il motore JavaScript V8 aggiungono il suffisso v8 e pesano all'incirca 23-27 MB contro i 5-6 MB delle build standard. Per un viewer di confronto che disattiva la compilazione dei moduli (Pdf.FormFill := False), la build standard senza V8 è sufficiente e mantiene la distribuzione più leggera

Collocate la DLL nella stessa directory dell'eseguibile, oppure in una qualsiasi directory presente nel PATH di sistema. Il componente la carica su richiesta quando il primo TPdf viene attivato, quindi una DLL mancante si manifesta in quel momento e non all'avvio dell'applicazione. Se distribuite un installer, l'approccio più affidabile è copiare la DLL nella cartella dell'applicazione durante l'installazione invece di affidarsi a una directory di sistema che un amministratore potrebbe poi ripulire

Le build con V8 sono utili soprattutto quando dovete interagire con le azioni JavaScript dei PDF, per esempio per attivare campi di calcolo o gestori di invio. Un viewer di confronto passivo non ha alcun motivo di eseguire JavaScript; impostare Pdf.FormFill := False prima di Active := True salta del tutto l'ambiente di compilazione dei moduli, il che significa anche che nessun motore JS viene inizializzato nemmeno se si usa la build standard. Quello è il valore predefinito corretto per un viewer in sola lettura a prescindere da quale variante di DLL distribuiate

Per ulteriori dettagli su PDFium Component e sulla sua API completa, visitate la pagina di prodotto di Delphi PDFium Component