Artículo técnico

Comparación de PDF lado a lado en Delphi con PDFium

Dos documentos abiertos a la vez, con el mismo número de página, cada uno en su propio panel desplazable: ese es el núcleo de un visor de comparación. PDFium Component lo entrega mediante un modelo de objetos directo en el que TPdf posee el archivo y TPdfView posee la visualización. Un documento, un TPdf, un TPdfView. Si quiere tres paneles, tiene tres pares. Las partes difíciles no son las llamadas a la API; son la aritmética de disposición cuando la ventana cambia de tamaño y la lógica de sincronización de páginas cuando decide qué vista debe seguir a cuál

Disposición del formulario

El formulario VCL contiene tres contenedores TScrollBox uno al lado del otro, cada uno con un TPdfView dentro y alineado con alClient para que llene la caja. Dos componentes TSplitter se ubican entre las cajas para que el usuario pueda ajustar el ancho de las columnas en tiempo de ejecución. Una barra de herramientas sobre los paneles lleva los botones de apertura, los controles de zoom y el conmutador de dos vistas / tres vistas

El modo de tres vistas es un booleano que el formulario mantiene internamente. Cuando cambia, usted recalcula los anchos y muestra u oculta la tercera columna. El enfoque más simple es limpiar todas las propiedades Align, ocultar los divisores y luego fijar posiciones absolutas:

Diagrama de disposición del formulario de un visor de comparación de PDF lado a lado en Delphi construido con PDFium Component, con barra de herramientas, tres cajas de desplazamiento con paneles TPdfView y divisores en modo de dos y de tres vistas
Cada panel es una caja de desplazamiento con un TPdfView dentro, y alternar entre dos y tres vistas es apenas otro conjunto de asignaciones de ancho
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;
    // Aplique el mismo (ClientHeight - alto de la barra) a los tres valores 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;

Fijar Align := alNone en las tres cajas antes de la aritmética entera evita que el motor de restricciones de la VCL pelee contra sus asignaciones. Restaure la visibilidad de los divisores después de posicionar si quiere el redimensionamiento por arrastre en el modo de dos vistas

El alto de cada caja de desplazamiento es el área de cliente menos el alto del panel de la barra de herramientas. Como la barra está acoplada arriba con alTop, ClientHeight - PanelButtons.Height le da el espacio vertical utilizable. Asigne esto a las tres cajas dentro de la misma llamada a UpdateLayout, para que nunca haya un cuadro en el que una caja sea más alta que las otras y provoque un parpadeo de disposición

Abrir un documento

Cada par de paneles necesita su propio procedimiento de apertura. El patrón es corto: desactive el componente, fije el nombre de archivo, actívelo y luego compruebe Active; si quedó en False, pida una contraseña y reintente. Tenga en cuenta que TPdfView.Active es lo que controla el renderizado, pero TPdf.Active es lo que realmente abre el archivo; son independientes. Fijar PdfView.Active := True cuando su TPdf vinculado todavía no está activo es inofensivo, pero no muestra nada

Diagrama de flujo de la apertura de un documento PDF con PDFium Component en Delphi, con la comprobación silenciosa de Active, un reintento con contraseña y un diálogo de error para archivos dañados o protegidos
Una carga fallida deja Active en False sin lanzar excepción, así que el flujo lo comprueba, reintenta una vez con contraseña y finalmente informa el problema en lugar de mostrar un panel en blanco
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;

  // Las fallas de carga son silenciosas: Active queda en False sin lanzar.
  if not PdfComponent.Active then
  begin
    // Lo más probable es un archivo con contraseña; dé un reintento al usuario.
    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;

Compruebe siempre PdfComponent.Active después de la asignación; un archivo dañado o una contraseña equivocada hacen que la carga falle en silencio, sin lanzar una excepción en la ruta predeterminada. Fijar PdfViewComponent.PageNumber := 1 de forma explícita tras una apertura exitosa evita arrastrar un número de página viejo del documento anterior

El diálogo de mensaje del final es intencional: usted quiere que los archivos corruptos o no admitidos salgan a la superficie de inmediato en lugar de quedar tragados como un panel en blanco silencioso. Un usuario que no ve nada no tiene idea de si el archivo se cargó y simplemente está vacío, o si el componente lo rechazó. Informar la falla mantiene visible el error

Seguimiento del panel activo

Cuando el usuario hace clic dentro de un panel, ese panel pasa a ser el activo. El formulario mantiene un campo privado FActivePdfView: TPdfView. La retroalimentación visual es un cambio de color de borde en el TScrollBox contenedor: fíjelo en clHighlight para el activo y en clWindow para los demás. Conecte esto a cada TPdfView.OnClick y al procedimiento de apertura, para que el foco siga al documento que acaba de abrir

Algunas operaciones se aplican a todos los paneles visibles y no solo al activo. Un booleano FAllViewsMode en el formulario gobierna esa rama. Cuando es verdadero, los cambios de zoom y la navegación de páginas se abren en abanico hacia cada panel que tenga un documento activo:

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;

Navegación de páginas sincronizada

La navegación sincronizada es opcional, pero útil en flujos de revisión de documentos donde ambos archivos cubren el mismo rango de páginas. La lógica pertenece a un manejador de eventos que se dispara después de que el usuario navega en una vista. Cuando una vista fuente cambia su PageNumber, el manejador propaga ese número a las demás vistas, sujeto a una salvaguarda: la vista de destino debe tener al menos esa cantidad de páginas, de lo contrario se omite

El PageNumber de TPdfView y el de TPdf son independientes. TPdf.PageNumber registra qué página considera actual el componente de documento; TPdfView.PageNumber registra lo que se muestra en pantalla. Para fines de navegación conviene la propiedad de la vista, no la del documento

Una casilla de verificación rotulada algo así como "Sync pages" le da control al usuario. Cuando está desmarcada, cada panel navega de forma independiente y el manejador sale de inmediato. Esa independencia importa en los casos donde los dos documentos tienen distinta cantidad de páginas, o donde el usuario quiere encontrar el pasaje equivalente en una traducción que empieza en otra página. Forzar siempre la sincronización volvería la herramienta más difícil de usar que un simple arreglo de dos ventanas en el escritorio

Un detalle para vigilar: fijar PdfView.PageNumber por programa dentro del manejador de sincronización dispara a su vez el evento de cambio de esa vista. Protéjase contra la recursión infinita con un indicador booleano que fije antes de la asignación y limpie inmediatamente después. El indicador es por formulario, no por vista, porque las tres vistas comparten el mismo manejador

Diagrama de la navegación de páginas sincronizada en un visor de comparación de PDF en Delphi con PDFium Component, con la casilla de sincronización, una salvaguarda de cantidad de páginas por vista de destino y un indicador contra la recursión
El número de página viaja de la vista fuente a todas las demás solo cuando la sincronización está habilitada y cada vista de destino realmente contiene esa página

Zoom por panel

Cada TPdfView lleva su propia propiedad Zoom, un Double en porcentaje donde Zoom := 100 significa tamaño real (100%). Fijarlo anula cualquier FitMode activo. Para un botón de ajustar al ancho en el panel activo, lea el zoom de ajuste desde PdfView.PageWidthZoom[PdfView.PageNumber] y asígnelo. Para ajustar a la página, use PageZoom[PageNumber]. Ambas son propiedades de arreglo indexadas por número de página basado en 1, así que protéjase contra un número de página cero antes de acceder a ellas

Cuando exporte la página actual a una imagen, lea la rotación desde la vista pero llame a RenderPage en el componente TPdf, no en la vista. La forma de mapa de bits de TPdf.RenderPage toma dimensiones explícitas en píxeles más un valor TRotation y un conjunto TRenderOptions. La variante de función devuelve un TBitmap cuya propiedad es del llamador y que usted libera por su cuenta después de guardar:

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;

El multiplicador 2x sobre el ancho y el alto da una salida más nítida para documentos con texto fino. El try/finally alrededor de la liberación del mapa de bits no es opcional; una cancelación de TSaveDialog igual pasa por el bloque finally, y usted quiere que el mapa de bits se libere sin importar lo que haya hecho el usuario

Requisitos de la DLL

PDFium Component envuelve la biblioteca nativa pdfium. Un proceso anfitrión de 32 bits necesita pdfium32.dll; uno de 64 bits necesita pdfium64.dll. Las variantes con el motor JavaScript V8 agregan el sufijo v8 y pesan aproximadamente entre 23 y 27 MB frente a los 5-6 MB de las compilaciones estándar. Para un visor de comparación que deshabilita el llenado de formularios (Pdf.FormFill := False), la compilación estándar sin V8 alcanza y mantiene la distribución más pequeña

Coloque la DLL en el mismo directorio que el ejecutable, o en cualquier directorio del PATH del sistema. El componente la carga bajo demanda cuando se activa el primer TPdf, así que una DLL ausente sale a la luz en ese momento y no al iniciar la aplicación. Si distribuye un instalador, el enfoque más confiable es copiar la DLL a la carpeta de la aplicación durante la instalación en lugar de depender de un directorio del sistema que un administrador quizá limpie más adelante

Las compilaciones con V8 son útiles sobre todo cuando necesita interactuar con acciones JavaScript de PDF, por ejemplo para disparar campos de cálculo o manejadores de envío. Un visor de comparación pasivo no tiene ninguna razón para ejecutar JavaScript; fijar Pdf.FormFill := False antes de Active := True se salta por completo el entorno de llenado de formularios, lo que además significa que no se inicializa ningún motor JS aunque se use la compilación estándar. Ese es el valor predeterminado correcto para un visor de solo lectura, sea cual sea la variante de DLL que distribuya

Para más detalles sobre PDFium Component y su API completa, visite la página de producto de Delphi PDFium Component