O HotPDF consegue construir um documento paginado a partir de uma árvore declarativa em vez de a partir de coordenadas. Monta-se um THPDFDOMDocument a partir de secções, pilhas, texto, listas e tabelas, entrega-se a THPDFDOMRenderer, e o renderizador mede, pagina, desenha os elementos fixos da página e, quando pedido, 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 orientado por coordenadas sabe por que razão isto importa. A primeira versão funciona. Depois a morada de um cliente passa a três linhas, uma tabela ganha linhas, um cabeçalho localizado quebra de linha, e todas as posições y a jusante ficam erradas. As correções acumulam-se como verificações manuais de quebra de página espalhadas pela lógica de negócio, e o requisito de PDF etiquetado que chega dois anos depois não pode ser aplicado retroativamente a um código que não faz ideia do que é um parágrafo
O que a árvore possui, e por que razão a posse é estrita
O DOM impõe posse única em todos os níveis: o documento possui as suas secções, uma secção possui o seu corpo, cabeçalho e rodapé, e pilhas, contentores e tabelas possuem os seus filhos. A reutilização acontece através de Clone ou através de uma fábrica registada, nunca ligando 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 libertado duas vezes na desmontagem
A consequência prática para o código que chama é que os auxiliares devolvem novas instâncias. Registar uma fábrica com RegisterComponent e chamar CreateComponent dá uma receita nomeada que produz um componente novo de cada vez, forma como um elemento fixo repetido, como um bloco de assinatura ou um rodapé legal, pertence à á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 é que 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. Numa tabela com dez mil linhas, isso clona as linhas restantes uma vez por página e transforma um documento linear num documento quadrático
O HotPDF divide de forma restrita, em alternativa. O renderizador de topo percorre os filhos do corpo por índice e nunca clona uma secção ou um corpo inteiro. Só as pilhas e os contentores aninhados que genuinamente atravessam um limite de página têm a sua subárvore afetada clonada, e os dois tipos de folha mais pesados transportam um cursor em vez de uma cópia: uma continuação de texto guarda o intervalo de caracteres de origem que ainda deve, e uma continuação de tabela guarda a fatia de linhas que ainda tem de colocar. Os documentos longos mantêm-se lineares, e os parágrafos longos custam o mesmo quer quebrem uma vez quer quebrem cinco vezes
A medição mantém-se honesta quanto a efeitos secundários. Exige-se que THPDFLayoutElement.Measure esteja livre de efeitos secundários de desenho, e a colocação real corre sempre através de THotPDF.PlaceLayoutElement, a mesma rotina central que volta a medir o fragmento colocado, configura a posse do transbordo e regista diagnósticos. O renderizador do DOM só decide a política de página nova, os elementos fixos da página, o espaçamento e o tempo de vida das continuações
As regras de cabeçalho de tabela que evitam um documento infinito
Repetir cabeçalhos de tabela ao longo das 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 caiba 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 que contém apenas o cabeçalho, seguida de outra página idêntica, indefinidamente
As páginas de continuação voltam a desenhar o cabeçalho, e essa cópia redesenhada é marcada como artefacto, e não como conteúdo, o que é a resposta correta tanto para a acessibilidade como para a extração de texto. A linha de cabeçalho original permanece na estrutura lógica da tabela exatamente uma vez. Ignorar isto faz com que um leitor de ecrã anuncie de novo os títulos das colunas a meio dos dados, e um extrator de texto insira uma linha de cabeçalho duplicada entre linhas de corpo
Existe também um teto defensivo na profundidade de continuação, porque um componente personalizado é livre de implementar Split de uma forma que devolve sempre uma cauda equivalente. O renderizador verifica o limite depois de destacar a cauda e antes de iniciar a página seguinte, e a iteração atual liberta a cauda no seu próprio bloco finally, pelo que um componente de terceiros com mau comportamento falha com um erro diagnosticável em vez de encher um disco
Um elemento lógico, muitos fragmentos de página
A etiquetagem automática é onde o modelo de paginação e o modelo de estrutura têm de concordar. Um parágrafo dividido entre duas páginas é um único parágrafo lógico, pelo que tem de permanecer um único elemento de estrutura. Mas os identificadores de conteúdo marcado são por página, pelo que cada fragmento visível precisa do seu próprio MCID na página em que aparece
O HotPDF resolve isto mantendo um único elemento de estrutura e acrescentando uma referência de conteúdo marcado ao seu array /K para cada fragmento, com o par /Pg e /MCID a identificar a página e o identificador. A entrada da ParentTree para esse MCID aponta de volta para o mesmo elemento. É exatamente isto que a ISO 14289 espera, e é a razão pela qual os clones de continuação são distintos dos clones normais: um Clone normal significa novo conteúdo lógico e recebe uma nova identidade semântica, enquanto o clone de continuação interno herda a identidade do componente que continua
A reutilização de elementos é procurada através de um índice de identidades semânticas ordenado por ponteiro de componente e pesquisado por comparação binária, o que mantém a pesquisa logarítmica em árvores grandes. O índice mantém apenas referências não proprietárias; o tempo de vida dos próprios objetos de estrutura permanece com o grafo de objetos PDF
Regras de estrutura que o renderizador impõe antecipadamente
Com GenerateStructure ativado, várias regras PDF/UA são verificadas enquanto a árvore está a ser renderizada, e não depois de o ficheiro existir. Os cabeçalhos começam no nível 1 e não podem saltar 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 em modo PDF/UA
Rejeitar cedo é a escolha deliberada aqui. Um validador que reporta um texto alternativo em falta depois de o documento estar escrito diz que um lote de dez mil extratos precisa de ser regenerado; um renderizador que recusa o componente diz qual componente, enquanto os dados que o produziram ainda estão em âmbito. A verificação de conformidade continua a pertencer ao pipeline como um passo separado, e a mecânica disso está descrita em validação 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 registo de estatísticas é mais útil do que parece à primeira vista. Um SplitCount que sobe abruptamente após uma alteração de modelo costuma significar que um componente começou a medir mais alto do que o seu contentor. Um MaximumContinuationDepth a subir lentamente é o aviso prévio de um componente cujo Split progride muito pouco por página. E comparar ArtifactCount com o número de páginas de continuação confirma que os cabeçalhos repetidos foram, de facto, etiquetados como artefactos
Onde o DOM se encaixa junto da API direta
O DOM não substitui o desenho direto; assenta sobre os mesmos objetos de página. Tudo o que o renderizador coloca pode ser intercalado com chamadas diretas sobre THotPDF, o que importa quando um relatório precisa de um elemento posicionado à mão, como uma imagem de assinatura numa localização exata. O fecho de páginas continua sob o controlo de AddPage e EndDoc, pelo que 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 fontes e pelo grafo de objetos do documento comum
Escolha o DOM quando o conteúdo é orientado por dados e o layout é orientado por regras, e mantenha o desenho direto para peças gráficas fixas. Se a dificuldade atual for especificamente a paginação de tabelas, vale a pena ler primeiro a abordagem mais restrita em gerar tabelas em PDF, e o comportamento ao nível do texto, como a justificação, está descrito em justificação de texto
O layout declarativo, a etiquetagem automática e a API de desenho direto vêm no mesmo componente para Delphi e C++Builder; a lista completa de funcionalidades está na página do componente PDF para Delphi HotPDF