Extraer texto, imágenes y fuentes de un PDF existente parece un problema resuelto hasta que se procesa un corpus real. Apunte un indexador de búsqueda a cuarenta mil archivos de clientes y los fallos se agrupan en unos pocos patrones reconocibles. Las palabras quedan unidas porque nadie indicó al extractor qué amplitud de separación cuenta como espacio. Otras páginas devuelven texto ilegible porque una fuente con subconjunto no incluye una correspondencia entre sus códigos de glifo y los caracteres reales. Y «el logotipo de la empresa» resulta ser nueve objetos de imagen independientes apilados detrás de una máscara suave. Nada de ello es un error de la biblioteca. Es la diferencia entre llamar a una función de extracción y comprender qué puede y qué no puede recuperar esa función de los bytes almacenados en disco
losLab PDF Library, la edición para Pascal, ofrece al código Delphi y C++Builder más de una forma de leer cada uno de esos tres flujos, y cada nivel ofrece garantías distintas. La clave es ajustar el nivel a la tarea: un índice de búsqueda, una revisión de redacciones y una comprobación previa de PDF/A piden cosas distintas de la misma página, y recurrir a la llamada equivocada desperdicia trabajo o genera una salida en la que no se puede confiar
Niveles de extracción de texto y lo que promete cada uno
GetPageText recibe una opción de 0 a 8, y ese número selecciona un motor, no un formato. Los valores de 0 a 2 realizan un pase ligero adecuado para una vista previa rápida. Los valores de 3 a 8 pasan por el motor consciente de la maquetación, que reconstruye líneas y espacios a partir de la posición real de los glifos en la página. Dentro de ese intervalo importan las variaciones: 4 y 6 dividen la salida en palabras, 5 y 6 emiten anchos por glifo, y 7 devuelve texto plano descartando deliberadamente los metadatos de fuente, color y bloque. La opción 7 es la indicada para alimentar un índice de búsqueda, ya que el índice solo necesita palabras
Ningún ajuste de opción puede rescatar un documento que nunca contenía esa información. PDF asigna códigos de caracteres a formas de glifos, y lo único que devuelve esos códigos a texto legible es el CMap ToUnicode de una fuente (ISO 32000-1 §9.10). Cuando se distribuye una fuente con subconjunto sin él, todos los extractores quedan bloqueados. Esta biblioteca, el copiar y pegar de un visor, un toolkit de la competencia: todos se reducen a adivinar a partir de los nombres de glifo o a no devolver nada. La respuesta práctica es detectar el problema, no hacer heroicidades. Asigne a la página una confianza baja y envíela a OCR, porque indexar silenciosamente contenido basura es peor que reconocer que no se puede leer
Para los casos que no cubren las opciones planas, tokenización personalizada, análisis forense del flujo de contenido o un embudo de texto construido según reglas propias, el decodificador está disponible un nivel más abajo. TPDFExtractor se construye sobre el diccionario de recursos de una página y la colección de fuentes. Su método ExtractTextW vuelve a pasar las operaciones de texto del flujo de contenido sin procesar por la misma maquinaria de fuentes para recuperar Unicode, y su evento OnFindObject entrega cada objeto a medida que pasa por el flujo. La mayoría del código nunca necesita llegar tan abajo. Las aplicaciones que sí lo necesitan agradecen que la capa sea pública y no esté oculta
Bloques posicionados: la unidad de los resultados de búsqueda y de la revisión de redacciones
El texto plano indica qué dice la página. Tarde o temprano, un producto también necesita saber dónde lo dice, para resaltar un resultado de búsqueda, dibujar un recuadro alrededor de un candidato a redacción o anclar una anotación en el lugar correcto. ExtractPageTextBlocks devuelve un identificador de una lista de fragmentos de texto, y cada fragmento incluye su texto, su cuadro delimitador y el nombre y tamaño de la fuente con que se configuró:
var
Pdf: TPDFlib;
Blocks, I: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
raise Exception.Create('load failed');
Pdf.SelectPage(1);
Blocks := Pdf.ExtractPageTextBlocks(0);
for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
Writeln(Format('%s [%s %.1f pt at %.0f,%.0f]',
[Pdf.GetTextBlockText(Blocks, I),
Pdf.GetTextBlockFontName(Blocks, I),
Pdf.GetTextBlockFontSize(Blocks, I),
Pdf.GetTextBlockBound(Blocks, I, 0),
Pdf.GetTextBlockBound(Blocks, I, 1)]));
Pdf.ReleaseTextBlocks(Blocks);
finally
Pdf.Free;
end;
end;
Un detalle de esta zona hace tropezar a más integraciones que ningún otro. SetTextExtractionArea, SetTextExtractionWordGap y SetTextExtractionOptions son estado a nivel de documento que persiste, no argumentos que se pasan en cada llamada. Configure una restricción de área para una función, por ejemplo leer solo la banda de cabecera para clasificar un documento, y truncará silenciosamente todas las extracciones posteriores con el mismo identificador, incluidos los niveles de GetPageText conscientes de la maquetación que se usen más tarde. Restablezca el estado de extracción entre tareas lógicas o proporcione a cada tarea su propio identificador de documento
El umbral de separación entre palabras es la palanca para aquel primer grupo de fallos, las palabras que quedan unidas. SetTextExtractionWordGap indica al motor de maquetación qué espacio horizontal, medido respecto a la separación de glifos de la propia página, separa una palabra de la siguiente. Una tabla densa necesita una separación menor que una página de marketing con composición abierta, por lo que un umbral ajustado para cada clase de documento supera a una constante global. Persiste en el documento igual que el resto del estado de extracción, así que conviene establecerlo deliberadamente, no una vez y olvidarse de él
Imágenes: flujos originales, no capturas de pantalla
La forma equivocada de extraer imágenes de un PDF es renderizar la página y recortarla. Eso vuelve a muestrear los píxeles, incorpora cualquier rotación y descarta el original. En cambio, GetPageImageList enumera los recursos de imagen reales a los que hace referencia la página, y cada elemento devuelve sus propiedades y sus datos originales sin alterar:
var
ImgList, I: Integer;
begin
Pdf.SelectPage(1);
ImgList := Pdf.GetPageImageList(0);
for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
begin
Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
Format('page1-img%.2d.bin', [I]));
end;
Pdf.ReleaseImageList(ImgList);
end;
Compruebe GetImageListItemFormatDesc antes de dar por sentado nada sobre un elemento, porque lo que referencia una página rara vez es una imagen ordenada por cada imagen visible. Una máscara suave aparece como su propia entrada independiente. El mismo XObject suele repetirse en muchas páginas, así que elimine duplicados por hash de contenido antes de archivar una exportación de «todas las imágenes», o escribirá el mismo logotipo cien veces. Los JPEG CMYK requieren gestión de color posterior, o se renderizan invertidos en visores que interpretan los canales literalmente. Cuando se necesita un inventario de todo el documento en lugar de recorrer página a página, FindImages junto con SetFindImagesMode analiza todo el archivo en un solo pase
Hay un límite que conviene explicar a las partes interesadas antes de que alguien redacte criterios de aceptación: la extracción de imágenes solo devuelve recursos rasterizados. Un logotipo o gráfico dibujado como trazados vectoriales no es una imagen en el sentido de los recursos y nunca aparecerá en una lista de imágenes, por clara que parezca la imagen en pantalla. Cuando el requisito real es entregar ese gráfico como archivo, el enfoque honesto es renderizar la región de la página a un mapa de bits, una operación distinta con una fidelidad diferente. Los dos tipos de salida no deberían compartir carpeta de exportación sin una etiqueta que indique cuál es cuál
Fuentes: una superficie de auditoría, no una función de exportación
La API de fuentes responde preguntas sobre las fuentes. No entrega los archivos de fuente en sí, y esa distinción determina todo lo que se puede construir con ella. Después de que FindFonts analice el documento, la enumeración recorre las fuentes por ID, y las llamadas de propiedades informan de la fuente que esté seleccionada en ese momento:
var
I: Integer;
begin
Pdf.FindFonts;
for I := 1 to Pdf.FontCount do // los índices de fuente empiezan en 1, no en 0
if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
Writeln(Format('%s type=%d embedded=%d subset=%d',
[Pdf.FontName, Pdf.FontType,
Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;
Vigile los límites del bucle. Los índices de fuente van de 1 a FontCount, mientras que los índices de bloques de texto y listas de imágenes de unos párrafos más arriba empiezan en cero. Llevar una convención a la otra provoca un error de uno arriba o abajo que omite la primera fuente o se sale del final, y superará pruebas superficiales porque la mayoría de los documentos tienen varias fuentes y la equivocada sigue pareciendo verosímil. Sea claro también sobre el alcance. Esta API no ofrece exportación de fuentes a nivel de bytes. Ninguna llamada devuelve el programa de fuente incrustado como archivo TTF u OTF, y la enumeración más la inspección de metadatos es todo el modelo previsto. Ese modelo sigue cubriendo lo que el trabajo de producción realmente pide de las fuentes: detección de subconjuntos por patrón de nombre, auditorías de incrustación antes de una conversión de archivo (una fuente no incrustada bloquea PDF/A de forma tajante, como detalla la comprobación previa de PDF/A y PDF/UA en Delphi) y diagnósticos de codificación cuando baja la confianza de extracción. También hay una razón de licencia para que el límite esté aquí. Un programa de fuente con subconjunto es material sujeto a licencia y, al carecer de la mayoría de sus glifos, de todos modos no sirve como fuente instalable. Tratarlo como metadato de auditoría en lugar de como activo extraíble es una postura defendible
Esa última llamada resulta muy útil en la clasificación inicial. Ejecute GetFontEncoding en cada fuente, léalo junto al indicador de subconjunto y podrá predecir la calidad de la extracción antes de recuperar un solo carácter. Una página en la que todas las fuentes son fuentes con subconjunto y tienen codificaciones no estándar es candidata a OCR solo con inspeccionarla, lo que permite a un proceso por lotes dirigirla correctamente sin malgastar antes un pase de extracción fallido
Extracción a escala sin cargar documentos
En un proceso por lotes, cargar un documento completo solo para leer una página desperdicia E/S, y el coste aumenta deprisa en un corpus. Las variantes de una sola llamada, ExtractFilePageText y ExtractFilePageTextBlocks, reciben directamente un nombre de archivo, contraseña y número de página, y omiten la carga completa. Para archivos de escala gigabyte hay un nivel aún más directo. La ruta de acceso directo abre un archivo mediante lecturas de xref por streaming, por lo que DAOpenFileReadOnly seguido de DAExtractPageText solo toca los objetos que realmente necesita esa página. Incluye un cambio de convención que conviene memorizar: las funciones DA se dirigen a las páginas mediante PageRef, un identificador de referencia de objeto obtenido de DAFindPage, nunca por número de página sin más. Pase el número donde corresponde el identificador y la llamada operará sobre el objeto equivocado sin generar un error, el peor tipo de fallo para depurar. El resto del toolkit de acceso directo se explica en fusión, división y acceso directo a PDF grandes
Si hay un hábito que separa el código de extracción que sobrevive a un corpus real del código que apenas avanza, es tratar la página como entrada no fiable en lugar de como fuente de datos limpia. El texto que no coincide con lo que renderiza el visor casi siempre es un problema de codificación, una ligadura que se contrae a un solo glifo o una fuente con subconjunto sin entradas ToUnicode, y la solución consiste en medir la confianza y derivar las páginas problemáticas a OCR, no en luchar contra los bytes. La API de fuentes nunca producirá un TTF u OTF, por diseño, así que los flujos de trabajo de fuentes deben basarse en preguntas de auditoría. Y el estado de extracción persistente, sobre todo el rectángulo de área, es un ajuste del que se es responsable durante toda la vida del identificador de documento, no un parámetro que se olvida después de una llamada. Acierte con esos tres reflejos y el resto de la API se comportará como corresponde
Las versiones de evaluación, los proyectos de demostración y la referencia completa de la API de extracción están en la página del producto losLab PDF Library for Delphi