La extracción de texto de PDF parece sencilla hasta que se tropieza con un documento en el que la capa de texto está ausente, corrupta o dividida en decenas de pequeñas secuencias de caracteres sin un orden significativo. PDFium VCL ofrece dos puntos de entrada: el array Character[] para acceso en bruto basado en índice a cada glifo de una página, y ReadablePageContent para una vista estructurada que reconstruye párrafos y encabezados a partir del árbol de etiquetas del PDF o mediante análisis heurístico. Ninguno es siempre la elección correcta, por lo que entender qué expone cada uno es fundamental.
Apertura del documento y el problema del fallo silencioso
TPdf abre un fichero estableciendo FileName y activando Active := True. El detalle crítico: Active := True nunca lanza una excepción. Si el fichero no existe, está protegido con contraseña o está corrupto, PDFium captura el error internamente y Active simplemente permanece en False. Eso significa que todo bucle de extracción debe protegerse contra esto:
Pdf := TPdf.Create(nil);
try
Pdf.FileName := ‘report.pdf’;
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage(‘Could not open PDF (damaged or wrong password)’);
Exit;
end;
// extraction follows here
finally
Pdf.Active := False;
Pdf.Free;
end;
Los ficheros protegidos con contraseña requieren establecer Pdf.Password := ‘...’ antes de Active := True. No hay segunda oportunidad: una vez que Active falla, hay que cerrar y reabrir con la contraseña correcta.
Extracción página a página con Character[]
El enfoque de más bajo nivel recorre cada carácter de cada página. Se establece Pdf.PageNumber para cargar la capa de texto de esa página y luego se iteran las entradas de CharacterCount usando la propiedad Character[]. Dos indicadores por entrada merecen comprobarse: CharacterGenerated[i] marca los glifos sintéticos insertados por el renderizador (guiones suaves en los saltos de línea, por ejemplo) que no tienen un valor Unicode real, y CharacterMapError[i] señala que PDFium no pudo mapear el glifo a un punto de código, algo que ocurre con codificaciones de fuente que carecen de tabla ToUnicode.
procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
Page, I: Integer;
Line: string;
Ch: WideChar;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Line := ‘’;
for I := 0 to Pdf.CharacterCount - 1 do
begin
if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
Continue;
Ch := Pdf.Character[I];
if Ch = #13 then
Ch := #10; // normalize CR to LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
El resultado es una cadena plana de puntos de código Unicode en el orden en que PDFium los enumera, que es el orden en que aparecen en el flujo de contenido, no necesariamente de izquierda a derecha. Para la mayoría de los documentos con escritura latina producidos por herramientas ofimáticas estándar esto es correcto. Para PDFs escaneados con OCR que tienen secuencias de glifos inusuales, o para texto de derecha a izquierda, el orden puede ser incorrecto. Ahí es cuando ReadablePageContent resulta más útil.
Extracción estructurada con ReadablePageContent
ReadablePageContent sube un nivel: devuelve un registro TPdfReadableContent cuyo array Fragments contiene fragmentos de contenido etiquetado, cada uno con un Kind que identifica párrafos, encabezados, elementos de lista, celdas de tabla, etc. Cuando el PDF lleva un árbol de estructura (se comprueba con Pdf.IsTagged), la fuente es rosStructure y el orden de lectura es autoritativo. Para ficheros sin etiquetar, PDFium recurre a rosHeuristic, que agrupa caracteres por sus cajas delimitadoras en unidades de lectura plausibles pero sin garantía de exactitud.
procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
Page: Integer;
Content: TPdfReadableContent;
Fragment: TPdfContentFragment;
begin
for Page := 1 to Pdf.PageCount do
begin
Content := Pdf.ReadablePageContent(Page);
for Fragment in Content.Fragments do
begin
case Fragment.Kind of
cfHeading : Output.Add(‘# ‘ + Fragment.Text);
cfParagraph : Output.Add(Fragment.Text);
cfListItem : Output.Add(‘- ‘ + Fragment.Text);
else
Output.Add(Fragment.Text);
end;
end;
end;
end;
Si Content.Source = rosHeuristic y la salida parece desordenada, la capa de texto del documento probablemente no fue escrita teniendo en cuenta el orden de lectura. En ese punto la única solución fiable es volver a exportar desde la aplicación de origen con etiquetado adecuado, o ejecutar un paso de postprocesado que ordene los orígenes de caracteres primero por Y y luego por X.
Qué aportan CharacterOrigin y CharacterRectangle
Ambas propiedades devuelven la posición de un carácter en el espacio de página (puntos, origen en la esquina inferior izquierda, Y creciendo hacia arriba). CharacterOrigin[i] es el punto de anclaje de la línea base del glifo; CharacterRectangle[i] es la caja delimitadora completa. Estos son los bloques de construcción para todo lo que va más allá del texto plano: detectar límites de columnas, agrupar caracteres en líneas comparando coordenadas Y dentro de una tolerancia, o construir un mapa de prueba de colisión para la selección de texto en un visor. Si se necesita encontrar qué carácter está bajo un clic del ratón, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) hace esa búsqueda directamente sin necesidad de iterar los rectángulos.
Instalación de la DLL
PDFium VCL delega todo el análisis de PDF a una DLL nativa, pdfium32.dll o pdfium64.dll según la plataforma de destino. El componente incluye un script CopyDlls.bat que copia el fichero correcto al directorio del sistema de Windows. Ejecutarlo una vez como Administrador en la máquina de desarrollo es suficiente; para el despliegue se copia la DLL junto al ejecutable de la aplicación. Las variantes con V8 habilitado (pdfium32v8.dll, pdfium64v8.dll) son considerablemente más grandes y solo son necesarias si los PDFs contienen JavaScript que deba ejecutarse. Para la extracción pura de texto, la versión estándar es la elección correcta.
Si la DLL no está presente en tiempo de ejecución, Active := True fallará silenciosamente igual que con un fichero ausente, porque el componente captura el error de carga internamente. Conviene probar siempre en una máquina limpia antes de distribuir.
Uso de FontSize[] junto a Character[] para análisis de maquetación
Más allá del texto plano, el API de nivel de carácter expone FontSize[i], que devuelve el tamaño en puntos renderizado de cada glifo. Combinado con CharacterOrigin[i] y CharacterRectangle[i], permite distinguir el cuerpo del texto de los encabezados sin depender del árbol de estructura. Una secuencia de caracteres donde el tamaño de fuente supera un umbral es casi con certeza un encabezado en un documento sin etiquetar. La misma técnica se aplica a la detección de pies de imagen (texto pequeño bajo la caja delimitadora de una imagen) o notas al pie (texto pequeño cerca de la parte inferior de la página). Nada de esto requiere renderizado; las tres propiedades leen directamente desde la capa de texto que PDFium construye durante Active := True.
Un matiz: FontSize[i] refleja el tamaño después de aplicar la CTM (matriz de transformación actual) de la página, por lo que un documento donde el autor escaló toda la página informará tamaños ajustados proporcionalmente. Si se comparan tamaños entre páginas con diferentes dimensiones, hay que normalizar respecto a la altura del MediaBox de cada página antes de tomar decisiones de umbral.
Escritura de la salida en un fichero
La clase TStringList de Delphi gestiona la salida UTF-8 de forma limpia desde XE. Se establece WriteBOM := False si se necesita un fichero sin BOM (muchos consumidores posteriores así lo esperan):
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
ExtractAllText(Pdf, Lines);
Lines.WriteBOM := False;
Lines.SaveToFile(‘output.txt’, TEncoding.UTF8);
finally
Lines.Free;
end;
end;
Para documentos muy grandes donde la memoria es una preocupación, conviene escribir directamente en un TStreamWriter con TEncoding.UTF8 dentro del bucle de páginas en lugar de acumular todo en una lista primero.
Los APIs Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent y CharacterIndexAtPos mostrados aquí forman parte del componente PDFium VCL para Delphi y C++Builder.