O HotPDF renderiza tabelas HTML através do seu perfil de paged-media HTML5 usando uma grelha de ocupação real para rowspan e colspan, alturas de linha medidas em vez de estimativas por contagem de caracteres, e linhas de cabeçalho repetidas em cada página de continuação. Duas situações fazem-no recusar repetir um cabeçalho, e conhecê-las à partida é mais barato do que fazer debugging de uma célula duplicada mais tarde
A classe de documento que obriga a isto é a que qualquer equipa de relatórios acaba por distribuir: uma fatura ou um relatório de conformidade em que a fonte de verdade é HTML, a tabela corre por quatro páginas, e o cabeçalho tem de estar legível em todas. Qualquer coisa menos um layout de tabela real produz as duas falhas que os leitores notam de imediato, um cabeçalho que aparece uma vez na página um e linhas cujas alturas foram adivinhadas por contagem de caracteres
Porque é que a capacidade de tabelas passou para o renderizador HTML?
Porque a alternativa perde rich text, e rich text é a razão de o conteúdo ser HTML à partida. O plano óbvio parece reutilização: o HotPDF já tem um objeto de tabela do layout DOM com uma grelha a sério, por isso basta ligar o parser HTML a ele e obter o spanning de graça. O problema é com o que esse objeto de tabela desenha. As suas células transportam texto e um estilo, e o seu caminho de desenho emite output de texto simples, por isso tudo o que o HTML realmente continha além de uma fonte e uma cor — links, superscritos, mudanças de tamanho inline, cor por run — desaparece quando chega à página
A direção que sobrevive ao contacto com documentos reais é a inversa. Mover as capacidades do motor de tabelas — a grelha de ocupação, a medição real, a repetição de cabeçalhos e a ponderação de colunas — para dentro do renderizador HTML, e deixar a renderização de rich text onde já funciona. É uma alteração maior do que a ponte, e é a alteração que mantém um hyperlink dentro de uma célula de tabela um hyperlink
Rowspan sem union-find
Células em span criam grupos de linhas atómicos, mas o fecho sobre esses grupos não precisa de uma estrutura disjoint-set geral, porque a ocupação é sempre um intervalo contíguo. Uma célula com rowspan="3" a começar na linha K ocupa as linhas K até K+2 e nada mais, por isso a informação de grupo reduz-se a um marcador de fim por linha
O algoritmo são duas linhas de intenção. Quando coloca uma célula em span que começa em K e acaba em E, registe GroupEnd[K] := Max(GroupEnd[K], E). Depois percorra as linhas uma vez em ordem inversa e aplique G[R] := G[G[R]], que propaga cada fim de linha para trás através de spans sobrepostos e produz o fecho transitivo numa única passagem. O que obtém é, para cada linha, a última linha que tem de ficar na mesma página que ela, que é exatamente o que o passo de paginação precisa para decidir onde uma rutura pode cair
Distribuir a altura é a outra metade. Quando uma célula em span precisa de mais espaço vertical do que as linhas que cobre atualmente proporcionam, o excedente vai para a última linha do span, e não espalhado por igual. Processe as células em span depois de as alturas de linha ordinárias estarem resolvidas, e depois complete a última linha de cada span. Espalhar o excedente por igual parece mais justo e produz output visivelmente errado: linhas que só contêm células curtas de uma linha ficam inchadas porque qualquer célula não relacionada três linhas acima calhou ser alta
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Stats: THPDFHTMLImportStatistics;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'audit-report.pdf';
Pdf.BeginDoc;
Importer := THPDFHTMLImporter.Create(Pdf);
try
Importer.Margin := 48;
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Importer.MaxDOMNodes := 200000;
Importer.MaxLayoutOperations := 2000000;
if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
begin
Stats := Importer.Statistics;
Writeln('tables ', Stats.TableCount,
' page breaks ', Stats.PageBreakCount);
end;
finally
Importer.Free;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
RenderHTML5 recebe uma folha de estilo de autoria opcional como segundo argumento, que é onde as regras de impressão pertencem. Mantenha a folha de estilo de ecrã fora daí. O perfil é versionado, e HTML5ProfileMilestones reporta que grupos de capacidade a build atual implementa — himParserCascade, himPagedLayout, himTablesForms e himBoundedResources — para que uma aplicação possa degradar-se deliberadamente em vez de descobrir uma lacuna em produção
A medição tem de concordar com o desenho, exatamente
A altura de linha só está correta quando o código que mede as linhas embrulhadas as embrulha pela mesma regra que o código que as desenha. Isto soa óbvio e é a fonte mais comum de tabelas cujos limites não alinham com os conteúdos. O HotPDF mede com um contador de linhas greedy, e esse contador tem de casar com a semântica de wrapping do caminho de output de rich text em três respeitos específicos: quebra só em espaços, nunca parte uma palavra, e uma palavra mais larga do que a coluna recebe uma linha própria
O segundo requisito é a fonte. A medição tem de correr com a fonte da própria célula, definida através de SetFont com o nome real, o conjunto de estilos e o tamanho antes de chamar a função de largura, e não com a fonte que calhasse estar ativa. Texto a negrito é rotineiramente mais de dez por cento mais largo do que o regular ao mesmo tamanho, o que chega para mudar uma célula de três linhas numa de quatro. Uma tabela em que as células de cabeçalho são a negrito e as de corpo não, medida com uma única fonte, estará errada precisamente nas linhas que os leitores olham primeiro
Acertar nisto muda o que se pode afirmar num teste. O efeito observável de uma medição precisa é o espaçamento entre linhas, não as contagens de glifos: uma linha de uma única linha tem cerca de 20 points de altura enquanto uma estimativa por contagem de caracteres do mesmo conteúdo prevê duas linhas e cerca de 35. Afirme sobre a distância vertical entre linhas. E lembre-se de que o user space do PDF tem Y a crescer para cima, por isso um cabeçalho sentado acima de uma linha de corpo significa que o valor Y do cabeçalho é o maior, o oposto do que o instinto de coordenadas de ecrã escreve
Quando é que o HotPDF recusa repetir um cabeçalho?
Em dois casos, ambos os quais produziriam output visivelmente errado se avançasse. O primeiro é um bloco de cabeçalho que contém uma célula em span que se estende para lá do cabeçalho, para linhas do corpo. Repetir o cabeçalho desenharia esse conteúdo de célula uma segunda vez numa posição onde já não pertence, por isso o cabeçalho é desenhado uma vez e a tabela continua sem ele. O segundo é um cabeçalho mais alto do que 90 por cento da altura útil da página, onde a repetição deixaria quase nenhum espaço para dados e a tabela não progrediria
Ambas as recusas são deliberadas e silenciosas por design, porque a alternativa é pior. Se o seu cabeçalho não repete e esperava que repetisse, verifique a markup à procura de um rowspan a cruzar o limite do thead antes de suspeitar do motor. Esse único padrão de markup responde pela maioria das surpresas
// Os pesos das colunas vêm da markup, por isso a folha de estilo de impressão é
// o sítio para os controlar. As larguras são tratadas como pesos, não como pixels
const
PrintStyleSheet =
'table { width: 100%; }' +
'thead th { font-weight: bold; background: #eee; }' +
'td.amount { text-align: right; }';
// Uma linha de cabeçalho que traz um rowspan a cruzar para o corpo suprime
// a repetição do cabeçalho. Mantenha os spans dentro de uma secção:
// <thead><tr><th rowspan="2">Item</th>...</tr></thead> ok
// <tr><th rowspan="3">Item</th>... a cruzar para o tbody, sem repetição
As larguras de coluna comportam-se como pesos e não como medições absolutas, que é o comportamento que mantém uma tabela utilizável quando o conteúdo não casa com a estimativa do autor. Uma coluna declarada a 30 por cento recebe aproximadamente 30 por cento da largura disponível, mas a distribuição respeita a largura mínima de que cada coluna realmente precisa, por isso uma coluna estreita que contenha um token longo e indivisível não transborda silenciosamente a caixa da tabela
Onde isto encaixa num pipeline de documentos
O trabalho de tabelas senta-se dentro do perfil de paged-media mais abrangente, e as regras de paginação, os orçamentos de recursos e o tratamento de CSS descritos em o caminho de importação de paged-media HTML5 aplicam-se sem alteração a documentos que contêm tabelas. Se os seus dados não começam como HTML, a rota de construção direta em construir tabelas diretamente num PDF evita a camada de parsing por completo e dá-lhe o mesmo comportamento de grelha através de uma API. E porque a altura de linha depende em última análise de onde as linhas quebram, a discussão de medição em justificação de texto e quebra de linhas é a peça complementar para quem afina output tabular denso
A lição reutilizável aqui nem sequer é sobre tabelas. Quando um subsistema novo precisa de uma capacidade que um subsistema antigo já tem, pergunte qual dos dois possui a coisa mais difícil de reimplementar. A aritmética da grelha são algumas dezenas de linhas e muda-se facilmente. A renderização de rich text com links inline, superscritos e estilos por run não é, por isso a grelha mudou e o texto ficou. O HotPDF distribui os dois caminhos como parte do componente PDF Delphi HotPDF, por isso a escolha entre input HTML e construção direta é uma decisão de projeto e não da biblioteca