Artigo Técnico

Config da biblioteca PDFium: quando o Brotli troca o Skia

No PDFium Component para Delphi, ligar BrotliEnabled ou IsolatePerDocument em TPdfLibraryConfiguration mudava a build Skia incluída para o renderer AGG sem erro nenhum, porque ambas as opções elevam o FPDF_LIBRARY_CONFIG a uma versão em que o PDFium lê o m_RendererType ao pé da letra. Desde a v3.123.0 o renderer por omissão mantém-se o próprio por omissão da DLL, e desde a v3.125.0 um pedido Skia ou Fontations que a DLL não consiga honrar lança um EPdfError apanhável em vez de matar o processo

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

Como decide o FPDF_LIBRARY_CONFIG que renderer o PDFium usa?

O FPDF_InitLibraryWithConfig consulta o m_RendererType só quando o campo Version da estrutura é 4 ou superior, 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 por omissão da build, que é Skia nas 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 de cada vez, e cada capacidade chegou junto com um número de versão novo. O PDFium Component constrói a estrutura nativa em LoadLibrary a partir do seu TPdfLibraryConfiguration e eleva a versão só até onde as opções que definiu exigem

Versão da estruturaCampo que acrescentaDefinido por
2m_pIsolate, m_v8EmbedderSlotSempre escritos; 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. As versões são cumulativas: uma estrutura de versão 6 também é uma estrutura de versão 4 e de versão 5, por isso o PDFium lê o m_RendererType e o m_FontLibraryType mesmo que só tenha pedido Brotli. O que estiver nesses dois campos nesse momento torna-se o renderer e o backend de fontes, queira escolhê-los ou não

Escada de versões do FPDF_LIBRARY_CONFIG do PDFium Component da versão 2 à versão 7 mostrando que opção TPdfLibraryConfiguration acrescenta m_RendererType, m_FontLibraryType, m_BrotliEnabled e m_IsolatePerDocument, e porque as versões cumulativas fazem de um campo de renderer a zero uma escolha AGG deliberada em vez de um valor não definido em qualquer build
cada opção eleva a versão da estrutura e todos os campos anteriores continuam vivos, por isso o zero em m_RendererType chega ao PDFium como um pedido AGG explícito

Porque é que ligar o Brotli mudava o renderer para AGG?

Antes da v3.123.0, o PDFium Component escrevia FPDF_RENDERERTYPE_AGG no m_RendererType para prpDefault, por isso 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 acompanham o componente são builds Skia, por isso isto acertava na implantação por omissão, não numa exótica

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

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

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

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

Comparação de hash de píxeis do PDFium Component mostrando o hash Skia da renderização por omissão 502D77C3711B4ACF, a configuração BrotliEnabled anterior à v3.123.0 a coincidir com uma corrida prpAgg explícita com hash F75B5EB4728ADE87, e o wrapper corrigido a resolver prpDefault através do export FPDF_RenderPageSkia de volta ao hash Skia original
um hash de píxeis apanha o que as capturas de ecrã escondem: ligar o Brotli costumava renderizar todas as páginas com AGG, e o por omissão corrigido agora coincide com 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 seu valor zero, FPDF_FONTBACKENDTYPE_FREETYPE, é também o por omissão do PDFium quando o campo não é lido de todo. Escrever FreeType para pfbpDefault reproduz portanto exatamente o por omissão nativo. Valores zero não são sempre errados, só nunca são automaticamente certos

Com a v3.123.0 ou posterior, o código de arranque que escreveria naturalmente agora faz o que diz:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Tem de correr antes de qualquer coisa carregar a biblioteca nativa
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // eleva o FPDF_LIBRARY_CONFIG à versão 6
  // O Renderer fica prpDefault: resolvido 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-se de que o BrotliEnabled só torna streams /BrotliDecode de PDF 2.0 descodificáveis quando a própria DLL foi compilada com PDF_ENABLE_BROTLI. A flag é um pedido, e numa build sem suporte Brotli não tem efeito nenhum. 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 ficheiros não confiáveis

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

O PDFium não devolve um erro por um renderer ou backend de fontes em falta na build: o FPDF_InitLibraryWithConfig falha um CHECK nativo, que em Windows aparece como uma exceção de breakpoint e, sem um handler de exceções estruturadas à volta da chamada, termina o processo. O header diz tanto, avisando que um valor não suportado "também falhará 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 incluído está no segundo grupo: renderiza com Skia mas usa FreeType para fontes. Pedir prpSkia junto com pfbpFontations contra ele produzia External exception 80000003 do lado Delphi. Quando o depurador ou um handler de exceções por acaso apanha isso, a situação continua irrecuperável:

  • O PDFium fica meio inicializado
  • A configuração de todo o processo já está selada, por isso o ConfigurePdfLibrary recusa uma configuração corrigida
  • Tentar de novo com uma configuração diferente no mesmo processo já não é possível

Esta é a falha oposta ao bug do Brotli. Ali o campo guardava um valor que ninguém escolheu e o PDFium aceitava-o em silêncio. Aqui o campo guarda um valor que o chamador escolheu deliberadamente e o PDFium não aceita discussão nenhuma sobre ele. Ambos são problemas que um wrapper tem de resolver antes da chamada nativa, porque depois dela não resta nada para apanhar

Como o PDFium Component preverifica Skia e Fontations

Desde a v3.125.0, o LoadLibrary valida a configuração depois de ligar os exports da DLL e antes de chamar o FPDF_InitLibraryWithConfig, e torna um renderer ou backend de fontes não suportado num EPdfError com uma mensagem que nomeia a definição em falta e as alternativas. A DLL é descarregada e a configuração é desselada, por isso o chamador pode escolher outras definições e carregar de novo

A decisão em si vive na função pura PdfLibraryConfigurationSupportError, que recebe a configuração mais dois Booleans que descrevem a build e devolve uma string vazia quando a combinação é segura. Como não toca em estado nativo nenhum, 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 é detetado da 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, por isso a verificação é exata
  • Fontations não tem export próprio. O único rasto que deixa são as crates de fontes Rust que puxa para o binário, por isso o PDFium Component varre o ficheiro da biblioteca carregada à procura dos nomes de crates skrifa e read-fonts (também read_fonts). A varredura corre só quando pfbpFontations é pedido, e um ficheiro que não possa ser lido conta como "sem Fontations"

A verificação Fontations é uma heurística, e pode errar num sentido: uma build Fontations despojada de todas essas strings seria rejeitada mesmo que pudesse ter funcionado. Essa troca foi feita de propósito. Uma rejeição falsa custa-lhe uma exceção que pode apanhar e um fallback para FreeType. Uma aceitação falsa custa-lhe o processo

Desselar importa tanto como a verificação. O LoadLibrary sela a configuração no próprio início do carregamento, por isso sem a reposição uma rejeição de capacidade deixaria o ConfigurePdfLibrary a responder a cada tentativa com o EPdfError "PDFium library configuration is already sealed". O caminho da rejeição chama primeiro UnloadLibrary; a sua chamada FPDF_DestroyLibrary é segura nesse ponto porque o PDFium ainda não foi inicializado e devolve imediatamente. Outras falhas de carga, como uma DLL em falta ou um desacordo de arquitetura, mantêm o selo, por isso um ciclo 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 unidade: Windows.LoadLibrary tem o mesmo nome
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Uma rejeição de capacidade descarrega a DLL e dessela a configuração.
      // Uma DLL que não chegou a carregar fica selada: tentar de novo 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 unidade que também use Windows ou Winapi.Windows, um LoadLibrary sem qualificação resolve para a unidade que aparecer por último na cláusula uses; quando essa é a função Win32, a chamada sem parâmetros falha a compilar com um erro de contagem de argumentos que não diz nada sobre o PDFium

Fluxo de preverificação LoadLibrary do PDFium Component em que ConfigurePdfLibrary sela a configuração, a verificação de capacidade testa o export FPDF_RenderPageSkia e evidência de strings skrifa, um pedido não suportado lança um EPdfError apanhável e dessela para repetir, enquanto uma DLL que nunca carrega mantém PdfLibraryConfigurationSealed true
a validação corre depois dos exports ligarem e antes da inicialização, por isso um backend em falta falha como um EPdfError apanhável 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 seu próprio isolate por documento e falha um CHECK nativo se lhe entregar também um. Strings vazias em UserFontPaths são rejeitadas. E qualquer chamada depois da primeira tentativa de carga falha com "PDFium library configuration is already sealed"

Essa última regra tem uma consequência prática: não pode sondar a DLL primeiro e configurá-la depois. O GetSkiaRenderCapabilities, o V8FeaturesAvailable, abrir um documento, e a maioria dos outros pontos de entrada chama LoadLibrary internamente, o que sela a configuração no acto. Chamar UnloadLibrary mais tarde também não a reabre. Configure primeiro, depois carregue, depois faça perguntas, 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, segura para 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 o LoadLibrary aplicou quando construiu o 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;

Registar essa linha uma vez no arranque é barato, e é a primeira coisa que quer num ticket de suporte que diga "o texto parece diferente no servidor". O PDFium.Loaded é qualificado pela mesma razão que o LoadLibrary: dentro de um método de formulário ou componente, um Loaded nu liga a TComponent.Loaded

Duas maneiras como uma struct de configuração C versionada corre mal

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

  1. Campo definido, versão demasiado baixa. Escreva m_BrotliEnabled = 1 numa estrutura de versão 2 e o PDFium nunca o olha. A chamada tem sucesso e os streams Brotli continuam indescodáveis. A defesa é derivar a versão dos campos realmente em uso, que é o que o LoadLibrary faz, em vez de a fixar no código
  2. Versão alta suficiente, campo zero significa algo. Eleve a versão para 6 e todos os campos até à versão 6 ficam agora vivos. O FillChar põe o m_RendererType a FPDF_RENDERERTYPE_AGG, que é um renderer real, não "não definido". A defesa é escrever todos os campos que a versão escolhida cobre com um valor intencional, e resolver o "por omissão" contra a build real em vez de o assumir

Segue-se uma terceira regra para valores que podem fazer crash ao 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 é uma heurística. Um símbolo exportado é prova. Um nome de crate numa tabela de strings é um bom palpite

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 carga de documento sela-o
  • Atualize para a v3.123.0 ou posterior se definir BrotliEnabled ou IsolatePerDocument e esperar saída Skia dos runtimes incluídos
  • Deixe o Renderer em prpDefault a menos que precise de um rasterizador específico; agora resolve para o por omissão da build em todas as versões da estrutura
  • Use o PdfNativeRendererType com GetSkiaRenderCapabilities.PageRender para registar que renderer está realmente 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 pode reconfigurar; depois de uma carga de DLL falhada fica True
  • Trate a deteção de Fontations como heurística e mantenha um fallback para FreeType
  • Escreva PDFium.LoadLibrary e PDFium.Loaded com o nome da unidade para evitar colisões de nomes Win32 e TComponent

Se a DLL falhar antes de a configuração sequer interessar, comece por diagnosticar falhas de carga da DLL PDFium em Delphi, e para a forma como o componente encontra o binário certo em cada plataforma veja carregar a biblioteca nativa PDFium em qualquer alvo. Uma vez o renderer resolvido, cache de renderização e táticas de 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 verificações de configuração como estas, por isso a inicialização nativa falha como uma exceção Pascal que pode tratar em vez de uma saída do processo. Detalhes do produto e transferências estão na página do produto PDFium Component para Delphi