PDFium Component versión 3.117.0 enlaza una tabla que se parte en un límite de página cuando los dos fragmentos tocan los bordes de la página o cuando no hay texto de cuerpo debajo del primer fragmento y encima del segundo, ignorando los encabezados y pies de página repetidos. ExtractDocumentTables aplica esa prueba consciente del contenido como alternativa a la prueba de márgenes anterior, rechaza un fragmento de la página siguiente cuya primera fila sea una sola celda de título a todo el ancho, y conserva la fila suelta que se desborda a la página siguiente como parte de su cadena de continuación
El artículo sobre detección y extracción de tablas presentaba la continuación como cuatro puertas estrictas y trataba "toca el borde de la página" como una de ellas. Esa descripción era correcta para la versión que cubría, y también era equivocada para la mayoría de las tablas que la gente realmente le pasa al componente. Este artículo es la corrección: qué documentos no puede manejar la prueba de márgenes, qué la reemplazó y los dos casos laterales que el fix arrastró consigo
¿Por qué falla la prueba de márgenes de página en los exports de Word?
La prueba de márgenes falla porque un procesador de textos deja de acomodar filas en el margen inferior, no en el borde del papel. Con el ContinuationMargin por defecto de 36 puntos, la regla original exigía que el borde inferior del primer fragmento quedara a menos de 36 puntos del fondo de la página y que el borde superior del fragmento posterior quedara a menos de 36 puntos de la parte superior. Un documento exportado desde Word con sus márgenes por defecto de una pulgada deja la última fila a por lo menos 72 puntos del fondo de la página, y más todavía si hay un pie, así que la condición nunca se cumplía. Cada tabla larga de esos documentos volvía como fragmentos independientes con ContinuationGroup en cero, y quien llamaba quedaba otra vez cosiendo a mano. La prueba sigue teniendo sentido para aquello alrededor de lo que se diseñó: reportes generados por motores de maquetación que llenan la página hasta una caja de contenido fija y arrancan la siguiente pegada al borde superior. No es una regla mala, es una regla incompleta, y por eso la versión 3.117.0 la conservó y añadió un segundo camino en lugar de reemplazarla
¿Qué comprueba en su lugar la prueba consciente del contenido?
La prueba consciente del contenido comprueba si algo que no sea la tabla ocupa el espacio entre los dos fragmentos, usando las cajas de palabras de cada página en lugar de la geometría de la página. Mientras ExtractDocumentTables recorre el documento, registra por página el borde inferior más bajo de cualquier palabra cuya parte superior quede por encima de la banda de pie y el borde superior más alto de cualquier palabra cuyo fondo quede por debajo de la banda de encabezado. Las dos bandas miden ContinuationMargin puntos de profundidad, así que la misma opción hace ahora doble trabajo: holgura del borde de página y altura de las zonas de encabezado y pie repetidos. Un par de fragmentos pasa cuando el borde inferior del primero está a la altura o por debajo del texto de cuerpo más bajo de su página y el borde superior del segundo está a la altura o por encima del texto de cuerpo más alto de la página siguiente, cada uno dentro de AlignmentTolerance. En palabras simples: la tabla era lo último de la página N y lo primero de la página N+1, y un número de página o un título del documento en la banda del margen no cuentan. Esa exclusión no es arbitraria. ISO 32000-1 §14.8.2.2 clasifica los encabezados y pies repetidos como artefactos de paginación, contenido que existe por el salto de página y no a pesar de él, y la misma idea que permite a un lector de etiquetas saltárselos es la que permite a una tabla continuar más allá de ellos. El artículo sobre contenido marcado cubre cómo los archivos etiquetados declaran esos artefactos de forma explícita; aquí la clasificación se infiere de la posición, porque la mayoría de las tablas exportadas no llevan etiqueta alguna
Las dos pruebas se combinan con OR. Un reporte de motor de maquetación cuyas tablas llegan al borde del papel pasa la primera; un export de Word cuyas tablas se detienen en el margen pasa la segunda; un documento que hace las dos cosas pasa dos veces. Solo después de que una de ellas tenga éxito corren las puertas restantes, y corren en un orden fijo: los números de página deben ser adyacentes, el fragmento posterior no debe abrir con una fila de título, y los límites de columna deben coincidir dentro del doble de AlignmentTolerance, que con los valores por defecto son 6 puntos. La enumeración es TPdfTableContinuation con los valores ptcNone, ptcStart, ptcMiddle y ptcEnd. Un fragmento que quedó marcado ptcEnd y después enlaza hacia otra página más se promueve a ptcMiddle, así que una tabla de tres páginas se lee inicio, medio y fin en orden de página. Los números de grupo empiezan en 1 y 0 significa sin enlazar, y ToJson emite la misma información en los miembros continuation y continuationGroup, que es la forma que conviene preferir si un servicio downstream hace la costura
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'itinerary-from-word.pdf';
Pdf.LoadDocument;
Options := TPdfTableExtractionOptions.Default;
Options.DetectContinuations := True; // por defecto; se muestra para mayor claridad
Options.ContinuationMargin := 54; // pie de dos líneas, ~50 pt de alto
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
case Tables[I].Continuation of
ptcStart:
Writeln(Format('group %d starts on page %d (%d rows)',
[Tables[I].ContinuationGroup, Tables[I].PageNumber,
Tables[I].RowCount]));
ptcMiddle, ptcEnd:
Writeln(Format('group %d continues on page %d (%d rows)',
[Tables[I].ContinuationGroup, Tables[I].PageNumber,
Tables[I].RowCount]));
else
Writeln(Format('standalone table on page %d (%d rows)',
[Tables[I].PageNumber, Tables[I].RowCount]));
end;
finally
Pdf.Free;
end;
end;
¿Cómo impide una fila de título que dos tablas se fusionen?
Un fragmento de la página siguiente cuya primera fila sea una sola celda que abarca todas las columnas se trata como tabla nueva, nunca como el resto de la anterior. Esta regla existe porque la prueba consciente del contenido, por sí sola, enlaza con demasiado entusiasmo. El caso que la sacó a la luz era un formulario tipo transcripción: una tabla termina cerca del fondo de la página 1, una segunda tabla con anchos de columna idénticos arranca cerca de la parte superior de la página 2, entre las dos no hay más que el pie, y las columnas coinciden al punto. Con la prueba de márgenes las dos nunca se encontraban porque ninguna tocaba un borde; con la prueba de contenido se enlazaron de inmediato, y un formulario con secciones se volvió una sola grilla incoherente. Lo que las separa se ve en la estructura de celdas. La segunda tabla abre con un título de sección como "RECIPIENT INFORMATION" maquetado como una sola celda combinada a todo el ancho, y una continuación genuina nunca hace eso, porque el título pertenece a la tabla que ya empezó en la página anterior. TableStartsWithCaptionRow codifica exactamente eso: el fragmento tiene al menos dos columnas y contiene una celda con RowIndex = 0, ColumnIndex = 0 y ColumnSpan = ColumnCount. La comprobación corre solo sobre el fragmento posterior, así que una tabla cuyo propio título esté en su primera página no se ve afectada; el título está en la página N y solo se inspecciona el fragmento de la página N+1
La comparación de columnas que viene después, TablesHaveMatchingColumns, es más estricta que "mismo número de columnas". Reconstruye las posiciones de los límites de cada fragmento a partir de los rectángulos de las celdas, interpola los límites que las celdas combinadas esconden y rechaza el par cuando algún límite se desvía más que la tolerancia. Dos tablas de cuatro columnas con proporciones distintas se mantienen por lo tanto separadas aun cuando todo lo demás coincida
¿Qué pasa con una fila suelta que se desborda a la página siguiente?
Una grilla con líneas que lleva una fila a la página siguiente ahora se detecta y se enlaza, siempre que termine dentro de una cadena de continuación; por sí sola se descarta. El MinRows por defecto de 2 existe para evitar que un par de líneas sueltas se reporte como tabla, pero una última fila empujada por el salto es una fila real que un piso duro de 2 descartaba en silencio, y el resto de la tabla parecía completa cuando no lo estaba. El escaneo a nivel de documento lo resuelve en tres pasos. Cuando DetectContinuations y DetectRuledTables están los dos activados, el pase por página corre el detector de grillas con el piso de filas bajado temporalmente a 1, y por eso ExtractTables ahora acepta MinRows de 1 para grillas con líneas, mientras que la detección por espacios en blanco mantiene un piso interno de 2. Las continuaciones se marcan sobre el resultado completo. Después se elimina toda tabla que sea más corta que el MinRows de quien llama y que no forme parte de ninguna cadena. El fragmento de una sola fila sobrevive solo porque quedó enlazado, y una grilla de una fila en medio de una página por lo demás normal se filtra exactamente como antes
// Reconstruir cada cadena como un solo CSV, descartando las filas de encabezado repetidas
// en los fragmentos de continuación
procedure ExportChains(const Tables: TPdfTables; const Folder: string);
var
I, R: Integer;
Lines: TStringList;
Csv: TStringList;
begin
Csv := TStringList.Create;
Lines := TStringList.Create;
try
for I := 0 to High(Tables) do
begin
if Tables[I].Continuation in [ptcNone, ptcStart] then
Csv.Clear;
Lines.Text := string(Tables[I].ToCsv);
if (Tables[I].Continuation in [ptcMiddle, ptcEnd]) and
(Lines.Count > 1) and (Tables[I].RowCount > 1) then
Lines.Delete(0); // encabezado repetido por el procesador de textos
for R := 0 to Lines.Count - 1 do
Csv.Add(Lines[R]);
if Tables[I].Continuation in [ptcNone, ptcEnd] then
Csv.SaveToFile(Format('%s\page%d-group%d.csv',
[Folder, Tables[I].PageNumber, Tables[I].ContinuationGroup]));
end;
finally
Lines.Free;
Csv.Free;
end;
end;
Dos detalles de esa rutina son deliberados. La fila desbordada nunca se recorta, porque la guarda sobre RowCount la conserva, y un procesador de textos que repite la fila de encabezado en cada página produce un fragmento cuya primera línea vuelve a ser el encabezado, así que descartar la línea cero en los fragmentos intermedios y finales es correcto para ese caso y equivocado para un generador que no repite encabezados. Revise un documento antes de soltar la rutina sobre una carpeta entera
Dónde se detienen todavía las reglas
La prueba consciente del contenido vale tanto como la capa de texto que lee. En una página escaneada sin nada de texto, los extremos de texto de cuerpo registrados caen de vuelta a los límites de la página, la condición de "nada entre medio" se cumple de forma vacua y solo quedan las puertas de la fila de título y de columnas; una grilla con líneas en esa página igual se encuentra como esqueleto vacío, así que la cadena puede enlazarse bien, pero de hecho no se verificó nada del texto que la rodea. Añada primero una capa de texto si eso le importa. Los pies renderizados como imágenes en vez de texto son invisibles para la lógica de bandas e inofensivos por el mismo motivo
Las bandas son un solo número. Un pie más profundo que ContinuationMargin deja sus líneas inferiores dentro de la zona de cuerpo, lo que hace que el fragmento anterior parezca seguido de texto y bloquea el enlace; suba la opción a la profundidad real de la banda, como hace el primer ejemplo. Súbala demasiado y un párrafo corto de cierre cerca del fondo de la página se cuela en la banda y se ignora, lo que enlaza la tabla con lo que venga después. La regla del título tiene un fallo espejado: un generador que escribe un banner combinado de "continuación" como primera fila de cada fragmento de continuación hará que esos fragmentos se rechacen como tablas nuevas, y el único remedio hoy es coser usted mismo por ContinuationGroup sin aflojar nada, porque la regla no tiene interruptor
Las tablas detectadas por espacios en blanco no reciben nada del alivio de una sola fila. La estrategia de espacios en blanco necesita dos filas alineadas para ver una tabla, así que una tabla sin líneas que desborda una fila se sigue reportando corta en esa fila. Cuando se tope con eso, las cajas de palabras detrás de los bloques de texto estructurado y el orden de lectura le dan las posiciones crudas para recuperarla. En el conjunto de muestras que impulsó este trabajo, trece exports de procesadores de texto y navegadores, los cinco documentos con tablas multipágina genuinas se enlazaron todos en cadenas únicas y el formulario tipo transcripción que antes se fusionaba se mantuvo separado, que es el listón contra el que se midió la versión, no una promesa sobre cualquier maquetación
El marcado de continuaciones, la regla del título y el pase de una sola fila viven todos en el camino a nivel de documento que comparten las compilaciones de Delphi, C++Builder y Lazarus; la API completa de extracción de tablas está descrita en la página de PDFium Component para Delphi