A extração de tabelas do PDFium Component, a partir da versão 3.117.0, trata um retângulo preenchido fino como uma linha de tabela. Com o DetectFilledRulings ativado, que é o predefinido, uma caixa preenchida alinhada aos eixos com espessura não superior a MaxRulingThickness (3 pontos) torna-se uma linha ao longo do seu eixo maior, uma caixa preenchida maior contribui com as suas quatro arestas, e todas as coordenadas das linhas são aproximadas dentro do RulingSnapTolerance (4 pontos) antes de a grelha ser montada. As tabelas exportadas do Word, do Google Docs e de navegadores chegam por isso ao detetor de grelhas como grelhas completas, em vez de caírem na deteção por espaços como fragmentos
O artigo anterior sobre deteção e extração de tabelas afirmava que a deteção por grelhas usa as linhas desenhadas e que cada segmento de caminho traçado é transformado em coordenadas de página. Essa frase era verdadeira e incompleta. Contar os objetos de caminho num conjunto de 13 documentos reais mostrou que 9 deles não contêm nenhum caminho traçado, e no entanto cada uma das suas páginas transporta centenas de retângulos preenchidos com 0,5 a 1 ponto de espessura. O detetor só de traços não via nada, todas as páginas caíam na deteção por espaços, e o resultado era um espalhamento de pequenos fragmentos em vez de tabelas. O preset de colunas compactas acrescentado na 3.116.4 suavizou isso ao nível do fragmento; a causa raiz era o detetor estar a ler o operador de pintura errado
Porque é que uma tabela exportada do Word não tem linhas traçadas?
Um processador de texto não pensa numa fronteira como uma linha; pensa nela como uma caixa com uma largura, e pinta essa caixa com um preenchimento. A ISO 32000-1 §8.5.2.1 define o operador re como acrescentando um subcaminho retangular, e a §8.5.3 separa os operadores de pintura: o S traça o caminho com a largura de linha atual, o f preenche o seu interior. Uma fronteira de célula de 0,5 pontos sai como x y w 0.5 re f, e a maquinaria de traço, largura de linha, junções e padrão de traços incluídos, nunca corre. O sombreado de célula é a mesma construção com uma caixa maior. Uma grelha traçada desenhada com m, l e S é o que o detetor original esperava, e é o que quase nada exportado de uma aplicação de escritório produz:
% uma fronteira de célula de uma exportação de processador de texto: uma caixa preenchida de 0,5 pt de altura
72 700 468 0.5 re f
% sombreado de célula: uma caixa preenchida do tamanho da célula
72 676 117 24 re f
% a linha de grelha traçada para a qual o detetor original foi escrito
72 700 m 540 700 l S
Para um detetor que só pergunta ao FPDFPath_GetDrawMode se o flag de traço está ligado, as duas caixas preenchidas são invisíveis. As palavras dentro das células chegam então à deteção por espaços, onde colunas separadas por um gutter de 6 pontos ficam abaixo do MinColumnGap predefinido de 12 pontos, e o que volta é o subconjunto de linhas que por acaso se alinhe o suficiente para passar o MinRows. É este o comportamento de fragmento, e não há afinação de parâmetros que o transforme na grelha que o autor desenhou
Como é que o PDFium Component transforma uma caixa preenchida numa linha?
O TableCollectObjectRulings inspeciona cada objeto de caminho, um subcaminho de cada vez. O modo de desenho vem do FPDFPath_GetDrawMode; um caminho conta como preenchido quando o DetectFilledRulings está ligado e o modo de preenchimento não é none. Cada ponto é transformado pela matriz do objeto e recolhido, até MaxSubpathPoints (8) por subcaminho, e qualquer segmento curvo marca o subcaminho como curvo. Quando o subcaminho fecha ou começa um novo MoveTo, o FlushSubpath decide o que era: um subcaminho curvo é descartado, e também o é qualquer polígono fechado cujos pontos não estejam todos dentro do PointTolerance (0,05 pontos) das arestas da caixa envolvente pelo menos num eixo. Um triângulo, uma seta angular ou uma aba arredondada nunca se torna uma linha, e é isso que mantém os elementos decorativos fora da grelha
O que sobrevive é um retângulo alinhado aos eixos, classificado pela sua caixa envolvente. Uma largura igual ou inferior ao MaxRulingThickness com altura acima dele dá uma linha vertical no centro horizontal, cobrindo a caixa de baixo a cima; o caso espelhado dá uma linha horizontal. Ambas as dimensões acima do limite significam uma célula sombreada, e a caixa contribui com quatro linhas, uma por aresta. Ambas as dimensões iguais ou inferiores ao limite não contribuem com nada, pelo que um marcador quadrado de 2 pontos não é confundido com uma linha. Um caminho traçado segue o caminho mais antigo através do AddLine, uma linha por segmento alinhado aos eixos, pelo que uma grelha desenhada com S é tratada exatamente como antes, e um caminho pintado com preenchimento e traço produz peças sobrepostas que a passagem de junção 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; // com base 1
Options := TPdfTableExtractionOptions.Default;
// estes são os valores predefinidos da 3.117.0, escritos por clareza
Options.DetectFilledRulings := True; // caixas preenchidas finas tornam-se linhas
Options.MaxRulingThickness := 3.0; // pontos; caixas mais espessas contam como sombreado
Options.RulingSnapTolerance := 4.0; // pontos; 0 desativa a aproximação
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;
O que é que o RulingSnapTolerance faz pelas tabelas de células sombreadas?
O RulingSnapTolerance é o que faz uma tabela construída só com sombreado ligar-se numa única grelha. Algumas exportações não desenham fronteira nenhuma: cada célula é uma caixa preenchida com a sua cor, e as caixas vizinhas estão separadas por um gutter de 1 a 3 pontos de branco. Cada caixa dá quatro linhas de aresta, mas a aresta direita de uma célula e a aresta esquerda da seguinte ficam a 2 pontos de distância, e o teste de conectividade usa o RulingTolerance, que é 1 ponto por predefinição. Sem aproximação, cada célula forma a sua própria componente ligada de quatro linhas, nenhuma componente chega ao MinRows, e a página não reporta nada. O TableSnapRulings recolhe todas as coordenadas X em jogo (a posição de cada linha vertical mais o início e o fim de cada horizontal) e todas as coordenadas Y da mesma forma, ordena cada lista, agrupa-a encadeando valores cujo vizinho difira no máximo a tolerância, substitui cada grupo pela sua média e depois move cada posição, início e fim para o centro de grupo mais próximo. Os dois lados de um gutter passam a ser a mesma linha, e a conectividade mantém-se
A aproximação corre antes do TableMergeRulings, que ordena as linhas e junta peças colineares que se toquem ou sobreponham dentro do RulingTolerance, e ambas correm antes de o TableDetectRuled ver os dados, pelo que a verificação de conectividade aos pares é proporcional ao número de linhas de grelha e não ao número de fragmentos por célula. Numa grelha traçada as passagens são inofensivas, porque coordenadas que já eram idênticas se aproximam a si mesmas. A única coisa a ter em conta é que o agrupamento em cadeia não tem limite de largura próprio: uma série de coordenadas a 3 pontos de distância umas das outras colapsa num único centro. Com os 4 pontos predefinidos isso só afeta colunas mais estreitas do que um caractere, mas se um documento tiver gutters reais de 3 pontos que tenham de ficar separados, baixe a tolerância ou ponha-a a 0 para desativar a aproximação:
// Isolar a estratégia de grelhas e comparar o que cada definição vê numa 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;
// Uma exportação do Word reporta tipicamente 0, N e depois menos de N:
// o detetor só de traços não vê nada, a aproximação liga as células sombreadas,
// e desativar a aproximação deixa cada célula sombreada como a sua própria ilha
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));
Linhas dentro de form XObjects
As ferramentas de composição de páginas embrulham frequentemente uma tabela, ou o corpo inteiro da página, num form XObject e pintam-no com Do. A ISO 32000-1 §8.10.1 especifica que a matriz do form é concatenada com a matriz de transformação atual quando o form é pintado, pelo que um retângulo dentro do form vive no espaço do form e só aterra na página depois de duas ou mais transformações. O TableCollectObjectRulings entra recursivamente nos objetos de form quando o IncludeFormXObjects está definido: lê a matriz do objeto, combina-a com a matriz do pai através do TableMultiplyMatrix, cuja ordem de argumentos significa «mapear pela primeira matriz e depois pela segunda», e enumera os filhos com FPDFFormObj_CountObjects e FPDFFormObj_GetObject, passando a matriz combinada para baixo. Aninhamento mais fundo do que o MaxFormDepth (8) é saltado em silêncio, o que é uma proteção contra ficheiros patológicos e não um limite que alguma exportação real aproxime. A razão pela qual a ordem da multiplicação importa é a mesma que se discute em prepend versus append de matrizes: trocar os operandos altera o termo de translação, e uma linha que devia aterrar no topo da página aterra na origem
Porque é que o orçamento de linhas quadruplicou?
O MaxRulingSegments predefinido subiu de 4096 para 16384 na 3.117.0 porque as fronteiras por célula chegam em números muito maiores do que as linhas traçadas de grelha. Uma tabela traçada de 30 linhas e 6 colunas são 38 segmentos de linha. A mesma tabela exportada como caixas preenchidas chega a quatro fronteiras por célula, 720 peças antes da junção, e um formulário com células sombreadas duplica isso. Duas tabelas dessas numa página teriam esgotado o orçamento antigo. O orçamento é imposto no TableAppendRuling através do Check, que lança EPdfError com a mensagem «Table ruling-segment budget exceeded»; não há resultado degradado, nem grelha parcial, e a passagem por espaços também não corre. Se definir um orçamento mais apertado para entrada não fidedigna, apanhe a exceção e decida, em vez de ler um resultado vazio como «não há tabelas»:
Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048; // deliberadamente apertado para entrada não fidedigna
try
Tables := Pdf.ExtractTables(Options);
except
on E: EPdfError do
begin
Log(E.Message); // 'Table ruling-segment budget exceeded'
Options.MaxRulingSegments := 16384; // o valor predefinido da 3.117.0
Tables := Pdf.ExtractTables(Options);
end;
end;
Resultados medidos e onde a abordagem para
Nos mesmos 13 documentos de amostra, a extração passou de 43 tabelas, 9 delas por grelhas e 34 fragmentos ou falsos positivos por espaços, para 41 tabelas por grelhas e zero falsos positivos por espaços. Parte dessa limpeza pertence a duas alterações companheiras da 3.117.0: as palavras já reclamadas por uma grelha são removidas antes de a deteção por espaços correr, para que uma tabela nunca seja reportada duas vezes, e uma fronteira de coluna por espaços tem agora de ser um corredor sem texto ao longo de todas as linhas que separa, e foi isso que impediu parágrafos justificados de pontuarem como tabelas 5x4. O leitor de retângulos preenchidos é o que moveu as próprias tabelas da coluna dos fragmentos para a coluna das grelhas
Vale a pena enunciar os limites com clareza. Uma página sem camada de texto continua a produzir o esqueleto da grelha, com todas as células vazias, porque as linhas vêm da geometria e o texto vem da página de texto; as páginas digitalizadas precisam de OCR primeiro. As formas preenchidas com curvas, cantos arredondados ou contornos não retangulares são descartadas por completo, pelo que uma tabela cujas fronteiras sejam desenhadas como contornos de retângulo arredondado precisa da deteção por espaços, como antes. Uma tabela sem fronteiras nem sombreado não é alterada por nada disto e continua a ser território da estratégia por espaços descrita no artigo sobre extração de tabelas; quando nem isso chega, as caixas de palavra e os blocos de texto estruturado e ordem de leitura são a matéria-prima para um leitor específico do domínio. A demo TableExtractionLab que acompanha o componente expõe o DetectFilledRulings no seu painel de opções, que é a forma mais rápida de ver como fica uma dada exportação com e sem ele; a API completa está descrita na página do PDFium Component para Delphi