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 estrutura | Campo que acrescenta | Definido por |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Sempre escritos; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform não nil |
| 4 | m_RendererType | Renderer diferente de prpDefault |
| 5 | m_FontLibraryType | FontBackend diferente de pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = 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
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 comRendereremprpDefault: hashF75B5EB4728ADE87prpAggexplícito: hashF75B5EB4728ADE87, 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
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
ConfigurePdfLibraryrecusa 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 oPdfNativeRendererTypeusa. 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
skrifaeread-fonts(tambémread_fonts). A varredura corre só quandopfbpFontationsé 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
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
- 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 oLoadLibraryfaz, em vez de a fixar no código - 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
FillCharpõe om_RendererTypeaFPDF_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
ConfigurePdfLibraryuma 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
BrotliEnabledouIsolatePerDocumente esperar saída Skia dos runtimes incluídos - Deixe o
RendereremprpDefaulta 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
PdfNativeRendererTypecomGetSkiaRenderCapabilities.PageRenderpara registar que renderer está realmente ativo - Espere um
EPdfError, não um crash, paraprpSkianuma DLL só-AGG oupfbpFontationsnuma 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.LoadLibraryePDFium.Loadedcom o nome da unidade para evitar colisões de nomes Win32 eTComponent
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