Artículo técnico

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

PDF almacena 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 viven en un diccionario XObject junto a la geometría de la página. PDFium Component expone eso a través de 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 que usted posee y debe liberar. Ese es todo el modelo de extracción. El bucle es de cuatro líneas; lo que requiere criterio es la plomería circundante

Apertura del documento

Lo primero que hay que saber sobre TPdf es que Active := True nunca genera una excepción. Fallas de carga, contraseñas incorrectas, archivos corruptos: todo se traga internamente y el componente simplemente permanece inactivo. Usted mismo debe verificar el indicador (flag) después de la asignación, o ingresará al bucle de la página 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');
    // proceder a la extracción
  finally
    Pdf.Free;
  end;
end;

Los archivos protegidos con contraseña siguen el mismo patrón: asigne Pdf.Password antes de establecer Active := True. Si la contraseña es incorrecta, Active permanece False y usted no obtiene ninguna excepción para atrapar. En una herramienta por lotes (batch) que procesa cientos de archivos, ese comportamiento silencioso es realmente útil: usted acumula las fallas en una lista en lugar de desenrollar la pila de llamadas (call stack) para cada una de ellas

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

BitmapCount es por página, por lo que establece Pdf.PageNumber antes de leerlo. Los números de página están basados en 1; el valor predeterminado es 0, lo que significa que no se carga ninguna página. La propiedad Bitmap[Index] está basada en 0 y devuelve un TBitmap propiedad del llamador. Usted debe liberarlo. Si descuida la liberación (free) dentro de un bucle largo sobre un documento grande, la memoria aumenta rápidamente, porque cada mapa de bits puede ocupar varios megabytes de datos de píxeles sin procesar 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 Assigned importa. Un pequeño número de generadores de PDF escribe image XObjects con dimensiones de píxeles cero o datos mal formados; 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: omítalo, registre la página y el índice si necesita la pista de auditoría, y continúe. El resto de la página aún puede producir imágenes válidas

Note que el bucle exterior establece Pdf.PageNumber en cada iteración. Esa asignación es lo que carga la página en el estado interno del componente y hace que BitmapCount sea significativo. Si lo omite, leerá el recuento de la misma página repetidamente. El patrón se siente redundante cuando lo escribe, pero así es como está diseñada la API: la página es un cursor, no una colección

Elección de un formato de salida

BMP es sin pérdidas y siempre está disponible sin unidades (units) adicionales, lo que lo convierte en un formato predeterminado sólido cuando aún no 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 es el adecuado. Un mapa de bits de 32 bits conlleva un canal alfa; PNG preserva eso sin pérdidas. Una imagen grande de 24 bits con tono continuo es un candidato para JPEG. Las imágenes más pequeñas o las dibujadas con una paleta limitada generalmente es mejor dejarlas como BMP en lugar de pasarlas por JPEG, lo que agrega artefactos de bloque en configuraciones de baja calidad y ahorra poco en las altas

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: sin pérdidas, sin unidades adicionales
  end;
end;

En la práctica, la selección de formato está impulsada por Bmp.PixelFormat y las dimensiones. Si PixelFormat = pf32bit usted necesita un formato que admita alfa; PNG es la opción obvia, aunque requiere la unidad PNGImage en versiones anteriores de Delphi. Para imágenes de 24 bits con un ancho superior a aproximadamente 300 píxeles, JPEG a una calidad de 85 proporciona una reducción de tamaño de tres a uno sobre BMP sin pérdidas perceptibles en la mayoría del contenido fotográfico. Por debajo de ese umbral, BMP tiene un tamaño comparable y evita la decisión de calidad por completo

Lo que BitmapCount cuenta y no cuenta

PDF distingue entre image XObjects y gráficos vectoriales dibujados con operadores de ruta. Una página que se ve visualmente compleja puede devolver un BitmapCount de cero si cada elemento es vectorial. Las páginas escaneadas casi siempre devuelven exactamente uno: el escáner escribe el escaneo completo como un único image XObject de página completa en la resolución a la que se configuró el escáner. Las páginas que mezclan texto compuesto con fotografías incrustadas devuelven una entrada por fotografía. Las líneas decorativas, los fondos sombreados y los bordes de las tablas por lo general no aparecen en absoluto en el recuento de mapas de bits

El recuento tampoco incluye imágenes en línea (inline images), un constructo PDF raramente usado donde los datos de imagen se incrustan directamente en el flujo de contenido de la página en lugar de como un XObject nombrado. Esos caen fuera de lo que esta API saca a la superficie; son lo suficientemente poco comunes en documentos reales que la mayoría de las herramientas de extracción simplemente no las manejan

Un detalle que vale la pena tener en cuenta: el BitmapCount que usted lee es para la página actual a partir de la última asignación de PageNumber. Si su código se ramifica o llama a cualquier función que cambie PageNumber entre el recuento y la extracción, es posible que lea menos imágenes de las que asignó espacio, o que se indexe más allá del final. Mantenga 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 formulario

El componente TPdfView expone las mismas propiedades BitmapCount y Bitmap[], pero la página desde la que lee es la página mostrada actualmente en la vista, no TPdf.PageNumber. Los dos punteros de página son independientes; establecer uno no mueve el otro. En una aplicación de formulario VCL con un visor en vivo, puede llamar a Pdf.PageNumber := N para conducir la extracción a través de TPdf mientras el visor se mantiene en lo último a lo que se desplazó el usuario. Esa separación es intencional y mantiene el estado de visualización del visor limpio mientras se ejecuta una extracción en segundo plano

Memoria y rendimiento en trabajos por lotes

A lo largo de un archivo grande, el presupuesto de memoria es lo principal a observar. Cada llamada a Bitmap[] asigna un nuevo TBitmap en el montón (heap), y en una página escaneada a 300 DPI, eso es fácilmente 25 MB de datos de píxeles sin procesar antes de cualquier codificación. Si procesa páginas en un bucle cerrado sin liberarlas entre iteraciones, el conjunto de trabajo crece linealmente con el número de imágenes. La forma correcta es siempre: recupere un mapa de bits, haga lo que necesite, libérelo, recupere el siguiente. Si necesita mantener referencias a varios mapas de bits a la vez para un paso de comparación, cuéntelos primero con BitmapCount y asigne su contenedor en consecuencia, luego libere cada uno tan pronto como termine con él, en lugar de aplazarlo a la limpieza del final del documento. En un documento con 500 páginas escaneadas, esa distinción puede significar la diferencia entre picos de RSS de 25 MB y 12 GB

Las propiedades BitmapCount y Bitmap[] que se muestran aquí forman parte del PDFium Component para Delphi y C++Builder