Extraer texto, imágenes y fuentes de un PDF existente parece un problema resuelto hasta que pasa un corpus real por él. Apunte un indexador de búsqueda a cuarenta mil archivos de clientes y las roturas se ordenan en unos cuantos montones reconocibles. Las palabras se pegan porque nadie le dijo al extractor qué tan ancha cuenta como espacio una separación. Otras páginas regresan como incomprensibles porque una fuente parcializada no trae un mapeo de sus códigos de glifo a caracteres reales. Y "el logo de la empresa" resulta ser nueve objetos de imagen separados apilados detrás de una máscara suave. Nada de eso es un error en la biblioteca. Es la diferencia entre llamar a una función de extracción y entender qué puede y qué no puede recuperar la función de los bytes en disco
losLab PDF Library, la edición Pascal, le da al código de Delphi y C++Builder más de una forma de leer cada una de esos tres flujos, y los niveles difieren en lo que garantizan. El truco es emparejar el nivel con la tarea: un índice de búsqueda, un revisor de redacción y un pase de preflight de PDF/A todos quieren cosas distintas de la misma página, y recurrir a la llamada equivocada desperdicia esfuerzo o produce una salida en la que no se puede confiar
Niveles de extracción de texto y lo que promete cada uno
GetPageText toma un valor de opciones de 0 a 8, y ese número elige un motor en lugar de un formato. Los valores de 0 a 2 corren un pase ligero que funciona para una vista previa rápida. Los valores de 3 a 8 se enrutan a través del motor con conciencia de diseño, que reconstruye líneas y espaciado a partir de dónde se asientan realmente los glifos en la página. Dentro de ese rango las variaciones importan: 4 y 6 dividen la salida en palabras, 5 y 6 emiten anchos por glifo, y 7 devuelve texto plano con fuente, color y metadatos de bloque descartados deliberadamente. La opción 7 es la que debe alimentar a un índice de búsqueda, ya que el índice quiere palabras y nada más
Ningún ajuste de opción puede rescatar un documento que nunca llevó la información para empezar. El PDF mapea códigos de caracteres a formas de glifo, y lo único que mapea esos códigos de vuelta a texto legible es un ToUnicode CMap de la fuente (ISO 32000-1 §9.10). Cuando una fuente parcializada se envía sin uno, todo extractor está atascado. Esta biblioteca, el copiar y pegar en un visor, un toolkit competidor: todos se reducen a adivinar a partir de nombres de glifo o a no devolver nada. La respuesta práctica es la detección, no heroísmo. Califique la página como de baja confianza y envíela a OCR, porque indexar la basura silenciosamente es peor que admitir que no la puede leer
Para los casos que las opciones planas no cubren, tokenización personalizada, forensia de flujo de contenido, un embudo de texto construido según sus propias reglas, el decodificador está disponible una capa más abajo. TPDFExtractor se construye sobre el diccionario de recursos y la colección de fuentes de una página. Su método ExtractTextW vuelve a correr las operaciones de texto del flujo de contenido crudo a través de la misma maquinaria de fuentes para recuperar Unicode, y su evento OnFindObject le entrega cada objeto a medida que fluye. La mayoría del código nunca necesita llegar tan profundo. Las aplicaciones que lo hacen son las que se alegran de que la capa sea pública en lugar de enterrada
Bloques posicionados: la unidad de resultados de búsqueda y revisión de redacción
El texto plano le dice lo que dice la página. Tarde o temprano un producto también necesita saber dónde lo dice, para poder resaltar un resultado de búsqueda, dibujar un cuadro alrededor de un candidato a redacción o anclar una anotación al lugar correcto. ExtractPageTextBlocks devuelve un identificador a una lista de ejecuciones de texto, y cada ejecución carga su texto, su cuadro delimitador y el nombre y tamaño de fuente en el que se estableció:
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 en esta área tropieza a las integraciones más que cualquier otro. SetTextExtractionArea, SetTextExtractionWordGap y SetTextExtractionOptions son estado a nivel de documento que persiste, no argumentos que se pasan por llamada. Configure una restricción de área para una característica, digamos leer solo la banda de encabezado para clasificar un documento, y truncará silenciosamente cada extracción que siga en el mismo identificador, incluyendo los niveles con conciencia de diseño GetPageText a los que recurre más tarde. O reinicie el estado de extracción entre tareas lógicas o dele a cada tarea su propio identificador de documento
El umbral de separación entre palabras es la palanca para ese primer montón de fallas, las palabras que se pegan. SetTextExtractionWordGap le dice al motor de diseño cuánto espacio horizontal, medido contra el espaciado de glifo propio de la página, separa una palabra de la siguiente. Una tabla densa quiere un espacio menor que una página de marketing con diseño suelto, así que un umbral afinado por clase de documento le gana a una constante global. Persiste en el documento como el resto del estado de extracción, así que planee establecerlo deliberadamente en lugar de una vez y olvidarlo
Imágenes: flujos originales, no capturas de pantalla
La forma equivocada de sacar imágenes de un PDF es renderizar la página y recortarla. Eso vuelve a muestrear los píxeles, fija cualquier rotación y descarta lo que fuera que el original era. GetPageImageList en su lugar enumera los recursos de imagen reales a los que la página hace referencia, y cada elemento devuelve sus propiedades y sus datos originales sin disturbar:
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;
Revise GetImageListItemFormatDesc antes de asumir nada sobre un elemento, porque lo que una página referencia rara vez es una foto ordenada por cada imagen visible. Una máscara suave aparece como su propia entrada separada. El mismo XObject a menudo se repite a lo largo de muchas páginas, así que deduplique por hash de contenido antes de archivar una exportación de "todas las imágenes", o escribirá el mismo logo cien veces. Los JPEG CMYK necesitan gestión de color aplicada aguas abajo, o se renderizan invertidos en visores que toman los canales al valor nominal. Cuando quiere un inventario a nivel de documento en lugar de una página a la vez, FindImages junto con SetFindImagesMode escanea todo el archivo en un solo pase
Hay una frontera que vale la pena plantear con las partes interesadas antes de que alguien escriba criterios de aceptación: la extracción de imágenes devuelve solo recursos de trama. Un logo o gráfico dibujado como trazados vectoriales no es una imagen en el sentido de recurso y nunca aparecerá en ninguna lista de imágenes, sin importar cuán claramente se lea como una imagen en pantalla. Cuando el requisito realmente es entregar ese gráfico como un archivo, el enfoque honesto es renderizar la región de la página a un mapa de bits, que es una operación distinta con fidelidad distinta. Los dos tipos de salida no pertenecen a la misma carpeta de exportación sin una etiqueta que diga cuál es cuál
Fuentes: una superficie de auditoría, no una característica de exportación
La API de fuentes responde preguntas sobre las fuentes. No le entrega los archivos de fuente en sí, y esa distinción da forma a todo lo que puede construir sobre ella. Después de que FindFonts escanea el documento, la enumeración recorre las fuentes por ID, y las llamadas de propiedad reportan sobre la fuente que esté actualmente seleccionada:
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 ciclo. Los índices de fuente van de 1 a FontCount, mientras que los índices de bloque de texto y lista de imágenes unos párrafos más arriba son de base cero. Lleve una convención a la otra y obtiene un error de off-by-one que o se salta la primera fuente o se sale del final, y pasará pruebas casuales porque la mayoría de los documentos tienen varias fuentes y la equivocada sigue pareciendo plausible. Sea claro también sobre el alcance. Esta API no tiene exportación a nivel de bytes. Ninguna llamada devuelve el programa de fuente incrustado como un archivo TTF u OTF, y la enumeración más la inspección de metadatos es todo el modelo previsto. Ese modelo aún cubre lo que el trabajo de producción realmente pregunta a las fuentes: detección de parcialización por patrón de nombre, auditorías de incrustación antes de una conversión de archivo (una fuente no incrustada es un bloqueador duro de PDF/A, como cubre preflight de PDF/A y PDF/UA en Delphi), y diagnósticos de codificación para cuando la confianza de extracción cae. También hay una razón de licencia por la que la frontera se asienta aquí. Un programa de fuente parcializada es material con licencia y, faltando la mayoría de sus glifos, inútil como fuente instalable de todos modos. Tratarlo como metadatos de auditoría en lugar de un activo extraíble es la postura que puede defender
Esa última llamada carga su peso en triaje. Corra GetFontEncoding en cada fuente, léalo junto con la bandera de parcialización, y puede predecir la calidad de extracción antes de jalar un solo carácter. Una página cuyas fuentes son todas parcializadas con codificaciones no estándar es una candidata a OCR solo con inspección, lo que deja que un pipeline por lotes la enrute correctamente sin primero desperdiciar un pase de extracción fallido en ella
Extracción a escala sin cargar documentos
En un pipeline por lotes, cargar un documento entero solo para leer una página es I/O desperdiciado, y se acumula rápido a lo largo de un corpus. Las variantes de llamada única, ExtractFilePageText y ExtractFilePageTextBlocks, toman un nombre de archivo, contraseña y número de página directamente y se saltan la carga completa. Para archivos a escala de gigabytes hay un cambio aún más bajo. La ruta de acceso directo abre un archivo a través de lecturas de xref en flujo, así que DAOpenFileReadOnly seguido de DAExtractPageText toca solo los objetos que esa página realmente necesita. Viene con un cambio de convención que vale la pena memorizar: las funciones DA direccionan páginas por PageRef, un identificador de referencia a objeto que obtiene de DAFindPage, nunca por número de página crudo. Pase el número donde va el identificador y la llamada opera sobre el objeto equivocado sin levantar un error, que es el peor tipo de error para depurar. El resto del toolkit de acceso directo se expone en fusión, división y acceso directo de PDF grandes
Si hay un solo hábito que separa al código de extracción que sobrevive un corpus real del código que cojea, es tratar la página como entrada no confiable en lugar de una fuente de datos limpia. El texto que no concuerda con lo que el visor renderiza casi siempre es un problema de codificación, una ligadura que colapsa a un glifo o una fuente parcializada a la que le faltan sus entradas ToUnicode, y la solución es medir la confianza y desviar las páginas malas a OCR, no pelear con los bytes. La API de fuentes nunca producirá un TTF u OTF, por diseño, así que construya los flujos de trabajo de fuente en torno a preguntas de auditoría. Y el estado de extracción persistente, el rectángulo de área sobre todo, es un ajuste que usted posee por la vida de un identificador de documento, no un parámetro que olvida tras una llamada. Saque esos tres reflejos bien y el resto de la API se comporta
Las compilaciones 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 para Delphi