O HotPDF Component dispõe texto CSS multicoluna quando o THPDFHTMLImporter.RenderHTML5 converte HTML em PDF no Delphi. O motor de HTML paginado resolve column-count e column-width contra a caixa de conteúdo, equilibra o último fragmento de cada contentor multicol com uma pesquisa binária, enche as páginas anteriores coluna a coluna até ao fundo da página, e suporta column-fill, column-span: all e column-rule
A razão habitual para querer isto é aborrecida e real: um boletim de produto a duas colunas, um anexo de termos e condições composto em três colunas estreitas, ou um PDF de notas de versão que devia parecer a newsletter impressa que a equipa de marketing já desenhou em HTML. Sem suporte de colunas, o importador empilha todos os parágrafos numa medida larga única, o comprimento de linha dobra, e o documento lê-se como um ficheiro de log. Com ele, a mesma folha de estilos que comanda a pré-visualização do browser comanda o PDF, e a parte difícil torna-se a paginação, com que os browsers quase nunca têm de se preocupar no ecrã
Como decide o HotPDF quantas colunas usar?
O HotPDF resolve a contagem de colunas com as mesmas regras que o pseudo-algoritmo do CSS Multi-column Layout Module Level 1 §3.4, aplicadas à largura da caixa de conteúdo W depois de removidos padding e bordas. Um column-count declarado é tomado como está. Quando só é dado o column-width, o motor calcula N = Trunc((W + gap) / (column-width + gap)) com um piso de uma coluna. Quando ambos estão presentes, o resultado é o menor dos dois, pelo que a contagem declarada pode limitar a contagem derivada mas nunca inflacioná-la
Um caso concreto: uma caixa de conteúdo de 483pt (A4 com margens laterais de 56pt, arredondadas) e column-width: 140pt; column-gap: 12pt dá Trunc(495 / 152) = 3 colunas, cada uma com (483 - 2 * 12) / 3 = 153pt de largura. Acrescentar column-count: 2 à mesma regra baixa-a para duas colunas, porque o mínimo ganha. A palavra-chave de gap normal resolve-se a 1em do tamanho da fonte do contentor, e essa resolução acontece na altura do layout em vez de na altura da análise, pelo que um font-size declarado depois do column-gap na mesma regra ainda produz o gap certo
Uma salvaguarda não está na especificação, e está lá de propósito. Depois de a largura da coluna ser calculada, qualquer valor abaixo de 12pt colapsa o contentor de volta a uma única coluna. Uma folha de estilos que diga column-count: 9999, seja por acidente ou porque veio de um template não fidedigno, produziria de outra forma milhares de colunas com larguras negativas ou microscópicas e um quebrador de linhas a tentar encaixar um glifo por linha. Regressar a uma coluna mantém a saída legível e o custo do layout limitado. Contagens zero ou negativas nunca chegam tão longe: o validador aceita só auto ou um inteiro positivo para column-count, e column-width e column-gap recebem auto ou normal respetivamente, ou um comprimento em pontos ou em
Renderizar uma secção HTML multicoluna em PDF no Delphi
A saída multicoluna não precisa de nenhum interruptor no importador. O THPDFHTMLImporter é criado com o documento THotPDF em que desenha, o BeginDoc já tem de ter corrido, e as propriedades de colunas chegam pela cascata normal, seja na marcação seja na folha de estilos de autor passada como segundo argumento do RenderHTML5
uses
SysUtils, HPDFDoc, HPDFHTMLImport;
procedure BuildBulletin(const FileName: string);
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Html, Css: WideString;
begin
Pdf := THotPDF.Create(nil);
Importer := THPDFHTMLImporter.Create(Pdf);
try
Pdf.FileName := FileName;
Pdf.BeginDoc;
Importer.Margin := 56;
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Css := 'section.news { column-width: 140pt; column-gap: 12pt; ' +
' column-rule: 0.5pt solid #999999 } ' +
'section.news h2 { column-span: all }';
Html := '<section class="news">' +
'<p>Release 4.2 ships the new export pipeline...</p>' +
'<p>Licensing changes take effect on 1 March...</p>' +
'<h2>Known issues</h2>' +
'<p>Import of legacy archives & templates...</p>' +
'</section>';
if not Importer.RenderHTML5(Html, Css) then
raise Exception.Create('Nothing was emitted');
Pdf.EndDoc;
finally
Importer.Free;
Pdf.Free;
end;
end;
Note a duplicação de escapes no último parágrafo: & dentro da string Pascal é a entidade HTML de um ampersand, que o importador descodifica de volta para um único carácter. O resto da cascata, orçamentos e callbacks de recursos comportam-se exatamente como descrito na visão geral da importação de paged media HTML5 do HotPDF; colunas são só mais um modo de layout dentro do mesmo perfil
Equilibrar colunas com uma pesquisa binária
O HotPDF equilibra colunas procurando a menor altura de coluna à qual um preenchimento guloso ainda coloca todas as linhas restantes. A passagem gulosa é simples: caminhe as linhas para a coluna um até a próxima linha exceder a altura candidata, depois continue na coluna dois, e por aí adiante. A pesquisa começa com a linha mais alta como limite inferior e a soma de todas as alturas de linha como limite superior, parte o intervalo ao meio em cada ronda, e para quando os limites estão a menos de 0,05pt
Uma pesquisa binária só funciona se o predicado for monótono, e aqui é: uma coluna mais alta nunca pode conter menos linhas do que uma mais baixa, pelo que "todas as linhas cabem" vira de falso para verdadeiro exatamente uma vez à medida que a altura cresce. Essa propriedade é o que garante a convergência e garante que o preenchimento guloso final à altura encontrada consome todas as linhas, em vez de deixar uma linha órfã que dispararia uma página extra espúria. Também explica um detalhe do resultado: a banda equilibrada é tão curta quanto as caixas de linha permitem, não necessariamente colunas iguais, porque as linhas são indivisíveis e a última coluna pode acabar uma linha ou duas acima das outras
O que acontece quando as colunas atravessam uma quebra de página?
Colunas que não cabem na página atual são preenchidas sequencialmente até ao fundo da página, e só o fragmento de página final é equilibrado. Este é o comportamento que o CSS Multi-column Layout Level 1 §7.1 prescreve para column-fill: balance em contextos fragmentados, e é o aspeto de uma newsletter impressa: colunas cheias em todas as páginas exceto a última, em que o texto restante é dividido uniformemente em vez de deixar uma coluna alta ao lado de uma vazia
Cada fragmento de página passa pelo mesmo fragmentador de forma independente. Se as linhas restantes couberem em N colunas da altura disponível, o fragmento é equilibrado; caso contrário todas as colunas se enchem até ao fundo da página e o ciclo começa uma página nova. A altura natural do contentor inteiro, que o motor usa para decisões de break-inside: avoid e medição de extensão, é a altura da banda equilibrada com capacidade ilimitada, pelo que um contentor que caberia numa página nova como bloco equilibrado é movido para lá intacto em vez de fatiado. Quando uma única linha é mais alta do que uma página inteira, o motor relaxa a restrição, coloca a linha de qualquer forma no topo de uma página, e reporta um diagnóstico hdkPaginationConstraint com o sujeito multi-column
column-fill: auto e caixas de altura fixa
Com column-fill: auto e uma height definida, o HotPDF enche as colunas por ordem, e cada altura de coluna é o menor entre o espaço de página restante e a altura do contentor menos o seu padding e bordas. O mínimo importa mais do que parece. Usar apenas o espaço de página restante deixaria a primeira coluna engolir todas as linhas sempre que a página tiver mais espaço livre do que a caixa. Sem altura definida, o column-fill: auto regride para o equilíbrio, como os browsers fazem, já que um contentor multicol sem restrições não tem altura de coluna para encher; a primeira implementação perdeu essa regressão, e uma das 27 verificações de regressão apanhou-a
column-span: all e parágrafos aninhados
O HotPDF suporta column-span: all em filhos diretos de um contentor multicoluna, seguindo o CSS Multi-column Layout Level 1 §6.1. Um filho abrangente divide o contentor em segmentos: os filhos antes dele formam uma banda de colunas, o elemento abrangente renderiza por toda a largura, e os filhos depois dele começam uma banda nova. Cada banda é equilibrada, preenchida, pautada e paginada por si, pelo que um título no meio de uma secção de três colunas produz duas bandas equilibradas com um título de largura total entre elas, que é o layout que as pessoas realmente desenham nos mockups
Chegar lá exigiu corrigir primeiro um problema menos visível. Um contentor como <div style="column-count:2"> com vários filhos <p> costumava renderizar todos os parágrafos numa única coluna empilhada, porque cada parágrafo era disposto como o seu próprio bloco de texto e a declaração de colunas no pai nunca se aplicava. O HotPDF agora recolhe o texto dos filhos de bloco aninhados num único fluxo para o contentor. O recoletor tem de inserir uma quebra dura em cada fronteira de bloco enquanto o faz; saltá-la concatena a última palavra de um parágrafo com a primeira palavra do seguinte na mesma linha, um bug fácil de perder quando o texto de teste acaba por acaso em pontuação
Esse fluxo fundido tem um âmbito deliberado. É usado apenas quando a subárvore inteira é fluxo de texto puro. Um contentor que contenha controlos de formulário, imagens, tabelas, listas ou <hr> em qualquer lugar dentro dele mantém a rota de blocos ordinária e renderiza numa única coluna, porque o recoletor de texto fundido não consegue exprimir a sua geometria. As tabelas em particular têm as suas próprias regras de paginação, cobertas no artigo sobre tratamento de rowspan e cabeçalhos de tabela repetidos no HotPDF, e misturá-las numa banda de colunas quebraria essas garantias
Onde vai a pauta de colunas, e como a testar
O HotPDF desenha o column-rule na linha média de cada gap, na aresta direita de uma coluna mais metade do gap, do topo ao fundo da banda em cada fragmento de página, como o CSS Multi-column Layout Level 1 §4 descreve. A largura predefine-se a 1pt. A cor predefine-se à cor de texto do elemento quando não é declarada ou é currentColor. O atalho column-rule sem palavra-chave de estilo é tratado como solid, a modo de como o atalho border é comummente lido, enquanto none e hidden suprimem a pauta
A geometria das colunas é observável sem analisar o PDF. O evento OnLineLayout no THPDFHTMLImporter dispara uma vez por linha desenhada com um registo THPDFHTMLLineLayout, e como o motor desenha cada coluna separadamente, o campo X de cada linha é a aresta esquerda da sua coluna. Colunas adjacentes diferem exatamente pela largura da coluna mais o gap, o que torna fácil distinguir um gap de 12pt de um gap de 20pt num teste de regressão
type
TColumnProbe = class
public
Lines: TStringList;
constructor Create;
destructor Destroy; override;
procedure LineDrawn(Sender: TObject; const Line: THPDFHTMLLineLayout);
end;
constructor TColumnProbe.Create;
begin
inherited Create;
Lines := TStringList.Create;
end;
destructor TColumnProbe.Destroy;
begin
Lines.Free;
inherited;
end;
procedure TColumnProbe.LineDrawn(Sender: TObject;
const Line: THPDFHTMLLineLayout);
begin
// PageIndex é de base zero; X é a aresta esquerda da coluna da linha
Lines.Add(Format('page=%d x=%.1f y=%.1f %s',
[Line.PageIndex, Line.X, Line.Y, string(Line.Text)]));
end;
// Ligações, antes do RenderHTML5:
// Probe := TColumnProbe.Create;
// Importer.OnLineLayout := Probe.LineDrawn;
Duas notas práticas de escrever testes contra este evento. O PageIndex é de base zero, que é a primeira coisa que uma asserção escrita à mão tende a errar. E o X inclui deslocamentos de alinhamento, por isso para texto centrado ou alinhado à direita compare posições de colunas em fixtures alinhadas à esquerda. A pauta em si não aparece no evento; para afirmar a sua posição, varra o fluxo de conteúdo descomprimido pelos operadores verticais m e l
Limites e armadilhas que valem a pena conhecer
Alguns comportamentos são aproximações, e é melhor conhecê-los antes de uma revisão de layout do que depois. O espaçamento de parágrafos dentro de um fluxo de colunas fundido é aproximado por uma linha da altura de linha do contentor em cada fronteira de bloco, pelo que as margens por parágrafo dentro de uma banda de colunas não são reproduzidas exatamente. Quando o column-span: all divide um contentor, cada segmento repete a caixa de padding e fundo do contentor, o que mantém os segmentos visualmente contínuos mas não é uma caixa única no sentido estrito do CSS. Todo o valor column-rule-style diferente de none e hidden passa a validação, mas a pauta é traçada como linha sólida
Valores não suportados não falham em silêncio. Definir StrictCSS como True transforma cada diagnóstico numa exceção EHPDFHTMLUnsupportedCSS, enquanto o OnDiagnostic deixa um trabalho em lote registá-los e continuar. Note que o modo estrito se aplica também ao diagnóstico de paginação, pelo que um documento com uma linha mais alta do que uma página levantará sob StrictCSS em vez de relaxar a restrição em silêncio
procedure TReportJob.HtmlDiagnostic(Sender: TObject;
const Diagnostic: THPDFHTMLDiagnostic);
begin
case Diagnostic.Kind of
hdkUnsupportedProperty, hdkUnsupportedValue:
FLog.Add('CSS ' + Diagnostic.Subject + ': ' + Diagnostic.MessageText);
hdkPaginationConstraint:
FLog.Add('Layout relaxed (' + Diagnostic.Subject + '): ' +
Diagnostic.MessageText);
else
FLog.Add(Diagnostic.MessageText);
end;
end;
// Autoria de templates: falhe depressa em tudo o que o motor ignoraria
Importer.StrictCSS := FValidatingTemplate;
Importer.OnDiagnostic := HtmlDiagnostic;
O modo estrito é a configuração certa enquanto um template está a ser escrito, já que um erro de digitação como column-span: both é rejeitado imediatamente em vez de deixar silenciosamente o título fluir dentro das colunas. Em produção, registar através do OnDiagnostic é normalmente a troca melhor, porque uma declaração estranha em HTML fornecido pelo cliente não deve custar o documento inteiro. Diagnósticos idênticos (mesmo tipo, sujeito e valor) são reportados uma vez enquanto couberem na lista MaxDiagnostics, que predefine-se a 256 entradas, pelo que uma declaração que a cascata visita para centenas de elementos não inunda o log
FAQ
O HotPDF suporta o atalho columns?
Sim. O atalho columns aceita no máximo uma contagem de inteiro positivo e um comprimento de coluna em qualquer ordem, e auto repõe ambos os longhands. Os valores resolvidos seguem as mesmas regras de contagem e largura das propriedades individuais
Pode um contentor multicoluna conter imagens ou tabelas?
Pode contê-las, mas o HotPDF renderiza então esse contentor numa única coluna. O fluxo de colunas só é aplicado quando todos os descendentes são fluxo de texto puro, porque imagens, tabelas, listas e controlos de formulário precisam da sua própria geometria de caixa e regras de paginação
Por que a última página mostra colunas equilibradas enquanto as anteriores estão cheias?
Essa é a regra do CSS para column-fill: balance em paged media: só o fragmento final é equilibrado. As páginas anteriores enchem cada coluna até ao fundo da página para que nenhum espaço se desperdice a meio do documento
Layout multicoluna, o perfil de paged media HTML5 e o resto do pipeline de HTML para PDF fazem parte do HotPDF Component for Delphi and C++Builder, que também cobre desenho direto de PDF, formulários, encriptação e extração de texto