Teknisk artikel

PDFium Library Config: när Brotli tyst byter ut Skia

I PDFium Component for Delphi använde att slå på BrotliEnabled eller IsolatePerDocument i TPdfLibraryConfiguration att byta det medföljande Skia-bygget till AGG-renderaren utan något fel, för båda alternativen höjer FPDF_LIBRARY_CONFIG till en version där PDFium läser m_RendererType bokstavligt. Sedan v3.123.0 förblir standardrenderaren DLL:ens eget standardval, och sedan v3.125.0 kastar en Skia- eller Fontations-förfrågan DLL:en inte kan hedra ett fångbart EPdfError i stället för att döda processen

Ingen av buggarna tillkännagav sig själv. Den första gav sidor som såg fina ut, bara renderade av en annan rasteriserare, med något annorlunda kantutjämning och textkanter än bygget du skeppade och testade. Den andra tillkännagav sig visserligen, högljutt, genom att dra ner värdprocessen inifrån nativ initiering. Båda kommer från samma ställe: en versionshanterad C-struktur vars fält bara räknas när versionsnumret säger att de gör, och vars nollvärden inte är "osatta" utan verkliga val

Hur avgör FPDF_LIBRARY_CONFIG vilken renderare PDFium använder?

FPDF_InitLibraryWithConfig konsulterar m_RendererType bara när strukturernas Version-fält är 4 eller högre, och från den versionen använder den värdet exakt som skrivet. Under version 4 ignorerar PDFium fältet och väljer byggets standard, vilket är Skia i byggen kompilerade med PDF_USE_SKIA och AGG överallt annars

Varje senare fält följer samma mönster. Strukturen växte en förmåga i taget, och varje förmåga anlände tillsammans med ett nytt versionsnummer. PDFium Component bygger den nativa strukturen i LoadLibrary från din TPdfLibraryConfiguration och höjer versionen bara så långt som alternativen du satt kräver

StrukturversionFält den lägger tillSätts av
2m_pIsolate, m_v8EmbedderSlotAlltid skrivet; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform inte nil
4m_RendererTypeRenderer annat än prpDefault
5m_FontLibraryTypeFontBackend annat än pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Fällan sitter i de två sista raderna. Versioner är kumulativa: en version 6-struktur är också en version 4- och version 5-struktur, så PDFium läser m_RendererType och m_FontLibraryType fastän du bara begärde Brotli. Vad som än ligger i de två fälten i det ögonblicket blir renderaren och typsnittsbackenden, oavsett om du menade att välja dem eller inte

PDFium Component FPDF_LIBRARY_CONFIG-versionsstege från version 2 till version 7 som visar vilket TPdfLibraryConfiguration-alternativ som lägger till m_RendererType, m_FontLibraryType, m_BrotliEnabled och m_IsolatePerDocument, och varför kumulativa versioner gör ett nollställt renderarfält till ett medvetet AGG-val i stället för ett osatt värde på vilket bygge som helst
Varje alternativ höjer strukturversionen och varje tidigare fält förblir levande, så nollan i m_RendererType anländer hos PDFium som en explicit AGG-förfrågan

Varför bytte att slå på Brotli renderaren till AGG?

Före v3.123.0 skrev PDFium Component FPDF_RENDERERTYPE_AGG i m_RendererType för prpDefault, så varje konfiguration som knuffade strukturen till version 6 eller 7 tvingade AGG på ett Skia-bygge. De pdfium.dll- och pdfium.v8.dll-körningar som medföljer komponenten är Skia-byggen, så det här drabbade standarddistributionen, inte någon exotisk

Mappningen såg harmlös ut när den skrevs. Vid version 2 eller 3 läses fältet aldrig, så prpDefault menade verkligen "vad DLL:en nu gör". I stunden BrotliEnabled (version 6) eller IsolatePerDocument (version 7) kom in i bilden gjorde samma kod om "ingen preferens" till en explicit AGG-förfrågan. Inget misslyckades. PDFium initierades normalt, renderade varje sida och returnerade ingen felkod, för ur dess synvinkel hade anroparen bett om AGG och fått AGG

En pixelhash gör bytet synligt där skärmbilder inte gör det. Att rendera första sidan av samma exempeldokument under tre konfigurationer gav:

  • Standardkonfiguration: hash 502D77C3711B4ACF
  • BrotliEnabled = True med Renderer lämnad på prpDefault: hash F75B5EB4728ADE87
  • Explicit prpAgg: hash F75B5EB4728ADE87, identisk med Brotli-körningen

Fixen i v3.123.0 är den publika funktionen PdfNativeRendererType, som löser en TPdfRendererPreference till värdet som skrivs in i m_RendererType. prpAgg och prpSkia mappas ett till ett. prpDefault mappas nu till Skia när den laddade DLL:en exporterar FPDF_RenderPageSkia och till AGG annars. Den exporten kompileras under samma PDF_USE_SKIA-villkor som Skia-standardvalet själv, vilket gör den till den enda byggegenskap du kan observera utanför DLL:en. Efter fixen ger Brotli-konfigurationen samma hash som standarden

PDFium Component-jämförelse av pixelhashar som visar standard-Skia-renderingshashen 502D77C3711B4ACF, BrotliEnabled-konfigurationen före v3.123.0 matchande en explicit prpAgg-körning med hash F75B5EB4728ADE87, och den fixade wrappern som löser prpDefault via FPDF_RenderPageSkia-exporten tillbaka till den ursprungliga Skia-hashen
En pixelhash fångar det skärmbilder döljer: att slå på Brotli renderade förr varje sida med AGG, och det fixade standardvalet matchar nu den orörda konfigurationen

Typsnittsbackenden hade aldrig samma problem. m_FontLibraryType läses från version 5 och framåt, och dess nollvärde, FPDF_FONTBACKENDTYPE_FREETYPE, är också PDFiums standard när fältet inte läses alls. Att skriva FreeType för pfbpDefault reproducerar därför det nativa standardvalet exakt. Nollvärden är inte alltid fel, de är bara aldrig automatiskt rätt

Med v3.123.0 eller senare gör uppstartskoden du naturligt skulle skriva nu vad den säger:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Måste köras innan något laddar det nativa biblioteket
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // höjer FPDF_LIBRARY_CONFIG till version 6
  // Renderer förblir prpDefault: löses till Skia på byggen som exporterar
  // FPDF_RenderPageSkia och till AGG på enbart-AGG-byggen
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Kom ihåg att BrotliEnabled bara gör PDF 2.0 /BrotliDecode-strömmar avkodningsbara när DLL:en själv byggts med PDF_ENABLE_BROTLI. Flaggan är en förfrågan, och på ett bygge utan Brotli-stöd har den ingen effekt. TPdfLibraryConfiguration.Hardened är samma som Default utom att AllowMachineTime är False, vilket blockerar dokument-JavaScript från att läsa den verkliga klockan; det är en rimlig utgångspunkt för bearbetning på serversidan av otillförlitliga filer

Vad händer när du begär en backend DLL:en inte innehåller?

PDFium returnerar inget fel för en renderare eller typsnittsbackend som saknas i bygget: FPDF_InitLibraryWithConfig sänker en nativ CHECK, som på Windows yttrar sig som ett breakpoint-undantag och, utan en strukturerad undantagshanterare runt anropet, avslutar processen. Headern säger liknande, med en varning om att ett värde som inte stöds "likaså misslyckas med en omedelbar krasch"

De två konkreta fallen är ett enbart-AGG-bygge som tar emot FPDF_RENDERERTYPE_SKIA, och ett bygge utan Fontations som tar emot FPDF_FONTBACKENDTYPE_FONTATIONS. Den medföljande Skia-körningen är i den andra gruppen: den renderar med Skia men använder FreeType för typsnitt. Att begära prpSkia tillsammans med pfbpFontations mot den gav External exception 80000003 på Delphi-sidan. Räkar debuggern eller en undantagshanterare fånga det är situationen ändå oåterkallelig:

  • PDFium lämnas halvinitierad
  • Den processvida konfigurationen är redan låst, så ConfigurePdfLibrary vägrar en rättad konfiguration
  • Att försöka igen med en annan konfiguration i samma process är inte längre möjligt

Det här är motsatsen till Brotli-buggens fel. Där höll fältet ett värde ingen valt och PDFium accepterade det tyst. Här håller fältet ett värde anroparen medvetet valt och PDFium accepterar ingen diskussion om det alls. Båda är problem som en wrapper måste lösa före det nativa anropet, för efter det finns inget kvar att fånga

Hur PDFium Component förrkontrollerar Skia och Fontations

Sedan v3.125.0 validerar LoadLibrary konfigurationen efter att DLL-exporterna bundits och innan FPDF_InitLibraryWithConfig anropas, och förvandlar en renderare eller typsnittsbackend som inte stöds till ett EPdfError med ett meddelande som namnger den felande inställningen och alternativen. DLL:en lossas och konfigurationen olåses, så anroparen kan välja andra inställningar och ladda igen

Beslutet självt bor i den rena funktionen PdfLibraryConfigurationSupportError, som tar konfigurationen plus två booleska värden som beskriver bygget och returnerar en tom sträng när kombinationen är säker. Eftersom den rör inget nativt tillstånd kan du anropa den från dina egna tester med vilken förmågekombination som helst. Inuti LoadLibrary kommer de två booleska värdena från olika slags bevis, och de förtjänar olika grad av tillit:

  • Skia upptäcks från närvaron av FPDF_RenderPageSkia-exporten, samma signal PdfNativeRendererType använder. Exporten och Skia-renderaren kompileras under ett villkor, så kontrollen är exakt
  • Fontations har ingen egen export. Det enda spår den lämnar är Rust-typsnitts-crates den drar in i binären, så PDFium Component skannar den laddade biblioteksfilen efter cratenamnen skrifa och read-fonts (också read_fonts). Skanningen körs bara när pfbpFontations begärs, och en fil som inte går att läsa räknas som "ingen Fontations"

Fontations-kontrollen är en heuristik, och den kan ha fel i en riktning: ett Fontations-bygge avskalat på vart och ett av de strängarna skulle avvisas fastän det kunde ha fungerat. Den avvägningen gjordes med flit. Ett falskt avslag kostar dig ett undantag du kan fånga och en återgång till FreeType. Ett falskt godkännande kostar dig processen

Att olåsa spelar lika stor roll som kontrollen. LoadLibrary låser konfigurationen allra i början av laddningen, så utan återställningen skulle ett förmågavslag lämna ConfigurePdfLibrary som svarar varje nytt försök med EPdfError "PDFium library configuration is already sealed". Avslagsvägen anropar UnloadLibrary först; dess FPDF_DestroyLibrary-anrop är säkert vid det laget för PDFium inte har initierats ännu och returnerar omedelbart. Andra laddningsfel, som en saknad DLL eller en arkitekturmismatch, behåller låset, så en försöksloop måste skilja de två åt:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // enhetskvalificerat: Windows.LoadLibrary heter samma sak
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Ett förmågavslag lossar DLL:en och olåser konfigurationen.
      // En DLL som inte alls kunde laddas förblir låst: att försöka igen hjälper inte
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Notera det explicita PDFium.LoadLibrary. I en enhet som också använder Windows eller Winapi.Windows löser sig ett okvalificerat LoadLibrary till den enhet som står sist i uses-satsen; är det Win32-funktionen misslyckas det parameterlösa anropet att kompilera med ett argumentantalsfel som inte säger något om PDFium

PDFium Component LoadLibrary-förrkontrollflöde där ConfigurePdfLibrary låser konfigurationen, förmågakontrollen testar FPDF_RenderPageSkia-exporten och skrifa-strängbevis, en begäran som inte stöds kastar ett fångbart EPdfError och olåser för nytt försök, medan en DLL som aldrig laddar håller PdfLibraryConfigurationSealed true
Valideringen körs efter att exporterna binder och före initieringen, så en saknad backend misslyckas som ett EPdfError du kan fånga i stället för en nativ CHECK som dödar processen

Validering som händer ännu tidigare

ConfigurePdfLibrary avvisar vissa kombinationer innan någon DLL är inblandad, alla med EPdfError. En explicit FontBackend, inklusive pfbpFreeType, kräver Renderer = prpSkia, för PDFium konsulterar bara typsnittsbackenden för Skia-renderaren. IsolatePerDocument kräver att V8Isolate är nil, eftersom PDFium skapar sitt eget isolat per dokument och sänker en nativ CHECK om du också räcker fram en. Tomma strängar i UserFontPaths avvisas. Och varje anrop efter det första laddningsförsöket misslyckas med "PDFium library configuration is already sealed"

Den sista regeln har en praktisk konsekvens: du kan inte sondera DLL:en först och konfigurera den efteråt. GetSkiaRenderCapabilities, V8FeaturesAvailable, att öppna ett dokument och de flesta andra ingångspunkter anropar LoadLibrary internt, vilket låser konfigurationen på stället. Att anropa UnloadLibrary senare öppnar den inte heller igen. Konfigurera först, ladda sedan, ställ sedan frågor, vilket är exakt ordningen en diagnostikrutin bör följa:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // en kopia, säker att inspektera
  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;
  // Samma upplösning LoadLibrary tillämpade när det byggde 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;

Att logga den raden en gång vid uppstart är billigt, och det är det första du vill ha i ett supportärende som säger "texten ser annorlunda ut på servern". PDFium.Loaded är kvalificerat av samma skäl som LoadLibrary: inuti en form- eller komponentmetod binder en naken Loaded till TComponent.Loaded

Två sätt en versionshanterad C-konfigurationsstruktur går fel

Varje versionshanterad konfigurationsstruktur, vare sig det är FPDF_LIBRARY_CONFIG, en Win32-cbSize-post eller ett plugin-ABI, misslyckas på två symmetriska sätt, och en wrapper måste vakta mot båda. Det första är att fylla ett fält medan versionen lämnas för låg; det andra är att höja versionen medan ett fält lämnas på ett nollvärde som biblioteket läser som ett medvetet val

  1. Fält satt, version för låg. Skriv m_BrotliEnabled = 1 i en version 2-struktur och PDFium tittar aldrig på den. Anropet lyckas och Brotli-strömmar förblir oavkodningsbara. Försvaret är att härleda versionen från de fält som faktiskt används, vilket är vad LoadLibrary gör, i stället för att hårdkoda en
  2. Version hög nog, nollfält betyder något. Höj versionen till 6 och varje fält upp till version 6 är nu levande. FillChar nollställer m_RendererType till FPDF_RENDERERTYPE_AGG, vilket är en riktig renderare, inte "osatt". Försvaret är att skriva varje fält den valda versionen täcker med ett medvetet värde, och att lösa "standard" mot det faktiska bygget i stället för att anta det

En tredje regel följer för värden som kan krascha den anropade: validera dem mot vad binären kan göra före anropet, med de starkaste bevis som finns, och var ärlig i koden och dokumentationen när det beviset är en heuristik. En exporterad symbol är ett bevis. Ett cratenamn i en strängtabell är en bra gissning

Snabbreferens: PDFium Component bibliotekskonfiguration

  • Anropa ConfigurePdfLibrary en gång, innan något laddar DLL:en; varje förmågefråga eller dokumentladdning låser den
  • Uppgradera till v3.123.0 eller senare om du sätter BrotliEnabled eller IsolatePerDocument och förväntar Skia-utdata från de medföljande körningarna
  • Lämna Renderer på prpDefault om du inte behöver en specifik rasteriserare; den löses nu till byggets standard vid varje strukturversion
  • Använd PdfNativeRendererType med GetSkiaRenderCapabilities.PageRender för att logga vilken renderare som faktiskt är aktiv
  • Räkna med EPdfError, inte en krasch, för prpSkia på en enbart-AGG-DLL eller pfbpFontations på en icke-Fontations-DLL i v3.125.0 eller senare
  • Efter ett förmågavslag är PdfLibraryConfigurationSealed False och du får konfigurera om; efter en misslyckad DLL-laddning förblir den True
  • Behandla Fontations-upptäckten som heuristisk och behåll en FreeType-återgång
  • Skriv PDFium.LoadLibrary och PDFium.Loaded med enhetsnamnet för att undvika Win32- och TComponent-namnkrockar

Misslyckas DLL:en innan konfigurationen ens spelar roll, börja med diagnos av PDFium DLL-laddningsfel i Delphi, och för hur komponenten hittar rätt binär på varje plattform se att ladda det nativa PDFium-biblioteket på valfritt mål. När renderaren är städad täcker rendercache- och mjuk zoom-taktik hur man håller sidrenderingen snabb i en visare

PDFium Component wrappar PDFium-motorn för Delphi och C++Builder med konfigurationskontroller som dessa, så nativ initiering misslyckas som ett Pascal-undantag du kan hantera i stället för en processutgång. Produktdetaljer och nedladdningar finns på produktsidan för PDFium Component for Delphi