Technical Article

Crea un visor de PDF en Delphi con PDFium VCL

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 fichero, lo descifra y responde preguntas sobre el número de páginas y los metadatos. TPdfView es el control visual que pinta páginas en pantalla y gestiona el desplazamiento, el zoom y la página que el usuario está viendo en ese momento. PDFium VCL envuelve el mismo motor de renderizado que viene dentro de Chrome, por lo que los glifos, el antialiasing y el color que se obtienen en el canvas coinciden con lo que los usuarios ya ven en su navegador. El trabajo no está en el renderizado. Está en conectar el objeto de documento a la vista, cargar sin fallar con un fichero dañado o protegido con contraseña, y dar al usuario los pocos controles que hacen que un visor parezca terminado: cambiar de página, cambiar el zoom y ajustar la página a la ventana.

Esto recorre ese ensamblaje en el orden en que realmente se construye. Todo lo que hay aquí renderiza una sola página a la vez, que es lo que la mayoría de los flujos de trabajo con documentos necesitan. Si se necesitan páginas apiladas en una columna de desplazamiento continuo, esa es una decisión de diseño diferente tratada en un artículo separado y no es el camino aquí.

Conectar TPdf a TPdfView

Coloca un TPdf y un TPdfView en el formulario y luego indica a la vista qué documento mostrar. Esa única asignación es todo el enlace entre el documento no visual y el control que lo pinta.

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

Antes de que nada de esto funcione, la biblioteca nativa de PDFium debe estar en la máquina. PDFium VCL llama a pdfium32.dll o pdfium64.dll según la plataforma de destino, y el documento simplemente se niega a abrirse si no se encuentra la DLL. Distribuye la DLL correspondiente junto a tu ejecutable, o colócala donde el cargador del sistema pueda encontrarla. Las compilaciones con V8 habilitado solo existen para PDF que llevan JavaScript que se quiera ejecutar, algo que un visor básico no hace, así que usa la DLL estándar a menos que haya 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 fallo. Ese instinto está equivocado aquí, y equivocarse produce un visor que parece correcto hasta que alguien le da un fichero roto. Establecer Active := True no lanza una excepción si la carga falla. PDFium VCL 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 de vuelta después de establecerla.

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

Hay dos cosas que merecen atención. La primera es que PageNumber existe en ambos objetos y los dos son independientes. Pdf.PageNumber es la noción del documento sobre la página actual; PdfView.PageNumber es la página que el control muestra realmente, y es la que se establece para mover al usuario por el fichero. Establecer uno no mueve al otro, por lo que un visor siempre conduce la propiedad de la vista. La segunda es la indexación basada en 1: las páginas van de 1 a Pdf.PageCount, no desde 0, lo que atrapa a cualquiera acostumbrado a arrays de base cero.

Gestionar un fichero cifrado

Los documentos cifrados se integran en el mismo flujo de carga. Si la contraseña de apertura se establece antes de la activación, el documento se descifra al abrirse; si es incorrecta o falta, Active permanece en False exactamente igual que con un fichero corrupto. Por tanto, la recuperación consiste en pedir una contraseña e intentar la activación de nuevo.

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;       // must be set before 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;

Como el fallo es silencioso tanto para una contraseña incorrecta como para un fichero dañado, no se puede distinguir entre los dos solo con Active. En la práctica eso es aceptable para un visor: el usuario o proporciona la contraseña correcta o aprende que el fichero no se puede abrir, y el mensaje dice lo mismo en ambos casos.

Navegar por el documento

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

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;

// the four navigation buttons reduce to one call each
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;

Un cuadro de texto "ir a la página N" es la misma llamada a GoToPage alimentada desde un entero analizado, y el acotamiento cubre el caso en que el usuario escribe 9999 en un fichero de diez páginas. Mantén UpdatePageLabel como el único lugar que escribe "Página 3 de 12" para que el indicador nunca se desincronice con lo que muestra la vista.

Zoom: porcentajes explícitos y modos de ajuste

El zoom en TPdfView llega en dos variantes que interactúan, y entender esa interacción es la diferencia entre un control de zoom que se comporta bien y uno que lucha contra 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 ti y lo siga recalculando a medida que cambia el tamaño de la ventana.

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

Esta es la parte que confunde a la gente. Asignar Zoom directamente restablece FitMode a pfmNone. Eso es el comportamiento correcto, no un error: en el momento en que el usuario elige exactamente el 150%, la vista ya no puede también respetar "ajustar al ancho", porque las dos peticiones entran en conflicto. La consecuencia para la interfaz es que un botón de zoom y un botón de ajustar a la página son estados mutuamente excluyentes, y la barra de herramientas debe mostrar el modo activo. Cuando el usuario hace clic en ajustar a la página, establece FitMode; cuando hace clic en un zoom numérico, establece Zoom y deja que borre el modo de ajuste por sí solo.

Si se prefiere calcular el valor de ajuste manualmente, quizás para inicializar un control deslizante de zoom con el porcentaje de ajuste actual, los helpers por página 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, al tamaño completo o al tamaño real respectivamente.

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Lo que realmente necesita un visor terminado

El título original exagera el trabajo. El visor anterior tiene unas pocas decenas de líneas y ya hace lo que un flujo de trabajo con documentos necesita: abrir un fichero, sobrevivir a uno defectuoso, mostrar una página, moverse entre páginas y cambiar la ampliación a mano o por ajuste. PDFium hace las partes difíciles en silencio. Las fuentes incrustadas se resuelven, las anotaciones y los campos de formulario se pintan donde el documento los coloca, y la página que se ve coincide con la que vería un usuario de Chrome, porque es el mismo motor el que dibuja ambas.

Desde esta base las adiciones son incrementales en lugar de estructurales. La selección de texto y la búsqueda leen de la misma capa de texto que PDFium ya construye; los metadatos como Pdf.Title y Pdf.Author están a una sola lectura de propiedad de distancia; la rotación y la escala de grises son opciones de renderizado que se pasan al dibujar una página en un mapa de bits. Ninguno de esos cambios altera la columna vertebral que se tiene aquí, que es el objeto de documento, la vista y el flujo de carga y navegación que los conecta. Consigue que esa columna sea correcta y el resto es decoración.

Los componentes TPdf y TPdfView usados a lo largo del artículo forman parte de PDFium VCL para Delphi y C++Builder, cuya página de producto contiene la referencia completa del visor.