Artículo técnico

Crear un visor de PDF interactivo en Delphi con TPDFlibViewer

Incrustar un panel de lectura de PDF en una aplicación empresarial de Delphi no requiere escribir un bucle de renderizado, un motor de desplazamiento o un modelo de zoom. losLab PDF Library incluye TPDFlibViewer, un control VCL que abre un documento con una sola llamada y ofrece selección de texto, arrastre, zoom de marquesina, capturas instantáneas, resaltado de regiones, resaltado de búsqueda de texto completo, interacción con anotaciones e impresión como comportamiento integrado. Este artículo recorre los cinco pasos para convertir un formulario vacío en un visor funcional: soltar el control, conectar las acciones listas para usar, agregar resaltado de búsqueda, controlar los clics en las anotaciones e imprimir

¿Por qué usar un control de visor en lugar de escribir un bucle de renderizado?

El argumento de la separación en capas va primero, porque explica lo que realmente está adquiriendo. Renderizar una página a un mapa de bits es el 20 por ciento fácil; el otro 80 por ciento es todo lo que lo rodea: una caché de mapas de bits de página con un presupuesto de desalojo, diseño de desplazamiento entre páginas de diferentes tamaños, modos de zoom que se vuelven a calcular al cambiar el tamaño, pruebas de impacto (hit-testing) que asignan un píxel del mouse de regreso a las coordenadas de la página en cualquier zoom y rotación, y una programación de repintado que no parpadea. TPDFlibViewer se encarga de todo eso y delega el trabajo de rasterizado al mismo motor descrito en el artículo de renderizado multimotor, por lo que la capa de control y la capa de renderizado siguen siendo reemplazables por separado. El código de su formulario se comunica con un control de Windows desplazable, no con un renderizador

¿Cómo incrustar un visor de PDF en una aplicación Delphi?

TPDFlibViewer.LoadFromFile abre un documento a través de la propia instancia interna TPDFlib del control y devuelve 1 si tiene éxito, por lo que el visor mínimo es una llamada al constructor, una asignación de Parent y una carga. Si ya tiene una instancia de TPDFlib en otra parte de la aplicación, AttachLibrary muestra su documento seleccionado en su lugar, manteniendo la propiedad de su lado

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 propiedad ViewerMode selecciona lo que hace el mouse, a partir de cinco valores: vmSelect es la herramienta de selección de texto predeterminada, vmHand permite arrastrar para desplazarse con un cursor de mano mientras que un clic simple sigue los enlaces, vmZoom es un zoom de marquesina que amplía la banda arrastrada para llenar el área de visualización, vmSnapshot captura una región arrastrada al portapapeles (con un evento OnSnapshot y un interruptor SnapshotToClipboard), y vmHighlight dibuja resaltados de región translúcidos persistentes. La herramienta de resaltado merece una nota sobre las coordenadas: las regiones se almacenan en el espacio de usuario de la página, no en píxeles del cliente, por lo que las marcas de AddHighlightRegion sobreviven al zoom, a la rotación de la vista y a la navegación de páginas sin necesidad de llevar registros de su lado. En una vista rotada, la dirección del arrastre puede incluso ir en contra de los ejes de la página, lo que produce un ancho o alto negativo después del mapeo de rotación inversa; el control normaliza el rectángulo por usted

¿Cómo conectar una barra de herramientas de PDF en Delphi sin código de acoplamiento?

La unidad PDFlibViewerActions elimina por completo el código de acoplamiento de la barra de herramientas. Proporciona veinte descendientes de TAction listos para usar que cubren la navegación de páginas (TPDFlibViewerFirstPage, TPDFlibViewerPriorPage, TPDFlibViewerNextPage, TPDFlibViewerLastPage), zoom (TPDFlibViewerZoomIn, TPDFlibViewerZoomOut, TPDFlibViewerFitWidth, TPDFlibViewerFitPage, TPDFlibViewerActualSize), rotación de vista (TPDFlibViewerRotateClockwise, TPDFlibViewerRotateAntiClockwise), impresión, diálogos para abrir y guardar archivos, copiar texto seleccionado y una acción de cambio por cada herramienta de mouse (TPDFlibViewerSelectMode, TPDFlibViewerHandMode, TPDFlibViewerMarqueeZoomMode, TPDFlibViewerSnapshotMode, TPDFlibViewerHighlightMode). Cada acción apunta a un control a través de su propiedad publicada Viewer y mantiene su propio estado: cada acción se deshabilita cuando no hay ningún documento abierto, las acciones de navegación realizan el seguimiento de la posición de la página, la copia realiza el seguimiento de si existe una selección, y las acciones de modo y ajuste mantienen sincronizado su estado marcado con la herramienta activa y el modo de zoom. En tiempo de diseño, todas se registran en la categoría losLab PDF del editor de lista de acciones; en tiempo de ejecución, las mismas clases ocupan tres líneas cada una

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;

Vale la pena conocer una trampa de alcance de unidad si decide crear su propio registro de acciones sobre este patrón. RegisterActions se declara en System.Actions, no en Vcl.ActnList, y el error del compilador que se obtiene con solo Vcl.ActnList en la cláusula uses es sumamente engañoso: el literal de matriz de clases [TAction1, TAction2, ...] pasa a analizarse como un conjunto, lo que produce una cadena de errores E2010 Incompatible types: 'Integer' and 'class of ...' que parecen un desajuste de tipos en sus clases de acción. La solución es una única entrada en la sección uses, pero el diagnóstico apunta a todas partes excepto a ella. Para las barras de herramientas de zoom, TPDFlibViewer.EffectiveZoomPercent informa el zoom real en efecto, incluyendo el factor calculado en los modos de ajuste a ancho y ajuste a página, de modo que un paso de acercamiento puede basarse en la ampliación real actual en lugar del último valor asignado

Resaltado de resultados de búsqueda y la inversión de coordenadas detrás de este

HighlightSearchHits busca una consulta en todo el documento y dibuja una barra translúcida sobre cada coincidencia, devolviendo la cantidad de resultados; ClearSearchHighlights, SearchHighlightCount y la propiedad SearchHighlightColor completan la interfaz. Dado que los rectángulos de coincidencia se almacenan en el espacio de la página y se vuelven a proyectar en cada dibujo, las barras permanecen adheridas a su texto mientras el usuario se desplaza, hace zoom o rota la vista. Si utiliza el componente complementario TPDFlibSearchPanel, su método RunSearch dibuja las mismas barras en el visor adjunto automáticamente a través de la propiedad HighlightInView, que está activada de manera predeterminada

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 sutil, y la razón para preferir la ruta integrada en lugar de implementar una propia a partir de resultados de búsqueda sin procesar, es la conversión del sistema de coordenadas. El motor de texto informa los rectángulos de coincidencia en coordenadas que dependen de la configuración Origin de la biblioteca: con Origin 0 o 3, el eje Y se ejecuta de abajo hacia arriba como en el espacio de usuario de PDF nativo, y con Origin 2 o 3, el eje X se mide desde el borde derecho. La superposición del visor funciona en un espacio de página de arriba hacia abajo y alineado a la izquierda, por lo que cada rectángulo de coincidencia debe invertirse (Y = PageHeight - Bottom, y cuando corresponda, X = PageWidth - Right) antes de almacenarse; tomar el borde superior informado tal cual bajo el origen predeterminado dibuja el resaltado en la posición reflejada verticalmente en la página. TPDFlibViewer realiza esta conversión según el origen activo al registrar cada coincidencia, y luego aplica el mapeo de rotación hacia adelante al momento de dibujar, razón por la cual las barras se ubican correctamente en cualquier ángulo. La translucidez misma utiliza la API AlphaBlend de Windows con un canal alfa de origen constante, ya que el lienzo VCL no tiene composición alfa nativa. Las API subyacentes de búsqueda de texto y geometría se analizan en detalle en el artículo sobre búsqueda de texto y enumeración de elementos de página

Clics en anotaciones, sugerencias y exportación de archivos adjuntos

TPDFlibViewer trata las anotaciones como objetos interactivos de primera clase. Hacer clic en una anotación que no sea un enlace activa el evento OnAnnotClick con el número de página, el índice de anotación basado en 1 y la cadena del subtipo; AnnotAtPagePoint expone la misma prueba de impacto mediante programación y omite las anotaciones emergentes (Popup) complementarias para que siempre obtenga la anotación que el usuario realmente ve. Al pasar el cursor sobre una anotación, se muestra su texto de Contents como la sugerencia (hint) del control a través del interruptor AnnotHints, que está activado de manera predeterminada y coopera con la propiedad estándar ShowHint de la VCL. Para las anotaciones FileAttachment, SaveAnnotAttachmentToFile escribe el archivo incrustado en el disco y devuelve 1 si tiene éxito, lo que completa un flujo de trabajo de clic para guardar en unas pocas líneas

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;

Impresión y los límites que debe planificar

La impresión no requiere una integración independiente. PrintDoc muestra el diálogo de impresión estándar de Windows (selección de impresora, rango de páginas, copias, intercalación) y pasa el trabajo al motor de impresión de la biblioteca, mientras que PrintDoc(False) envía todo el documento a la impresora predeterminada sin ninguna interfaz de usuario. PrintDocRange es completamente programático con un nombre de impresora explícito, página de inicio y fin, cantidad de copias y bandera de intercalación, lo que representa el punto de entrada adecuado para trabajos de impresión por lotes o activados por servidor impulsados desde la misma instancia del visor

Dos límites merecen mención sincera. En primer lugar, TPDFlibViewer es un control VCL de Windows; la capa del visor se excluye de las compilaciones sin VCL (NOVCL), por lo que los servicios de consola y los objetivos que no son VCL deben usar la API de renderizado subyacente de TPDFlib directamente. En segundo lugar, el uso sin interfaz gráfica (headless) es compatible de forma deliberada pero acotado: CaptureRegion renderiza cualquier región del cliente —páginas más resaltados— en un mapa de bits propiedad de la llamada y funciona en un control sin contenedor (unparented) y sin un identificador de ventana, debido a que la ruta de dibujo evita llamadas VCL que obligarían a la creación del identificador. Esto hace que la verificación a nivel de píxel en pruebas automatizadas sea práctica, pero cualquier cosa que realmente necesite un bucle de mensajes, como las herramientas interactivas del mouse, sigue perteneciendo a una sesión de interfaz de usuario real. En cuanto al rendimiento, el control mantiene una caché de páginas de memoria con un límite configurable mediante SetCacheLimit y puede persistir páginas renderizadas entre sesiones; el artículo sobre la caché de páginas del visor en disco detalla esa capa y su comportamiento de DPI por monitor

Cinco pasos, entonces: colocar el control y llamar a LoadFromFile, adjuntar las acciones listas para usar de PDFlibViewerActions, llamar a HighlightSearchHits para la búsqueda en la página, controlar OnAnnotClick para flujos de trabajo de anotaciones y exponer PrintDoc. Cada pieza mostrada aquí se incluye en la versión estándar de losLab PDF Library para Delphi, donde el control del visor, las clases de acción y el motor de renderizado se instalan juntos