A PDF Library for Delphi renderiza HTML em uma página de PDF com layout bidimensional real: display: flex e display: grid são medidos e posicionados, em vez de degradados para blocos empilhados, e as notas de rodapé são reservadas na parte inferior da caixa que carrega sua referência, com numeração que permanece contínua entre colunas e páginas. Os pontos de entrada são os conhecidos, DrawHTMLTextBox para uma única caixa e DrawHTMLStory para fluxo em várias colunas
Isso importa porque o HTML é como a maioria do conteúdo de relatório chega hoje. Templates são escritos por pessoas que escrevem CSS, painéis são projetados como cartões, e um renderizador que silenciosamente colapsa uma linha flex em quatro blocos empilhados produz um documento que não se parece nada com o design. Até essa capacidade existir, o único contêiner bidimensional que o mecanismo media era a tabela, então todo layout de cartão tinha que ser reescrito como uma tabela manualmente
O que mudou no modelo de layout?
O loop principal anterior mantinha uma única caixa de linha e avançava página abaixo. Esse modelo trata conteúdo em linha e blocos empilhados perfeitamente e não consegue expressar um contêiner cujos filhos são dimensionados em relação uns aos outros. As tabelas eram a única exceção, com sua própria medição em duas passagens
Flex e grid adicionam cada um uma passagem de medição limitada sobre os filhos de um contêiner, e a palavra importante é limitada. Um contêiner flex mede até 256 filhos diretos em um array fixo. Um grid usa uma matriz de ocupação de no máximo 64 por 64 células para posicionamento automático determinístico. Esses tetos existem para que uma folha de estilo hostil ou gerada não consiga provocar recursão sem limite ou memória de posicionamento quadrática, o que é uma preocupação real quando o HTML vem de um template que um cliente edita
Como os itens flex ganham seus tamanhos
Na direção da linha, o contêiner soma a base de cada item junto com seus pesos de crescimento e encolhimento, depois distribui o espaço restante, positivo ou negativo, de acordo com esses pesos. Com flex-wrap, cada linha é resolvida independentemente, então uma linha que se quebra em duas atribui espaço livre por linha, em vez de em todo o contêiner. Na direção da coluna, a mesma distribuição do eixo principal roda contra uma altura explícita ou a altura do conteúdo
justify-content, align-items, gap e as direções invertidas operam sobre geometria que já foi medida. Eles movem caixas; nunca disparam uma remedição do conteúdo do item. Essa separação é o que impede que um painel complexo meça 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 de retorno é a string de continuação, que é como cada ponto de entrada de desenho de HTML relata o que não coube. Passe-a para a próxima caixa ou para a próxima página, e o fluxo retoma de onde parou
Posicionamento em grid, e o que uma trilha pode ser
As trilhas de grid aceitam comprimentos fixos, porcentagens, a unidade fr, expressões simples de repeat() e minmax(). O posicionamento automático preenche a matriz de ocupação de forma determinística, então o mesmo HTML sempre produz o mesmo arranjo. Coordenadas explícitas podem se sobrepor, o que é proposital: um design que sobrepõe um selo a um cartão está expressando intenção, não um erro. Quando apenas um eixo é dado explicitamente, o posicionamento busca apenas no outro eixo
Itens que abrangem várias linhas contribuem com sua altura medida de volta para as linhas que cobrem, calculada em média entre elas, o que impede que um item alto que abrange várias linhas espreme uma única linha enquanto deixa suas 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);
Filhos de flex e grid são renderizados pelo mesmo renderizador HTML que todo o resto, o que é a propriedade que torna o recurso utilizável, e não um mundo à parte. Fontes, a cascata CSS, links, imagens, tabelas e outros contêineres flex ou grid aninhados se comportam dentro de um item flex exatamente como se comportam no nível superior, e o plano de layout externo registra os comandos finais de texto e retângulo, de modo que um desenho repetido reutiliza o cache de medição existente
Por que 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 sua referência; é conteúdo que precisa aparecer na parte inferior da mesma caixa que sua referência. Isso inverte a ordem usual de medição, porque o espaço disponível para o texto do corpo agora depende de conteúdo que ainda não foi diagramado
O renderizador, portanto, mede a nota quando encontra a referência, e subtrai a área da nota do orçamento de altura do corpo da caixa delimitada atual. Se a referência, o texto do corpo até ali e a nota não couberem todos juntos, o marcador da nota de rodapé e tudo depois dele se movem juntos para a string de continuação. Essa regra é o que evita as duas falhas clássicas: uma nota sobrepondo o texto do corpo, e uma nota encalhada em uma página cuja referência está na anterior
Em uma caixa delimitada, a área da nota fica fixada na parte inferior, com uma régua separadora acima dela. Na medição sem limite, onde não há altura de caixa à qual se fixar, a área da nota segue imediatamente depois do corpo. A numeração é carregada em um campo de extensão na pilha de continuação, então DrawHTMLTextBox e DrawHTMLStory mantêm a sequência rodando entre colunas e páginas, e uma string de continuação produzida antes de esse campo existir ainda retoma corretamente
// Notas de rodapé dentro de uma maté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, // máximo de páginas para esta matéria
Html);
if Remainder <> '' then
Log('story exceeded its page budget');
Orientação prática para autores de templates
Projete dentro dos tetos documentados. Um contêiner flex com mais de 256 filhos diretos é quase sempre uma tabela de dados vestida de flex, e o caminho de tabela a mede melhor de qualquer forma. Um grid maior que 64 por 64 é uma planilha, e o mesmo conselho se aplica. Para texto de corpo em várias colunas, o comportamento de coluna e hifenização descrito em hifenização e colunas de texto balanceadas governa a aparência do fluxo dentro de cada coluna
Meça antes de desenhar quando um layout precisa caber. GetHTMLTextHeight relata a altura que uma determinada largura exigiria, que é a forma barata 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 excepcional: é o mecanismo pelo qual conteúdo longo é paginado, não um sinal de erro
Quando o HTML vem de um motor de relatórios em vez de templates escritos à mão, a rota orientada a conjunto de dados em o motor de relatórios orientado a dataset combina bem com isso, gerando a marcação que o flex e o grid depois organizam. E quando o mesmo conteúdo também precisa sair do PDF de novo, o caminho de exportação semântica em exportação de PDF para Markdown e DOCX fecha o ciclo
Layout de HTML, geração de relatórios e exportação semântica fazem parte de uma única biblioteca para Delphi, C++Builder e Free Pascal; a lista completa de recursos está na página da PDF Library for Delphi