Artículo técnico

Extracción de texto PDF por orden estructural con HotPDF

Todo extractor de texto geométrico está adivinando. Lee los glifos que una página dibuja, los ordena por línea base y posición horizontal, y espera que la disposición visual coincida con el orden en que una persona leería. En un reporte de una sola columna esa adivinanza acierta. En un artículo de revista a dos columnas, un formulario con barra lateral o una tabla cuyas celdas se emitieron columna por columna, se equivoca de maneras difíciles de notar y costosas de descubrir aguas abajo. HotPDF responde a esto con ExtractLoadedPageStructureText, que ignora por completo la geometría: recorre el árbol de estructura del documento en el orden de autoría definido por ISO 32000-1 §14.8.4 y después 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 de replegarse al extractor geométrico en lugar de fallar. Ese diseño de dos vías 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 atiende a uno de ellos no es un pipeline

Por qué la extracción geométrica se equivoca en el orden de lectura

Porque un flujo de contenido PDF no lleva ningún orden de lectura. Es una secuencia de operadores de dibujo, y un productor es libre de emitirlos en la secuencia que le convenga a su propio motor de maquetación. Los procesadores de texto suelen emitir en orden de flujo y el ordenamiento geométrico se ve bien. Las herramientas de maquetación, los diseñadores de formularios y los generadores de reportes con frecuencia no: un pie de página puede emitirse antes del cuerpo, una tabla puede llenarse 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 una página PDF a dos columnas que muestra la extracción geométrica ordenada por línea base entrelazando columnas frente a la extracción por orden estructural MCID en HotPDF
Ordenar glifos por línea base entrelaza dos columnas en un sinsentido, mientras que el árbol de estructura reproduce el orden que el productor declaró

El modo de fallo es silencioso. Un extractor geométrico nunca reporta un error, simplemente devuelve prosa cuyas oraciones están empalmadas de 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 archivos sin etiquetas; el punto de la vía por 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 y demás. Las hojas de ese árbol son referencias de contenido marcado, enteros que nombran un tramo del flujo de contenido de la página. Del 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 además una entrada /Pg que nombra la página a la que pertenece, que es lo que hace posible el recorrido por página 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 con tramos BDC MCID en el flujo de contenido de página de HotPDF
Las hojas del árbol son referencias de contenido marcado, y cada elemento lleva una entrada Pg que permite al recorrido filtrar por la página actual

HotPDF recorre ese árbol con un tope de profundidad de 128 niveles y filtra por /Pg de modo que solo la página actual contribuya. 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

Este es el detalle de implementación que vuelve 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 flujo de contenido sabe qué ámbito BDC está abierto en el momento de procesar cada operador Tj o TJ. La extracción por orden estructural no necesita entonces una segunda pasada por el flujo de contenido. Recoge la secuencia MCID del árbol de estructura, luego 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ósticos del que 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: respaldo 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 en parte. Los productores añaden una línea decorativa, un número de página o una marca de agua tardía fuera de todo ámbito BDC, y esos glifos no pertenecen a ningún MCID. Descartarlos sería la implementación prolija y la equivocada, porque el mismo hueco aparece también cuando un productor etiqueta el cuerpo pero olvida la tabla, y perderían la tabla sin notarlo

HotPDF anexa los glifos sin dueño como una cola geométrica después del texto ordenado estructuralmente y reporta su número mediante el parámetro de salida UntaggedGlyphCount. Ese número es una señal de calidad sobre la que pueden actuar. Un puñado de glifos en una página de dos mil es mobiliario de página y puede ignorarse. Cuarenta por ciento de la página fuera del árbol de estructura significa que el etiquetado es decorativo y que el extractor geométrico es la respuesta más honesta para ese archivo

Flujo de decisión para la extracción de texto estructural de HotPDF con respaldo geométrico cuando una página no tiene árbol de estructura utilizable o un etiquetado decorativo
True significa orden estructural con la cola sin etiqueta anexada, y False manda 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 vale distinguirlos porque solo uno de ellos es un defecto del documento. El primero es un PDF sin etiquetas corriente: sin /StructTreeRoot, nada que recorrer, y False es sencillamente 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 reporta False en lugar de inventar uno

Ese último caso aparece en archivos editados a mano y en salidas de herramientas que emiten contenido marcado con fines de contenido opcional o de artefacto sin construir un árbol de estructura. Si ustedes mismos producen PDF etiquetados, la misma asimetría es la que comprueba la validación PDF/UA, y la contraparte del 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 la obvia: si están 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 un lector de pantalla. La captura de datos es el caso comercial mayor. Los formularios gubernamentales etiquetados, los reportes regulatorios y los adjuntos de factura electrónica llevan etiquetas y valores de campos en el orden declarado, y leerlos en ese orden elimina toda una clase de errores de mapeo que la extracción geométrica crea en maquetaciones de varias columnas

El consumidor más nuevo es la recuperación para modelos de lenguaje. Trocear un documento para embeddings es solo tan bueno como el orden del texto, y un trozo que empalma dos columnas produce oraciones que nunca existieron. La extracción por orden estructural es el arreglo más barato disponible para eso, porque en los documentos etiquetados el orden correcto ya está en el archivo 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 ningún renderizador externo. Los detalles completos de la API de la familia de extracción de documentos cargados están en la página del producto HotPDF Delphi PDF component