Artículo técnico

Comparación de PDF en paralelo en Delphi con PDFium Component

Dos documentos abiertos a la vez, 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 ofrece esto a través de un modelo de objetos sencillo donde TPdf posee el fichero y TPdfView posee la visualización. Un documento, un TPdf, un TPdfView. Si usted quiere tres paneles, usted tiene tres pares. Las partes difíciles no son las llamadas a la API; son la aritmética de diseño cuando se cambia el tamaño de la ventana y la lógica de sincronización de páginas cuando decide qué vista debe seguir a cuál

Diseño del formulario

El formulario VCL alberga tres contenedores TScrollBox uno al lado del otro, cada uno con un TPdfView en su interior y alineado a alClient para que llene la caja. Dos componentes TSplitter se sitúan 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 rastrea internamente. Cuando se invierte, usted recalcula las anchuras y muestra u oculta la tercera columna. El enfoque más simple es borrar todas las propiedades Align, ocultar los divisores y, a continuación, establecer posiciones absolutas:

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;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  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;

Establecer Align := alNone en las tres cajas antes de la aritmética de enteros evita que el motor de restricciones VCL se pelee con sus asignaciones. Restaure la visibilidad de los divisores después de posicionar si desea arrastrar para cambiar el tamaño en el modo de dos vistas

La altura de cada caja de desplazamiento es el área del cliente menos la altura del panel de la barra de herramientas. Debido a que la barra de herramientas está acoplada en la parte superior con alTop, ClientHeight - PanelButtons.Height le da el espacio vertical utilizable. Asigne esto a las tres cajas dentro de la misma llamada UpdateLayout de modo que nunca haya un fotograma en el que una caja sea más alta que las demás y cause un parpadeo en el diseño

Abrir un documento

Cada par de paneles necesita su propio procedimiento de apertura. El patrón es corto: desactive el componente, establezca el nombre del fichero, intente activar, y capture EPdfError si el fichero requiere una contraseña. Tenga en cuenta que TPdfView.Active es lo que controla el renderizado, pero TPdf.Active es lo que realmente abre el fichero; son independientes. Establecer PdfView.Active := True cuando su TPdf enlazado aún no está activo es inofensivo pero no muestra nada

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 := '';

  try
    PdfComponent.Active := True;
  except
    on E: EPdfError do
    begin
      if InputQuery('Password', 'Enter document password:', Password) then
      begin
        PdfComponent.Password := Password;
        PdfComponent.Active   := True;
      end
      else
        raise;
    end;
  end;

  if PdfComponent.Active then
  begin
    PdfViewComponent.PageNumber := 1;
    SetActivePdfView(PdfViewComponent);
  end;
end;

Siempre compruebe PdfComponent.Active después de la asignación; un fichero dañado o una contraseña incorrecta hace que la carga falle silenciosamente sin lanzar una excepción en la ruta predeterminada. Establecer PdfViewComponent.PageNumber := 1 de forma explícita después de una apertura exitosa evita un número de página obsoleto del documento anterior

El código de manejo de contraseñas anterior lanza un error en cualquier error que no sea el mensaje conocido de contraseña. Eso es intencional: usted quiere que los ficheros corruptos o no soportados afloren de inmediato en lugar de que sean tragados como un panel silencioso en blanco. Un usuario que no ve nada no tiene idea de si el fichero se cargó y simplemente está vacío, o si el componente lo rechazó. Lanzar el error lo mantiene visible

Seguimiento del panel activo

Cuando el usuario hace clic dentro de un panel, ese panel se vuelve activo. El formulario rastrea un campo privado FActivePdfView: TPdfView. La retroalimentación visual es un cambio de color del borde en el TScrollBox que lo contiene: establézcalo en clHighlight para el activo y 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 en lugar de solo al activo. Un booleano FAllViewsMode en el formulario impulsa esa rama. Cuando es verdadero, los cambios de zoom y la navegación de páginas se distribuyen a cada panel que tiene 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 para flujos de trabajo de revisión de documentos donde ambos ficheros 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 por una vista. Cuando una vista de origen cambia su PageNumber, el manejador propaga ese número a las otras vistas, sujeto a una guarda: la vista de destino debe tener al menos esa cantidad de páginas, de lo contrario se omite

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

Una casilla de verificación etiquetada con algo como "Sync pages" le da el control al usuario. Cuando está desmarcada, cada panel navega independientemente y el manejador sale inmediatamente. Esa independencia es importante para casos de uso donde los dos documentos tienen diferentes recuentos de páginas, o donde el usuario quiere encontrar el pasaje equivalente en una traducción que comienza en una página diferente. Forzar siempre la sincronización haría que la herramienta fuera más difícil de usar que una simple disposición de escritorio de dos ventanas

Una cosa a tener en cuenta: establecer PdfView.PageNumber mediante programación dentro del manejador de sincronización disparará a su vez el evento de cambio en esa vista. Protéjase contra la recursividad infinita con una bandera booleana que usted establece antes de la asignación y borra inmediatamente después. La bandera es por formulario, no por vista, porque las tres vistas comparten el mismo manejador

Zoom por panel

Cada TPdfView lleva su propia propiedad Zoom, un Double en porcentaje donde Zoom := 100 significa tamaño real (100%). Establecerlo anula cualquier FitMode activo. Para un botón de ajustar a la anchura 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 matriz indexadas por el número de página en base 1, así que protéjase contra un número de página cero antes de acceder a ellas

Cuando usted exporta 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 la función devuelve un TBitmap propiedad del llamador que usted libera por sí mismo 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 de 2x sobre la anchura y la altura da una salida más nítida para los documentos con texto fino. El try/finally alrededor de la liberación del mapa de bits no es opcional; una cancelación en TSaveDialog aún 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 host de 32 bits necesita pdfium32.dll; un host de 64 bits necesita pdfium64.dll. Las variantes con el motor de JavaScript V8 añaden el sufijo v8 y pesan aproximadamente entre 23 y 27 MB frente a las compilaciones estándar de 5 a 6 MB. Para un visor de comparación que desactiva el rellenado de formularios (Pdf.FormFill := False), la compilación estándar sin V8 es suficiente 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, de manera que una DLL faltante aflora en ese momento en lugar de al inicio de la aplicación. Si usted distribuye un instalador, el enfoque más fiable es copiar la DLL en la carpeta de la aplicación durante la instalación en lugar de confiar en un directorio del sistema que un administrador podría limpiar más tarde

Las compilaciones de V8 son principalmente útiles cuando necesita interactuar con las acciones de 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; establecer Pdf.FormFill := False antes de Active := True omite por completo el entorno de relleno de formularios, lo que también significa que no se inicializa ningún motor de JS incluso si se utiliza la compilación estándar. Ese es el valor predeterminado correcto para un visor de sólo lectura sin importar qué variante de la DLL usted distribuya

Para obtener más detalles sobre el componente PDFium Component y su API completa, visite la página del producto Delphi PDFium Component