Artículo técnico

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

Un equipo de procesamiento de reclamos tenía treinta años de archivos en papel pasando por un escáner de hojas. El escáner arrojaba un JPEG por página en una carpeta, con nombres como 0001.jpg, 0002.jpg, y así sucesivamente. Lo que el archivo realmente necesitaba era un PDF por cada expediente (case file), con las páginas en orden, para que un revisor pudiera abrir un solo documento en lugar de hacer clic a través de cien miniaturas de imágenes. Ese último paso, convertir una pila numerada de escaneos en un solo PDF ordenado, es el trabajo que realizaremos aquí

PDFium Component lo maneja directamente. Más allá de renderizar y extraer texto, el componente puede construir un PDF desde cero: crear un documento vacío, agregar una página en blanco del tamaño que desee, colocar una imagen en esa página en coordenadas del espacio de usuario, y luego guardar. Toda la secuencia (pipeline) reside en el componente TPdf, por lo que un convertidor por lotes es un bucle sobre los nombres de archivo más un puñado de llamadas

La forma de la conversión

Deben suceder tres cosas por cada escaneo. Usted decide el tamaño de la página, coloca la imagen dentro de la página dejando un margen, y avanza a la siguiente página. PDFium Component le brinda un método para cada una: AddPage crea una página en blanco con un tamaño dado, AddImage (o AddPicture si ya tiene un TPicture) dibuja el mapa de bits (bitmap) en la página actual, y PageNumber le dice al componente a qué página se dirigen las siguientes llamadas de dibujo

El único detalle con el que la gente tropieza es el sistema de coordenadas. El espacio de usuario del PDF coloca el origen en la esquina inferior izquierda de la página, con Y aumentando hacia arriba, lo opuesto a las coordenadas de pantalla que los desarrolladores de Delphi usan por reflejo. El X, Y que 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 el tamaño en píxeles del archivo de origen. Hágalo al revés y sus escaneos caerán fuera de la página o al revés en relación con donde esperaba que estuvieran

Crear el documento y una página por escaneo

Comience con un documento vacío. CreateDocument asigna un PDF nuevo y deja el componente activo, por lo que no hay un paso de apertura separado. A partir de ahí, recorre la lista de archivos escaneados y, por cada uno, agrega una página, la hace actual y coloca la imagen. Las dimensiones de la página aquí son A4 en puntos (595 × 842 en vertical), el tamaño de hoja estándar para la correspondencia archivada

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // Ancho A4 en puntos
  PageH = 842.0;   // Alto A4 en puntos
  Margin = 36.0;   // Borde de media pulgada alrededor de cada escaneo
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // nuevo, vacío, ya activo
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // índice de página basado en 1
      Pdf.PageNumber := I + 1;                // hace actual a la nueva página
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Cada iteración crea una página e inmediatamente establece PageNumber en ella. Esa segunda línea es importante: AddPage inserta la página, pero los métodos de dibujo actúan sobre cualquier página que sea actual, por lo que establecer PageNumber es lo que apunta AddImage a la página que acaba de hacer. Omítala y sus imágenes se apilarán en la página que haya estado cargada antes

Una suposición se esconde en ese bucle: el orden de Files. Un escáner nombra las páginas desde 0001.jpg hasta 0100.jpg, pero una enumeración de directorios no siempre las devuelve ordenadas, y en el momento en que se encuentra con page9.jpg junto a page10.jpg, una simple ordenación de cadenas (string sort) pone la página 10 antes que la página 9. Ordene la lista explícitamente antes del bucle, y prefiera nombres rellenados con ceros al momento de escanear 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 escaneo y mantener su relación de aspecto

Un escaneo rara vez tiene la misma forma que la página. Si lo estira para llenar la hoja, distorsiona el texto; si lo coloca a tamaño completo de píxeles, se desborda. La solución es escalar por la menor de las dos proporciones, ajuste al ancho o ajuste al alto, y centrar lo que sobra. Como el origen se encuentra en la parte inferior izquierda, centrar significa dividir el espacio sobrante en partes iguales y sumarlo 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. a través de unidades gráficas de la VCL
    
    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // Ajustar dentro de los márgenes sin distorsionar el escaneo.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Centrar: espacio sobrante dividido en partes iguales. Y se mide desde el fondo de la página.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

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

Esto carga el archivo una vez para leer sus dimensiones en píxeles, calcula una única escala uniforme y pasa el rectángulo de colocación a AddImage. AddImage acepta una ruta de archivo directamente y la enruta a través de la misma secuencia de imágenes que AddPicture, por lo que cualquier formato que las unidades de gráficos de la VCL reconozcan funciona sin tener que tratar casos especiales. Si ya tiene la imagen decodificada en un TPicture desde un panel de vista previa, llame a AddPicture(Pic, X, Y, DrawW, DrawH) con el mismo rectángulo y omita la segunda lectura de archivo

Omitir la decodificación para escaneos en JPEG

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

var
  Stream: TFileStream;
begin
  // ... después de AddPage + PageNumber para la página actual ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Incrusta los bytes del JPEG tal cual; sin ciclo de decodificación/recodificación.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Aún necesita calcular X, Y, DrawW y DrawH de la misma manera, ya que necesita las dimensiones en píxeles para escalar. Léalos del archivo o a través de un análisis rápido (parse) del encabezado, luego pase el flujo sin procesar (raw stream) a AddJpegImage. Para escaneos en PNG o TIFF, la ruta de AddImage es la correcta; reserve el atajo JPEG para el formato al que realmente se aplica

Etiquetar cada página

Los escaneos archivados son más fáciles de auditar cuando cada página lleva el nombre de su archivo fuente. AddText dibuja una cadena (string) en una coordenada del espacio de usuario, para que la leyenda se asiente justo debajo de la imagen. Recuerde el eje Y invertido: para poner una etiqueta debajo del escaneo, debe restar desde el borde inferior de la imagen en lugar de sumar

// Leyenda debajo del escaneo: Y disminuye hacia la parte inferior de la página.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Un último punto sobre cómo guardar. SaveAs es una función que devuelve un Boolean, así que en código de producción verifique su resultado en lugar de asumir que la escritura tuvo éxito; un disco lleno o una ruta de salida bloqueada fallan silenciosamente de otra manera. Una vez que el bucle termina y el archivo está escrito, usted 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 componentes básicos cubren trabajos relacionados. Cambie la regla de tamaño por página y obtendrá un álbum de fotos con una imagen por hoja; mantenga el bucle pero lea desde un origen TIFF de varias páginas y tendrá un convertidor de archivo de fax. Si desea ver el panorama más amplio sobre la creación de archivos PDF mediante programación, consulte crear documentos PDF desde cero con PDFium Component; para renderizar el resultado de nuevo a la pantalla más adelante, consulte convertir páginas PDF en imágenes JPEG con PDFium Component

PDFium Component de loslab.com agrupa las API de creación de documentos, renderizado y texto utilizadas a lo largo de esta serie