O HotPDF consegue construir um documento paginado a partir de uma árvore declarativa em vez de a partir de coordenadas. Você monta um THPDFDOMDocument a partir de seções, pilhas, texto, listas e tabelas, entrega-o ao THPDFDOMRenderer, e o renderizador mede, pagina, desenha os elementos de página e, quando solicitado, emite a árvore de estrutura PDF/UA que torna o resultado acessível. O código de layout nunca calcula uma coordenada y
Quem já manteve um gerador de relatórios baseado em coordenadas sabe por que isso importa. A primeira versão funciona. Depois o endereço de um cliente cresce para três linhas, uma tabela ganha linhas, um título localizado quebra em mais de uma linha, e todas as posições y seguintes ficam erradas. As correções se acumulam como verificações manuais de quebra de página espalhadas pela lógica de negócio, e a exigência de PDF marcado (tagged) que chega dois anos depois não dá para encaixar em um código que não faz ideia do que é um parágrafo
O que a árvore possui, e por que a posse é rígida
O DOM impõe posse única em cada nível: o documento possui suas seções, uma seção possui seu corpo, cabeçalho e rodapé, e pilhas, contêineres e tabelas possuem seus filhos. A reutilização acontece por meio de Clone ou de uma fábrica registrada, nunca anexando o mesmo objeto a dois pais. Essa regra não é cerimônia. Um componente que aparecesse duas vezes na árvore seria medido duas vezes com restrições diferentes e liberado duas vezes ao final
A consequência prática para o código que chama é que os auxiliares retornam novas instâncias. Registrar uma fábrica com RegisterComponent e chamar CreateComponent dá a você uma receita nomeada que produz um componente novo a cada vez, o que é como um elemento repetido, como um bloco de assinatura ou um rodapé jurídico, deve entrar na árvore
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // emite a árvore de estrutura PDF/UA
Doc.Language := 'en-US';
Section := Doc.AddSection;
Section.PageWidth := 595; // A4 em pontos
Section.PageHeight := 842;
Section.MarginLeft := 56;
Section.MarginTop := 56;
Section.MarginRight := 56;
Section.MarginBottom := 56;
Section.Style.FontName := 'Helvetica';
Section.Style.FontSize := 10;
Section.Body.AddHeading('Annual maintenance report', 1);
Section.Body.AddText('Every asset inspected during the reporting ' +
'period is listed below, grouped by site.');
Section.Body.AddSpacer(12);
Table := THPDFDOMTable.Create('assets');
Table.AddColumn(3); // pesos, não larguras absolutas
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // linha de cabeçalho
Row[0].Text := 'Asset';
Row[1].Text := 'Last service';
Row[2].Text := 'Status';
for I := 0 to High(Assets) do
begin
Row := Table.AddRow(16);
Row[0].Text := Assets[I].Name;
Row[1].Text := Assets[I].ServiceDate;
Row[2].Text := Assets[I].Status;
end;
Section.Body.Add(Table);
end;
Como a paginação evita um custo quadrático?
A forma ingênua de paginar uma árvore é clonar o que não coube e transportá-lo para a página seguinte. Em uma tabela com dez mil linhas, isso clona as linhas restantes uma vez por página e transforma um documento linear em um quadrático
Em vez disso, o HotPDF divide de forma bem restrita. O renderizador de nível superior percorre os filhos do corpo por índice e nunca clona uma seção ou um corpo inteiro. Apenas pilhas e contêineres aninhados que realmente atravessam um limite de página têm sua subárvore afetada clonada, e os dois tipos de folha mais pesados carregam um cursor em vez de uma cópia: uma continuação de texto armazena o intervalo de caracteres de origem que ainda deve, e uma continuação de tabela armazena a fatia de linhas que ainda precisa posicionar. Documentos longos permanecem lineares, e parágrafos longos custam o mesmo, quebrem uma vez ou cinco vezes
A medição permanece honesta quanto a efeitos colaterais. THPDFLayoutElement.Measure é obrigado a não ter efeitos colaterais de desenho, e a posição real sempre passa por THotPDF.PlaceLayoutElement, a mesma rotina central que remede o fragmento posicionado, configura a posse do overflow e registra diagnósticos. O renderizador DOM decide apenas a política de nova página, os elementos de página, o espaçamento e o ciclo de vida das continuações
As regras de cabeçalho de tabela que evitam um documento infinito
Repetir cabeçalhos de tabela entre páginas parece simples e esconde dois modos de falha. O HotPDF exige que as linhas de cabeçalho apareçam apenas na primeira sequência de linhas consecutivas, e que a primeira divisão comporte todas as linhas de cabeçalho mais pelo menos uma linha de corpo. Sem a segunda regra, um cabeçalho mais alto do que o espaço restante produz uma página contendo apenas o cabeçalho, seguida de outra página idêntica, para sempre
As páginas de continuação redesenham o cabeçalho, e essa cópia redesenhada é marcada como artefato, e não como conteúdo, que é a resposta correta tanto para acessibilidade quanto para extração de texto. A linha de cabeçalho original permanece na estrutura lógica da tabela exatamente uma vez. Ignore isso e um leitor de tela anuncia os títulos de coluna novamente no meio dos dados, e um extrator de texto insere uma linha de cabeçalho duplicada entre linhas do corpo
Há também um teto defensivo na profundidade de continuação, porque um componente personalizado tem liberdade para implementar Split de uma forma que sempre retorna uma cauda equivalente. O renderizador verifica o limite depois de desanexar a cauda e antes de iniciar a próxima página, e a iteração atual libera a cauda em seu próprio bloco finally, então um componente de terceiros mal-comportado falha com um erro diagnosticável em vez de encher um disco
Um elemento lógico, muitos fragmentos de página
A marcação automática é onde o modelo de paginação e o modelo de estrutura precisam concordar. Um parágrafo dividido entre duas páginas é um único parágrafo lógico, então precisa permanecer um único elemento de estrutura. Mas os identificadores de conteúdo marcado são por página, então cada fragmento visível precisa do seu próprio MCID na página em que aparece
O HotPDF resolve isso mantendo um único elemento de estrutura e anexando uma referência de conteúdo marcado ao seu array /K para cada fragmento, com o par /Pg e /MCID identificando a página e o identificador. O slot da ParentTree para esse MCID aponta de volta para o mesmo elemento. É exatamente isso que a ISO 14289 exige, e é o motivo pelo qual os clones de continuação são diferentes dos clones comuns: um Clone comum significa conteúdo lógico novo e recebe uma identidade semântica nova, enquanto o clone de continuação interno herda a identidade do componente que continua
A reutilização de elementos é procurada por meio de um índice de identidades semânticas ordenado por ponteiro de componente e pesquisado por comparação binária, o que mantém a busca logarítmica em árvores grandes. O índice mantém apenas referências não proprietárias; o ciclo de vida dos próprios objetos de estrutura permanece com o grafo de objetos do PDF
Regras de estrutura que o renderizador impõe antecipadamente
Com GenerateStructure ativado, várias regras do PDF/UA são verificadas enquanto a árvore está sendo renderizada, em vez de depois que o arquivo já existe. Os títulos começam no nível 1 e não podem pular níveis. LI só pode aparecer dentro de L, e Lbl e LBody só dentro de LI. TR pertence a uma tabela, e TH e TD a uma linha. Uma figura sem texto alternativo é rejeitada no modo PDF/UA
Rejeitar cedo é a escolha deliberada aqui. Um validador que relata um texto alternativo ausente depois que o documento já foi escrito diz que um lote de dez mil demonstrativos precisa ser regenerado; um renderizador que recusa o componente diz qual componente, enquanto os dados que o produziram ainda estão no escopo. A verificação de conformidade continua fazendo parte do pipeline como uma etapa separada, e a mecânica disso é abordada em validação de PDF/A, PDF/X e PDF/UA
var
Pdf: THotPDF;
Renderer: THPDFDOMRenderer;
Stats: THPDFDOMRenderStatistics;
begin
Pdf := THotPDF.Create(nil);
Renderer := THPDFDOMRenderer.Create;
try
Pdf.FileName := 'maintenance-report.pdf';
Pdf.BeginDoc;
Stats := Renderer.Render(Doc, Pdf);
Pdf.EndDoc;
Writeln(Format('%d page(s), %d placement(s), %d split(s)',
[Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
[Stats.StructureElementCount, Stats.MarkedContentCount,
Stats.ArtifactCount]));
Writeln(Format('deepest continuation chain: %d',
[Stats.MaximumContinuationDepth]));
finally
Renderer.Free;
Doc.Free;
Pdf.Free;
end;
end;
O registro de estatísticas é mais útil do que parece à primeira vista. Um aumento acentuado em SplitCount depois de uma mudança de template geralmente significa que um componente passou a medir mais alto do que seu contêiner. Um MaximumContinuationDepth subindo aos poucos é o alerta antecipado para um componente cujo Split avança pouco a cada página. E comparar ArtifactCount com o número de páginas de continuação confirma que os cabeçalhos repetidos realmente foram marcados como artefatos
Onde o DOM se encaixa ao lado da API direta
O DOM não substitui o desenho direto; ele se apoia sobre os mesmos objetos de página. Qualquer coisa que o renderizador posicione pode ser intercalada com chamadas diretas em THotPDF, o que importa quando um relatório precisa de um elemento posicionado manualmente, como uma imagem de assinatura em um local exato. O fechamento de páginas permanece sob o controle de AddPage e EndDoc, então o modo de descarga imediata não mantém páginas concluídas em memória, e a memória residente continua governada pelas continuações atuais, pelos recursos de fonte e pelo grafo de objetos comum do documento
Escolha o DOM quando o conteúdo é orientado a dados e o layout é orientado a regras, e mantenha o desenho direto para arte fixa. Se a sua dor atual é especificamente a paginação de tabelas, vale a pena ler primeiro a abordagem mais restrita em geração de tabelas em PDF, e o comportamento em nível de texto, como a justificação, está descrito em justificação de texto
Layout declarativo, marcação automática e a API de desenho direto são entregues no mesmo componente para Delphi e C++Builder; a lista completa de recursos está na página do componente PDF para Delphi HotPDF