Artigo Técnico

Reformatação de conteúdo PDF em HTML responsivo no Delphi

O PDFium Component transforma um PDF de layout fixo em um modelo semântico que pode ser reformatado, usando BuildReflowDocument, e exporta esse modelo como HTML autocontido por meio de ToHtml. Títulos continuam sendo títulos, itens de lista continuam sendo itens de lista, e tabelas detectadas na página saem como marcação de tabela real, com células de cabeçalho e mesclagens preservadas. Nada na saída referencia um script ou uma folha de estilos externa

O motivo para querer isso é que uma página PDF é um conjunto de glifos posicionados, o que é exatamente errado para a tela de um celular, um leitor de tela ou um índice de busca. Toda tentativa de resolver isso extraindo texto simples perde a estrutura que tornava o documento legível, e toda tentativa de resolver isso convertendo páginas em imagens perde o texto por completo. Um modelo de reformatação preserva os dois: as palavras e as relações entre elas

De onde vem a informação semântica?

Tudo começa em GetStructuredText, a única fonte de texto e semântica no componente. Quando o PDF carrega uma árvore de estrutura, um PDF marcado (tagged) como definido na cláusula 14.7 da ISO 32000-1, o modelo segue a hierarquia lógica que o produtor registrou. Quando não carrega, e a maioria dos PDFs encontrados na prática não carrega, o modelo recorre à ordem de layout físico já calculada para fins de ordem de leitura

Essa escolha mantém um limite rígido: nenhum segundo analisador de PDF e nenhum segundo mecanismo de renderização são introduzidos para responder perguntas que o existente já consegue responder. A mecânica de ordem de leitura subjacente é descrita em blocos de texto estruturado e ordem de leitura, e o modelo de reformatação é uma camada semântica em cima dela, não uma substituição

Cada nó registra de onde veio sua informação, de modo que um consumidor pode distinguir um título que o documento declarou de um título que as heurísticas de layout inferiram. Pipelines sensíveis a confiança devem ler esse campo em vez de tratar todos os nós como igualmente autoritativos

Uma árvore achatada, e por que não é uma árvore de objetos

O modelo é uma árvore achatada em pré-ordem: um array de nós em que cada nó carrega um ParentIndex e uma Depth, em vez de um registro recursivo ou um grafo de objetos com posse. Páginas, títulos, parágrafos, listas, itens de lista, figuras, legendas, tabelas, linhas e células, todos vivem nesse único array linear

Dois benefícios decorrem disso. Os consumidores podem percorrer o array em ordem sem recursão, o que torna a emissão de HTML, Markdown ou uma visualização em árvore um simples loop. E o layout permanece portável entre Delphi, C++Builder e Free Pascal, que diferem em como tratam tipos gerenciados recursivos através de uma fronteira de ABI. Um registro recursivo de arrays dinâmicos é exatamente o tipo de construção que compila em todo lugar e se comporta de forma sutilmente diferente em cada um

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfReflowOptions;
  Doc: TPdfReflowDocument;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.LoadDocument;

    Options := TPdfReflowOptions.Default;
    Options.FullDocument := True;
    Options.DetectTables := True;
    Options.IncludeCss := True;          // bloco de estilo embutido, sem arquivo externo
    Options.MaxNodes := 200000;          // orçamento com falha fechada
    Options.MaxCharacters := 4000000;

    Doc := Pdf.BuildReflowDocument(Options);

    for I := 0 to High(Doc.Nodes) do
      case Doc.Nodes[I].Kind of
        prnkHeading:
          Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
        prnkParagraph:
          Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Copy(Doc.Nodes[I].Text, 1, 60)]));
        prnkTable:
          Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
      end;

    Writeln(Format('%d node(s), %d table(s), %d character(s)',
      [Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
  finally
    Pdf.Free;
  end;
end;

Como se evita que as tabelas apareçam duas vezes?

A detecção de tabelas roda depois que o texto estruturado já foi coletado para uma página, o que cria um risco óbvio: o mesmo conteúdo de célula existe tanto nos blocos de texto quanto na tabela detectada. Emitir os dois produz HTML em que cada tabela é seguida pelo seu próprio conteúdo de novo, como parágrafos soltos

A regra que resolve isso é geométrica. Quando uma tabela detectada cobre mais da metade da área de um bloco de texto, o nó da tabela substitui aquele bloco em vez de se juntar a ele. A indexação de células dentro de uma linha é construída contando em compartimentos (buckets), então construir o modelo permanece linear em células mais linhas, em vez de reescanear cada célula para cada linha, o que importa em documentos financeiros onde uma única página pode carregar centenas de células

A estrutura detectada é honesta quanto a ser uma detecção. Uma tabela com linhas de régua é reconhecida de forma mais confiável do que uma alinhada puramente por espaços em branco, e a confiança do nó reflete isso. Para conteúdo em que uma tabela errada é melhor do que nenhuma tabela, mantenha a detecção ativada; para conversão de arquivamento em que uma tabela errada é pior, filtre por confiança

Exportando HTML que permanece autocontido

ToHtml percorre o modelo que já foi construído e nunca revisita o PDFium, então exportar duas vezes não custa nada extra e não pode produzir um resultado diferente a partir do mesmo modelo. Textos e valores de atributo são escapados de forma uniforme, os níveis de título são limitados ao intervalo de h1 a h6 que o HTML de fato define, e células de cabeçalho, RowSpan e ColumnSpan passam adiante exatamente como escritos

O CSS opcional é um simples bloco de estilo embutido. Não há script, nenhuma web font e nenhum recurso externo de qualquer tipo, o que é o que torna a saída segura para incorporar em um e-mail, um visualizador de ajuda ou um controle de navegador em sandbox:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // mantém os limites de página visíveis
  Options.PreserveLineBreaks := False;   // deixa o navegador quebrar os parágrafos

  Html := Pdf.BuildReflowDocument(Options).ToHtml;

  Bytes := TEncoding.UTF8.GetBytes(string(Html));
  Stream := TFileStream.Create('report.html', fmCreate);
  try
    if Length(Bytes) > 0 then
      Stream.WriteBuffer(Bytes[0], Length(Bytes));
  finally
    Stream.Free;
  end;
end;

PreserveLineBreaks é a opção que mais vale a pena considerar. Uma quebra de linha em PDF é uma decisão de composição tipográfica tomada para uma largura de página fixa, então preservá-la em uma tela estreita reproduz exatamente o problema que a reformatação existe para resolver. Preserve as quebras para poesia, listagens de código e endereços; descarte-as para prosa

Orçamentos, cancelamento e estado da página

Caracteres, nós, tabelas e células têm cada um um teto, e cada um é verificado antes da alocação, não depois, de modo que um documento malformado ou hostil falha de forma limpa em vez de consumir memória até que outra coisa falhe. O token de cancelamento é verificado nos limites de página, bloco, tabela, linha e célula, o que mantém responsivo o escaneamento cancelado de um documento de mil páginas

Um comportamento importa especificamente para aplicações com interface gráfica: todo o escaneamento do documento roda dentro de um escopo que restaura a página ativa, então sucesso, falha de orçamento e cancelamento deixam a página atual do chamador intocada. Um visualizador que permite ao usuário exportar enquanto olha a página 340 se encontra ainda na página 340 depois

Para que a reformatação serve, e para que não serve

A saída de reformatação é uma excelente entrada para indexação de busca, visualizações de leitura acessíveis, exibição móvel e migração de conteúdo. Não é um conversor que preserva fidelidade: posições absolutas, fontes exatas, arte vetorial e geometria de página precisa ficam fora de seu propósito por design. Quando um trabalho precisa que a página tenha a mesma aparência, renderize-a; quando precisa que a página seja legível em outro lugar, reformate-a

Especificamente para tecnologia assistiva, o modelo de reformatação se combina com os recursos de leitura descritos em construindo um leitor acessível, e documentos que carregam uma árvore de estrutura genuína produzem modelos visivelmente melhores, o que é um bom argumento para validar a marcação a montante, como descrito em validação da árvore de estrutura PDF/UA

Reformatação, texto estruturado, validação de marcação e renderização compartilham um único objeto de documento entre Delphi, C++Builder e Lazarus; a API completa está descrita na página do PDFium Component para Delphi