HotPDF recupera tablas de un PDF existente mediante ExtractLoadedTypedTables, una API Delphi que fusiona los fragmentos de fila producidos por la pasada de layout, construye una rejilla canónica de columnas por tabla, continúa la tabla a través de un salto de página cuando la geometría lo permite y devuelve cada celda como un valor tipado con procedencia de página, extensión de columna y límites. ExportLoadedTypedTables escribe el mismo resultado directamente en CSV o JSON. El escenario que hace que merezca la pena construir esto es aburrido y extremadamente habitual. Un registro de facturas de cuarenta páginas, una sola tabla lógica, impreso con la cabecera repetida al principio de cada página. Ejecute una pasada ingenua de orden de lectura y obtendrá cuarenta tablas, treinta y nueve filas de cabecera espurias y una columna de moneda que se desplaza una posición a la izquierda en cada fila donde la celda central estaba vacía. Limpiar todo eso después, dentro de la aplicación caller, es donde los proyectos de importación documental van a morir
¿Por qué una página PDF entrega fragmentos en lugar de una tabla?
Porque una página PDF no lleva semántica de tabla alguna salvo que el documento esté etiquetado. El content stream contiene operadores que muestran texto y matrices de posicionamiento (ISO 32000-1 §9.4.3) y nada más; la caja con líneas que se ve en pantalla es un trazado no relacionado que ningún extractor tiene obligación de correlacionar con el texto. Los tipos de elemento de estructura Table, TR, TH y TD solo viven en la jerarquía de estructura lógica de un PDF etiquetado (ISO 32000-1 §14.8.4), y la inmensa mayoría de los documentos empresariales que circulan no están etiquetados. Todo lo que se describe a continuación es recuperación geométrica, no parsing, y conviene decirlo claramente antes de construir un informe de conciliación encima
Por eso HotPDF ejecuta primero un análisis semántico de layout sobre los glifos extraídos, la misma pasada que sustenta la extracción de texto en orden estructural de un PDF cargado y las exportaciones estructuradas HTML y XML. Esa pasada agrupa las baselines en runs cuyas celdas se alinean verticalmente y solo continúa un run mientras las filas consecutivas tengan el mismo número de celdas. Para un motor de layout la regla es correcta y barata. Para un caller tiene la forma equivocada: una sola fila con una celda interior vacía divide una tabla visual en dos source tables. La capa de tablas tipadas existe precisamente para volver a unir las piezas
Rejillas canónicas de columnas y el control ColumnTolerance
ExtractLoadedTypedTables fusiona los fragmentos de la misma página antes de hacer cualquier otra cosa, y fusiona a partir de la geometría de las columnas, no del texto de las filas. Dos tablas de origen adyacentes en una página se unen cuando ambas tienen al menos dos columnas, cuando el hueco vertical entre la última fila de la primera y la primera fila de la segunda se mantiene dentro de la banda de tolerancia y cuando las posiciones iniciales de sus columnas están alineadas. Los inicios de columna que están dentro de ColumnTolerance se colapsan en una columna canónica y se promedian al fusionarse. La tolerancia predeterminada es de 12 unidades de user space, adecuada para la tipografía empresarial normal y susceptible de aumentarse en layouts con tracking ancho o mucha indentación
Lo que ocurre con una fila a la que le falta un valor interior es la parte importante. HotPDF ajusta cada celda a la posición inicial de columna canónica más cercana y después establece ColumnSpan a la distancia desde esa columna hasta la siguiente ocupada, en lugar de desplazar las celdas restantes hacia la izquierda. Una fila de tres celdas en una rejilla de cinco columnas conserva sus valores bajo las cabeceras correctas y registra exactamente dónde están los huecos. Esa es la diferencia entre una tabla que se puede conciliar y otra que atribuye el dinero a quien no corresponde sin hacer ruido
var
Pdf: THotPDF;
Options: THPDFTypedTableExtractionOptions;
Tables: THPDFTypedTables;
Info: THPDFTypedTableExtractionInfo;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('register.pdf', '') <= 0 then
Exit;
Options := THPDFTypedTableExtractionOptions.Default;
Options.ColumnTolerance := 12; // unidades de user space
Options.MinimumTableConfidence := 0.55; // por debajo se descartan las tablas
Options.DateOrder := ttdoDMY; // 03/04/2026 es 3 de abril
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount frente a Info.SourceTableCount muestra cuánto se ha fusionado
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
¿Qué garantiza realmente la fusión entre páginas?
Garantiza conservadurismo, deliberadamente. HotPDF une dos tablas a través de un límite de página solo cuando MergeAcrossPages está activado, cuando la segunda tabla comienza exactamente en el índice de página posterior a aquel donde termina la primera, cuando ambas tienen al menos dos columnas y cuando al menos dos inicios de columna canónicos se alinean dentro de ColumnTolerance. La condición de páginas consecutivas es la que sostiene todo. Los callers pasan PageIndices como un open array en el orden que quieran, y sin esa comprobación una petición para las páginas 3, 9 y 14 podría soldar tres tablas sin relación en un único resultado con apariencia totalmente plausible. El coste es que una continuación real que salte una página, un apéndice intercalado o un escaneo dúplex con el reverso en blanco vuelve como dos tablas y ninguna opción lo relaja. Volver a unirlas es una decisión de política que solo puede tomar la aplicación caller, por eso la API expone FirstPageIndex, LastPageIndex, SourceTableCount y un PageIndex por fila, y deja la decisión donde corresponde
Las cabeceras repetidas se etiquetan, nunca se borran
ExtractLoadedTypedTables nunca elimina una fila de cabecera repetida del resultado. Cuando una fusión entre páginas encuentra que la tabla entrante comienza con un texto de cabecera idéntico al de la tabla acumulada, comparado después de recortar y plegar mayúsculas, marca esas filas con IsHeader e IsRepeatedHeader y las añade de todos modos en el orden de origen. Borrar es una decisión con pérdida e irreversible, y cada consumidor quiere una respuesta distinta: una importación CSV quiere eliminar las repeticiones, un audit trail quiere conservarlas con sus números de página y una herramienta de diff quiere mantener el orden de origen byte a byte. Por eso la biblioteca informa y el caller decide
var
T, R, C: Integer;
Row: THPDFTypedTableRow;
Total: Double;
begin
Total := 0;
for T := 0 to High(Tables) do
for R := 0 to High(Tables[T].Rows) do
begin
Row := Tables[T].Rows[R];
if Row.IsRepeatedHeader then
Continue; // conservar solo el primer bloque de cabecera
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Valores tipados y los separadores que debe suministrar
La inferencia de tipos se ejecuta en un orden fijo que resuelve las ambigüedades en la única dirección sensata: primero booleano, después fecha, porcentaje, moneda y número normal, y cualquier elemento que no coincida se queda como string. El orden impide que 2026 en una columna de fecha lo decida un parser numérico antes de que el parser de fechas pueda verlo. La moneda se reconoce a partir de un $, £, ¥ o € inicial, o de un código ISO 4217 de tres letras seguido de un espacio, y el código se conserva en CurrencyCode. Es importante que HotPDF no adivina su locale. DecimalSeparator, ThousandsSeparator y DateOrder proceden de las opciones, porque 1.234 puede ser un número o mil doscientos treinta y cuatro según un dato que el PDF no contiene. El Text Unicode sin procesar se conserva en cada celda junto al valor tipado, por lo que una suposición incorrecta siempre se puede recuperar sin una segunda pasada de extracción
var
Stream: TFileStream;
Info: THPDFTypedTableExtractionInfo;
begin
Stream := TFileStream.Create('tables.json', fmCreate);
try
if not Pdf.ExportLoadedTypedTables([0, 1, 2], ttefJSON,
Stream, Options, Info) then
case Info.Status of
ttesInvalidOptions: ReportBadConfiguration;
ttesBudgetExceeded: ReportOversizedDocument;
ttesCancelled: ReportUserCancelled;
ttesWriteFailed: ReportDestinationProblem;
else
ReportExtractionFailure;
end;
finally
Stream.Free;
end;
end;
Los dos formatos de exportación responden a preguntas distintas y deliberadamente no son equivalentes. CSV escribe como campos vacíos las columnas de continuación de una extensión fusionada, que es lo que espera una hoja de cálculo o un cargador masivo. JSON conserva todo lo que conocía la extracción: el valor tipado bajo su propio tipo, columnSpan, la confianza por celda y por fila, los límites de la celda y la procedencia de página y tabla de origen. Ambos formatos preparan todo el documento en un buffer acotado en memoria y solo después publican en el stream de destino, restaurando los bytes, la longitud y la posición originales si la escritura falla a mitad, de modo que una exportación fallida nunca deja un archivo a medio escribir. Los presupuestos de páginas, glifos por página, tablas, filas, celdas, caracteres y bytes de salida se contabilizan por separado, y las filas se cuentan antes de asignar memoria porque un SetLength por fila degenera en copias cuadráticas mucho antes del techo predeterminado de un millón de filas
Dónde se rinde la recuperación geométrica de tablas
Ser explícito sobre los modos de fallo es más útil que una lista de funciones, porque cada uno de estos puntos necesita una política propia del caller en lugar de otro valor de opción
- No se recuperan las fusiones verticales. HotPDF informa de
ColumnSpanpara las extensiones horizontales y dejaRowSpanen 1, por lo que una celda que abarque tres filas de la tabla impresa llega como una celda más dos huecos - La detección de cabecera depende de los datos, no de lo visual. El bloque de cabecera es el run de filas anterior a la primera fila que contiene un valor tipado no string, por lo que una tabla cuyo cuerpo sea completamente textual informa de
HeaderRowCountigual a cero por mucho estilo que tenga - Las tablas por debajo de
MinimumTableConfidencese descartan del resultado sin error. CompareInfo.TableCountconInfo.SourceTableCountcuando necesite saber si se ha descartado algo - Un run necesita al menos dos filas y al menos dos columnas antes de que la pasada de layout lo llame tabla, así que una pseudo-tabla de una línea o un layout de dos columnas con prosa larga no es una tabla, correctamente aunque no le resulte útil
- Las páginas escaneadas no contienen operadores de texto, así que no hay nada que recuperar geométricamente hasta que exista una capa de texto OCR en la página
Si sus PDF salen de su propia pila de informes, la solución más barata para todo esto es aguas arriba: emita tablas etiquetadas o conserve los datos de origen, y trate la extracción como fallback para documentos que no haya producido. Para todo lo demás, merece la pena aprender la pipeline en este orden, porque cada capa se apoya en la inferior: empiece por la extracción de texto simple de un PDF cargado, suba a la API de tablas tipadas cuando haya que conservar la geometría y consulte el renderizado de una tabla de datos en un PDF nuevo cuando esté en el lado de generación y pueda decidir lo recuperable que será la salida
ExtractLoadedTypedTables y ExportLoadedTypedTables se incluyen en el componente PDF Delphi HotPDF nativo para Delphi y C++Builder, sin DLL externa ni dependencia de runtime; la página del producto contiene la referencia completa de opciones, estados y records de la API de tablas tipadas