Artículo técnico

Extracting Text from PDF Files with PDFium Component in Delphi

La extracción de texto de un PDF parece sencilla hasta que se topa con un documento donde la capa de texto está ausente, dañada o dividida en docenas de pequeños fragmentos de caracteres sin un orden coherente. El PDFium Component le ofrece dos puntos de entrada: el arreglo Character[] para un acceso directo basado en índices a cada glifo de una página, y ReadablePageContent para obtener una vista estructurada que reconstruye párrafos y encabezados a partir del árbol de etiquetas del PDF o de un análisis heurístico. Ninguno de los dos es siempre la elección correcta, por lo que es importante entender lo que expone cada uno

Abrir el documento y la trampa de la falla silenciosa

TPdf abre un archivo configurando FileName y asignando Active := True. El detalle crítico: Active := True nunca genera una excepción. Si el archivo no existe, está protegido por contraseña o está dañado, PDFium captura el error internamente y Active simplemente permanece en False. Esto significa que cada bucle de extracción debe protegerse contra este comportamiento:

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 archivos protegidos por contraseña requieren que se asigne Pdf.Password := '...' antes de Active := True. No hay segundas oportunidades: una vez que Active falla, debe cerrar el archivo y volver a abrirlo con la contraseña correcta

Extracción página por página con Character[]

El enfoque de nivel más bajo consiste en recorrer cada carácter de cada página. Establezca Pdf.PageNumber para cargar la capa de texto de esa página y luego recorra las entradas de CharacterCount utilizando la propiedad Character[]. Vale la pena verificar dos banderas (flags) en cada entrada: CharacterGenerated[i] marca glifos sintéticos insertados por el renderizador (por ejemplo, guiones opcionales en saltos de línea) que no tienen un valor Unicode real, y CharacterMapError[i] indica que PDFium no pudo asignar el glifo a un punto de código, lo cual sucede con codificaciones de fuentes que carecen de una 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 lineal 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 el orden de lectura de izquierda a derecha. Para la mayoría de los documentos con caracteres latinos generados por herramientas de oficina estándar esto es suficiente. Para PDFs escaneados procesados con OCR que tengan secuencias de glifos inusuales, o para texto de derecha a izquierda, el orden puede ser incorrecto. Es entonces cuando ReadablePageContent resulta más útil

Extracción estructurada con ReadablePageContent

ReadablePageContent sube un nivel: devuelve un registro TPdfReadableContent cuyo arreglo Fragments contiene fragmentos de contenido etiquetados, cada uno con un Kind que identifica párrafos, encabezados, elementos de lista, celdas de tabla, etc. Cuando el PDF contiene un árbol de estructura (verifique Pdf.IsTagged), el origen es rosStructure y el orden de lectura es el definitivo. Para archivos sin etiquetar, PDFium recurre a rosHeuristic, que agrupa caracteres según sus cajas de delimitación en unidades de lectura lógicas pero sin garantizar precisión

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 el resultado aparece desordenado, es probable que la capa de texto del documento no se haya creado pensando en el orden de lectura. En ese punto, la única solución confiable es volver a exportar desde la aplicación de origen con el etiquetado adecuado, o ejecutar un paso de procesamiento posterior que ordene las coordenadas de inicio de los caracteres por Y y luego por X

Qué le ofrecen CharacterOrigin y CharacterRectangle

Ambas propiedades devuelven la posición de un carácter en el espacio de la página (puntos, origen en la esquina inferior izquierda, Y aumentando hacia arriba). CharacterOrigin[i] es el punto de anclaje de la línea base del glifo; CharacterRectangle[i] es la caja de delimitación completa. Estos son los bloques de construcción para cualquier tarea que vaya más allá del texto plano: detectar límites de columnas, agrupar caracteres en líneas comparando coordenadas Y dentro de un rango de tolerancia o construir un mapa de detección de clics para la selección de texto en un visor. Si necesita saber qué carácter se encuentra debajo de un clic del mouse, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) realiza esa búsqueda directamente sin necesidad de recorrer los rectángulos manualmente

Colocar la DLL en su lugar

El PDFium Component delega todo el análisis de PDF a una DLL nativa, ya sea pdfium32.dll o pdfium64.dll según la plataforma de destino. El componente incluye un script CopyDlls.bat que copia el archivo correcto en el directorio del sistema de Windows. Ejecutarlo como Administrador una vez en la computadora de desarrollo es suficiente; para el despliegue, simplemente copie la DLL junto al ejecutable de la aplicación. Las variantes habilitadas para V8 (pdfium32v8.dll, pdfium64v8.dll) son considerablemente más grandes y solo son necesarias si sus PDFs contienen código JavaScript que deba ejecutarse. Para una extracción de texto pura, la compilación estándar es la elección correcta

Si la DLL no está presente en tiempo de ejecución, Active := True fallará silenciosamente tal como lo hace con un archivo faltante, porque el componente captura internamente el error de carga. Realice siempre pruebas en una computadora limpia antes de distribuir su aplicación

Usar FontSize[] junto con Character[] para análisis de diseño

Además del texto sin formato, la API a nivel de caracteres expone FontSize[i], que devuelve el tamaño en puntos renderizado de cada glifo. Combinado con CharacterOrigin[i] y CharacterRectangle[i], esto le permite distinguir el texto del cuerpo de los encabezados sin depender del árbol de estructura. Un fragmento de caracteres donde el tamaño de la fuente supere un límite determinado es casi seguro un encabezado en un documento sin etiquetar. La misma técnica se aplica para detectar leyendas (texto de tamaño pequeño debajo de la caja de delimitación de una imagen) o notas al pie (texto pequeño cerca del final 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 matriz de transformación actual (CTM) de la página, por lo que un documento donde el autor haya escalado toda la página reportará tamaños ajustados proporcionalmente. Si está comparando tamaños entre páginas con dimensiones diferentes, normalice los valores frente a la altura del MediaBox de cada página antes de definir los límites

Escribir la salida en un archivo

La clase TStringList de Delphi maneja la salida UTF-8 correctamente desde la versión XE. Establezca WriteBOM := False si necesita un archivo sin BOM (muchos procesos posteriores fallan con un BOM inicial):

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 el uso de memoria sea crítico, escriba directamente en un TStreamWriter con TEncoding.UTF8 dentro del bucle de páginas, en lugar de acumular todo en una lista primero

Las propiedades de archivos adjuntos mostradas aquí forman parte del PDFium Component para Delphi y C++Builder