Artículo técnico

Extracción de imágenes de archivos PDF con PDFium Component en Delphi

El formato PDF almacena las imágenes como objetos de primera clase dentro de sus flujos de contenido. Cuando una página hace referencia a una fotografía, un escaneo o un diagrama, los datos de los píxeles residen en un diccionario XObject junto a la geometría de la página. PDFium Component expone esto a través de dos propiedades en TPdf: BitmapCount, que devuelve la cantidad de mapas de bits incrustados presentes en la página actual, y Bitmap[Index], que decodifica uno de ellos y lo convierte en un TBitmap de su propiedad que usted debe liberar. Ese es el modelo de extracción completo. El bucle consta de cuatro líneas; lo que exige criterio es la estructura que lo rodea

Apertura del documento

Lo primero que hay que saber sobre TPdf es que Active := True nunca lanza excepciones. Fallos de carga, contraseñas incorrectas, archivos corruptos: todo ello se traga internamente y el componente simplemente permanece inactivo. Usted debe comprobar la bandera por sí mismo tras la asignación, o de lo contrario avanzará hacia el bucle de páginas con PageCount devolviendo cero y se preguntará por qué no se extrajo nada

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // proceed to extraction
  finally
    Pdf.Free;
  end;
end;

Los archivos protegidos por contraseña siguen el mismo patrón: asigne Pdf.Password antes de establecer Active := True. Si la contraseña es incorrecta, Active se mantiene en False y no obtiene ninguna excepción que atrapar. En una herramienta por lotes que procesa cientos de archivos, ese comportamiento silencioso resulta verdaderamente útil: le permite acumular los fallos en una lista en lugar de desenrollar la pila de llamadas (call stack) para cada uno de ellos

Iteración de páginas y extracción de mapas de bits

BitmapCount funciona por página, de manera que usted configura Pdf.PageNumber antes de leerlo. Los números de página empiezan en 1; el valor predeterminado es 0, lo que significa que no hay ninguna página cargada. La propiedad Bitmap[Index] se basa en 0 y devuelve un TBitmap propiedad del llamador. Usted debe liberarlo. Si omite la liberación dentro de un bucle largo sobre un documento grande, la memoria aumentará rápidamente, dado que cada mapa de bits puede representar varios megabytes de datos de píxeles sin procesar (raw pixel data) antes de cualquier compresión

procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

La guarda con Assigned es importante. Una pequeña cantidad de generadores de PDF escriben XObjects de imágenes con dimensiones de cero píxeles o datos malformados de alguna otra manera; en esos casos, el componente devuelve nil en vez de un mapa de bits vacío. Tratar un retorno nil como un error y detener la extracción es el reflejo equivocado: sálteselo, registre la página y el índice si necesita una pista de auditoría, y continúe. El resto de la página todavía puede proporcionar imágenes válidas

Note que el bucle externo establece Pdf.PageNumber en cada iteración. Esa asignación es la que carga la página en el estado interno del componente y dota de sentido a BitmapCount. Si la omite, leerá repetidamente el recuento de la misma página. El patrón parece redundante al escribirlo, pero así está diseñada la API: la página es un cursor, no una colección

Elección de un formato de salida

BMP carece de pérdidas y siempre está disponible sin unidades adicionales, lo cual lo convierte en una opción predeterminada sólida cuando aún no se sabe qué contiene la imagen. Cuando el tamaño del archivo importa, el formato de píxeles del TBitmap devuelto le indica qué códec resulta apropiado. Un mapa de bits de 32 bits conlleva un canal alfa; PNG lo preserva sin pérdidas. Una imagen de 24 bits grande y de tono continuo es candidata para JPEG. En general, es preferible dejar las imágenes más pequeñas o las trazadas con una paleta limitada como BMP en lugar de pasarlas a JPEG, ya que este último añade artefactos de bloque (blocking artifacts) en configuraciones de baja calidad y ahorra muy poco en las de alta calidad

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: lossless, no extra units
  end;
end;

En la práctica, la selección de formato se rige por Bmp.PixelFormat y las dimensiones. Si PixelFormat = pf32bit necesitará un formato que soporte alfa; PNG es la alternativa evidente, si bien exige la unidad PNGImage en versiones más antiguas de Delphi. Para imágenes de 24 bits con un ancho superior a unos 300 píxeles, un archivo JPEG con calidad 85 logra una reducción de tamaño de tres a uno respecto al formato BMP, sin que se perciba pérdida alguna en la mayoría del contenido fotográfico. Por debajo de este límite, BMP resulta equivalente en tamaño y evita por completo tener que adoptar decisiones relativas a la calidad

Lo que BitmapCount cuenta y lo que no cuenta

El formato PDF distingue entre XObjects de imágenes y gráficos vectoriales dibujados con operadores de trazado. Una página que parece visualmente compleja puede devolver un BitmapCount nulo si cada elemento es vectorial. Las páginas escaneadas casi siempre devuelven exactamente uno: el escáner escribe todo el escaneo como un único XObject de imagen a página completa, independientemente de la resolución que se haya fijado. Las páginas que mezclan texto tipográfico con fotografías incrustadas devuelven una entrada por fotografía. Las líneas ornamentales, los fondos sombreados y los bordes de tablas generalmente no figuran en absoluto en el recuento de mapas de bits

El recuento tampoco incluye las imágenes en línea (inline images), una construcción PDF poco empleada en la cual los datos de imagen se incrustan directamente en el flujo de contenido de la página en lugar de figurar como un XObject con nombre. Éstas caen fuera de lo que ofrece esta API; son lo suficientemente inusuales en documentos reales que la mayoría de herramientas de extracción sencillamente no las manejan

Un detalle que vale la pena tener en cuenta: el BitmapCount que lee es para la página actual al momento de la última asignación de PageNumber. Si su código se ramifica o llama a cualquier función que altere PageNumber entre el recuento y la extracción, corre el riesgo de leer menos imágenes de aquellas para las que asignó espacio, u obtener un índice que sobrepase el límite. Mantenga la lectura del recuento y el bucle de Bitmap[] en la misma página sin modificar PageNumber de por medio

Uso de TPdfView en una aplicación de formulario

El componente TPdfView expone las mismas propiedades BitmapCount y Bitmap[], pero la página de la que lee es la página actualmente mostrada en la vista, no TPdf.PageNumber. Ambos punteros de página son independientes; establecer uno no desplaza al otro. En una aplicación de formulario VCL que cuente con un visor en tiempo real, es posible llamar a Pdf.PageNumber := N para gestionar la extracción a través de TPdf, al tiempo que el visor permanece en la última posición a la que se desplazó el usuario. Esa separación es intencional y mantiene inalterado el estado de visualización del visor mientras se lleva a cabo una extracción en segundo plano

Memoria y rendimiento en trabajos por lotes

En un gran archivo documental, el presupuesto de memoria es lo más relevante a vigilar. Cada llamada a Bitmap[] asigna un nuevo TBitmap en el montón (heap) de memoria, y en una página escaneada a 300 DPI esto representa fácilmente 25 MB de datos de píxeles sin procesar antes de realizar cualquier codificación. Si usted procesa páginas en un bucle cerrado sin realizar liberaciones entre iteraciones, el conjunto de trabajo (working set) crece linealmente de acuerdo a la cantidad de imágenes. El procedimiento correcto es siempre: extraiga un mapa de bits, haga lo que necesite, libérelo, extraiga el siguiente. Si precisa mantener referencias a diversos mapas de bits en paralelo para una fase de comparación, cuéntelos antes con BitmapCount y asigne su contenedor acordemente; a continuación, libere cada uno de ellos apenas termine de usarlos, en lugar de diferirlo hasta la limpieza al final del documento. Tratándose de un documento de 500 páginas escaneadas, dicha precaución puede marcar la diferencia entre un pico RSS de 25 MB y uno de 12 GB

Las propiedades BitmapCount y Bitmap[] expuestas aquí forman parte del Componente PDFium Component para Delphi y C++Builder