Teknisk artikel

PDFium Library Config: Når Brotli lydløst bytter Skia ud

I PDFium Component til Delphi brugte det at slå BrotliEnabled eller IsolatePerDocument til i TPdfLibraryConfiguration til at bytte det medfølgende Skia-build ud med AGG-rendereren uden nogen fejl, fordi begge options hæver FPDF_LIBRARY_CONFIG til en version, hvor PDFium læser m_RendererType bogstaveligt. Siden v3.123.0 forbliver default-rendereren DLL'ens egen default, og siden v3.125.0 udløser en Skia- eller Fontations-forespørgsel, DLL'en ikke kan imødekomme, en EPdfError, du kan fange, i stedet for at dræbe processen

Ingen af de to bugs annoncerede sig selv. Den første producerede sider, der så fint ud, bare renderet af en anden rasterizer, med en anelse anderledes anti-aliasing og tekstkanter end det build, du skibede og testede. Den anden annoncerede sig selv, højtlydt, ved at tage værtsprocessen ned indefra nativ initialisering. Begge kommer fra samme sted: en versioneret C-struktur, hvis felter først tæller, når versionsnummeret siger det, og hvis nulværdier ikke er "usat", men reelle valg

Hvordan beslutter FPDF_LIBRARY_CONFIG, hvilken renderer PDFium bruger?

FPDF_InitLibraryWithConfig konsulterer m_RendererType kun, når strukturens Version-felt er 4 eller højere, og fra den version af bruger den værdien præcis, som den er skrevet. Under version 4 ignorerer PDFium feltet og vælger buildets default, som er Skia i builds kompileret med PDF_USE_SKIA og AGG alle andre steder

Hvert senere felt følger samme mønster. Strukturen voksede én capability ad gangen, og hver capability ankom sammen med et nyt versionsnummer. PDFium Component bygger den native struktur i LoadLibrary ud fra din TPdfLibraryConfiguration og hæver versionen kun så langt, som de options, du har sat, kræver

StrukturversionFelt, den tilføjerSat af
2m_pIsolate, m_v8EmbedderSlotAltid skrevet; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform ikke nil
4m_RendererTypeRenderer andet end prpDefault
5m_FontLibraryTypeFontBackend andet end pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Fælden sidder i de sidste to rækker. Versioner er kumulative: en version 6-struktur er også en version 4- og en version 5-struktur, så PDFium læser m_RendererType og m_FontLibraryType, selv om du kun bad om Brotli. Hvad end der sidder i de to felter i det øjeblik, bliver rendereren og font-backend'en, uanset om du havde tænkt dig at vælge dem eller ej

PDFium Component FPDF_LIBRARY_CONFIG-versionsstige fra version 2 til version 7, der viser hvilken TPdfLibraryConfiguration-option der tilføjer m_RendererType, m_FontLibraryType, m_BrotliEnabled og m_IsolatePerDocument, og hvorfor kumulative versioner gør et nulstillet rendererfelt til et bevidst AGG-valg frem for en usat værdi på ethvert build
Hver option hæver strukturversionen, og alle tidligere felter forbliver levende, så nullet i m_RendererType ankommer til PDFium som en eksplicit AGG-forespørgsel

Hvorfor byttede aktivering af Brotli rendereren ud med AGG?

Før v3.123.0 skrev PDFium Component FPDF_RENDERERTYPE_AGG ind i m_RendererType for prpDefault, så enhver konfiguration, der skubbede strukturen til version 6 eller 7, tvang AGG på et Skia-build. De pdfium.dll- og pdfium.v8.dll-runtimes, der skiber med komponenten, er Skia-builds, så dette ramte default-deploymentet, ikke et eksotisk

Mappingen så harmløs ud, da den blev skrevet. Ved version 2 eller 3 læses feltet aldrig, så prpDefault betød virkelig "whatever DLL'en gør". I det øjeblik BrotliEnabled (version 6) eller IsolatePerDocument (version 7) kom ind i billedet, forvandlede samme kode "ingen præference" til en eksplicit AGG-forespørgsel. Intet fejlede. PDFium initialiserede normalt, renderede hver side og returnerede ingen fejlkode, for set fra dens synsvinkel havde kalderen bedt om AGG og fået AGG

En pixel-hash gør byttet synligt dér, hvor skærmbilleder ikke gør. Rendering af første side af det samme eksempeldokument under tre konfigurationer gav:

  • Default-konfiguration: hash 502D77C3711B4ACF
  • BrotliEnabled = True med Renderer efterladt på prpDefault: hash F75B5EB4728ADE87
  • Eksplicit prpAgg: hash F75B5EB4728ADE87, identisk med Brotli-kørslen

Fixet i v3.123.0 er den offentlige funktion PdfNativeRendererType, som opløser en TPdfRendererPreference til værdien, der skrives ind i m_RendererType. prpAgg og prpSkia mapper én til én. prpDefault mapper nu til Skia, når den indlæste DLL eksporterer FPDF_RenderPageSkia, og til AGG ellers. Den export kompileres under samme PDF_USE_SKIA-betingelse som Skia-defaulten selv, hvilket gør den til den eneste build-egenskab, du kan observere udefra DLL'en. Efter fixet producerer Brotli-konfigurationen den samme hash som default'en

PDFium Component pixel-hash-sammenligning, der viser default Skia-render-hashen 502D77C3711B4ACF, pre-v3.123.0 BrotliEnabled-konfigurationen matchende en eksplicit prpAgg-kørsel med hashen F75B5EB4728ADE87, og den rettede wrapper, der opløser prpDefault gennem FPDF_RenderPageSkia-exporten tilbage til den oprindelige Skia-hash
En pixel-hash fanger, hvad skærmbilleder skjuler: at aktivere Brotli renderede hver side med AGG, og den rettede default matcher nu den urørte konfiguration

Font-backend'en havde aldrig det samme problem. m_FontLibraryType læses fra version 5, og dens nulværdi, FPDF_FONTBACKENDTYPE_FREETYPE, er også PDFiums default, når feltet slet ikke læses. At skrive FreeType for pfbpDefault gengiver derfor det native default præcist. Nulværdier er ikke altid forkerte, de er bare aldrig automatisk rigtige

Med v3.123.0 eller senere gør den startup-kode, du naturligt ville skrive, nu, hvad den siger:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Skal køre, før noget indlæser det native bibliotek
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // hæver FPDF_LIBRARY_CONFIG til version 6
  // Renderer forbliver prpDefault: opløses til Skia på builds, der eksporterer
  // FPDF_RenderPageSkia, og til AGG på AGG-only builds
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Husk, at BrotliEnabled kun gør PDF 2.0 /BrotliDecode-streams dekodbar, når DLL'en selv er bygget med PDF_ENABLE_BROTLI. Flaget er en forespørgsel, og på et build uden Brotli-understøttelse har det ingen effekt. TPdfLibraryConfiguration.Hardened er det samme som Default, bortset fra at AllowMachineTime er False, hvilket blokerer dokument-JavaScript fra at læse det rigtige ur; det er et fornuftigt udgangspunkt til server-side-behandling af utroværdige filer

Hvad sker der, når du beder om en backend, DLL'en ikke indeholder?

PDFium returnerer ingen fejl for en renderer- eller font-backend, der mangler i buildet: FPDF_InitLibraryWithConfig fejler en nativ CHECK, som på Windows viser sig som en breakpoint-exception og uden en structured exception handler omkring kaldet terminerer processen. Headeren siger det selv og advarer om, at en ikke-understøttet værdi "will similarly fail with an immediate crash"

De to konkrete tilfælde er et AGG-only-build, der modtager FPDF_RENDERERTYPE_SKIA, og et build uden Fontations, der modtager FPDF_FONTBACKENDTYPE_FONTATIONS. Den medfølgende Skia-runtime er i den anden gruppe: den renderer med Skia, men bruger FreeType til fonts. At bede om prpSkia sammen med pfbpFontations mod den gav External exception 80000003 på Delphi-siden. Når debuggere eller en exception-handler tilfældigt fanger det, er situationen stadig uoprettelig:

  • PDFium efterlades halvt initialiseret
  • Den procesvide konfiguration er allerede sealeret, så ConfigurePdfLibrary nægter en korrigeret konfiguration
  • At prøve igen med en anden konfiguration i samme proces er ikke længere muligt

Dette er den modsatte fejl af Brotli-buggen. Dér holdt feltet en værdi, ingen havde valgt, og PDFium accepterede den lydløst. Her holder feltet en værdi, kalderen bevidst har valgt, og PDFium accepterer ingen diskussion om den overhovedet. Begge er problemer, en wrapper må løse før det native kald, for bagefter er der intet tilbage at fange

Hvordan PDFium Component prechecker Skia og Fontations

Siden v3.125.0 validerer LoadLibrary konfigurationen efter binding af DLL-exporterne og før kaldet af FPDF_InitLibraryWithConfig og forvandler en ikke-understøttet renderer eller font-backend til en EPdfError med en besked, der nævner den skyldige indstilling og alternativerne. DLL'en aflæsses, og konfigurationen unseales, så kalderen kan vælge andre indstillinger og indlæse igen

Beslutningen bor selv i den rene funktion PdfLibraryConfigurationSupportError, som tager konfigurationen plus to Booleans, der beskriver buildet, og returnerer en tom streng, når kombinationen er sikker. Eftersom den ikke rører nativ tilstand, kan du kalde den fra dine egne tests med enhver capability-kombination. Inde i LoadLibrary kommer de to Booleans fra forskellige slags beviser, og de fortjener forskellige grader af tillid:

  • Skia detekteres ud fra tilstedeværelsen af FPDF_RenderPageSkia-exporten, samme signal som PdfNativeRendererType bruger. Exporten og Skia-rendereren kompileres under én betingelse, så tjekket er eksakt
  • Fontations har ingen export af egen art. Det eneste spor, den efterlader, er de Rust-font-crates, den trækker ind i binærfilen, så PDFium Component scanner den indlæste biblioteksfil for cratenavnene skrifa og read-fonts (også read_fonts). Scanningen kører kun, når pfbpFontations forespørges, og en fil, der ikke kan læses, tæller som "ingen Fontations"

Fontations-tjekket er en heuristik, og det kan tage fejl i én retning: et Fontations-build strippet for hver eneste af de strenge ville blive afvist, selv om det kunne have virket. Det trade blev bevidst. En falsk afvisning koster dig en exception, du kan fange, og en fallback til FreeType. En falsk accept koster dig processen

At unseale tæller lige så meget som tjekket. LoadLibrary sealer konfigurationen helt i starten af indlæsningen, så uden nulstillingen ville en capability-afvisning efterlade ConfigurePdfLibrary med at svare ethvert retry med EPdfError "PDFium library configuration is already sealed". Afvisningsvejen kalder UnloadLibrary først; dets FPDF_DestroyLibrary-kald er sikkert på det punkt, fordi PDFium endnu ikke er initialiseret og returnerer straks. Andre indlæsningsfejl, som en manglende DLL eller et arkitektur-mismatch, beholder seallet, så en retry-løkke må skelne de to:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // unit-kvalificeret: Windows.LoadLibrary hedder det samme
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // En capability-afvisning aflæsser DLL'en og unsealer konfigurationen.
      // En DLL, der slet ikke kunne indlæses, forbliver sealeret: retry hjælper ikke
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Bemærk den eksplicitte PDFium.LoadLibrary. I en unit, der også bruger Windows eller Winapi.Windows, opløser et ukvalificeret LoadLibrary til den unit, der optræder sidst i uses-klausulen; er det Win32-funktionen, fejler det parameterløse kald i kompileringen med en argumentantal-fejl, der siger ingenting om PDFium

PDFium Component LoadLibrary precheck-flow, hvor ConfigurePdfLibrary sealer konfigurationen, capability-tjekket tester FPDF_RenderPageSkia-exporten og skrifa-streng-beviser, en ikke-understøttet forespørgsel udløser en fanbar EPdfError og unsealer til retry, mens en DLL, der aldrig indlæses, holder PdfLibraryConfigurationSealed true
Validering kører efter exporterne binder og før initialisering, så en manglende backend fejler som en EPdfError, du kan fange, i stedet for en nativ CHECK, der dræber processen

Validering, der sker endnu tidligere

ConfigurePdfLibrary afviser visse kombinationer, før nogen DLL er involveret, alle med EPdfError. En eksplicit FontBackend, inklusive pfbpFreeType, kræver Renderer = prpSkia, for PDFium konsulterer kun font-backend'en for Skia-rendereren. IsolatePerDocument kræver, at V8Isolate er nil, da PDFium opretter sin egen isolate pr. dokument og fejler en nativ CHECK, hvis du også rækker den én. Tomme strenge i UserFontPaths afvises. Og ethvert kald efter det første indlæsningsforsøg fejler med "PDFium library configuration is already sealed"

Den sidste regel har en praktisk konsekvens: du kan ikke probe DLL'en først og konfigurere den bagefter. GetSkiaRenderCapabilities, V8FeaturesAvailable, at åbne et dokument og de fleste andre indgangspunkter kalder LoadLibrary internt, hvilket sealer konfigurationen på stedet. At kalde UnloadLibrary senere genåbner den heller ikke. Konfigurér først, indlæs så, og stil så spørgsmål, hvilket er præcis den orden, en diagnostisk rutine bør følge:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // en kopi, sikker at inspicere
  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;
  // Samme opløsning LoadLibrary anvendte, da den byggede 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;

At logge den linje én gang ved startup er billigt, og det er det første, du vil have i en support-sag, der lyder "teksten ser anderledes ud på serveren". PDFium.Loaded er kvalificeret af samme grund som LoadLibrary: inde i en form- eller komponentmetode binder en bar Loaded til TComponent.Loaded

To måder en versioneret C-konfigurationsstruktur tager fejl på

Enhver versioneret konfigurationsstruktur, om det er FPDF_LIBRARY_CONFIG, en Win32 cbSize-record eller et plugin-ABI, fejler på to symmetriske måder, og en wrapper må gardere sig mod begge. Den første er at udfylde et felt, mens versionen efterlades for lav; den anden er at hæve versionen, mens et felt efterlades på en nulværdi, som biblioteket læser som et bevidst valg

  1. Felt sat, version for lav. Skriv m_BrotliEnabled = 1 ind i en version 2-struktur, og PDFium kigger aldrig på den. Kaldet lykkes, og Brotli-streams forbliver udekoderbare. Forsvaret er at udlede versionen af de felter, der faktisk er i brug, hvilket LoadLibrary gør, frem for at hardkode én
  2. Version høj nok, nulfelt betyder noget. Hæv versionen til 6, og hvert felt op til version 6 er nu levende. FillChar nulstiller m_RendererType til FPDF_RENDERERTYPE_AGG, hvilket er en reel renderer, ikke "usat". Forsvaret er at skrive hvert felt, den valgte version dækker, med en bevidst værdi, og at opløse "default" mod det faktiske build i stedet for at antage det

En tredje regel følger for værdier, der kan crashe den kaldte: valider dem mod, hvad binærfilen kan, før kaldet, med det stærkeste tilgængelige bevis, og vær ærlig i kode og dokumentation, når det bevis er en heuristik. Et eksporteret symbol er bevis. Et cratenavn i en strengtabel er et godt gæt

Hurtig reference: PDFium Component bibliotekskonfiguration

  • Kald ConfigurePdfLibrary én gang, før noget indlæser DLL'en; enhver capability-forespørgsel eller dokumentindlæsning sealer den
  • Opgradér til v3.123.0 eller senere, hvis du sætter BrotliEnabled eller IsolatePerDocument og forventer Skia-output fra de medfølgende runtimes
  • Lad Renderer stå på prpDefault, medmindre du behøver en specifik rasterizer; den opløses nu til buildets default ved hver strukturversion
  • Brug PdfNativeRendererType med GetSkiaRenderCapabilities.PageRender til at logge, hvilken renderer der faktisk er aktiv
  • Forvent EPdfError, ikke et crash, for prpSkia på en AGG-only DLL eller pfbpFontations på en ikke-Fontations DLL i v3.125.0 eller senere
  • Efter en capability-afvisning er PdfLibraryConfigurationSealed False, og du må omkonfigurere; efter en fejlet DLL-indlæsning forbliver den True
  • Behandl Fontations-detektion som heuristik og hold en FreeType-fallback
  • Skriv PDFium.LoadLibrary og PDFium.Loaded med unit-navnet for at undgå Win32- og TComponent-navne-kollisioner

Fejler DLL'en, før konfigurationen overhovedet tæller, så start med diagnosticering af PDFium DLL-indlæsningsfejl i Delphi, og for hvordan komponenten finder den rigtige binærfil på hver platform, se indlæsning af PDFium native-biblioteket på ethvert mål. Når rendereren er på plads, dækker render cache og smooth zoom-taktikker, hvordan side-rendering holdes hurtig i en viewer

PDFium Component wrapper PDFium-motoren til Delphi og C++Builder med konfigurationstjek som disse, så nativ initialisering fejler som en Pascal-exception, du kan håndtere, frem for en proces-exit. Produktdetaljer og downloads står på produktsiden for PDFium Component for Delphi