Artículo técnico

Construir un visor de PDF en Delphi con PDFium Component

Un visor de PDF en Delphi se reduce a dos componentes y la conexión entre ellos. TPdf es el propietario del documento: abre el archivo, lo desencripta y responde preguntas sobre la cantidad de páginas y los metadatos. TPdfView es el control visual que dibuja las páginas en la pantalla y maneja el desplazamiento, el zoom y la página que el usuario está viendo actualmente. PDFium Component envuelve el mismo motor de renderizado que viene incluido en Chrome, por lo que los glifos, el suavizado (anti-aliasing) y el color que obtiene en el lienzo (canvas) coinciden con lo que sus usuarios ya ven en su navegador. El trabajo no está en el renderizado. Está en conectar el objeto del documento a la vista, cargarlo sin fallar en un archivo dañado o protegido con contraseña, y darle al usuario los pocos controles que hacen que un visor se sienta completo: pasar de página, cambiar el zoom, ajustar la página a la ventana

Este tutorial recorre ese ensamblaje en el orden en que realmente se construye. Todo aquí renderiza una sola página a la vez, que es lo que la mayoría de los flujos de trabajo de documentos requieren. Si necesita páginas apiladas en una columna de desplazamiento continuo, esa es una decisión de diseño diferente y no es el camino que tomaremos aquí

Conectar TPdf a TPdfView

Coloque un TPdf y un TPdfView en el formulario, luego indíquele a la vista qué documento mostrar. Esa única asignación es todo el vínculo entre el documento no visual y el control que lo dibuja

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf y PdfView fueron colocados en tiempo de diseño.
  PdfView.Pdf := Pdf;                 // la vista dibuja lo que contenga este documento
  PdfView.FitMode := pfmFitWidth;     // iniciar al usuario con un zoom razonable
end;

Antes de que algo de esto se ejecute, la biblioteca nativa de PDFium debe estar en el equipo (computadora). PDFium Component llama a pdfium32.dll o pdfium64.dll dependiendo de su plataforma de destino, y el documento simplemente se negará a abrir si no se puede encontrar la DLL. Distribuya la DLL correspondiente junto a su ejecutable, o colóquela donde el cargador del sistema la encuentre. Las compilaciones habilitadas para V8 existen solo para archivos PDF que contienen JavaScript que desea ejecutar, lo cual no hace un visor simple, así que elija la DLL estándar a menos que tenga una razón concreta para no hacerlo

Cargar un documento sin confiar en la entrada

El instinto es envolver la carga en un try/except y tratar una excepción lanzada como un fallo. Ese instinto es incorrecto aquí, y equivocarse produce un visor que se ve bien hasta que alguien le entrega un archivo dañado. Establecer Active := True no lanza una excepción ante un fallo de carga. PDFium Component captura el error interno y deja Active en False, por lo que la única forma honesta de saber si el documento se abrió es leer la propiedad después de establecerla

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // nunca lanza excepción; un fallo deja Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // la vista rastrea su propia página actual
  UpdatePageLabel;
end;

Dos cosas merecen atención. La primera es que PageNumber existe en ambos objetos y los dos son independientes. Pdf.PageNumber es la noción que tiene el documento de una página actual; PdfView.PageNumber es la página que el control realmente muestra, y es la que usted establece para mover al usuario a través del archivo. Establecer una no mueve la otra, por lo que un visor siempre controla la propiedad de la vista. La segunda es la indexación basada en 1: las páginas van del 1 a Pdf.PageCount, no desde el 0, lo que atrapa a cualquiera acostumbrado a arreglos (arrays) basados en cero

Manejar un archivo encriptado

Los documentos encriptados se integran en la misma ruta de carga. Si la contraseña de apertura se establece antes de la activación, el documento se desencripta a medida que se abre; si es incorrecta o falta, Active permanece en False exactamente como lo hace para un archivo corrupto. Así que la recuperación consiste en solicitar una contraseña e intentar la activación nuevamente

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // debe establecerse antes de Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

Dado que el fallo es silencioso tanto para una contraseña incorrecta como para un archivo dañado, no se pueden distinguir ambas situaciones solo a partir de Active. En la práctica, esto es aceptable para un visor: el usuario proporciona la contraseña correcta o se entera de que el archivo no se abrirá, y el mensaje se lee de la misma manera en ambos casos

Paginación a través del documento

Con el documento abierto, la navegación es aritmética sobre PdfView.PageNumber limitada por Pdf.PageCount. El único trabajo real es la restricción (clamping), para que los botones nunca empujen la página fuera de rango y los botones de primera y última página permanezcan deshabilitados en los extremos del archivo

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// los cuatro botones de navegación se reducen a una llamada cada uno
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

Una caja de texto "ir a la página N" es la misma llamada a GoToPage alimentada desde un número entero analizado (parsed), y la restricción cubre el caso en el que el usuario escribe 9999 en un archivo de diez páginas. Mantenga UpdatePageLabel como el único lugar que escribe "Página 3 de 12" para que la lectura nunca se desincronice de lo que muestra la vista

Zoom: porcentajes explícitos y modos de ajuste

El zoom en TPdfView viene en dos sabores que interactúan, y entender la interacción es la diferencia entre un control de zoom que se comporta bien y uno que pelea con el usuario. La ruta directa es la propiedad Zoom, un porcentaje donde 100 significa tamaño real. La otra ruta es FitMode, que le dice a la vista que calcule el zoom por usted y que lo siga recalculando a medida que la ventana cambia de tamaño

// ampliaciones fijas
PdfView.Zoom := 100;     // tamaño real
PdfView.Zoom := 50;      // mitad
PdfView.Zoom := 200;     // doble

// dejar que la vista ajuste la página a la ventana, y mantenerla ajustada al redimensionar
PdfView.FitMode := pfmFitWidth;   // el ancho de la página llena el control
PdfView.FitMode := pfmFitPage;    // página completa visible
PdfView.FitMode := pfmActualSize; // 1:1 con los puntos del documento

Aquí es donde la gente se tropieza. Asignar Zoom directamente restablece FitMode a pfmNone. Ese es el comportamiento correcto, no un error (bug): en el momento en que el usuario elige un 150% exacto, la vista ya no puede estar cumpliendo con "ajustar al ancho", porque ambas solicitudes entran en conflicto. La consecuencia para su interfaz de usuario (UI) es que un botón de acercar y un botón de ajustar a la página son estados mutuamente excluyentes, y la barra de herramientas debería hacer visible el modo activo. Cuando el usuario haga clic en ajustar a la página, establezca FitMode; cuando haga clic en un zoom numérico, establezca Zoom y deje que limpie el modo de ajuste por sí solo

Si prefiere calcular el valor de ajuste usted mismo, tal vez para alimentar un control deslizante de zoom con el porcentaje de ajuste actual, los ayudantes (helpers) por página le dan los números sin cambiar el modo. PageWidthZoom[N], PageZoom[N] y ActualSizeZoom[N] devuelven el porcentaje que ajustaría la página N al ancho, la ajustaría completa, o la renderizaría en su tamaño real

// alimentar una lectura de zoom desde el valor de ajuste al ancho de la página actual
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Lo que un visor completo realmente necesita

El visor anterior tiene unas pocas docenas de líneas, y ya hace el trabajo que requiere un flujo de trabajo de documentos: abrir un archivo, sobrevivir a uno malo, mostrar una página, moverse entre páginas y cambiar la ampliación a mano o por ajuste. PDFium hace las partes difíciles de forma silenciosa. Las fuentes incrustadas se resuelven, las anotaciones y los campos de formulario se dibujan donde el documento los ubica, y la página que ve coincide con la que vería un usuario de Chrome, porque es el mismo motor el que dibuja ambas

A partir de esta base, las adiciones son incrementales en lugar de estructurales. La selección de texto y la búsqueda leen desde la misma capa de texto que PDFium ya construye; metadatos como Pdf.Title y Pdf.Author están a una lectura de propiedad de distancia; la rotación y la escala de grises son opciones de renderizado que usted pasa cuando dibuja una página en un mapa de bits. Ninguna de esas cosas cambia la columna vertebral que tiene aquí, que es el objeto del documento, la vista y el flujo de cargar-luego-navegar que los conecta. Haga bien esa columna vertebral y el resto es decoración

Los componentes TPdf y TPdfView utilizados en todo el artículo son parte de PDFium Component para Delphi y C++Builder, que contiene la referencia completa del visor en la página de su producto