Technical Article

Multi-Engine PDF Rendering in Delphi: Built-in, Cairo, and PDFium with PDFlibPas

Três rasterizadores podem ler o mesmo PDF e discordar quanto ao seu aspeto. O motor integrado do PDFlibPas é o que é distribuído sem ficheiros adicionais e renderiza tudo com competência, razão pela qual ocupa a posição predefinida. O Cairo oferece um pipeline de transparências e suavização (anti-aliasing) diferente, tendendo a ser a opção escolhida quando as máscaras suaves (soft masks) ou modos de mistura (blend modes) falham noutros motores. O PDFium contém o código de renderização do Chrome, pelo que uma página que seja apresentada corretamente num navegador normalmente também é bem desenhada sob o PDFium, à custa de uma DLL volumosa e de uma arquitetura (bitness) à qual exige corresponder. Nenhum dos três motores é o correto em abstrato. A correção avalia-se por documento, e a única forma honesta de saber qual o motor que melhor processa um determinado conjunto de ficheiros (corpus) é submetê-lo a cada um deles.

Esta é a razão pela qual se deve tratar o motor de renderização como uma escolha em tempo de execução e não de compilação. O PDFlibPas, a biblioteca PDF para Delphi e C++Builder da losLab, disponibiliza os três sob uma única superfície de renderização, de modo que a decisão custa apenas um inteiro em vez de uma bifurcação no código. O restante processo resume-se a selecionar entre eles com segurança, confirmar quais os motores que o binário implementado de facto contém e evitar que o estado de renderização afete silenciosamente a tarefa seguinte.

Três rasterizadores sob uma única superfície de chamada

A biblioteca numera os seus motores. O motor 1 é o renderizador integrado, o predefinido, com opções de suavização GDI+ em Windows. O motor 2 é o Cairo e o motor 3 é o PDFium, ambos selecionados em tempo de execução através de SelectRenderer. Os dois motores externos são carregados a partir de DLLs cujos caminhos são fornecidos através de SetCairoFileName e SetPDFiumFileName antes de os selecionar. Independentemente do motor ativo, o trabalho processa-se através das mesmas chamadas: RenderPageToFile, RenderPageToStream, RenderDocumentToFile. Mudar de motor altera apenas um número; o resto do seu código de renderização não deteta qualquer alteração.

O modelo de destino vai muito além dos bitmaps. A classe do renderizador também suporta metaficheiros (WMF, EMF, EMF+), EPS, contextos de dispositivo diretos, impressoras e HTML5, surgindo o Cairo e o PDFium como destinos extra apenas quando compilados com esse suporte. A saída rasterizada é onde os três motores divergem de forma mais visível, razão pela qual é a utilizada nos exemplos apresentados.

Nunca assuma que um motor existe: realize testes no arranque

O Cairo e o PDFium são funcionalidades de compilação condicional, o que significa que um binário pode ser construído inteiramente sem o seu suporte. Quando isso acontece, solicitar o motor 2 ou 3 não gera qualquer exceção. O método SelectRenderer limita-se a devolver um valor diferente do ID que solicitou, e o código que ignore o valor de retorno continuará a renderizar com o motor que estivesse ativo. A salvaguarda consiste num teste de arranque que solicita a identificação a cada motor e regista o resultado:

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // engine 1 is always present
  if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
    Result := Result + ', cairo';
  if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
    Result := Result + ', pdfium';
  PDF.SelectRenderer(1);                       // restore the default before real work
end;

Execute esse teste uma vez no arranque e registe o resultado no diário ao lado de cada tarefa de renderização. A pergunta mais frequente quando um cliente relata uma divergência de renderização é saber quais os motores que a sua instalação possui, e uma linha no registo esclarece a dúvida sem necessidade de uma sessão de ambiente de trabalho remoto. Um efeito secundário útil: se SetPDFiumFileName devolver 0, já sabe que o problema está na DLL (caminho incorreto, arquitetura errada ou dependência em falta) e não num binário compilado sem suporte para PDFium, dado que a chamada do caminho não resolveu nada antes de SelectRenderer ser executado.

Ten output formats behind one Options integer

O parâmetro Options nas chamadas de renderização seleciona a codificação de saída: 0 é BMP, 1 JPEG, 2 WMF, 3 EMF, 4 EPS, 5 PNG, 6 GIF, 7 TIFF, 8 EMF+ e 9 HTML5. O formato PNG (5) é o padrão recomendado para pré-visualizações e imagens de arquivo de páginas. O JPEG (1), associado a SetJPEGQuality, é a melhor escolha para digitalizações fotográficas em que o tamanho do ficheiro seja mais importante do que as arestas nítidas.

Um dos formatos oculta um requisito relativo ao fluxo (stream) de destino. O fluxo do BMP escreve os dados da imagem primeiro e depois retrocede para o desvio 0x26 para corrigir os campos de resolução no cabeçalho. Si apontar isto para um fluxo unidirecional (forward-only), como um compressor ou um socket de rede, a chamada falhará com um erro que parece do motor, mas não é. Quando um destino não reposicionável for inevitável, renderize para PNG ou processe o BMP através de um fluxo de memória, copiando-o de seguida quando concluído.

O DPI que passa não é o DPI que obtém

Cada chamada de renderização aceita um argumento de DPI, mas a resolução final que obtém é esse valor multiplicado pela escala de renderização global. A propriedade SetRenderScale inicia-se em 1.0 e, após ser alterada, o novo fator aplica-se silenciosamente a todas as renderizações posteriores nessa instância:

PDF.SetRenderScale(2.0);                    // every later render is doubled
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // effectively 300 DPI
PDF.SetRenderScale(1.0);                    // reset, or your thumbnails arrive huge

A mesma retenção de valores aplica-se a SetRenderCropType e à definição de qualidade do JPEG. Num serviço que produza miniaturas, pré-visualizações e imagens com resolução de impressão a partir de uma única instância partilhada, estas definições residuais são a verdadeira causa do problema de as miniaturas passarem subitamente a ocupar 40 MB. Existem duas soluções limpas: repor o estado relevante no início de cada operação ou dedicar uma instância separada para cada perfil de saída para evitar fugas de definições.

Ajustar o motor predefinido antes de recorrer a outro

Uma percentagem surpreendente de pedidos de "necessidade de um motor diferente" revela-se afinal ser problemas de configuração sob disfarce. O renderizador integrado expõe o seu comportamento de suavização através de SetGDIPlusOptions e da família mais vasta de SetRenderOptions, enquanto SetGDIPlusFileName permite apontar para um runtime específico do GDI+ quando o ambiente de implementação disponibiliza um invulgar. Desenhos vetoriais pixelizados a baixos DPIs, texto desfocado em miniaturas ou efeito de degradação em gradientes: todos respondem a esses ajustes e a sua ativação não acarreta custos de instalação. Pelo contrário, adicionar o Cairo ou o PDFium implica distribuir mais DLLs, monitorizar uma segunda ou terceira arquitetura e assumir a responsabilidade de mantê-las atualizadas.

Assim, uma queixa relativa à qualidade segue uma ordem natural de operações. Reproduza-a primeiro com o DPI e escala exatos do cliente, dado que metade das vezes a diferença desaparece quando estes coincidem. Em seguida, experimente as opções de suavização do motor integrado. Apenas após estes passos deve colocar a página lado a lado entre motores com todas as outras variáveis constantes: renderize para PNG através dos motores 1, 2 e 3 com DPI idêntico e compare as três imagens. Habitualmente, dois dos três concordam, e essa maioria indica se o desvio se deve a uma interpretação diferente do documento ou se a expetativa inicial de qualidade estava incorreta. Três imagens concretas resolvem uma disputa de "renderização incorreta" muito mais rapidamente do que um parágrafo de adjetivos.

Uma cadeia de fallback que se explica a si mesma

Once probing and state discipline are in place, the fallback chain itself is short. A deteção de uma falha baseia-se em LastRenderError, que armazena a mensagem do próprio motor relativamente à renderização mais recente, permanecendo vazia em caso de sucesso:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // built-in first
  PDF.RenderPageToFile(200, Page, 5, OutFile);      // 5 = PNG
  if PDF.LastRenderError = '' then Exit;
  LogEngineFailure('built-in', Page, PDF.LastRenderError);
  if PDF.SelectRenderer(3) = 3 then                 // PDFium as the heavy fallback
  begin
    PDF.RenderPageToFile(200, Page, 5, OutFile);
    if PDF.LastRenderError = '' then Exit;
    LogEngineFailure('pdfium', Page, PDF.LastRenderError);
  end;
  raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;

Dois aspetos do design são importantes neste caso. A cadeia regista o motivo de cada alteração, uma vez que uma linha de registo com a indicação "esta página recorreu ao PDFium desde a versão 3.7" constitui um sinal de regressão que convém monitorizar e não ignorar. A ordem de fallback em si é uma política que deve ser definida por carga de trabalho. O motor integrado é implementado sem DLLs extra, o que o torna a primeira tentativa lógica na maioria das instalações, enquanto documentos com elevada densidade de grupos de transparência ou sombras invulgares constituem a justificação típica para integrar um motor alternativo. Nenhum motor é o mais rápido em geral, sendo este o motivo de escolha por chamada: avalie o desempenho de cada um com uma amostra dos seus documentos reais à resolução DPI de produção e repita a medição sempre que as DLLs do motor ou o perfil dos documentos mudarem. O corpus documental tem sempre a última palavra.

Além das páginas individuais: lotes TIFF e contextos de dispositivo ativos

Duas rotinas complementares completam a caixa de ferramentas. O método RenderAsMultipageTIFFToFile renderiza uma expressão de intervalo de páginas diretamente num TIFF multipágina, o formato tempo-arquivamento para entrega de arquivos a sistemas de gestão documental que antecedem o PDF. O método RenderPageToDC desenha diretamente num contexto de dispositivo (DC) do Windows para controlos de pré-visualização, condicionado pelo seu próprio trio de definições retidas (SetRenderDCOffset, SetRenderDCErasePage e o tipo de recorte) que exigem a mesma disciplina de reposição que o fator de escala. A pré-visualização no ecrã e a renderização do caminho de impressão contêm armadilhas suficientes para justificar um artigo dedicado, associado abaixo.

Where to go next

Um hábito que convém manter: dado que SelectRenderer afeta todas as chamadas posteriores da instância, uma única página problemática pode ser processada novamente noutro motor enquanto o resto do documento se mantém no motor predefinido. Para desenho de pré-visualizações, seleção de impressoras e gestão de DevMode, continue com o artigo sobre pré-visualização de impressão e contexto de dispositivo. Se a renderização alimentar um fluxo de processamento de alto volume em ficheiros volumosos, a abordagem baseada em handles no guia de acesso direto alia-se naturalmente à renderização por página através de DARenderPageToFile.

O empacotamento do motor, formatos suportados e compilações de teste são detalhados na página do produto PDFlibPas.