PDF almacena las imágenes como objetos de primera clase dentro de sus flujos de contenido. Cuando una página referencia una fotografía, un escaneo o un diagrama, los datos de píxeles residen en un diccionario XObject junto con la geometría de la página. PDFium VCL expone esto mediante dos propiedades en TPdf: BitmapCount, que devuelve cuántos mapas de bits incrustados hay en la página actual, y Bitmap[Index], que decodifica uno de ellos en un TBitmap cuya gestión corresponde al código llamante. Ese es todo el modelo de extracción. El bucle son cuatro líneas; lo que requiere criterio es la fontanería que lo rodea.
Apertura del documento
Lo primero que hay que saber sobre TPdf es que Active := True nunca lanza excepciones. Los fallos de carga, las contraseñas incorrectas y los ficheros corruptos se absorben internamente y el componente simplemente permanece inactivo. Es necesario comprobar el indicador manualmente tras la asignación, o se entrará en el bucle de páginas con PageCount devolviendo cero sin entender por qué no se extrae 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 ficheros protegidos con contraseña siguen el mismo patrón: asigna Pdf.Password antes de establecer Active := True. Si la contraseña es incorrecta, Active permanece en False y no se lanza ninguna excepción. En una herramienta por lotes que procesa cientos de ficheros, ese comportamiento silencioso resulta útil: se acumulan los fallos en una lista en lugar de desenrollar la pila de llamadas para cada uno.
Iteración por páginas y obtención de mapas de bits
BitmapCount es por página, así que hay que establecer Pdf.PageNumber antes de consultarlo. Los números de página son base 1; el valor por defecto es 0, lo que significa que no hay ninguna página cargada. La propiedad Bitmap[Index] es base 0 y devuelve un TBitmap cuya gestión corresponde al llamante. Es obligatorio liberarlo. Descuidar esa liberación dentro de un bucle largo sobre un documento grande hace que la memoria crezca rápidamente, porque cada mapa de bits puede ocupar varios megabytes de datos de píxeles sin comprimir.
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 comprobación con Assigned es importante. Un pequeño número de generadores de PDF escriben XObjects de imagen con dimensiones de píxel nulas o datos malformados; en esos casos el componente devuelve nil en lugar de un mapa de bits vacío. Tratar un retorno nil como un error y detener la extracción es el reflejo equivocado: hay que omitirlo, registrar la página e índice si se necesita la traza de auditoría, y continuar. El resto de la página puede aún producir imágenes válidas.
Nótese que el bucle exterior establece Pdf.PageNumber en cada iteración. Esa asignación es la que carga la página en el estado interno del componente y hace que BitmapCount tenga sentido. Si se omite, se lee el recuento de la misma página repetidamente. El patrón parece redundante al escribirlo, pero así está diseñado el API: la página es un cursor, no una colección.
Elección del formato de salida
BMP es sin pérdida y siempre está disponible sin unidades adicionales, lo que lo convierte en un valor por defecto razonable cuando aún no se sabe qué contiene la imagen. Cuando el tamaño del fichero importa, el formato de píxel del TBitmap devuelto indica qué códec es apropiado. Un mapa de bits de 32 bits lleva canal alfa; PNG lo conserva sin pérdida. Una imagen grande de 24 bits con tono continuo es candidata para JPEG. Las imágenes más pequeñas o las dibujadas con una paleta limitada generalmente es mejor dejarlas como BMP que pasarlas por JPEG, que añade artefactos de bloque en ajustes de calidad baja y apenas ahorra espacio en los altos.
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 la determinan Bmp.PixelFormat y las dimensiones. Si PixelFormat = pf32bit se necesita un formato que soporte canal alfa; PNG es la opción obvia, aunque requiere la unidad PNGImage en versiones antiguas de Delphi. Para imágenes de 24 bits con más de unos 300 píxeles de ancho, JPEG con calidad 85 ofrece una reducción de tamaño de tres a uno respecto a BMP sin pérdida perceptible en la mayoría del contenido fotográfico. Por debajo de ese umbral, BMP es comparable en tamaño y evita cualquier decisión sobre calidad.
Qué cuenta y qué no cuenta BitmapCount
PDF distingue entre los XObjects de imagen y los gráficos vectoriales dibujados con operadores de trayectoria. Una página visualmente compleja puede devolver un BitmapCount de cero si todos los elementos son vectoriales. Las páginas escaneadas casi siempre devuelven exactamente uno: el escáner escribe el escaneo completo como un único XObject de imagen a página completa a la resolución configurada. Las páginas que combinan texto compuesto con fotografías incrustadas devuelven una entrada por fotografía. Las líneas decorativas, los fondos sombreados y los bordes de tabla generalmente no aparecen en el recuento de mapas de bits.
El recuento tampoco incluye las imágenes en línea, una construcción PDF poco utilizada donde los datos de imagen se incrustan directamente en el flujo de contenido de la página en lugar de como un XObject con nombre. Estas quedan fuera de lo que expone este API; son suficientemente poco frecuentes en documentos reales como para que la mayoría de las herramientas de extracción simplemente no las gestionen.
Un detalle que conviene tener presente: el BitmapCount que se lee corresponde a la página actual en el momento de la última asignación de PageNumber. Si el código se ramifica o llama a alguna función que cambia PageNumber entre el recuento y la obtención, se pueden leer menos imágenes de las esperadas o indexar más allá del límite. Hay que mantener la lectura del recuento y el bucle Bitmap[] en la misma página sin tocar PageNumber entre medias.
Uso de TPdfView en una aplicación de formularios
Memoria y rendimiento en trabajos por lotes
En un archivo grande, el presupuesto de memoria es lo principal que hay que vigilar. Cada llamada a Bitmap[] asigna un nuevo TBitmap en el montículo, y en una página escaneada a 300 DPI eso supone fácilmente 25 MB de datos de píxeles sin codificar. Si se procesan páginas en un bucle apretado sin liberar entre iteraciones, el conjunto de trabajo crece linealmente con el número de imágenes. La forma correcta es siempre: obtener un mapa de bits, hacer lo necesario, liberarlo, obtener el siguiente. Si es necesario mantener referencias a varios mapas de bits a la vez para un paso de comparación, primero hay que contarlos con BitmapCount y asignar el contenedor en consecuencia, liberando cada uno en cuanto se termina en lugar de diferirlo a la limpieza al final del documento. En un documento con 500 páginas escaneadas esa diferencia puede suponer entre 25 MB y 12 GB de RSS pico.
El componente TPdfView expone las mismas propiedades BitmapCount y Bitmap[], pero la página de la que lee es la que muestra actualmente la vista, no TPdf.PageNumber. Los dos punteros de página son independientes; establecer uno no mueve el otro. En una aplicación de formularios VCL con un visor activo, se puede llamar a Pdf.PageNumber := N para dirigir la extracción a través de TPdf mientras el visor permanece en la última página que el usuario desplazó. Esa separación es intencionada y mantiene limpio el estado de visualización del visor mientras se ejecuta una extracción en segundo plano.
Las propiedades BitmapCount y Bitmap[] mostradas aquí forman parte del componente PDFium VCL para Delphi y C++Builder.