Artículo técnico

Extracción de texto de archivos PDF con PDFium Component en Delphi

La extracción de texto de un PDF parece sencilla hasta que se topa con un documento donde la capa de texto no existe, está dañada o se divide en decenas de pequeñas secuencias de caracteres sin un orden lógico. El Componente PDFium le proporciona dos puntos de entrada: la matriz Character[] para el acceso básico basado en índices 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 de un análisis heurístico. Ninguno de los dos es siempre la opción correcta, por lo que es importante comprender qué expone cada uno

Apertura del documento y la trampa del fallo silencioso

El componente TPdf abre un archivo estableciendo FileName y activando 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á corrupto, PDFium captura el error internamente y Active simplemente permanece en False. Esto significa que cada bucle de extracción debe protegerse contra esta situación:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('No se pudo abrir el PDF (dañado o contraseña incorrecta)');
    Exit;
  end;
  // la extracción continúa aquí
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Los archivos protegidos por contraseña requieren que se establezca Pdf.Password := '...' antes de Active := True. No hay segundas oportunidades: una vez que Active falla, debe cerrar y volver a abrir el archivo con la contraseña correcta

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

El enfoque de nivel más bajo recorre cada carácter de cada página. Establezca Pdf.PageNumber para cargar la capa de texto de esa página y, a continuación, itere por las entradas de CharacterCount utilizando la propiedad Character[]. Vale la pena comprobar dos banderas en cada entrada: CharacterGenerated[i] marca los 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] señala que PDFium no pudo asignar el glifo a un punto de código, lo que ocurre 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;   // normalizar 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 los enumera PDFium, 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 producidos por herramientas de oficina estándar esto es suficiente. Para PDFs escaneados que se sometieron a OCR con secuencias de glifos inusuales, o para texto de derecha a izquierda, el orden puede ser incorrecto. Es ahí donde ReadablePageContent resulta más útil

Extracción estructurada con ReadablePageContent

El método ReadablePageContent sube un nivel: devuelve un registro TPdfReadableContent cuya matriz Fragments contiene fragmentos de contenido etiquetados, cada uno con un Kind que identifica párrafos, encabezados, elementos de lista, celdas de tablas, etc. Cuando el PDF contiene un árbol de estructura (compruebe Pdf.IsTagged), la fuente es rosStructure y el orden de lectura es autoritativo. Para archivos no etiquetados, PDFium recurre a rosHeuristic, que agrupa caracteres por sus cuadros delimitadores (bounding boxes) en unidades de lectura plausibles, aunque no puede garantizar la 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 su salida no es legible, la capa de texto del documento probablemente no se escribió con el orden de lectura en mente. Llegados a ese punto, la única solución fiable es volver a exportar desde la aplicación de origen con el etiquetado correcto, o ejecutar un paso de postprocesamiento que ordene el origen de los caracteres por su posición Y y luego X

Qué le proporcionan 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, incremento de Y hacia arriba). CharacterOrigin[i] es el punto de anclaje de la línea base del glifo; CharacterRectangle[i] es el cuadro delimitador completo. Estos son los bloques de construcción para cualquier funcionalidad más allá del texto plano: detectar límites de columnas, agrupar caracteres en líneas comparando coordenadas Y dentro de una tolerancia, o crear un mapa de prueba de impacto (hit-test) para la selección de texto en un visor. Si necesita saber qué carácter se encuentra bajo un clic del ratón, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) realiza esa búsqueda directamente sin que tenga que iterar por los rectángulos

Instalación de la DLL en su lugar

El Componente PDFium delega todo el análisis de PDF en una DLL nativa, ya sea pdfium32.dll or 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 sola vez en una máquina de desarrollo es suficiente; para la distribución, debe copiar la DLL junto al ejecutable de la aplicación. Las variantes compatibles con V8 (pdfium32v8.dll, pdfium64v8.dll) son considerablemente más grandes y solo son necesarias si sus PDFs contienen JavaScript que deba ejecutarse. Para la 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 igual que ocurre con un archivo ausente, debido a que el componente captura el error de carga internamente. Realice siempre pruebas en una máquina limpia antes de la distribución

Uso de FontSize[] junto a Character[] para análisis de diseño

Más allá del texto plano, la API a nivel de carácter expone FontSize[i], que devuelve el tamaño de punto 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. Una secuencia de caracteres donde el tamaño de la fuente supera un umbral es casi con toda seguridad un encabezado en un documento no etiquetado. La misma técnica se aplica para detectar leyendas (texto pequeño debajo de un cuadro delimitador de imagen) o notas al pie (texto pequeño cerca del final de la página). Nada de esto requiere renderización; las tres propiedades leen directamente de la capa de texto que PDFium compila durante Active := True

Un matiz: FontSize[i] refleja el tamaño tras aplicar la matriz de transformación actual (CTM) de la página, por lo que un documento donde el autor escaló toda la página reportará tamaños ajustados proporcionalmente. Si va a comparar tamaños entre páginas con dimensiones diferentes, normalice los valores frente a la altura del MediaBox de cada página antes de tomar decisiones sobre los umbrales

Escritura de la salida en un archivo

La clase TStringList de Delphi gestiona limpiamente la salida UTF-8 desde la versión XE. Establezca WriteBOM := False si necesita un archivo sin BOM (muchos consumidores posteriores fallan ante 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 la memoria sea una limitación, escriba directamente a un TStreamWriter con TEncoding.UTF8 dentro del bucle de páginas, en lugar de acumular primero todo en una lista

Las API de Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent y CharacterIndexAtPos mostradas aquí forman parte del Componente PDFium para Delphi y C++Builder