Technisch artikel

PDFium Library Config: als Brotli stilletjes Skia omruilt

In PDFium Component for Delphi schakelde het aanzetten van BrotliEnabled of IsolatePerDocument in TPdfLibraryConfiguration de meegeleverde Skia-build vroeger foutloos om naar de AGG-renderer, omdat beide opties FPDF_LIBRARY_CONFIG naar een versie tillen waarin PDFium m_RendererType letterlijk leest. Sinds v3.123.0 blijft de default-renderer de eigen default van de DLL, en sinds v3.125.0 levert een Skia- of Fontations-verzoek die de DLL niet kan honoreren een vangbare EPdfError op in plaats van het proces te doden

Geen van beide bugs kondigde zich aan. De eerste produceerde pagina's die er prima uitzagen, alleen gerenderd door een andere rasterizer, met net iets andere anti-aliasing en textranden dan de build die u heeft verscheept en getest. De tweede kondigde zich wel aan, luid, door het hostproces neer te halen van binnenuit de native-initialisatie. Beide komen van dezelfde plek: een geversioneerde C-structuur waarvan de velden pas meetellen zodra het versienummer dat zegt, en waarvan nulwaarden geen niet-ingesteld zijn maar echte keuzes

Hoe beslist FPDF_LIBRARY_CONFIG welke renderer PDFium gebruikt?

FPDF_InitLibraryWithConfig raadpleegt m_RendererType alleen wanneer het veld Version van de structuur 4 of hoger is, en vanaf die versie gebruikt hij de waarde exact zoals geschreven. Onder versie 4 negeert PDFium het veld en pakt de build-default, Skia in builds gecompileerd met PDF_USE_SKIA en overal elders AGG

Elk later veld volgt hetzelfde patroon. De structuur groeide één capaciteit per keer, en elke capaciteit kwam samen met een nieuw versienummer. PDFium Component bouwt de native structuur in LoadLibrary op basis van uw TPdfLibraryConfiguration en tilt de versie alleen zover als de opties die u zet vereisen

StructuurversieVeld dat hij toevoegtGezet door
2m_pIsolate, m_v8EmbedderSlotAltijd geschreven; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform niet nil
4m_RendererTypeRenderer anders dan prpDefault
5m_FontLibraryTypeFontBackend anders dan pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

De val zit in de laatste twee rijen. Versies zijn cumulatief: een structuur van versie 6 is ook een structuur van versie 4 en versie 5, dus PDFium leest m_RendererType en m_FontLibraryType zelfs als u alleen om Brotli vroeg. Wat er op dat moment in die twee velden staat wordt de renderer en de font-backend, of u ze nu wilde kiezen of niet

PDFium Component FPDF_LIBRARY_CONFIG-versieladder van versie 2 tot versie 7 die toont welke TPdfLibraryConfiguration-optie m_RendererType, m_FontLibraryType, m_BrotliEnabled en m_IsolatePerDocument toevoegt, en waarom cumulatieve versies van een op nul gezet rendererveld een bewuste AGG-keuze maken in plaats van een niet-ingestelde waarde op elke build
Elke optie tilt de structuurversie en elk eerder veld blijft actief, dus de nul in m_RendererType komt bij PDFium binnen als een expliciet AGG-verzoek

Waarom schakelde het aanzetten van Brotli de renderer naar AGG?

Vóór v3.123.0 schreef PDFium Component FPDF_RENDERERTYPE_AGG in m_RendererType voor prpDefault, dus elke configuratie die de structuur naar versie 6 of 7 tillde forceerde AGG op een Skia-build. De runtimes pdfium.dll en pdfium.v8.dll die met de component meekomen zijn Skia-builds, dus dit trof de standaarddeployment, niet een exotische

De mapping zag er ongevaarlijk uit toen ze werd geschreven. Op versie 2 of 3 wordt het veld nooit gelezen, dus prpDefault betekende werkelijk wat de DLL maar doet. Zodra BrotliEnabled (versie 6) of IsolatePerDocument (versie 7) in beeld kwam, maakte dezelfde code van geen voorkeur een expliciet AGG-verzoek. Niets faalde. PDFium initialiseerde normaal, rendert elke pagina en gaf geen foutcode terug, want vanuit zijn gezichtspunt had de aanroeper om AGG gevraagd en AGG gekregen

Een pixelhash maakt de verwisseling zichtbaar waar screenshots dat niet doen. De eerste pagina van hetzelfde document renderen onder drie configuraties gaf:

  • Defaultconfiguratie: hash 502D77C3711B4ACF
  • BrotliEnabled = True met Renderer op prpDefault gelaten: hash F75B5EB4728ADE87
  • Expliciete prpAgg: hash F75B5EB4728ADE87, identiek aan de Brotli-run

De fix in v3.123.0 is de publieke functie PdfNativeRendererType, die een TPdfRendererPreference oplost naar de waarde die in m_RendererType wordt geschreven. prpAgg en prpSkia mappen één-op-één. prpDefault mapt nu op Skia wanneer de geladen DLL FPDF_RenderPageSkia exporteert en anders op AGG. Die export wordt onder dezelfde conditie PDF_USE_SKIA gecompileerd als de Skia-default zelf, waardoor hij de enige bouweigenschap is die u van buiten de DLL kunt waarnemen. Na de fix levert de Brotli-configuratie dezelfde hash op als de default

PDFium Component-vergelijking van pixelhashes met de Skia-renderhash 502D77C3711B4ACF van de default, de BrotliEnabled-configuratie vóór v3.123.0 die matcht met een expliciete prpAgg-run met hash F75B5EB4728ADE87, en de gefixte wrapper die prpDefault via de FPDF_RenderPageSkia-export terug resolved naar de oorspronkelijke Skia-hash
Een pixelhash vangt wat screenshots verbergen: Brotli aanzetten rendert elke pagina vroeger met AGG, en de gefixte default matcht nu de onaangeroerde configuratie

De font-backend had hetzelfde probleem nooit. m_FontLibraryType wordt vanaf versie 5 gelezen, en zijn nulwaarde, FPDF_FONTBACKENDTYPE_FREETYPE, is ook de default van PDFium wanneer het veld helemaal niet wordt gelezen. FreeType schrijven voor pfbpDefault reproduceert daardoor exact de native default. Nulwaarden zijn niet altijd fout, ze zijn alleen nooit automatisch goed

Met v3.123.0 of later doet de opstartcode die u van nature zou schrijven nu wat hij zegt:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Moet draaien voordat iets de nativebibliotheek laadt
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // tilt FPDF_LIBRARY_CONFIG naar versie 6
  // Renderer blijft prpDefault: resolved naar Skia op builds die
  // FPDF_RenderPageSkia exporteren en naar AGG op alleen-AGG-builds
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Denk eraan dat BrotliEnabled streams /BrotliDecode van PDF 2.0 alleen decodeerbaar maakt wanneer de DLL zelf met PDF_ENABLE_BROTLI is gebouwd. De vlag is een verzoek, en op een build zonder Brotli-ondersteuning heeft hij geen effect. TPdfLibraryConfiguration.Hardened is hetzelfde als Default behalve dat AllowMachineTime False is, wat document-JavaScript blokkeert in het lezen van de echte klok; dat is een redelijk startpunt voor server-side verwerking van onbetrouwbare bestanden

Wat gebeurt er wanneer u een backend vraagt die de DLL niet bevat?

PDFium geeft geen fout terug voor een renderer of font-backend die in de build ontbreekt: FPDF_InitLibraryWithConfig laat een native CHECK vallen, die op Windows aan de oppervlakte komt als een breakpoint-exception en, zonder structured exception handler rond de aanroep, het proces beëindigt. De header zegt dat letterlijk, met de waarschuwing dat een niet-ondersteunde waarde net zo faalt met een onmiddellijke crash

De twee concrete gevallen zijn een alleen-AGG-build die FPDF_RENDERERTYPE_SKIA krijgt, en een build zonder Fontations die FPDF_FONTBACKENDTYPE_FONTATIONS krijgt. De meegeleverde Skia-runtime zit in de tweede groep: hij rendert met Skia maar gebruikt FreeType voor fonts. prpSkia samen met pfbpFontations erop vragen leverde aan de Delphi-kant External exception 80000003 op. Vangt de debugger of een exception-handler dat toevallig op, de situatie blijft onherstelbaar:

  • PDFium blijft half-geïnitialiseerd achter
  • De procesbrede configuratie is al verzegeld, dus ConfigurePdfLibrary weigert een gecorrigeerde configuratie
  • Opnieuw proberen met een andere configuratie in hetzelfde proces is niet meer mogelijk

Dit is de spiegel van de Brotli-bug. Daar bevatte het veld een waarde die niemand had gekozen en accepteerde PDFium haar stilletjes. Hier bevat het veld een waarde die de aanroeper bewust koos en accepteert PDFium er helemaal geen discussie over. Beide zijn problemen die een wrapper vóór de native aanroep moet oplossen, want erna is er niets meer om te vangen

Hoe PDFium Component Skia en Fontations vooraf controleert

Sinds v3.125.0 valideert LoadLibrary de configuratie nadat de DLL-exports zijn gebonden en voordat FPDF_InitLibraryWithConfig wordt aangeroepen, en maakt van een niet-ondersteunde renderer of font-backend een EPdfError met een melding die de foutieve instelling en de alternatieven noemt. De DLL wordt ontlaadt en de configuratie wordt ontzegeld, dus de aanroeper kan andere instellingen kiezen en opnieuw laden

De beslissing zelf woont in de pure functie PdfLibraryConfigurationSupportError, die de configuratie plus twee booleans neemt die de build beschrijven en een lege string teruggeeft wanneer de combinatie veilig is. Omdat hij geen native toestand raakt, kunt u hem vanuit uw eigen tests met elke capaciteitencombinatie aanroepen. Binnen LoadLibrary komen de twee booleans uit verschillende soorten bewijs, en ze verdienen verschillende mate van vertrouwen:

  • Skia wordt gedetecteerd uit de aanwezigheid van de export FPDF_RenderPageSkia, hetzelfde signaal dat PdfNativeRendererType gebruikt. De export en de Skia-renderer worden onder één conditie gecompileerd, dus de controle is exact
  • Fontations heeft geen eigen export. Het enige spoor dat hij achterlaat zijn de Rust-fontcrates die hij in de binary trekt, dus PDFium Component scant het geladen bibliotheekbestand op de cratenamen skrifa en read-fonts (ook read_fonts). De scan draait alleen wanneer pfbpFontations wordt gevraagd, en een bestand dat niet kan worden gelezen telt als geen Fontations

De Fontations-controle is een heuristiek, en hij kan in één richting ernaast zitten: een Fontations-build waar elk van die strings is uitgeplukt zou worden geweigerd hoewel hij had kunnen werken. Die afweging is met opzet gemaakt. Een vals negatief kost u een exception die u kunt vangen en een fallback naar FreeType. Een vals positief kost u het proces

Ontzegelen telt net zo zwaar als de controle. LoadLibrary verzegelt de configuratie aan het allereerste begin van het laden, dus zonder de reset zou een capaciteitsweigering ConfigurePdfLibrary elke retry laten beantwoorden met EPdfError PDFium-bibliotheekconfiguratie is al verzegeld. Het weigerpad roept eerst UnloadLibrary aan; diens FPDF_DestroyLibrary-aanroep is op dat punt veilig omdat PDFium nog niet is geïnitialiseerd en direct terugkeert. Andere laadfalingen, zoals een ontbrekende DLL of een architectuurmismatch, houden de zegel vast, dus een retry-lus moet de twee uit elkaar houden:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // unit-gekwalificeerd: Windows.LoadLibrary heeft dezelfde naam
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Een capaciteitsweigering ontlaadt de DLL en ontzegelt de configuratie.
      // Een DLL die helemaal niet laadt blijft verzegeld: opnieuw proberen helpt niet
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Let op de expliciete PDFium.LoadLibrary. In een unit die ook Windows of Winapi.Windows gebruikt, lost een ongekwalificeerde LoadLibrary op naar welke unit het laatst in de uses-clausule staat; is dat de Win32-functie, dan compileert de parameterloze aanroep niet met een argumententelfout die niets over PDFium zegt

PDFium Component LoadLibrary-precheckstroom waarin ConfigurePdfLibrary de configuratie verzegelt, de capaciteitscontrole de FPDF_RenderPageSkia-export en het skrifa-stringbewijs test, een niet-ondersteund verzoek een vangbare EPdfError oplevert en ontzegelt voor retry, terwijl een DLL die nooit laadt PdfLibraryConfigurationSealed true houdt
Validatie draait nadat de exports binden en vóór de initialisatie, dus een ontbrekende backend faalt als een EPdfError die u kunt vangen in plaats van als een native CHECK die het proces doodt

Validatie die nog eerder plaatsvindt

ConfigurePdfLibrary weigert sommige combinaties voordat er überhaupt een DLL aan te pas komt, allemaal met EPdfError. Een expliciete FontBackend, pfbpFreeType inbegrepen, vereist Renderer = prpSkia, want PDFium raadpleegt de font-backend alleen voor de Skia-renderer. IsolatePerDocument vereist dat V8Isolate nil is, want PDFium maakt zijn eigen isolate per document en laat een native CHECK vallen als u er ook nog één aanreikt. Lege strings in UserFontPaths worden geweigerd. En elke aanroep na de eerste laadpoging faalt met PDFium-bibliotheekconfiguratie is al verzegeld

Die laatste regel heeft een praktisch gevolg: u kunt de DLL niet eerst aftasten en daarna configureren. GetSkiaRenderCapabilities, V8FeaturesAvailable, een document openen en de meeste andere ingangspunten roepen intern LoadLibrary aan, wat de configuratie ter plekke verzegelt. Later UnloadLibrary aanroepen heropent haar ook niet. Eerst configureren, dan laden, dan vragen stellen, en dat is precies de volgorde die een diagnostische routine hoort te volgen:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // een kopie, veilig te inspecteren
  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;
  // Dezelfde resolutie die LoadLibrary toepaste bij het bouwen van 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;

Die regel één keer bij de opstart loggen is goedkoop, en het is het eerste dat u wilt in een supportticket dat zegt de tekst ziet er op de server anders uit. PDFium.Loaded is om dezelfde reden gekwalificeerd als LoadLibrary: binnen een methode van een form of component bindt een kale Loaded aan TComponent.Loaded

Twee manieren waarop een geversioneerde C-configuratiestructuur misgaat

Elke geversioneerde configuratiestructuur, of dat nu FPDF_LIBRARY_CONFIG is, een Win32-record met cbSize, of een plugin-ABI, faalt op twee symmetrische manieren, en een wrapper moet tegen beide waken. De eerste is een veld vullen terwijl de versie te laag blijft; de tweede is de versie tillen terwijl een veld op een nulwaarde blijft staan die de library als een bewuste keuze leest

  1. Veld gezet, versie te laag. Schrijf m_BrotliEnabled = 1 in een structuur van versie 2 en PDFium kijkt er nooit naar. De aanroep slaagt en Brotli-streams blijven ondecodeerbaar. Het verweer is de versie af te leiden uit de velden die werkelijk in gebruik zijn, wat LoadLibrary doet, in plaats van er één hard te coderen
  2. Versie hoog genoeg, nulveld betekent iets. Til de versie naar 6 en elk veld tot versie 6 is nu actief. FillChar zet m_RendererType op nul, dus FPDF_RENDERERTYPE_AGG, een echte renderer, geen niet-ingesteld. Het verweer is elk veld dat de gekozen versie dekt met een bewuste waarde te vullen, en default te resolven tegen de werkelijke build in plaats van haar aan te nemen

Voor waarden die de aangesprokene kunnen laten crashen volgt een derde regel: valideer ze vóór de aanroep tegen wat de binary aankan, met het sterkste beschikbare bewijs, en wees eerlijk in de code en de documentatie wanneer dat bewijs een heuristiek is. Een geëxporteerd symbool is bewijs. Een cratenaam in een stringtabel is een goede gok

Snelnaslag: de libraryconfiguratie van PDFium Component

  • Roep ConfigurePdfLibrary één keer aan, voordat iets de DLL laadt; elke capaciteitsvraag of documentload verzegelt haar
  • Upgrade naar v3.123.0 of later als u BrotliEnabled of IsolatePerDocument zet en Skia-output verwacht van de meegeleverde runtimes
  • Laat Renderer op prpDefault staan tenzij u een specifieke rasterizer nodig heeft; hij resolve nu op elke structuurversie naar de build-default
  • Gebruik PdfNativeRendererType met GetSkiaRenderCapabilities.PageRender om te loggen welke renderer werkelijk actief is
  • Verwacht een EPdfError, geen crash, voor prpSkia op een alleen-AGG-DLL of pfbpFontations op een DLL zonder Fontations in v3.125.0 of later
  • Na een capaciteitsweigering is PdfLibraryConfigurationSealed False en mag u herconfigureren; na een mislukte DLL-load blijft hij True
  • Behandel Fontations-detectie als heuristiek en houd een FreeType-fallback over
  • Schrijf PDFium.LoadLibrary en PDFium.Loaded met de unitnaam om naamclashes met Win32 en TComponent te vermijden

Faalt de DLL voordat configuratie überhaupt ertoe doet, begin dan bij PDFium-DLL-laadfalingen in Delphi diagnosticeren, en voor hoe de component op elk platform de juiste binary vindt zie de PDFium-nativebibliotheek laden op elk doelplatform. Zodra de renderer geregeld is, behandelt render cache- en vloeiende zoom-tactieken hoe u paginarendering snel houdt in een viewer

PDFium Component wikkelt de PDFium-engine voor Delphi en C++Builder met dit soort configuratiecontroles, dus native-initialisatie faalt als een Pascal-exception die u kunt afhandelen in plaats van als een proceseinde. Productdetails en downloads staan op de productpagina PDFium Component for Delphi