La extracción de tablas de PDFium Component, desde la versión 3.117.0, trata un rectángulo relleno fino como una línea de tabla. Con DetectFilledRulings activado, que es lo por defecto, una caja rellena alineada con los ejes y de grosor no mayor que MaxRulingThickness (3 puntos) se convierte en una línea a lo largo de su eje mayor, una caja rellena mayor aporta sus cuatro bordes, y cada coordenada de línea se ajusta dentro de RulingSnapTolerance (4 puntos) antes de ensamblar la rejilla. Las tablas exportadas desde Word, Google Docs y navegadores llegan por tanto al detector de rejillas como rejillas completas en vez de caer a la detección por espacios en blanco como fragmentos
El artículo anterior sobre detección y extracción de tablas decía que la detección de rejillas usa las líneas dibujadas y que cada segmento de trazado se transforma a coordenadas de página. Esa frase era cierta e incompleta. Contar los objetos de trazado en un conjunto de 13 documentos de muestra del mundo real mostró que 9 de ellos no contienen ningún trazado, y sin embargo cada una de sus páginas lleva cientos de rectángulos rellenos de 0,5 a 1 punto de grosor. El detector que solo miraba trazos no veía nada, todas las páginas caían a la detección por espacios en blanco, y la salida era un rosario de fragmentos pequeños en vez de tablas. El preset de columnas compactas añadido en la 3.116.4 suavizó eso a nivel de fragmento; la causa raíz era que el detector estaba leyendo el operador de pintado equivocado
¿Por qué una tabla exportada de Word no tiene líneas trazadas?
Un procesador de textos no piensa en un borde como una línea; piensa en una caja con un grosor, y pinta esa caja con un relleno. ISO 32000-1 §8.5.2.1 define el operador re como el que añade un subcamino rectangular, y §8.5.3 separa los operadores de pintado: S traza el camino con el grosor de línea actual y f rellena su interior. Un borde de celda de 0,5 puntos sale como x y w 0.5 re f, y la maquinaria de trazo, incluidos el grosor de línea, las uniones y el patrón de guiones, no llega a ejecutarse. El sombreado de celda es la misma construcción con una caja mayor. Una rejilla trazada con m, l y S es lo que esperaba el detector original, y es lo que no produce prácticamente nada exportado desde una aplicación de oficina:
% un borde de celda de un export de procesador de textos: una caja rellena de 0.5 pt de alto
72 700 468 0.5 re f
% sombreado de celda: una caja rellena del tamaño de la celda
72 676 117 24 re f
% la línea de rejilla trazada para la que se escribió el detector original
72 700 m 540 700 l S
Para un detector que solo pregunta a FPDFPath_GetDrawMode si el flag de trazo está puesto, las dos cajas rellenas son invisibles. Las palabras de dentro de las celdas llegan entonces a la detección por espacios en blanco, donde columnas separadas por un canal de 6 puntos quedan por debajo del MinColumnGap por defecto de 12 puntos, y lo que vuelve es el subconjunto de filas que dé la casualidad de alinearse lo bastante bien para pasar MinRows. Ese es el comportamiento de fragmento, y no hay ajuste de parámetros que lo convierta en la rejilla que dibujó el autor
¿Cómo convierte PDFium Component una caja rellena en una línea?
TableCollectObjectRulings inspecciona cada objeto de trazado subcamino a subcamino. El modo de pintado viene de FPDFPath_GetDrawMode; un camino cuenta como relleno cuando DetectFilledRulings está activado y el modo de relleno no es none. Cada punto se transforma por la matriz del objeto y se recoge, hasta MaxSubpathPoints (8) por subcamino, y cualquier segmento curvo marca el subcamino como curvo. Cuando el subcamino se cierra o empieza un MoveTo nuevo, FlushSubpath decide qué era: un subcamino curvo se descarta, y también cualquier polígono cerrado cuyos puntos no estén todos a menos de PointTolerance (0,05 puntos) de los bordes de la caja envolvente en al menos un eje. Un triángulo, un galón o una pestaña redondeada nunca se convierte en línea, y eso es lo que mantiene el arte decorativo fuera de la rejilla
Lo que sobrevive es un rectángulo alineado con los ejes, clasificado por su caja envolvente. Un ancho igual o menor que MaxRulingThickness con una altura mayor da una línea vertical en el centro horizontal, que recorre la caja de abajo arriba; el caso simétrico da una línea horizontal. Que ambas dimensiones superen el umbral significa celda sombreada, y la caja aporta cuatro líneas, una por borde. Que ambas dimensiones estén en el umbral o por debajo no aporta nada, así que un bullet cuadrado de 2 puntos no se confunde con una línea. Un camino trazado sigue la ruta antigua por AddLine, una línea por segmento alineado con los ejes, así que una rejilla dibujada con S se maneja exactamente igual que antes, y un camino pintado con relleno y trazo a la vez produce piezas solapadas que la pasada de fusión colapsa:
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
Mode: string;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'itinerary-from-word.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 1; // base 1
Options := TPdfTableExtractionOptions.Default;
// estos son los valores por defecto de la 3.117.0, explicitados por claridad
Options.DetectFilledRulings := True; // las cajas rellenas finas pasan a ser líneas
Options.MaxRulingThickness := 3.0; // puntos; las cajas más gruesas cuentan como sombreado
Options.RulingSnapTolerance := 4.0; // puntos; 0 desactiva el ajuste
Options.IncludeFormXObjects := True;
Tables := Pdf.ExtractTables(Options);
for I := 0 to High(Tables) do
begin
if Tables[I].DetectionMode = ptdmRuled then
Mode := 'ruled'
else
Mode := 'whitespace';
Writeln(Format('%dx%d %s, confidence %.2f',
[Tables[I].RowCount, Tables[I].ColumnCount, Mode,
Tables[I].Confidence]));
end;
finally
Pdf.Free;
end;
end;
¿Qué hace RulingSnapTolerance por las tablas de celdas sombreadas?
RulingSnapTolerance es lo que hace que una tabla construida solo con sombreado se conecte en una única rejilla. Algunos exports no dibujan borde ninguno: cada celda es una caja rellena de su propio color, y las cajas vecinas están separadas por un canal de blanco de 1 a 3 puntos. Cada caja da cuatro líneas de borde, pero el borde derecho de una celda y el izquierdo de la siguiente quedan a 2 puntos, y la prueba de conectividad usa RulingTolerance, que por defecto es 1 punto. Sin el ajuste, cada celda forma su propio componente conexo de cuatro líneas, ningún componente llega a MinRows, y la página no informa de nada. TableSnapRulings reúne todas las coordenadas X en juego (la posición de cada línea vertical más el inicio y el fin de cada horizontal) y todas las Y igual, ordena cada lista, la agrupa encadenando valores cuyo vecino no difiere más que la tolerancia, sustituye cada grupo por su media y luego mueve cada posición, inicio y fin al centro de grupo más cercano. Los dos lados de un canal pasan a ser la misma línea, y la conectividad se sostiene
El ajuste corre antes de TableMergeRulings, que ordena las líneas y une las piezas colineales que se tocan o se solapan dentro de RulingTolerance, y ambos corren antes de que TableDetectRuled llegue a ver los datos, así que la prueba de conectividad por pares es proporcional al número de líneas de rejilla y no al número de fragmentos por celda. En una rejilla trazada las pasadas son inofensivas, porque las coordenadas que ya eran idénticas se ajustan a sí mismas. Lo único que hay que tener en cuenta es que el agrupado en cadena no tiene límite de anchura propio: una serie de coordenadas separadas 3 puntos entre sí colapsa en un único centro. Con el valor por defecto de 4 puntos eso solo afecta a columnas más estrechas que un carácter, pero si un documento tiene canales reales de 3 puntos que deben seguir separados, baja la tolerancia o ponla a 0 para desactivar el ajuste:
// Aísla la estrategia de rejilla y compara qué ve cada ajuste en una página
function CountRuledTables(Pdf: TPdf; FilledRulings: Boolean;
SnapTolerance: Double): Integer;
var
Options: TPdfTableExtractionOptions;
begin
Options := TPdfTableExtractionOptions.Default;
Options.DetectWhitespaceTables := False;
Options.DetectFilledRulings := FilledRulings;
Options.RulingSnapTolerance := SnapTolerance;
Result := Length(Pdf.ExtractTables(Options));
end;
// Un export de Word típicamente da 0, N y luego menos de N:
// solo-trazo no ve nada, el ajuste conecta las celdas sombreadas,
// y desactivar el ajuste deja cada celda sombreada como su propia isla
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));
Líneas dentro de form XObjects
Las herramientas de maquetación envuelven con frecuencia una tabla, o todo el cuerpo de la página, en un form XObject y lo pintan con Do. ISO 32000-1 §8.10.1 especifica que la matriz del formulario se concatena con la matriz de transformación actual cuando se pinta el formulario, así que un rectángulo dentro del formulario vive en espacio de formulario y solo aterriza en la página tras dos o más transformaciones. TableCollectObjectRulings entra recursivamente en los objetos de formulario cuando IncludeFormXObjects está activado: lee la matriz del objeto, la combina con la matriz padre mediante TableMultiplyMatrix, cuyo orden de argumentos significa «pasa por la primera matriz y luego por la segunda», y enumera los hijos con FPDFFormObj_CountObjects y FPDFFormObj_GetObject, pasando hacia abajo la matriz combinada. Un anidamiento más profundo que MaxFormDepth (8) se salta en silencio, que es una defensa contra archivos patológicos y no un límite que ningún export real roce. El motivo de que importe el orden de multiplicación es el mismo que se trata en anteponer frente a añadir matrices: intercambiar los operandos mueve el término de traslación, y una línea que debería aterrizar en lo alto de la página aterriza en el origen
¿Por qué se cuadruplicó el presupuesto de líneas?
El MaxRulingSegments por defecto subió de 4096 a 16384 en la 3.117.0 porque los bordes por celda llegan en cantidades mucho mayores que las líneas de rejilla trazadas. Una tabla trazada de 30 filas y 6 columnas son 38 segmentos de línea. Esa misma tabla exportada como cajas rellenas son hasta cuatro bordes por celda, 720 piezas antes de fusionar, y un formulario con celdas sombreadas lo dobla. Dos tablas así en una página habrían agotado el presupuesto antiguo. El presupuesto se aplica en TableAppendRuling mediante Check, que lanza EPdfError con el mensaje «Table ruling-segment budget exceeded»; no hay resultado degradado, no hay rejilla parcial, y la pasada de espacios en blanco tampoco corre. Si pones un presupuesto más ajustado por tu cuenta para entradas no confiables, captura la excepción y decide, en vez de leer un resultado vacío como «no hay tablas»:
Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048; // deliberadamente ajustado para entradas no confiables
try
Tables := Pdf.ExtractTables(Options);
except
on E: EPdfError do
begin
Log(E.Message); // 'Table ruling-segment budget exceeded'
Options.MaxRulingSegments := 16384; // el valor por defecto de la 3.117.0
Tables := Pdf.ExtractTables(Options);
end;
end;
Resultados medidos y dónde se para el enfoque
Sobre los mismos 13 documentos de muestra, la extracción pasó de 43 tablas, 9 de ellas de rejilla y 34 fragmentos de espacios en blanco o falsos positivos, a 41 tablas de rejilla y ningún falso positivo de espacios en blanco. Parte de esa limpieza corresponde a dos cambios acompañantes de la 3.117.0: las palabras que ya ha reclamado una rejilla se eliminan antes de que corra la detección por espacios en blanco, así que una tabla nunca se informa dos veces, y un límite de columna por espacios en blanco debe ser ahora un corredor libre de texto en todas las filas que separa, que es lo que impidió que párrafos justificados puntuaran como tablas de 5x4. El lector de rectángulos rellenos es lo que movió las tablas en sí de la columna de fragmentos a la de rejillas
Los límites merecen decirse sin rodeos. Una página sin capa de texto sigue dando el esqueleto de la rejilla, con todas las celdas vacías, porque las líneas vienen de la geometría y el texto viene de la página de texto; las páginas escaneadas necesitan OCR antes. Las formas rellenas con curvas, esquinas redondeadas o contornos no rectangulares se descartan por completo, así que una tabla cuyos bordes estén dibujados como contornos de rectángulo redondeado necesita la detección por espacios en blanco como antes. Una tabla sin bordes ni sombreado no cambia con nada de esto y sigue siendo territorio de la estrategia de espacios en blanco que describe el artículo de extracción de tablas; cuando ni eso basta, las cajas de palabra y los bloques de texto estructurado y orden de lectura son la materia prima para un lector específico del dominio. La demo TableExtractionLab que se distribuye con el componente expone DetectFilledRulings en su panel de opciones, que es la forma más rápida de ver cómo queda un export concreto con y sin él; la API completa se describe en la página de PDFium Component para Delphi