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 el cableado entre ellos. TPdf posee el documento: abre el fichero, lo descifra y responde preguntas sobre el recuento de páginas y los metadatos. TPdfView es el control visual que pinta las páginas en pantalla y maneja el desplazamiento, el zoom y la página que el usuario está mirando. PDFium Component envuelve el mismo motor de renderizado que se distribuye dentro de Chrome, de modo que los glifos, el antialiasing y el color que obtiene en el lienzo coinciden con lo que sus 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 crash un fichero dañado o protegido con contraseña, y dar al usuario el puñado de controles que hacen que un visor se sienta acabado: pasar la página, cambiar el zoom, ajustar la página a la ventana

Esto recorre ese ensamblaje en el orden en que realmente lo construye. Todo lo de aquí renderiza una sola página a la vez, que es lo que quiere la mayoría de los flujos de trabajo de documentos. Si necesita páginas apiladas en una columna con desplazamiento continuo, esa es una decisión de maquetación distinta y no el camino de aquí

Conectar TPdf a TPdfView

Suelte un TPdf y un TPdfView en el formulario, y luego dígale 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

Arquitectura de visor PDF en Delphi donde TPdf posee el documento, TPdfView lo pinta, y una única asignación de propiedad los vincula sobre la DLL PDFium
TPdf es dueño del documento mientras que TPdfView lo pinta, y una única asignación conecta ambos sobre el motor PDFium compartido
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf y PdfView se colocaron en tiempo de diseño.
  PdfView.Pdf := Pdf;                 // la vista pinta lo que contiene este documento
  PdfView.FitMode := pfmFitWidth;     // comienza al usuario en un zoom razonable
end;

Antes de que nada de esto se ejecute, la biblioteca nativa PDFium tiene que estar en la máquina. PDFium Component carga pdfium.dll tanto en Win32 como en Win64; las dos compilaciones comparten ese nombre de fichero y se distribuyen en carpetas separadas DLLs\Win32 y DLLs\Win64, y el documento simplemente se niega a abrirse si la DLL no se puede encontrar o su bitness no coincide con el de su ejecutable. Distribuya la DLL que corresponda a su plataforma de destino junto a su ejecutable, o colóquela donde el cargador del sistema la encuentre. Las compilaciones con V8 existen solo para los PDF que llevan JavaScript que usted quiera ejecutar, cosa que un visor normal no hace, así que alcance 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 está mal aquí, y equivocarse produce un visor que se ve bien hasta que alguien le entrega un fichero roto. Poner Active := True no lanza excepción en un fallo de carga. PDFium Component atrapa el error interno y deja Active en False, así que la única manera honesta de saber si el documento se abrió es leer la propiedad de vuelta después de asignarla

Flujo de decisión de carga para un visor Delphi de PDFium donde asignar Active nunca lanza excepción, un false silencioso significa contraseña errónea o fichero dañado, y sigue un único reintento de contraseña
La activación nunca lanza excepción en caso de fallo, de modo que el visor lee Active de vuelta y responde a un false silencioso con un único reintento de contraseña
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // nunca lanza excepción; el fallo deja Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // la vista sigue su propia página actual
  PdfView.Active := True;             // hasta que la vista está activa no sostiene ninguna página
  UpdatePageLabel;
end;

La vista tiene su propio interruptor Active, y hasta que no es True el control no sostiene ninguna página: abrir el documento y asignar PageNumber a secas deja el visor en blanco. Dos cosas más 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 de una página actual; PdfView.PageNumber es la página que el control realmente muestra, y es la que usted ajusta para mover al usuario por el fichero. Asignar una no mueve la otra, de manera que un visor siempre conduce la propiedad de la vista. La segunda es la indexación en base 1: las páginas van desde 1 hasta Pdf.PageCount, no desde 0, lo cual pilla desprevenido a todo aquel que esté acostumbrado a los arrays en base cero

Manejar un fichero encriptado

Los documentos cifrados se integran en la misma ruta de carga. Si la contraseña de apertura se asigna antes de la activación, el documento se descifra al abrirse; si es incorrecta o falta, Active se queda en False exactamente igual que con un fichero corrupto. Así que la recuperación es pedir una contraseña e intentar la activación otra vez

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 asignarse 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;
  PdfView.Active := True;
end;

Como el fallo es silencioso tanto para una contraseña incorrecta como para un fichero dañado, no se pueden distinguir los dos a partir de Active solo. En la práctica eso es aceptable para un visor: el usuario o bien aporta la contraseña correcta o bien se entera de que el fichero no se abrirá, y el mensaje se lee igual de ambas formas

Navegar por el documento

Con el documento abierto, la navegación es aritmética sobre PdfView.PageNumber delimitada por Pdf.PageCount. El único trabajo real es el pinzamiento, de modo que los botones nunca empujen la página fuera del rango y los botones del principio y del final permanezcan deshabilitados 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;

// 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;

Un cuadro de texto de "ir a la página N" es la misma llamada a GoToPage alimentada por un entero analizado, y la sujeción cubre el caso donde el usuario teclea 9999 en un fichero de diez páginas. Mantenga UpdatePageLabel como el único lugar que escribe "Página 3 de 12" de modo que la lectura nunca se desvíe en sincronía de lo que la vista muestra

Zoom: porcentajes explícitos y modos de ajuste

El zoom en TPdfView llega en dos sabores que interactúan, y comprender la interacción es la diferencia entre un control de zoom que se comporta y uno que se pelea con el usuario. La ruta directa es la propiedad Zoom, un porcentaje donde 100 significa el tamaño real. La otra ruta es FitMode, la cual le indica a la vista que calcule el zoom por usted y que lo mantenga recalculado mientras se cambia el tamaño de la ventana

Interacción de Zoom y FitMode en un visor Delphi de PDFium donde asignar un Zoom exacto limpia FitMode a pfmNone y elegir un modo de ajuste devuelve el zoom a la vista
Asignar un zoom exacto limpia el modo de ajuste y elegir un modo de ajuste devuelve el cálculo del zoom a la vista
// magnificaciones fijas
PdfView.Zoom := 100;     // tamaño real
PdfView.Zoom := 50;      // la mitad
PdfView.Zoom := 200;     // doble

// deja que la vista dimensione la página a la ventana y la mantenga dimensionada al redimensionar
PdfView.FitMode := pfmFitWidth;   // el ancho de la página llena el control
PdfView.FitMode := pfmFitPage;    // toda la página visible
PdfView.FitMode := pfmActualSize; // 1:1 con los puntos del documento

Aquí está la parte con la que la gente tropieza. Asignar Zoom directamente restablece FitMode a pfmNone. Ese es el comportamiento correcto, no un error: en el momento en que el usuario escoge un 150% exacto, la vista ya no puede a la vez honrar el "ajustar a la anchura", porque ambas peticiones entran en conflicto. La consecuencia para su UI es que un botón de ampliar 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 hace clic en ajustar a la página, fije FitMode; cuando haga clic en un zoom numérico, fije Zoom y deje que elimine el modo de ajuste por su cuenta

Si prefiere calcular el valor de ajuste usted mismo, quizás para alimentar un slider de zoom con el porcentaje de ajuste actual, los 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 a tamaño real

// siembra una lectura del zoom a partir del valor de ajuste a la anchura de la página actual
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Lo que realmente necesita un visor terminado

El visor anterior consta de unas pocas decenas de líneas, y ya hace el trabajo que necesita un flujo de trabajo de documentos: abre un fichero, sobrevive a uno malo, muestra una página, se mueve entre páginas y cambia la ampliación a mano o por ajuste. PDFium hace las partes difíciles silenciosamente. Las fuentes incrustadas se resuelven, las anotaciones y los campos de formulario se pintan allí donde el documento los coloca, y la página que usted ve concuerda con la que vería un usuario de Chrome, porque es el mismo motor el que dibuja ambas

A partir de esta base las incorporaciones son incrementales antes que 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 a un mapa de bits. Ninguno de estos altera la espina dorsal que tiene aquí, que es el objeto documento, la vista, y el flujo de cargar-luego-navegar que los conecta. Entienda bien esa espina dorsal y el resto es decoración

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