Technical Article

Combinar imágenes escaneadas en un PDF con PDFium VCL en Delphi

Un equipo de gestión de siniestros tenía treinta años de expedientes en papel que pasaban por un escáner de alimentación. El escáner generaba un JPEG por página en una carpeta, nombrados 0001.jpg, 0002.jpg, etc. Lo que el archivo necesitaba realmente era un PDF por expediente, con las páginas en orden, para que un revisor pudiese abrir un único documento en lugar de ir haciendo clic en cientos de miniaturas. Ese último paso, convertir una pila numerada de scans en un PDF ordenado, es el trabajo que se describe aquí.

PDFium VCL lo resuelve directamente. Más allá de la renderización y la extracción de texto, el componente puede construir un PDF desde cero: crear un documento vacío, añadir una página en blanco del tamaño que se quiera, colocar una imagen en esa página con coordenadas en el espacio de usuario y guardar. Toda la cadena reside en el componente TPdf, así que un convertidor por lotes es un bucle sobre nombres de fichero más un puñado de llamadas.

La forma de la conversión

Con cada scan hay que hacer tres cosas: decidir el tamaño de página, colocar la imagen dentro dejando un margen y avanzar a la página siguiente. PDFium VCL ofrece un método para cada una: AddPage crea una página en blanco de un tamaño dado, AddImage (o AddPicture si ya se tiene un TPicture) dibuja el mapa de bits en la página activa, y PageNumber indica al componente sobre qué página actúan las llamadas de dibujo siguientes.

El detalle que más confunde es el sistema de coordenadas. El espacio de usuario PDF sitúa el origen en la esquina inferior izquierda de la página, con Y creciendo hacia arriba, lo contrario de las coordenadas de pantalla que los desarrolladores Delphi usan por instinto. El par X, Y que se pasa a AddImage es la esquina inferior izquierda del rectángulo de la imagen, y Width, Height son el tamaño de colocación en puntos, no las dimensiones en píxeles del fichero fuente. Invertirlo hace que los scans caigan fuera de la página o girados respecto a donde se esperaba.

Crear el documento y una página por scan

Se empieza con un documento vacío. CreateDocument asigna un PDF nuevo y deja el componente activo, por lo que no hay ningún paso de apertura separado. A partir de ahí se recorre la lista de ficheros escaneados, y para cada uno se añade una página, se activa como página actual y se coloca la imagen. Las dimensiones de página aquí son A4 en puntos (595 × 842 en vertical), el tamaño estándar para la correspondencia archivada.

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // A4 width in points
  PageH = 842.0;   // A4 height in points
  Margin = 36.0;   // half-inch border around each scan
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // new, empty, already active
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // 1-based page index
      Pdf.PageNumber := I + 1;                // make the new page current
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Cada iteración crea una página y establece inmediatamente PageNumber en ella. Esa segunda línea importa: AddPage inserta la página pero los métodos de dibujo actúan sobre la página que esté activa, de modo que asignar PageNumber es lo que apunta AddImage a la página recién creada. Omitirlo hace que las imágenes se apilen sobre la página que estuviese cargada antes.

En ese bucle hay una suposición oculta: el orden de Files. Un escáner nombra las páginas de 0001.jpg a 0100.jpg, pero una enumeración de directorio no siempre las devuelve ordenadas, y en cuanto aparece page9.jpg junto a page10.jpg una ordenación de cadenas simple coloca la página 10 antes que la 9. Hay que ordenar la lista explícitamente antes del bucle y usar nombres con ceros de relleno en el momento del escaneo para que el orden léxico coincida con el orden de las páginas. La secuencia de páginas es lo primero que nota un revisor, y es el error más barato de prevenir.

Colocar un scan y mantener su relación de aspecto

Un scan raramente tiene la misma proporción que la página. Si se estira para llenar la hoja se distorsiona el texto; si se coloca al tamaño completo en píxeles se desborda. La solución es escalar por el menor de los dos ratios, ajuste por anchura o por altura, y centrar el espacio sobrante. Como el origen está en la esquina inferior izquierda, centrar significa dividir el espacio sobrante en partes iguales y añadirlo tanto a X como a Y.

procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP, JPG, PNG, etc. via the VCL graphics units

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // Fit inside the margins without distorting the scan.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Center: leftover space split evenly. Y measured from the page bottom.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

Esta función carga el fichero una vez para leer sus dimensiones en píxeles, calcula una escala uniforme única y pasa el rectángulo de colocación a AddImage. AddImage acepta una ruta de fichero directamente y la dirige a través del mismo procesamiento de imágenes que AddPicture, por lo que cualquier formato que reconozcan las unidades gráficas VCL funciona sin tratamiento especial. Si la imagen ya está decodificada en un TPicture de un panel de previsualización, basta con llamar a AddPicture(Pic, X, Y, DrawW, DrawH) con el mismo rectángulo y omitir la segunda lectura del fichero.

Evitar la decodificación para scans JPEG

Los escáneres casi siempre emiten JPEG. Cargar un JPEG en un TPicture lo decodifica a mapa de bits, y luego PDFium lo vuelve a codificar al guardar: dos ciclos con pérdida que no son necesarios. AddJpegImage incrusta los bytes comprimidos originales directamente en la página desde un stream, lo cual es más rápido y visualmente más limpio para un lote de gran volumen.

var
  Stream: TFileStream;
begin
  // ... after AddPage + PageNumber for the current page ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Embeds the JPEG bytes as-is; no decode/re-encode cycle.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Aún se calculan X, Y, DrawW y DrawH de la misma manera, ya que se necesitan las dimensiones en píxeles para escalar. Se leen del fichero o de un análisis rápido de la cabecera, y luego se pasa el stream bruto a AddJpegImage. Para scans PNG o TIFF la vía AddImage es la correcta; hay que reservar el atajo JPEG para el formato al que realmente se aplica.

Etiquetar cada página

Los scans archivados son más fáciles de auditar cuando cada página lleva el nombre del fichero fuente. AddText dibuja una cadena en una coordenada del espacio de usuario, de modo que un pie de foto puede situarse justo debajo de la imagen. Hay que recordar el eje Y invertido: para colocar una etiqueta debajo del scan, se resta desde el borde inferior de la imagen en lugar de sumar.

// Caption below the scan: Y decreases toward the page bottom.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Un último apunte sobre el guardado. SaveAs es una función que devuelve un Boolean, por lo que en código de producción conviene comprobar su resultado en lugar de asumir que la escritura se realizó correctamente; un disco lleno o una ruta de salida bloqueada falla en silencio de lo contrario. Una vez que el bucle termina y el fichero está escrito, se tiene exactamente lo que el archivo necesitaba: un PDF ordenado por expediente, páginas escaladas para ajustarse, listo para leer en cualquier visor.

Los mismos bloques básicos cubren trabajos relacionados. Cambiar la regla de tamaño por página da un álbum de fotos con una imagen por hoja; mantener el bucle pero leer de una fuente TIFF multipágina da un convertidor de archivo de fax. Para una visión más amplia de la construcción de PDF por programación, véase crear documentos PDF desde cero con PDFium VCL; para renderizar el resultado de vuelta a pantalla más adelante, véase convertir páginas PDF a imágenes JPEG con PDFium VCL.

El PDFium VCL Component de loslab.com reúne las API de creación de documentos, renderización y texto utilizadas a lo largo de esta serie.