HotPDF Component extrae texto Unicode de cualquier PDF que cargue en Delphi mediante dos llamadas: ExtractLoadedPageText devuelve el texto del flujo de lectura de una página, y ExtractLoadedPageTextLayout (añadido en la versión v2.263.0) reconstruye la disposición visual de la página como texto plano, de modo que las columnas, la sangría y la alineación de las tablas sobreviven en la salida. Ambas funcionan en documentos que HotPDF no creó, que es el caso que realmente importa: la factura que un cliente le envió por correo electrónico, el informe que entregó una oficina de escaneo, el contrato generado por un software que ya nadie puede nombrar
Llegar allí requirió más mecanismos de los que sugieren las dos firmas, porque un PDF no almacena texto de la misma manera que un archivo de texto. Este artículo recorre ambos modos de extracción, luego abre el capó para mostrar las tres partes subyacentes (el lector de CMap, el intérprete de flujos de contenido y la cadena de fallbacks de decodificación de fuentes) porque saber cómo funciona el mapeo es la diferencia entre encogerse de hombros ante una salida inservible y diagnosticarla
¿Por qué la extracción de texto es más difícil que leer cadenas del archivo?
Un flujo de contenido PDF registra códigos de caracteres, no caracteres. Los operadores Tj y TJ (ISO 32000-1 §9.4.3) transportan cadenas de bytes cuyo significado depende enteramente de la fuente seleccionada por la etiqueta Tf precedente: el byte 0x41 podría ser la letra A bajo WinAnsi, un glifo arbitrario en una fuente subdividida o la mitad de un CID de dos bytes en una fuente CJK compuesta. La norma ISO 32000-1 §9.10 define la extracción de texto exactamente como este problema de decodificación (mapear cada código de nuevo a Unicode utilizando cualquier información que proporcione el diccionario de fuentes) y la norma es explícita al señalar que un archivo conforme no está obligado a proporcionar suficiente información para hacerlo
Esta última cláusula explica cada informe de error de "por qué copiar y pegar de este PDF produce caracteres ilegibles" que haya visto jamás. Un productor que incrusta una fuente subdividida sin tabla /ToUnicode ha escrito un archivo que se representa perfectamente pero se extrae como un sinsentido, porque el mapeo de código a glifo existe pero el mapeo de código a Unicode nunca se incluyó. Por lo tanto, cualquier API de extracción honesta es una cadena de fallbacks que hace el mejor esfuerzo posible, y la pregunta útil es qué tan profunda es esa cadena
Extracción del flujo de lectura con ExtractLoadedPageText
Para la indexación de búsqueda, la coincidencia de palabras clave o el envío de texto a una canalización de análisis, la llamada que desea es ExtractLoadedPageText. La firma es function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean: los índices de página empiezan en cero, el resultado llega como una UnicodeString nativa de Delphi y la función devuelve False cuando la página no tiene un flujo de contenido legible en lugar de generar una excepción
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText now holds the reading-flow text of the document
finally
Pdf.Free;
end;
end;
Los saltos de línea en la salida provienen de una heurística deliberadamente simple: cuando el origen vertical de un glifo se mueve más de la mitad del tamaño de la fuente actual (la firma de un paso Td o T* en el flujo de contenido), se inserta una nueva línea. Los caracteres que el decodificador no puede resolver se convierten en espacios en lugar de desaparecer, por lo que los límites de las palabras sobreviven incluso cuando los glifos individuales no lo hacen. Lo que este modo no intenta es la agrupación por orden de lectura o la detección de varias columnas: una página de dos columnas sale intercalada en el orden del flujo de contenido, que suele ser el orden visual, pero no siempre
¿Cuándo debería utilizar en su lugar la extracción que preserva el diseño?
ExtractLoadedPageTextLayout es la llamada correcta siempre que la posición tenga un significado: tablas, formularios, listados de código, cualquier cosa que pretenda comparar (diff), buscar (grep) o analizar por columnas. En lugar de aplanar los glifos en un flujo, los agrupa en líneas base, ordena cada línea base por la coordenada X y reproduce los espacios en blanco horizontales y verticales en una cuadrícula de caracteres monoespaciados dimensionada a partir del avance medio del glifo y el tamaño de la fuente. Los espacios anchos entre ejecuciones en la misma línea base se convierten en ejecuciones de espacios; los espacios grandes entre líneas base se convierten en líneas en blanco. El resultado se lee tal como se ve la página
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Columns, indentation and table alignment survive as
// spaces and blank lines on a character grid
end;
Los dos modos comparten cada byte del mecanismo de decodificación y difieren solo en cómo organizan los glifos decodificados, por lo que la elección no cuesta nada en fidelidad. Elija ExtractLoadedPageText cuando solo importen las palabras y ExtractLoadedPageTextLayout cuando importe la organización. La detección del orden de lectura de varias columnas sigue estando fuera del alcance de ambos: una representación en cuadrícula de una página de dos columnas muestra ambas columnas una al lado de la otra, fielmente, lo cual para la comparación (diff) es exactamente correcto y para el reflujo de prosa no lo es
¿Cómo decodifica HotPDF los códigos de caracteres a Unicode?
HotPDF Component resuelve cada código de carácter a través de una cadena de fallbacks ordenada por prioridad: primero el CMap /ToUnicode incrustado de la fuente, luego la entrada /Encoding (flujo o CMap con nombre), luego (para fuentes compuestas) los archivos CMap estándar de Adobe para colecciones de caracteres como Adobe-GB1, Adobe-CNS1, Adobe-Japan1 y Adobe-KR, y finalmente las tablas integradas WinAnsi y MacRoman para fuentes simples. Una estrategia que no puede ofrecer una respuesta se degrada silenciosamente a la siguiente en lugar de generar una excepción, y un código que agota toda la cadena se resuelve como 0 para que el llamador pueda contar los fallos en lugar de adivinar
El CMap /ToUnicode (ISO 32000-1 §9.10.3) se sitúa en primer lugar porque es el mapeo que el productor escribió específicamente para la extracción. La ruta CMap estándar de Adobe es importante para documentos CJK que utilizan CMaps predefinidos como UniGB-UTF16-H en lugar de incrustar nada: HotPDF incluye los archivos de colección en su directorio resources\CMap, los localiza en relación con el ejecutable en tiempo de ejecución y almacena en caché cada mapa analizado por proceso; vale la pena saberlo porque el más grande de ellos, el mapa Adobe-GB1, tiene aproximadamente 2 MB de texto de origen que no desea volver a analizar por página. Si el directorio no existe, el decodificador simplemente omite los CMaps respaldados por disco y funciona con tablas incrustadas más las codificaciones integradas. Este es el reflejo en el lado de la lectura del problema de modelado cubierto en el artículo sobre modelado de texto de secuencias de comandos complejas con HotPDF, donde se enfrenta la misma distinción de código frente a glifo en el momento de la escritura
Dos trampas de sintaxis de CMap que vale la pena conocer
Los archivos CMap parecen fáciles de analizar y no lo son, y dos detalles explican la mayoría de los fallos del analizador en el primer intento. El primero es que el recuento de registros viene antes de la palabra clave de la sección: una sección dice 2 beginbfchar, no beginbfchar 2. Un analizador que espera el recuento después de la palabra clave de la sección consume el número como un token perdido y luego encuentra cero entradas en cada sección. El enfoque robusto (aquel por el que se decidió el lector de HotPDF) consiste en ignorar el recuento por completo y realizar un bucle hasta la palabra clave coincidente endbfchar / endbfrange, lo que tiene la ventaja de tolerar archivos del mundo real cuyos recuentos son simplemente erróneos
La segunda trampa es que los destinos de bfchar y bfrange son cadenas UTF-16BE, no enteros. El destino <D83DDE00> significa U+1F600 (un par de sustitutos que deben recombinarse en un punto de código) y leer esos cuatro bytes como un entero big-endian produce un valor sin sentido en cada punto de código fuera del Plano Multilingüe Básico. Los emojis en los PDF ya no son exóticos, por lo que un decodificador que omite la recombinación de sustitutos falla en los archivos que realmente tienen sus usuarios. HotPDF analiza primero el literal hexadecimal a bytes sin procesar, luego recombina las unidades de código UTF-16BE, lo que también cubre los destinos de múltiples caracteres que producen los mapeos de ligaduras
Descender al nivel de glifo con ExtractLoadedPageGlyphs
Ambas llamadas de texto se basan en ExtractLoadedPageGlyphs, y la THPDFGlyphArray subyacente también está disponible para su código. Cada THPDFGlyphRecord lleva el punto de código Unicode resuelto junto con el código de carácter sin procesar, el ancho en bytes del código (1, 2, o 4, determinado por el codespacerange de CMap), la clave y el tamaño del recurso de fuente activo, el origen X e Y del espacio de usuario y el avance horizontal. Eso es suficiente para construir una detección de límites de palabras, un resaltado posicionado o un algoritmo de diseño personalizado sin tocar el flujo de contenido usted mismo
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Contar los registros con Unicode = 0, como se mostró anteriormente, es la forma honesta de medir la calidad de la extracción en un documento determinado antes de confiar en el texto en fases posteriores. Los registros de glifos también anclan cada carácter al operando de origen en el flujo de contenido, que es lo que hace posible la búsqueda y reemplazo de texto de documentos cargados de HotPDF sobre la misma base
¿Qué archivos PDF no entregarán su texto?
Algunos archivos derrotan a cualquier extractor, y es mejor detectarlos que entregar su salida. Los documentos escaneados son el caso más evidente: una página que es una imagen grande no contiene ningún operador de texto, por lo que la extracción devuelve correctamente una cadena vacía; la solución es OCR, y extraer las imágenes de la página del PDF cargado es el primer paso de esa canalización. Las fuentes subdivididas sin una tabla /ToUnicode son el caso más difícil: si la ruta /Encoding y los CMaps estándar también resultan vacíos, esos glifos se resuelven a 0 y aparecen como espacios en las llamadas de texto. Los documentos cifrados se extraen normalmente siempre que los cargue con su contraseña a través de la sobrecarga de LoadFromFile, de modo que los flujos se descifren antes de que el intérprete los vea
Vale la pena exponer claramente un límite más estrecho: la cadena de decodificación lee flujos CMap y de contenido a través de la ruta Flate de HotPDF, por lo que una fuente cuyo flujo ToUnicode utiliza un filtro inusual se degrada a la siguiente estrategia en lugar de hacer fallar la página. En la práctica, FlateDecode cubre casi todo lo producido en las últimas dos décadas, y la degradación es silenciosa por diseño: obtiene el mejor texto que permite el archivo en lugar de una excepción. El mismo mecanismo de objetos del lado de lectura que resuelve los diccionarios de fuentes aquí también potencia la edición de metadatos en documentos cargados, por lo que una canalización de ingesta de documentos puede extraer, inspeccionar y anotar en una sola pasada
La extracción de texto, la representación que preserva el diseño, el acceso a nivel de glifo y las funciones de búsqueda y reemplazo basadas en ellas forman parte de la versión estándar de HotPDF Component para Delphi y C++Builder: sin DLL externas, sin servicios de texto del sistema operativo, solo Object Pascal por el que puede avanzar paso a paso cuando un archivo extraño aterrice en su cola