Artículo técnico

Extracción de texto PDF en orden estructural con HotPDF

Todo extractor de texto geométrico está adivinando. Lee los glifos que dibuja una página, los ordena por línea base y posición horizontal, y espera que la disposición visual coincida con el orden en que leería una persona. En un informe de una sola columna esa suposición es correcta. En un artículo de revista a dos columnas, un formulario con barra lateral o una tabla cuyas celdas se emitieron columna a columna, está mal de formas difíciles de notar y caras de descubrir aguas abajo. HotPDF responde a esto con ExtractLoadedPageStructureText, que ignora la geometría por completo: recorre el árbol de estructura del documento en orden de autoría tal como lo define ISO 32000-1 §14.8.4 y luego reensambla los glifos de la página por su identificador de contenido marcado. Para un PDF etiquetado eso no es una heurística: es el orden que la aplicación productora declaró

La función devuelve False cuando la página no tiene un árbol de estructura utilizable, que es la señal para caer al extractor geométrico en lugar de fallar. Ese diseño de dos rutas importa más que el algoritmo: la admisión real de documentos ve formularios gubernamentales etiquetados y salida de escáner en la misma carpeta, y un pipeline que solo maneja uno de ellos no es un pipeline

Por qué el extractor geométrico se equivoca con el orden de lectura?

Porque un content stream de PDF no lleva orden de lectura alguno. Es una secuencia de operadores de dibujo, y un productor es libre de emitirlos en la secuencia que convenga a su propio motor de maquetación. Los procesadores de texto suelen emitir en orden de flujo y el ordenamiento geométrico parece correcto. Las herramientas de maquetación, los diseñadores de formularios y los generadores de informes con frecuencia no: un pie de página puede emitirse antes del cuerpo, una tabla puede rellenarse por columnas y una página a dos columnas puede intercalar líneas de ambas columnas porque el compositor las resolvió juntas

Comparación de página PDF a dos columnas mostrando la extracción geométrica ordenada por línea base entrelazando columnas frente a la extracción MCID en orden estructural de HotPDF
Ordenar glifos por línea base entrelaza dos columnas en un sinsentido, mientras que el árbol de estructura repite el orden que el productor declaró

El modo de fallo es silencioso. Un extractor geométrico nunca informa de un error, solo devuelve prosa cuyas frases están empalmadas desde dos columnas. Cualquier cosa que consuma ese texto, un índice de búsqueda, un mapeador de campos de factura electrónica, un pipeline de recuperación que alimenta a un modelo de lenguaje, hereda el daño sin aviso. HotPDF también incluye los extractores geométricos para documentos cargados, y siguen siendo la herramienta correcta para ficheros sin etiquetas; el objetivo de la ruta de orden estructural es dejar de adivinar cuando el documento ya lleva la respuesta

Qué guarda realmente el árbol de estructura

Un PDF etiquetado guarda una segunda descripción paralela de la página. El catálogo apunta a un /StructTreeRoot cuyos hijos /K forman un árbol de elementos de estructura: /Document, /Sect, /P, /Table, /TR, /TD, etcétera. Las hojas de ese árbol son referencias de contenido marcado, enteros que nombran un tramo del content stream de la página. En el lado del contenido, esos tramos se abren con un operador BDC que lleva un /MCID y se cierran con EMC. Cada elemento de estructura lleva también una entrada /Pg que nombra la página a la que pertenece, y eso es lo que hace posible el recorrido por páginas en un documento cuyo árbol de estructura abarca cientos de páginas

Anatomía del árbol de estructura PDF que vincula elementos StructTreeRoot como Sect, Table, TR y TD a tramos BDC MCID del content stream de la página en HotPDF
Las hojas del árbol son referencias de contenido marcado, y cada elemento lleva una entrada Pg que permite al recorrido filtrar a la página actual

HotPDF recorre ese árbol con un tope de profundidad de 128 niveles y filtra por /Pg de modo que solo contribuye la página actual. La salida del recorrido no es texto: es una lista ordenada de valores MCID, el orden de autoría de los tramos de contenido marcado de esta página. Reensamblar el texto es entonces cuestión de reproducir los glifos en ese orden

El MCID se registra durante la extracción de glifos, no se busca después

Ese es el detalle de implementación que hace barata la función. HotPDF ya registra el identificador de contenido marcado activo en cada glifo que extrae, en el campo MCID de THPDFGlyphRecord, porque el intérprete del content stream sabe qué ámbito BDC está abierto en el momento de procesar cada operador Tj o TJ. La extracción en orden estructural no necesita, por tanto, una segunda pasada sobre el content stream. Recolecta la secuencia de MCID del árbol de estructura, agrupa los glifos ya extraídos por MCID y los emite en esa secuencia

var
  Pdf: THotPDF;
  PageCount, I, Untagged: Integer;
  PageText, AllText: UnicodeString;
  Report: TStrings;   // sumidero de diagnóstico propiedad de quien llama
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('accessible-form.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
    begin
      if Pdf.ExtractLoadedPageStructureText(I, PageText, Untagged) then
      begin
        // Orden de autoría directo del árbol de estructura
        if Untagged > 0 then
          Report.Add(Format('page %d: %d glyphs outside the structure tree',
            [I, Untagged]));
      end
      else
        // Sin árbol de estructura utilizable en esta página: fallback geométrico
        Pdf.ExtractLoadedPageText(I, PageText);
      AllText := AllText + PageText + #13#10;
    end;
  finally
    Pdf.Free;
  end;
end;

Los glifos sin etiqueta se cuentan, nunca se descartan en silencio

Una página puede estar etiquetada a medias. Los productores añaden un filete decorativo, un número de página o una marca de agua tardía fuera de cualquier ámbito BDC, y esos glifos no pertenecen a ningún MCID. Descartarlos sería la implementación ordenada y la equivocada, porque el mismo hueco aparece también cuando un productor etiqueta el cuerpo pero olvida la tabla, y perderíais la tabla sin enteraros

HotPDF añade los glifos sin reclamar como cola geométrica tras el texto ordenado estructuralmente e informa de su número mediante el parámetro de salida UntaggedGlyphCount. Ese número es una señal de calidad sobre la que podéis actuar. Un puñado de glifos en una página de dos mil es mobiliario de página y puede ignorarse. Que el cuarenta por ciento de la página esté fuera del árbol de estructura significa que el etiquetado es decorativo y el extractor geométrico es la respuesta más honesta para ese fichero

Flujo de decisión para la extracción de texto estructural de HotPDF con fallback geométrico cuando una página no tiene árbol de estructura utilizable o etiquetado decorativo
True significa orden estructural con la cola sin etiqueta añadida, y False envía la página al extractor geométrico en lugar de fallar
function ExtractPageBestEffort(Pdf: THotPDF; PageIndex: Integer;
  out AText: UnicodeString; out UsedStructure: Boolean): Boolean;
var
  Untagged, TotalGlyphs: Integer;
  Glyphs: THPDFGlyphArray;
begin
  UsedStructure := False;
  if Pdf.ExtractLoadedPageStructureText(PageIndex, AText, Untagged) then
  begin
    TotalGlyphs := 0;
    if Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
      TotalGlyphs := Length(Glyphs);
    // Confiar en el árbol de estructura solo cuando reclama la mayor parte de la página
    if (TotalGlyphs = 0) or (Untagged * 4 <= TotalGlyphs) then
    begin
      UsedStructure := True;
      Result := True;
      Exit;
    end;
  end;
  Result := Pdf.ExtractLoadedPageText(PageIndex, AText);
end;

Qué hace que la función devuelva False

Tres casos, y merece la pena distinguirlos porque solo uno de ellos es un defecto del documento. El primero es un PDF ordinario sin etiquetas: sin /StructTreeRoot, nada que recorrer, y False es simplemente la verdad. El segundo es una página escaneada cuyo texto viene de una capa OCR que nunca se etiquetó. El tercero es el interesante: contenido que lleva operadores BDC con valores /MCID pero cuya página no tiene entrada /StructParents y cuyo árbol de estructura nunca referencia esos identificadores. El contenido marcado existe, el lado de la estructura no, y no hay orden que recuperar. HotPDF informa de False en lugar de inventárselo

Ese último caso aparece en ficheros editados a mano y en la salida de herramientas que emiten contenido marcado para optional content o artefactos sin construir un árbol de estructura. Si vosotros producís PDF etiquetados, la misma asimetría es lo que comprueba la validación PDF/UA, y la contrapartida en el lado del escritor se cubre en el DOM de maquetación que emite salida etiquetada y paginada

Dónde el orden estructural se paga solo

La auditoría de accesibilidad es el caso obvio: si estáis certificando un documento contra PDF/UA, el orden de lectura que anunciará un lector de pantalla es exactamente el orden estructural, así que extraerlo es la forma de revisarlo sin lector de pantalla. La captura de datos es el caso comercial mayor. Los formularios gubernamentales etiquetados, las declaraciones reguladas y los adjuntos de factura electrónica llevan etiquetas de campo y valores en orden declarado, y leerlos en ese orden elimina toda una clase de errores de mapeo que la extracción geométrica crea en maquetaciones a varias columnas

El consumidor más nuevo es la recuperación para modelos de lenguaje. Trocear un documento para embeddings vale solo tanto como el orden del texto, y un trozo que empalma dos columnas produce frases que nunca existieron. La extracción en orden estructural es el arreglo más barato disponible para eso, porque en los documentos etiquetados el orden correcto ya está en el fichero y solo hay que leerlo

HotPDF es un componente VCL nativo para Delphi y C++Builder, así que el recorrido del árbol de estructura y la reproducción de glifos corren ambos en proceso contra un documento cargado sin renderizador externo implicado. Los detalles completos de la API de la familia de extracción de documentos cargados están en la página de producto de HotPDF Delphi PDF component