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