Artigo Técnico

Renderizar páginas PDF em bitmap no Delphi com HotPDF

O HotPDF renderiza uma página de PDF carregado em um TBitmap do Delphi por meio de uma única chamada: RenderLoadedPageToBitmap(PageIndex, DPI). A função interpreta o content stream da página e devolve um bitmap RGB de 24 bits, cuja posse é de quem chamou, na resolução que você escolher, que é exatamente o que uma faixa de miniaturas, uma visualização de impressão ou um pipeline de exportação de PDF para imagem precisa. Este artigo percorre a API e depois a parte que separa um renderizador utilizável de um brinquedo: desenhar texto a partir dos próprios programas de fonte incorporados, e não de fontes do sistema parecidas

Por que renderizar uma página de PDF é mais difícil do que desenhar uma imagem?

Uma página de PDF não é uma figura. É um programa: um fluxo de operadores que constroem caminhos, selecionam fontes, definem cores e posicionam glifos, executado contra o modelo gráfico definido na ISO 32000-1 §8. Nada no arquivo diz como cada pixel se parece. Para produzir um bitmap você precisa executar esse programa — manter uma matriz de transformação atual, uma pilha de estado gráfico para q/Q, um caminho de recorte, espaços de cor de preenchimento e de traço — e rasterizar o resultado. É por isso que "só mostre a página 3 como imagem" é um interpretador de content stream, e não uma conversão de formato de arquivo

O renderizador do HotPDF, apresentado na v2.253.0, é construído como seis unidades desacopladas que espelham esse modelo: um núcleo de matriz afim para a álgebra de transformação [a b c d e f] do PDF, uma pilha de estado gráfico, um resolvedor de espaços de cor (DeviceRGB, DeviceGray, DeviceCMYK, Indexed), um construtor de caminhos que faz a ponte entre os operadores de caminho do PDF e o GDI, uma camada de métricas de fonte que lê os arrays /Widths para avanços corretos, e o interpretador que despacha os operadores e conduz as outras cinco. Os XObjects de imagem passam pela mesma pilha de decodificação que a biblioteca usa na extração, então todo filtro de imagem que o HotPDF decodifica para extração — incluindo imagens JPEG 2000 comprimidas com JPXDecode — também aparece na saída renderizada

Arquitetura de renderização de páginas do HotPDF: um interpretador de content stream de PDF conduz seis unidades desacopladas e produz um TBitmap RGB de 24 bits cuja posse é de quem chamou
O interpretador despacha os operadores enquanto as seis unidades cuidam da matriz, do estado, da cor, do caminho, das métricas e do trabalho com glifos

Renderizando uma página carregada em um TBitmap

RenderLoadedPageToBitmap recebe um índice de página que começa em zero e um valor de DPI, em que 72 DPI mapeia uma unidade do espaço do usuário do PDF para um pixel. Ele devolve nil em caso de falha (índice fora do intervalo, recursos ausentes) em vez de levantar exceção, então um visualizador pode pular uma página ruim e seguir em frente. Quem chama é dono do bitmap devolvido e precisa liberá-lo

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report.pdf') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);  // página 1 a 144 DPI
      if Bmp <> nil then
      try
        Image1.Picture.Assign(Bmp);
      finally
        Bmp.Free;  // quem chama é dono do bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

O argumento de DPI faz o trabalho de escala em todos os cenários comuns. Uma faixa de miniaturas renderiza a 36 ou 48 DPI e recebe bitmaps pequenos e rápidos; uma visualização em tela a 96 ou 144 DPI acompanha a densidade típica de exibição; um caminho de exportação a 300 DPI produz imagens com qualidade de impressão. A rotação de página vinda da entrada /Rotate e a inversão de origem do /MediaBox (o PDF coloca a origem no canto inferior esquerdo, o GDI no superior esquerdo) são tratadas dentro da matriz de página para dispositivo, então uma página US Letter a 72 DPI volta com exatamente 612×792 pixels e na orientação certa

Por que miniaturas de PDF renderizadas mostram glifos errados?

Glifos errados ou aproximados na saída renderizada de um PDF quase sempre significam que o renderizador está substituindo por uma fonte do sistema em vez de usar a fonte incorporada no arquivo. O primeiro renderizador do HotPDF fazia exatamente isso: removia o prefixo de subconjunto de /BaseFont (transformando ABCDEF+Arial em Arial), pedia ao GDI uma fonte de sistema com esse nome e desenhava o texto com ela. Para um documento que usa Arial ou Times New Roman com codificação padrão, o resultado fica parecido. Mas é uma aproximação, e ela quebra de maneiras bem definidas

Fontes incorporadas como subconjunto são o pior caso. Uma fonte de subconjunto pode carregar só os quarenta glifos que o documento de fato usa, com códigos de caractere atribuídos em uma ordem privada daquele arquivo — o código 1 pode ser "T", o código 2 "h", e assim por diante. Uma fonte do sistema não sabe nada sobre essa atribuição privada, então o texto ou desaparece ou sai com os caracteres completamente errados. Codificações personalizadas, fontes de símbolos, fontes de código de barras e qualquer família não instalada na máquina de renderização falham do mesmo jeito. Um renderizador que para na substituição por fonte do sistema produz miniaturas em que a página é reconhecível — até a página usar as fontes que tornaram a incorporação necessária, para começo de conversa

HotPDF: a substituição por fonte do sistema remove o prefixo de subconjunto de ABCDEF+Arial e desenha glifos errados, enquanto a renderização de glifos incorporados reproduz os contornos do FontFile com as formas exatas
A substituição só sobrevive em famílias comuns do sistema e falha exatamente nas fontes de subconjunto que tornaram a incorporação necessária

Renderização de glifos incorporados: desenhar a partir do próprio programa de fonte

O HotPDF fechou essa lacuna ao longo de cinco versões (da v2.268.0 à v2.272.0) analisando os programas de fonte incorporados e reproduzindo os contornos dos glifos deles como caminhos vetoriais preenchidos do GDI. O texto em uma página renderizada agora vem dos mesmos dados de contorno que um visualizador em conformidade usa, o que significa que fontes de subconjunto, codificações personalizadas e famílias não instaladas renderizam com as formas exatas delas. A cobertura foi construída por sabor de fonte:

Para fontes Type0/CIDFontType2 com um programa TrueType incorporado (FontFile2), o renderizador analisa as tabelas glyf e loca diretamente: os contornos quadráticos são convertidos nas Béziers cúbicas que o GDI entende, os pontos implícitos sobre a curva entre pontos consecutivos fora da curva são reconstruídos, e os glifos compostos são reproduzidos recursivamente. Tanto o layout Identity quanto o CIDToGIDMap explícito em stream são suportados, e os avanços de CID respeitam as entradas de largura /W e /DW, de modo que o texto Identity-H de dois bytes avança corretamente

Os programas CFF (FontFile3, sejam CIDFontType0C, Type1C ou um invólucro OpenType) ganham um interpretador completo de charstrings Type 2: linhas, curvas, a família flex, máscaras de hint e chamadas de sub-rotinas locais e globais com o viés de sub-rotina correto. Programas CFF com chave CID mapeiam os códigos de caractere pelo charset da fonte, o que importa para fontes de subconjunto cuja ordem de glifos difere da ordem de CID, e a seleção de font DICT por glifo através de FDArray/FDSelect é respeitada. Fontes TrueType simples (não CID) resolvem códigos de um byte pela própria tabela cmap da fonte incorporada, com uma cadeia robusta de subtabelas — primeiro os formatos Unicode 4 e 12, depois as subtabelas de símbolo com o espelho de uso privado F000, depois os formatos Macintosh legados — enquanto as fontes Type1 simples resolvem pela codificação embutida do programa CFF

Dois refinamentos completam o quadro. Primeiro, os dicionários /Encoding de fontes simples são resolvidos segundo a prioridade que a ISO 32000-1 §9.6.6 prescreve: os arrays /Differences sobrepõem-se à codificação base, que se sobrepõe ao mapa do próprio programa de fonte — o caminho de que dependem o TeX e as cadeias de ferramentas derivadas do PostScript, com os nomes de glifo resolvendo pela Adobe Glyph List, pelo charset CFF ou pela cmap TrueType. Segundo, as fontes Type3, cujos glifos são eles próprios pequenos content streams, são reproduzidas pelo renderizador com a matriz da fonte, o tamanho da fonte e a matriz de texto compostos; os /Widths em espaço de glifo são interpretados pela /FontMatrix como a ISO 32000-1 §9.6.5 exige, e os procedimentos de glifo que declaram uma caixa delimitadora d1 são recortados por ela, de modo que um glifo de código de barras malformado não consiga pintar fora da célula dele. Quando um código não pode ser mapeado — um programa danificado, um caractere sem mapeamento — o renderizador recorre ao desenho com fonte do sistema para aquele glifo, em vez de descartar o trecho de texto

Como deixar renderizações repetidas rápidas?

A resposta que o HotPDF entrega é um cache de páginas por uso mais recente: RenderLoadedPageToBitmapCached guarda até RenderCacheCapacity páginas renderizadas (8 por padrão), com chave formada pelo índice da página e pelo DPI, e um acerto de cache devolve uma cópia nova, de posse de quem chamou, sem tocar no content stream — em geral milhares de vezes mais rápido do que reinterpretar a página. Esse padrão serve exatamente aos visualizadores: um usuário alternando entre duas páginas, ou um evento de redimensionamento que pede de novo a mesma página no mesmo DPI, acerta o cache todas as vezes

HotPDF: fluxo do cache de páginas renderizadas por uso mais recente: um acerto devolve uma cópia nova de TBitmap sem reinterpretar a página, uma falha renderiza e armazena até RenderCacheCapacity páginas
Um acerto de cache pula o content stream por completo, enquanto a invalidação e o orçamento de memória mantêm as renderizações repetidas corretas e seguras
// Faixa de miniaturas: a primeira passagem renderiza, voltar a rolar acerta o cache
for I := 0 to ThumbCount - 1 do
begin
  Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
  if Bmp <> nil then
  try
    ThumbList.AddThumbnail(I, Bmp);
  finally
    Bmp.Free;
  end;
end;

// Depois de editar no lugar uma página carregada:
Pdf.InvalidateRenderedPageCache;  // a próxima renderização reflete a mudança

Seja honesto sobre a conta de memória antes de aumentar a capacidade. Uma página US Letter a 300 DPI tem 2550×3300 pixels, cerca de 25 MB como bitmap de 24 bits, então oito páginas em cache na resolução de exportação ocupam por volta de 200 MB. No DPI de miniatura, essas mesmas oito entradas custam bem menos de um megabyte. Dimensione RenderCacheCapacity para o DPI em que você realmente faz cache, e chame InvalidateRenderedPageCache depois de qualquer edição no lugar — o cache tem chave apenas de página e DPI, e não enxerga que o conteúdo subjacente mudou. Carregar um novo documento o limpa automaticamente

Um segundo cache trabalha por baixo do cache de páginas: os XObjects de imagem decodificados ficam em um armazenamento com orçamento de bytes limitado por ImageCacheMaxBytes (32 MB por padrão), com despejo por menos recentemente usado. Um logotipo ou uma imagem de papel timbrado repetida em todas as páginas é decodificada uma vez por carregamento do documento, e não uma vez por operador Do, o que praticamente reduz à metade o tempo de renderização em páginas com imagens compartilhadas e acelera na mesma medida a exportação de TIFF de várias páginas. InvalidateRenderedPageCache limpa esse cache também

O que ainda é renderizado de forma aproximada

O renderizador mira o subconjunto comum de PDFs de documento, e vale saber onde ficam as bordas. Os espaços de cor CalRGB, Lab e baseados em ICC são aproximados em vez de gerenciados por cor — espaços de cor de dispositivo, paletas Indexed e consultas de cor por função Type 0 amostrada são tratados, mas um arquivo de produção gráfica que dependa de intenções de renderização ICC não ficará colorimetricamente exato. Padrões de sombreamento (sh) e modos de mesclagem além do alfa simples também estão fora de escopo, e a recursão de Form XObject tem limite de profundidade como proteção contra ciclos. Para notas fiscais, relatórios, contratos e formulários — páginas feitas de texto, caminhos e imagens — a saída é fiel; para uma prova de design cheia de gradientes e grupos de transparência, trate o bitmap como visualização, não como prova de cor

A leitura prática: se o seu pipeline gera documentos com o HotPDF ou consome PDFs de negócio típicos, RenderLoadedPageToBitmap faz o percurso de ida e volta com as formas exatas dos glifos incorporados, avanços de CID corretos e geometria de página correta. As aproximações vivem nos cantos do modelo gráfico que documentos de negócio raramente visitam

RenderLoadedPageToBitmap, a variante com cache e o pipeline de renderização de glifos incorporados descrito aqui acompanham o HotPDF Delphi Component para Delphi e C++Builder — uma biblioteca VCL nativa sem dependências de DLLs externas, cobrindo criação, edição, extração de texto e renderização de páginas de PDF em um único pacote