Artigo Técnico

HTML para PDF em Delphi: o Percurso Paged-Media do HTML5

O HotPDF converte HTML em páginas PDF através de THPDFHTMLImporter.RenderHTML5, um perfil de paged-media versionado com um DOM real, uma cascata CSS real, tabelas, quebras de página e geração opcional de AcroForm a partir de controlos de formulário HTML. Funciona inteiramente no processo: sem motor de navegador, sem Chromium, sem conversor externo e, por predefinição, sem qualquer acesso a ficheiros ou à rede

A razão pela qual existe um ponto de entrada separado, em vez de um RenderHTML melhorado, é a compatibilidade. O método mais antigo tem vindo a renderizar faturas e cartas em aplicações de produção há anos, e um DOM tolerante com uma cascata completa altera a saída de documentos que foram afinados em função do comportamento antigo. Por isso, o método antigo mantém a sua semântica exata e tudo o que é novo vive por trás de RenderHTML5, cuja versão de perfil é reportada por HTML5ProfileVersion e cujos marcos suportados são listados por HTML5ProfileMilestones

O que é que o perfil HTML5 acrescenta, na prática?

Quatro capacidades são relevantes na prática. Um DOM estrutural em que html e body são contentores em vez de nós de texto. Uma cascata CSS com a precedência correta. Layout de tabelas. E controlo explícito de quebras de página, que é o que significa "paged media": o HTML foi concebido para uma janela de visualização com deslocamento, e transformá-lo em páginas exige decidir onde o conteúdo pode quebrar

A questão dos contentores parece um pormenor de implementação, mas é, na realidade, a diferença entre funcionar e não funcionar. Se os elementos raiz forem tratados como contentores de texto, todo o DOM colapsa num único bloco de texto no momento da análise, e cada regra de folha de estilos, quebra de página, tabela e subárvore de formulário aplicada depois disso não incide sobre nada. Qualquer renderizador de HTML que produza uma parede de texto indiferenciada a partir de um documento estruturado cometeu exatamente este erro

A ordem da cascata, explicada em detalhe

A precedência do CSS não é uma única ordenação. O HotPDF aplica as declarações normais das regras correspondentes por ordem de especificidade e depois por ordem de origem, aplica de seguida as declarações normais inline, depois as declarações !important das regras correspondentes e, por fim, o !important inline. Quatro passagens, por esta ordem

O HotPDF resolve cada propriedade CSS em Delphi através de quatro passagens de cascata ordenadas, desde a correspondência de regras normais até às declarações !important inline
As passagens posteriores vencem empates de forma deliberada: as declarações inline têm precedência sobre as folhas de estilos, e o !important tem precedência sobre tudo o que for declarado normalmente

As implementações que reduzem isto a "o último ganha" acertam nos casos comuns e erram nos casos interessantes, o que é pior do que errar de forma uniforme, porque a falha só aparece em modelos que usam !important para substituir uma folha de estilos base — precisamente os modelos escritos por quem espera que a cascata funcione:

uses
  HPDFDoc, HPDFHTMLImport;

var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Html, Css: WideString;
begin
  Pdf := THotPDF.Create(nil);
  Importer := THPDFHTMLImporter.Create(Pdf);
  try
    Pdf.FileName := 'statement.pdf';
    Pdf.BeginDoc;

    Importer.Margin := 56;              // pontos
    Importer.BaseFontName := 'Arial';
    Importer.BaseFontSize := 10;
    Importer.CreateAcroForms := False;  // renderizar controlos, não criar campos

    Css := '@page { margin: 56pt } ' +
           'table { border-collapse: collapse } ' +
           'tr.total { page-break-inside: avoid; font-weight: bold }';
    Html := LoadTemplate('statement.html');

    if not Importer.RenderHTML5(Html, Css) then
      raise Exception.Create('nothing was emitted - check the markup');

    Pdf.EndDoc;

    Writeln(Format('profile=%d nodes=%d rules=%d layout ops=%d',
      [Importer.Statistics.ProfileVersion,
       Importer.Statistics.ParsedNodeCount,
       Importer.Statistics.CSSRuleCount,
       Importer.Statistics.LayoutOperationCount]));
  finally
    Importer.Free;
    Pdf.Free;
  end;
end;

O segundo argumento é uma folha de estilos de autor aplicada em conjunto com o que quer que o documento já traga, o que constitui a forma correta de impor regras internas, como margens de página e controlo de linhas órfãs, sem editar cada modelo

Porque é que o importador nunca vai buscar seja o que for?

Um conversor de HTML para PDF que resolve referências externas é, no fundo, um motor de falsificação de pedidos do lado do servidor disfarçado de gerador de documentos. Aponte-o a markup fornecido por utilizadores e ele irá alegremente ir buscar http://169.254.169.254/ ou um URL de ficheiro a partir do seu servidor de aplicações, e incorporar o resultado num PDF que alguém descarrega

O HotPDF não lê, por isso, nem ficheiros nem rede por predefinição. As imagens e outros recursos chegam exatamente por duas vias: como URIs data: dentro do markup, ou através do callback OnResource, onde o seu código decide o que um URI significa e fornece os bytes:

O importador de HTML do HotPDF para Delphi resolve todos os URIs de recursos localmente, aceitando URIs data: e callbacks OnResource, ao mesmo tempo que recusa pedidos a ficheiros e à rede, sujeitos a limites de contagem e de bytes
Só os bytes que chegam localmente atingem a página, as referências rejeitadas incrementam o contador, e nenhum pedido sai alguma vez do processo
procedure TReportForm.SupplyResource(Sender: TObject;
  const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
  var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
  Handled := False;
  // Sirva apenas a partir de um diretório de recursos locais na lista de permissões. Nunca
  // passe o URI a um cliente HTTP, e nunca o combine com um caminho
  // sem primeiro o validar
  if StartsText('asset:', URI) then
  begin
    Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
    ContentType := 'image/png';
    Handled := Length(Data) > 0;
  end;
end;

Cada recurso que efetivamente chega continua a ser contabilizado em relação a três limites distintos: quantos recursos, quantos bytes cada um, e quantos bytes no total. Um callback que devolve fielmente tudo o que lhe é pedido não pode ser levado a esgotar a memória por um documento que referencia a mesma imagem de 50 MB duzentas vezes

Quatro limites, um único modo de falha

Os nós do analisador, as regras CSS, as operações de layout e os recursos são medidos de forma independente, porque falham de forma independente. Um documento pode ser pequeno e patologicamente dispendioso de dispor; outro pode ser superficial, com uma folha de estilos concebida para fazer explodir a correspondência de seletores. Esgotar qualquer um deles gera EHPDFHTMLImportBudget, uma classe de exceção dedicada, de modo a que o esgotamento de limites possa ser capturado separadamente de uma entrada malformada

As estatísticas são reiniciadas no início de cada chamada a RenderHTML5, pelo que os números descrevem uma importação e não o tempo de vida do objeto importador. Num serviço que renderiza modelos não confiáveis, registe ParsedNodeCount, LayoutOperationCount e ResourceRejectCount por documento: uma contagem de rejeições crescente costuma significar que o autor do modelo está a referenciar recursos que o seu callback não reconhece, e uma contagem de operações de layout próxima do limite significa que o próximo documento, ligeiramente maior, irá falhar

Escolher entre a importação de HTML e a API de layout

A importação de HTML é a melhor opção quando o conteúdo é criado como HTML por pessoas que não são programadores Delphi, quando os modelos mudam com mais frequência do que o código, ou quando o mesmo conteúdo também tem de ser enviado por email. A árvore de layout declarativo descrita em layout PDF declarativo com saída marcada (tagged) é a melhor opção quando o documento é gerado a partir de dados pelo seu próprio código, quando precisa que a estrutura PDF/UA seja emitida automaticamente, ou quando o controlo preciso sobre a paginação importa mais do que a conveniência de criação

Não são mutuamente exclusivas. Uma abordagem comum é usar importação de HTML para o corpo de um relatório, com cabeçalho, rodapé e blocos de assinatura desenhados diretamente, uma vez que se trata de geometria fixa que nenhum modelo deveria controlar. As tabelas são a única área onde vale a pena medir as duas abordagens: tabelas financeiras complexas paginam por vezes melhor através da via dedicada a tabelas descrita em gerar tabelas em PDF

Defina CreateAcroForms de forma deliberada. Desativado, os controlos de formulário HTML são renderizados como arte estática, que é o que um extrato impresso pretende. Ativado, tornam-se campos AcroForm interativos reais, que é o que um formulário preenchível pretende, e o comportamento dos campos que se segue é abordado em adicionar campos de formulário a um PDF carregado

A importação de HTML, o layout declarativo e a API de desenho direto têm todos como alvo o mesmo objeto de documento em Delphi e C++Builder; a lista completa de funcionalidades está disponível na página do componente PDF HotPDF para Delphi