Artigo Técnico

Config PDFium: quando o Brotli troca o Skia em silêncio

No PDFium Component para Delphi, ligar BrotliEnabled ou IsolatePerDocument no TPdfLibraryConfiguration costumava trocar a build Skia embutida pelo renderer AGG sem erro nenhum, porque as duas opções elevam o FPDF_LIBRARY_CONFIG a uma versão em que o PDFium lê o m_RendererType literalmente. Desde a v3.123.0 o renderer padrão continua sendo o padrão da própria DLL, e desde a v3.125.0 um pedido de Skia ou Fontations que a DLL não possa honrar lança um EPdfError capturável em vez de matar o processo

Nenhum dos bugs se anunciou. O primeiro produzia páginas que pareciam boas, só que renderizadas por um rasterizador diferente, com anti-aliasing e bordas de texto ligeiramente diferentes da build que você entregou e testou. O segundo se anunciou, e alto, derrubando o processo host de dentro da inicialização nativa. Ambos vêm do mesmo lugar: uma estrutura C versionada cujos campos só contam quando o número de versão diz que contam, e cujos valores zero não são "não definido" mas escolhas reais

Como o FPDF_LIBRARY_CONFIG decide qual renderer o PDFium usa?

O FPDF_InitLibraryWithConfig consulta o m_RendererType só quando o campo Version da estrutura é 4 ou maior, e a partir dessa versão usa o valor exatamente como escrito. Abaixo da versão 4 o PDFium ignora o campo e escolhe o padrão da build, que é Skia em builds compiladas com PDF_USE_SKIA e AGG em todo o resto

Cada campo posterior segue o mesmo padrão. A estrutura cresceu uma capacidade por vez, e cada capacidade chegou junto com um novo número de versão. O PDFium Component monta a estrutura nativa no LoadLibrary a partir do seu TPdfLibraryConfiguration e eleva a versão só até onde as opções que você definiu exigem

Versão da estruturaCampo que adicionaDefinido por
2m_pIsolate, m_v8EmbedderSlotSempre gravado; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform não nil
4m_RendererTypeRenderer diferente de prpDefault
5m_FontLibraryTypeFontBackend diferente de pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

A armadilha está nas duas últimas linhas. Versões são cumulativas: uma estrutura de versão 6 também é uma estrutura de versão 4 e de versão 5, então o PDFium lê o m_RendererType e o m_FontLibraryType mesmo que você só tenha pedido Brotli. O que estiver nesses dois campos naquele momento vira o renderer e o backend de fontes, você quis escolhê-los ou não

Escada de versões do FPDF_LIBRARY_CONFIG do PDFium Component da versão 2 à 7 mostrando qual opção do TPdfLibraryConfiguration adiciona m_RendererType, m_FontLibraryType, m_BrotliEnabled e m_IsolatePerDocument, e por que versões cumulativas fazem um campo de renderer zerado ser uma escolha AGG deliberada e não um valor não definido em qualquer build
Cada opção eleva a versão da estrutura e todo campo anterior continua vivo, então o zero no m_RendererType chega ao PDFium como um pedido AGG explícito

Por que ligar o Brotli trocava o renderer para AGG?

Antes da v3.123.0, o PDFium Component escrevia FPDF_RENDERERTYPE_AGG no m_RendererType para o prpDefault, então qualquer configuração que empurrasse a estrutura para a versão 6 ou 7 forçava AGG numa build Skia. Os runtimes pdfium.dll e pdfium.v8.dll que vêm com o componente são builds Skia, então isso acertava o deployment padrão, não um exótico

O mapeamento parecia inofensivo quando foi escrito. Na versão 2 ou 3 o campo nunca é lido, então prpDefault realmente significava "o que a DLL fizer". No momento em que BrotliEnabled (versão 6) ou IsolatePerDocument (versão 7) entrou em cena, o mesmo código transformou "sem preferência" num pedido AGG explícito. Nada falhou. O PDFium inicializou normalmente, renderizou cada página, e devolveu nenhum código de erro, porque do ponto de vista dele o chamador tinha pedido AGG e recebido AGG

Um hash de pixels torna a troca visível onde screenshots não mostram. Renderizar a primeira página do mesmo documento de amostra sob três configurações deu:

  • Configuração padrão: hash 502D77C3711B4ACF
  • BrotliEnabled = True com Renderer em prpDefault: hash F75B5EB4728ADE87
  • prpAgg explícito: hash F75B5EB4728ADE87, idêntico à rodada Brotli

O conserto na v3.123.0 é a função pública PdfNativeRendererType, que resolve um TPdfRendererPreference para o valor gravado no m_RendererType. prpAgg e prpSkia mapeiam um para um. O prpDefault agora mapeia para Skia quando a DLL carregada exporta FPDF_RenderPageSkia e para AGG caso contrário. Esse export é compilado sob a mesma condição PDF_USE_SKIA que o padrão Skia em si, o que o torna a única propriedade de build observável de fora da DLL. Depois do conserto, a configuração Brotli produz o mesmo hash da padrão

Comparação de hash de pixels do PDFium Component mostrando o hash de render Skia padrão 502D77C3711B4ACF, a configuração BrotliEnabled anterior à v3.123.0 igual a uma rodada prpAgg explícita com hash F75B5EB4728ADE87, e o wrapper corrigido resolvendo o prpDefault pelo export FPDF_RenderPageSkia de volta ao hash Skia original
Um hash de pixels pega o que screenshots escondem: ligar o Brotli renderizava cada página com AGG, e o padrão corrigido agora iguala a configuração intocada

O backend de fontes nunca teve o mesmo problema. O m_FontLibraryType é lido a partir da versão 5, e o valor zero dele, FPDF_FONTBACKENDTYPE_FREETYPE, também é o padrão do PDFium quando o campo não é lido de forma alguma. Gravar FreeType para o pfbpDefault portanto reproduz o padrão nativo exatamente. Valores zero nem sempre estão errados, eles só nunca estão automaticamente certos

Com v3.123.0 ou posterior, o código de inicialização que você escreveria naturalmente agora faz o que diz:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Precisa rodar antes de qualquer coisa carregar a biblioteca nativa
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // eleva FPDF_LIBRARY_CONFIG à versão 6
  // Renderer fica prpDefault: resolve para Skia em builds que exportam
  // FPDF_RenderPageSkia e para AGG em builds só AGG
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Lembre que o BrotliEnabled só torna streams /BrotliDecode do PDF 2.0 decodificáveis quando a própria DLL foi construída com PDF_ENABLE_BROTLI. A flag é um pedido, e numa build sem suporte a Brotli ela não tem efeito. O TPdfLibraryConfiguration.Hardened é igual ao Default exceto que AllowMachineTime é False, o que impede o JavaScript do documento de ler o relógio real; é um ponto de partida razoável para processamento no servidor de arquivos não confiáveis

O que acontece quando você pede um backend que a DLL não contém?

O PDFium não devolve erro para um renderer ou backend de fontes ausente da build: o FPDF_InitLibraryWithConfig falha num CHECK nativo, que no Windows aparece como uma exceção de breakpoint e, sem um structured exception handler em volta da chamada, termina o processo. O header diz isso, avisando que um valor não suportado "falhará de forma similar com um crash imediato"

Os dois casos concretos são uma build só AGG que recebe FPDF_RENDERERTYPE_SKIA, e uma build sem Fontations que recebe FPDF_FONTBACKENDTYPE_FONTATIONS. O runtime Skia embutido está no segundo grupo: ele renderiza com Skia mas usa FreeType para fontes. Pedir prpSkia junto com pfbpFontations contra ele produziu External exception 80000003 do lado Delphi. Quando o debugger ou um exception handler por acaso pega isso, a situação continua irrecuperável:

  • O PDFium fica meio inicializado
  • A configuração de todo o processo já está travada, então o ConfigurePdfLibrary recusa uma configuração corrigida
  • Repetir com uma configuração diferente no mesmo processo não é mais possível

Este é o fracasso oposto ao bug do Brotli. Ali o campo carregava um valor que ninguém escolheu e o PDFium aceitava em silêncio. Aqui o campo carrega um valor que o chamador escolheu deliberadamente e o PDFium não aceita discussão nenhuma a respeito. Ambos são problemas que um wrapper tem de resolver antes da chamada nativa, porque depois dela não sobra nada para capturar

Como o PDFium Component pré-checa Skia e Fontations

Desde a v3.125.0, o LoadLibrary valida a configuração depois de vincular os exports da DLL e antes de chamar o FPDF_InitLibraryWithConfig, e transforma um renderer ou backend de fontes não suportado num EPdfError com uma mensagem que nomeia a configuração ofensora e as alternativas. A DLL é descarregada e a configuração é destravada, então o chamador pode escolher outras configurações e carregar de novo

A decisão em si vive na função pura PdfLibraryConfigurationSupportError, que recebe a configuração mais dois Booleans descrevendo a build e devolve uma string vazia quando a combinação é segura. Como ela não toca em estado nativo, você pode chamá-la dos seus próprios testes com qualquer combinação de capacidades. Dentro do LoadLibrary os dois Booleans vêm de tipos diferentes de evidência, e merecem níveis diferentes de confiança:

  • Skia é detectado pela presença do export FPDF_RenderPageSkia, o mesmo sinal que o PdfNativeRendererType usa. O export e o renderer Skia são compilados sob uma condição, então a checagem é exata
  • Fontations não tem export próprio. O único rastro que deixa são as font crates Rust que puxa para o binário, então o PDFium Component escaneia o arquivo da biblioteca carregada atrás dos nomes de crate skrifa e read-fonts (também read_fonts). O escaneamento roda só quando pfbpFontations é pedido, e um arquivo que não pode ser lido conta como "sem Fontations"

A checagem de Fontations é uma heurística, e pode errar num sentido: uma build Fontations despojada de todas essas strings seria rejeitada embora pudesse funcionar. O trade foi feito de propósito. Uma rejeição falsa custa uma exceção que você pode capturar e um fallback para FreeType. Uma aceitação falsa custa o processo

Destravar importa tanto quanto a checagem. O LoadLibrary trava a configuração bem no começo do carregamento, então sem o reset uma rejeição de capacidade deixaria o ConfigurePdfLibrary respondendo toda repetição com o EPdfError "A configuração da biblioteca PDFium já está travada". O caminho de rejeição chama UnloadLibrary primeiro; a chamada FPDF_DestroyLibrary dele é segura nesse ponto porque o PDFium ainda não foi inicializado e retorna imediatamente. Outras falhas de carregamento, como uma DLL ausente ou um mismatch de arquitetura, mantêm a trava, então um loop de repetição tem de distinguir as duas:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // qualificado pela unit: Windows.LoadLibrary tem o mesmo nome
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Uma rejeição de capacidade descarrega a DLL e destrava a configuração.
      // Uma DLL que não carregou de forma alguma fica travada: repetir não ajuda
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Note o PDFium.LoadLibrary explícito. Numa unit que também usa Windows ou Winapi.Windows, um LoadLibrary sem qualificação resolve para a unit que aparecer por último na uses clause; quando essa é a função Win32, a chamada sem parâmetros não compila com um erro de contagem de argumentos que não diz nada sobre o PDFium

Fluxo de pré-checa do LoadLibrary do PDFium Component em que o ConfigurePdfLibrary trava a configuração, a checagem de capacidade testa o export FPDF_RenderPageSkia e a evidência de strings skrifa, um pedido não suportado lança um EPdfError capturável e destrava para repetir, enquanto uma DLL que nunca carrega mantém PdfLibraryConfigurationSealed true
A validação roda depois de os exports vincular e antes da inicialização, então um backend ausente falha como um EPdfError que você pode capturar em vez de um CHECK nativo que mata o processo

Validação que acontece ainda mais cedo

O ConfigurePdfLibrary rejeita algumas combinações antes de qualquer DLL entrar em cena, todas com EPdfError. Um FontBackend explícito, incluindo pfbpFreeType, exige Renderer = prpSkia, porque o PDFium só consulta o backend de fontes para o renderer Skia. O IsolatePerDocument exige V8Isolate nil, já que o PDFium cria o isolate próprio dele por documento e falha num CHECK nativo se você também entregar um. Strings vazias em UserFontPaths são rejeitadas. E qualquer chamada depois da primeira tentativa de carregamento falha com "A configuração da biblioteca PDFium já está travada"

Essa última regra tem uma consequência prática: você não pode espionar a DLL primeiro e configurá-la depois. O GetSkiaRenderCapabilities, o V8FeaturesAvailable, abrir um documento e a maior parte dos outros pontos de entrada chama o LoadLibrary internamente, o que trava a configuração na hora. Chamar o UnloadLibrary depois também não a reabre. Configure primeiro, depois carregue, depois pergunte, que é exatamente a ordem que uma rotina de diagnóstico deve seguir:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // uma cópia, seguro inspecionar
  if not PDFium.Loaded then
  begin
    if PdfLibraryConfigurationSealed then
      Exit('PDFium failed to load; configuration is sealed');
    Exit('PDFium not loaded; configuration can still change');
  end;
  // A mesma resolução que LoadLibrary aplicou ao montar FPDF_LIBRARY_CONFIG
  if PdfNativeRendererType(Config.Renderer,
    GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
    Renderer := 'Skia'
  else
    Renderer := 'AGG';
  Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
    [Renderer, BoolToStr(Config.BrotliEnabled, True),
     BoolToStr(Config.IsolatePerDocument, True)]);
end;

Registrar essa linha uma vez na inicialização é barato, e é a primeira coisa que você quer num ticket de suporte que diz "o texto parece diferente no servidor". O PDFium.Loaded é qualificado pelo mesmo motivo que o LoadLibrary: dentro de um método de form ou componente, um Loaded nu vincula ao TComponent.Loaded

Duas maneiras de uma struct de configuração C versionada dar errado

Toda estrutura de configuração versionada, seja um FPDF_LIBRARY_CONFIG, um registro Win32 com cbSize, ou uma ABI de plugin, falha de duas maneiras simétricas, e um wrapper precisa se guardar das duas. A primeira é preencher um campo deixando a versão baixa demais; a segunda é elevar a versão deixando um campo num valor zero que a biblioteca lê como escolha deliberada

  1. Campo definido, versão baixa demais. Grave m_BrotliEnabled = 1 numa estrutura de versão 2 e o PDFium nunca olha para ele. A chamada tem sucesso e streams Brotli continuam indecodificáveis. A defesa é derivar a versão dos campos efetivamente em uso, que é o que o LoadLibrary faz, em vez de fixar um
  2. Versão alta o bastante, campo zero significa algo. Eleve a versão para 6 e todo campo até a versão 6 agora está vivo. O FillChar zera o m_RendererType para FPDF_RENDERERTYPE_AGG, que é um renderer real, não "não definido". A defesa é gravar todo campo que a versão escolhida cobre com um valor intencional, e resolver o "padrão" contra a build real em vez de assumi-lo

Uma terceira regra se segue para valores que podem derrubar o chamado: valide-os contra o que o binário consegue fazer antes da chamada, usando a evidência mais forte disponível, e seja honesto no código e na documentação quando essa evidência for uma heurística. Um símbolo exportado é prova. Um nome de crate numa tabela de strings é um bom chute

Referência rápida: configuração da biblioteca do PDFium Component

  • Chame o ConfigurePdfLibrary uma vez, antes de qualquer coisa carregar a DLL; qualquer consulta de capacidade ou carregamento de documento o trava
  • Atualize para v3.123.0 ou posterior se você define BrotliEnabled ou IsolatePerDocument e espera saída Skia dos runtimes embutidos
  • Deixe o Renderer em prpDefault a menos que precise de um rasterizador específico; ele agora resolve para o padrão da build em toda versão da estrutura
  • Use o PdfNativeRendererType com GetSkiaRenderCapabilities.PageRender para registrar qual renderer está de fato ativo
  • Espere um EPdfError, não um crash, para prpSkia numa DLL só AGG ou pfbpFontations numa DLL sem Fontations na v3.125.0 ou posterior
  • Depois de uma rejeição de capacidade, PdfLibraryConfigurationSealed é False e você pode reconfigurar; depois de um carregamento de DLL que falhou ele continua True
  • Trate a detecção de Fontations como heurística e mantenha um fallback FreeType
  • Escreva PDFium.LoadLibrary e PDFium.Loaded com o nome da unit para evitar conflitos de nome com Win32 e TComponent

Se a DLL falha antes de a configuração sequer importar, comece por diagnosticar falhas de carregamento da DLL PDFium em Delphi, e para como o componente encontra o binário certo em cada plataforma veja carregar a biblioteca nativa PDFium em qualquer alvo. Uma vez ajustado o renderer, táticas de cache de render e zoom suave cobre como manter a renderização de páginas rápida num viewer

O PDFium Component embrulha o motor PDFium para Delphi e C++Builder com checagens de configuração como estas, para que a inicialização nativa falhe como uma exceção Pascal que você pode tratar em vez de uma saída do processo. Detalhes de produto e downloads estão na página de produto do PDFium Component para Delphi