A PDF Library for Delphi renderiza HTML numa página PDF com layout bidimensional real: display: flex e display: grid são medidos e colocados em vez de serem degradados para blocos empilhados, e as notas de rodapé são reservadas na parte inferior da caixa que transporta a sua referência, com numeração que se mantém contínua ao longo de colunas e páginas. Os pontos de entrada são os habituais, DrawHTMLTextBox para uma única caixa e DrawHTMLStory para fluxo em várias colunas
Isto importa porque o HTML é a forma como a maioria do conteúdo de relatórios chega atualmente. Os modelos são criados por pessoas que escrevem CSS, os painéis são concebidos como cartões, e um renderizador que colapsa silenciosamente uma linha flex em quatro blocos empilhados produz um documento que não se assemelha de todo ao design. Até esta capacidade existir, o único contentor bidimensional que o motor media era a tabela, pelo que todo o layout de cartões tinha de ser reescrito à mão como uma tabela
O que mudou no modelo de layout?
O ciclo principal anterior mantinha uma única caixa de linha e avançava pela página abaixo. Esse modelo trata perfeitamente o conteúdo em linha e os blocos empilhados, e não consegue exprimir um contentor cujos filhos são dimensionados uns em relação aos outros. As tabelas eram a única exceção, com a sua própria medição em duas passagens
O flex e o grid acrescentam cada um uma passagem de medição limitada sobre os filhos de um contentor, e a palavra importante é limitada. Um contentor flex mede até 256 filhos diretos para um array fixo. Uma grid usa uma matriz de ocupação de, no máximo, 64 por 64 células para colocação automática determinística. Esses tetos existem para que uma folha de estilos hostil ou gerada não consiga provocar recursão sem limite ou memória de colocação quadrática, o que é uma preocupação real quando o HTML vem de um modelo que um cliente edita
Como os itens flex obtêm os seus tamanhos
Na direção da linha, o contentor soma a base de cada item juntamente com os seus pesos de crescimento e encolhimento, e depois distribui o espaço restante, positivo ou negativo, de acordo com esses pesos. Com flex-wrap, cada linha é resolvida independentemente, pelo que uma linha que quebra em duas linhas atribui espaço livre por linha em vez de em todo o contentor. Na direção da coluna, a mesma distribuição do eixo principal corre contra uma altura explícita ou a altura do conteúdo
justify-content, align-items, gap e as direções inversas operam sobre geometria já medida. Movem caixas; nunca despoletam uma nova medição do conteúdo do item. Essa separação é o que impede um painel complexo de medir os seus filhos várias vezes:
uses
PDFlibrary;
var
Lib: TPDFlib;
Html, Remainder: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
Lib.SetPageSize('A4');
Lib.NewPage;
Html :=
'<div style="display:flex; gap:12px;">' +
' <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Revenue</b><br/>EUR 4,182,300</div>' +
' <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Margin</b><br/>18.4%</div>' +
' <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Backlog</b><br/>92 days</div>' +
'</div>';
Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
if Remainder <> '' then
Log('content did not fit - carry the remainder to the next box');
Lib.SaveToFile('dashboard.pdf');
finally
Lib.Free;
end;
end;
O valor devolvido é a string de continuação, que é a forma como cada ponto de entrada de desenho de HTML reporta o que não coube. Passe-a para a caixa seguinte ou para a página seguinte e o fluxo retoma onde parou
Colocação na grid, e o que uma faixa pode ser
As faixas de grid aceitam comprimentos fixos, percentagens, a unidade fr, expressões simples de repeat() e minmax(). A colocação automática preenche a matriz de ocupação de forma determinística, pelo que o mesmo HTML produz sempre a mesma disposição. As coordenadas explícitas podem sobrepor-se, o que é deliberado: um design que sobrepõe um selo a um cartão está a exprimir intenção, não um erro. Quando só um eixo é dado explicitamente, a colocação procura apenas no outro eixo
Os itens que se estendem por várias linhas contribuem a sua altura medida de volta para as linhas que cobrem, calculada em média entre elas, o que impede que um item alto que se estende comprima uma única linha enquanto deixa as vizinhas baixas:
Html :=
'<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
' gap:10px;">' +
' <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
' <div>Inspector</div>' +
' <div>Date</div>' +
' <div style="grid-column:2 / span 2;">Findings summary</div>' +
'</div>';
Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);
Os filhos flex e grid são renderizados através do mesmo renderizador de HTML que tudo o resto, o que é a propriedade que torna a funcionalidade utilizável, e não um mundo à parte. Os tipos de letra, a cascata CSS, as ligações, as imagens, as tabelas e outros contentores flex ou grid aninhados comportam-se dentro de um item flex exatamente como se comportam ao nível de topo, e o plano de layout exterior regista os comandos finais de texto e retângulo, pelo que a repetição do desenho reutiliza a cache de medição existente
Por que razão as notas de rodapé são um problema de paginação?
Uma nota de rodapé não é conteúdo que flui depois do parágrafo que contém a sua referência; é conteúdo que tem de aparecer na parte inferior da mesma caixa que a sua referência. Isso inverte a ordem de medição habitual, porque o espaço disponível para o texto do corpo passa a depender de conteúdo que ainda não foi disposto
O renderizador mede, por isso, a nota quando encontra a referência, e subtrai a área da nota ao orçamento de altura do corpo da caixa limitada atual. Se a referência, o texto do corpo até então e a nota não couberem todos, o marcador de rodapé e tudo o que vem depois passam em conjunto para a string de continuação. Essa regra é o que evita as duas falhas clássicas: uma nota a sobrepor-se ao texto do corpo, e uma nota isolada numa página cuja referência está na página anterior
Numa caixa limitada, a área da nota é fixada à parte inferior, com uma régua separadora acima dela. Na medição sem limite, onde não há altura de caixa a que fixar, a área da nota segue imediatamente depois do corpo. A numeração é transportada num campo de extensão na pilha de continuação, pelo que DrawHTMLTextBox e DrawHTMLStory mantêm a sequência a correr ao longo de colunas e páginas, e uma string de continuação produzida antes desse campo existir continua ainda assim a retomar corretamente:
// As notas de rodapé dentro de uma história em várias colunas mantêm
// uma única sequência corrida
Html := LoadTemplate('chapter.html'); // usa marcadores float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
2, // colunas
16, // calha em pontos
20, // número máximo de páginas para esta história
Html);
if Remainder <> '' then
Log('story exceeded its page budget');
Orientação prática para autores de modelos
Faça o design dentro dos tetos documentados. Um contentor flex com mais de 256 filhos diretos é quase sempre uma tabela de dados disfarçada de flex, e o percurso de tabela mede-a melhor de qualquer forma. Uma grid maior do que 64 por 64 é uma folha de cálculo, e o mesmo conselho se aplica. Para texto de corpo em várias colunas, o comportamento de colunas e hifenização descrito em hifenização e colunas de texto equilibradas governa o aspeto do fluxo dentro de cada coluna
Meça antes de desenhar quando um layout tem de caber. GetHTMLTextHeight reporta a altura de que uma dada largura precisaria, o que é a forma económica de decidir entre um layout e outro antes de comprometer tinta. E trate uma string de continuação não vazia como normal, e não como excecional: é o mecanismo pelo qual o conteúdo longo se pagina, não um sinal de erro
Quando o HTML vem de um motor de relatórios em vez de modelos escritos à mão, o percurso orientado por conjunto de dados em o motor de relatórios por conjunto de dados combina bem com isto, gerando a marcação que o flex e o grid depois dispõem. E quando o mesmo conteúdo também tem de sair do PDF outra vez, o percurso de exportação semântica em exportar PDF para Markdown e DOCX fecha o ciclo completo
O layout de HTML, a geração de relatórios e a exportação semântica fazem parte de uma única biblioteca para Delphi, C++Builder e Free Pascal; a lista completa de funcionalidades está na página da PDF Library for Delphi