Technischer Artikel

PDFium Library Config: Brotli tauscht Skia still und leise

In PDFium Component for Delphi schaltete das Einschalten von BrotliEnabled oder IsolatePerDocument in TPdfLibraryConfiguration den gebündelten Skia-Build stumm auf den AGG-Renderer um, denn beide Optionen heben FPDF_LIBRARY_CONFIG auf eine Version, in der PDFium m_RendererType wörtlich liest. Seit v3.123.0 bleibt der Default-Renderer der eigene Default der DLL, und seit v3.125.0 wirft eine Skia- oder Fontations-Anfrage, die die DLL nicht einlösen kann, ein abfangbares EPdfError, statt den Prozess zu killen

Keiner der beiden Bugs machte sich bemerkbar. Der erste produzierte Seiten, die gut aussahen, nur eben von einem anderen Rasterizer gerendert, mit leicht anderem Anti-Aliasing und anderen Textkanten als im Build, den Sie ausgeliefert und getestet haben. Der zweite machte sich ausgesprochen bemerkbar, indem er den Host-Prozess aus der nativen Initialisierung heraus mitnahm. Beide kommen daher: eine versionierte C-Struktur, deren Felder erst zählen, wenn die Versionsnummer es sagt, und deren Nullwerte nicht „nicht gesetzt“ sind, sondern echte Entscheidungen

Wie entscheidet FPDF_LIBRARY_CONFIG, welchen Renderer PDFium benutzt?

FPDF_InitLibraryWithConfig konsultiert m_RendererType erst, wenn das Version-Feld der Struktur 4 oder höher ist, und ab dieser Version benutzt es den Wert exakt wie geschrieben. Unter Version 4 ignoriert PDFium das Feld und nimmt den Build-Default, also Skia in Builds, die mit PDF_USE_SKIA kompiliert wurden, und AGG überall sonst

Jedes spätere Feld folgt demselben Muster. Die Struktur wuchs eine Capability nach der anderen, und jede Capability kam gemeinsam mit einer neuen Versionsnummer. PDFium Component baut die native Struktur in LoadLibrary aus Ihrer TPdfLibraryConfiguration und hebt die Version nur so weit, wie es die von Ihnen gesetzten Optionen verlangen

StrukturversionFeld, das sie hinzufügtGesetzt durch
2m_pIsolate, m_v8EmbedderSlotImmer geschrieben; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform nicht nil
4m_RendererTypeRenderer anders als prpDefault
5m_FontLibraryTypeFontBackend anders als pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Die Falle sitzt in den letzten zwei Zeilen. Versionen sind kumulativ: Eine Version-6-Struktur ist auch eine Version-4- und eine Version-5-Struktur, PDFium liest also m_RendererType und m_FontLibraryType, obwohl Sie nur nach Brotli gefragt haben. Was in diesen zwei Feldern in dem Moment steht, wird Renderer und Font-Backend, ob Sie sie wählen wollten oder nicht

PDFium-Component-FPDF_LIBRARY_CONFIG-Versionsleiter von Version 2 bis Version 7, die zeigt, welche TPdfLibraryConfiguration-Option m_RendererType, m_FontLibraryType, m_BrotliEnabled und m_IsolatePerDocument hinzufügt, und warum kumulative Versionen ein genulltes Renderer-Feld auf jedem Build zu einer bewussten AGG-Entscheidung statt zu einem nicht gesetzten Wert machen
Jede Option hebt die Strukturversion, und jedes frühere Feld bleibt live, die Null in m_RendererType kommt bei PDFium also als ausdrückliche AGG-Anfrage an

Warum schaltete das Einschalten von Brotli den Renderer auf AGG um?

Vor v3.123.0 schrieb PDFium Component für prpDefault den Wert FPDF_RENDERERTYPE_AGG in m_RendererType, jede Konfiguration, die die Struktur auf Version 6 oder 7 hob, erzwang auf einem Skia-Build also AGG. Die mit der Komponente ausgelieferten Runtimes pdfium.dll und pdfium.v8.dll sind Skia-Builds, das traf also die Standard-Auslieferung, nicht irgendein exotisches Setup

Das Mapping sah beim Schreiben harmlos aus. Bei Version 2 oder 3 wird das Feld nie gelesen, prpDefault bedeutete also tatsächlich „was die DLL eben tut“. In dem Moment, in dem BrotliEnabled (Version 6) oder IsolatePerDocument (Version 7) ins Spiel kam, machte derselbe Code aus „keine Präferenz“ eine ausdrückliche AGG-Anfrage. Nichts schlug fehl. PDFium initialisierte normal, renderte jede Seite und lieferte keinen Fehlercode, denn aus seiner Sicht hatte der Aufrufer nach AGG gefragt und AGG bekommen

Ein Pixel-Hash macht den Tausch sichtbar, wo Screenshots es nicht tun. Die erste Seite desselben Referenzdokuments unter drei Konfigurationen gerendert ergab:

  • Default-Konfiguration: Hash 502D77C3711B4ACF
  • BrotliEnabled = True mit Renderer auf prpDefault: Hash F75B5EB4728ADE87
  • Explizites prpAgg: Hash F75B5EB4728ADE87, identisch mit dem Brotli-Lauf

Der Fix in v3.123.0 ist die öffentliche Funktion PdfNativeRendererType, die ein TPdfRendererPreference auf den Wert auflöst, der in m_RendererType geschrieben wird. prpAgg und prpSkia mappen eins zu eins. prpDefault mappt jetzt auf Skia, wenn die geladene DLL FPDF_RenderPageSkia exportiert, und auf AGG sonst. Dieser Export wird unter derselben PDF_USE_SKIA-Bedingung kompiliert wie der Skia-Default selbst, er ist damit die eine Build-Eigenschaft, die man von außerhalb der DLL beobachten kann. Nach dem Fix produziert die Brotli-Konfiguration denselben Hash wie die Default-Konfiguration

PDFium-Component-Pixel-Hash-Vergleich mit dem Default-Skia-Render-Hash 502D77C3711B4ACF, der v3.123.0-BrotliEnabled-Konfiguration, die einem expliziten prpAgg-Lauf mit Hash F75B5EB4728ADE87 glich, und dem gefixten Wrapper, der prpDefault über den FPDF_RenderPageSkia-Export zurück auf den ursprünglichen Skia-Hash auflöst
Ein Pixel-Hash erwischte, was Screenshots verstecken: Brotli zu aktivieren renderte jede Seite mit AGG, und der gefixte Default matcht jetzt die unangetastete Konfiguration

Das Font-Backend hatte dasselbe Problem nie. m_FontLibraryType wird ab Version 5 gelesen, und sein Nullwert, FPDF_FONTBACKENDTYPE_FREETYPE, ist zugleich PDFiums Default, wenn das Feld gar nicht gelesen wird. FreeType für pfbpDefault zu schreiben reproduziert den nativen Default also exakt. Nullwerte sind nicht immer falsch, sie sind nur nie automatisch richtig

Mit v3.123.0 oder später macht der Startup-Code, den Sie natürlicherweise schreiben würden, jetzt das, was er sagt:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Muss laufen, bevor irgendetwas die native Bibliothek lädt
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // hebt FPDF_LIBRARY_CONFIG auf Version 6
  // Renderer bleibt prpDefault: aufgelöst zu Skia auf Builds, die
  // FPDF_RenderPageSkia exportieren, und zu AGG auf reinen AGG-Builds
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Denken Sie daran: BrotliEnabled macht PDF-2.0-/BrotliDecode-Streams nur dann dekodierbar, wenn die DLL selbst mit PDF_ENABLE_BROTLI gebaut wurde. Das Flag ist eine Anfrage, auf einem Build ohne Brotli-Unterstützung wirkt es also nicht. TPdfLibraryConfiguration.Hardened ist dasselbe wie Default, nur dass AllowMachineTime auf False steht, was Dokument-JavaScript das Lesen der echten Uhr verwehrt; ein vernünftiger Startpunkt für die serverseitige Verarbeitung nicht vertrauenswürdiger Dateien

Was passiert, wenn man ein Backendor anfragt, das die DLL nicht enthält?

PDFium liefert für einen im Build fehlenden Renderer oder Font-Backend keinen Fehler zurück: FPDF_InitLibraryWithConfig lässt ein natives CHECK scheitern, das sich unter Windows als Breakpoint-Exception zeigt und, ohne einen strukturierten Exception-Handler um den Aufruf, den Prozess beendet. Der Header sagt es selbst, mit der Warnung, ein nicht unterstützter Wert „will similarly fail with an immediate crash“

Die zwei konkreten Fälle sind ein reiner AGG-Build, der FPDF_RENDERERTYPE_SKIA erhält, und ein Build ohne Fontations, der FPDF_FONTBACKENDTYPE_FONTATIONS erhält. Die gebündelte Skia-Runtime gehört zur zweiten Gruppe: Sie rendert mit Skia, benutzt für Fonts aber FreeType. prpSkia zusammen mit pfbpFontations gegen sie anzufragen produzierte auf Delphi-Seite External exception 80000003. Fängt der Debugger oder ein Exception-Handler das zufällig ab, ist die Lage trotzdem irreparabel:

  • PDFium bleibt halb initialisiert zurück
  • Die prozessweite Konfiguration ist bereits versiegelt, ConfigurePdfLibrary verweigert also eine korrigierte Konfiguration
  • Ein erneuter Versuch mit anderer Konfiguration im selben Prozess ist nicht mehr möglich

Das ist das Gegenstück zum Brotli-Bug. Dort hielt das Feld einen Wert, den niemand gewählt hatte, und PDFium schluckte ihn stillschweigend. Hier hält das Feld einen Wert, den der Aufrufer bewusst gewählt hat, und PDFium akzeptiert darüber keinerlei Diskussion. Beides sind Probleme, die ein Wrapper vor dem nativen Aufruf lösen muss, denn danach bleibt nichts mehr zum Abfangen übrig

Wie PDFium Component Skia und Fontations vorprüft

Seit v3.125.0 validiert LoadLibrary die Konfiguration nach dem Binden der DLL-Exporte und vor dem Aufruf von FPDF_InitLibraryWithConfig und macht aus einem nicht unterstützten Renderer oder Font-Backend ein EPdfError mit einer Meldung, die die betroffene Einstellung und die Alternativen benennt. Die DLL wird entladen und die Konfiguration entsiegelt, der Aufrufer kann also andere Einstellungen wählen und erneut laden

Die Entscheidung selbst wohnt der reinen Funktion PdfLibraryConfigurationSupportError inne, die die Konfiguration plus zwei Booleans zur Build-Beschreibung nimmt und bei sicherer Kombination einen leeren String liefert. Da sie keinen nativen Zustand anfasst, können Sie sie aus Ihren eigenen Tests mit jeder Capability-Kombination aufrufen. Innerhalb von LoadLibrary kommen die zwei Booleans aus unterschiedlichen Beweisarten, und sie verdienen unterschiedliche Grade an Vertrauen:

  • Skia wird über das Vorhandensein des FPDF_RenderPageSkia-Exports erkannt, dasselbe Signal, das PdfNativeRendererType benutzt. Export und Skia-Renderer werden unter einer Bedingung kompiliert, die Prüfung ist also exakt
  • Fontations hat keinen eigenen Export. Die einzige Spur, die es hinterlässt, sind die Rust-Font-Crates, die es in die Binärdatei zieht, PDFium Component durchsucht die geladene Bibliotheksdatei also nach den Crate-Namen skrifa und read-fonts (auch read_fonts). Die Suche läuft nur, wenn pfbpFontations angefragt wird, und eine nicht lesbare Datei zählt als „kein Fontations“

Die Fontations-Prüfung ist eine Heuristik, und sie kann in eine Richtung falsch liegen: Ein Fontations-Build, dem jede dieser Strings gestrippt wurde, würde abgelehnt, obwohl er funktioniert hätte. Dieser Trade war Absicht. Eine falsche Ablehnung kostet Sie eine Exception, die Sie abfangen können, und einen Fallback auf FreeType. Eine falsche Annahme kostet Sie den Prozess

Das Entsiegeln zählt genauso wie die Prüfung. LoadLibrary versiegelt die Konfiguration ganz am Anfang des Ladens, ohne den Reset würde eine Capability-Ablehnung also ConfigurePdfLibrary jeden erneuten Versuch mit EPdfError „PDFium library configuration is already sealed“ beantworten lassen. Der Ablehnungs-Pfad ruft zuerst UnloadLibrary; sein FPDF_DestroyLibrary-Aufruf ist an diesem Punkt gefahrlos, denn PDFium ist noch nicht initialisiert und kehrt sofort zurück. Andere Ladefehler, etwa eine fehlende DLL oder ein Architektur-Mismatch, behalten das Siegel, eine Retry-Schleife muss die beiden also auseinanderhalten:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // unit-qualifiziert: Windows.LoadLibrary trägt denselben Namen
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Eine Capability-Ablehnung entlädt die DLL und entsiegelt die Konfiguration.
      // Eine DLL, die gar nicht erst lud, bleibt versiegelt: Wiederholen hilft nicht
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Beachten Sie das explizite PDFium.LoadLibrary. In einer Unit, die auch Windows oder Winapi.Windows benutzt, löst ein unqualifiziertes LoadLibrary auf die Unit auf, die in der uses-Klausel zuletzt steht; ist das die Win32-Funktion, kompiliert der parameterlose Aufruf nicht, mit einem Argument-Count-Fehler, der nichts über PDFium verrät

PDFium-Component-LoadLibrary-Precheck-Ablauf, in dem ConfigurePdfLibrary die Konfiguration versiegelt, die Capability-Prüfung den FPDF_RenderPageSkia-Export und skrifa-String-Beweise testet, eine nicht unterstützte Anfrage ein abfangbares EPdfError wirft und für einen erneuten Versuch entsiegelt, während eine DLL, die nie lädt, PdfLibraryConfigurationSealed true hält
Die Validierung läuft nach dem Binden der Exporte und vor der Initialisierung, ein fehlendes Backend scheitert also als abfangbares EPdfError statt als natives CHECK, das den Prozess killt

Validierung, die noch früher passiert

ConfigurePdfLibrary lehnt einige Kombinationen ab, bevor irgendeine DLL im Spiel ist, alle mit EPdfError. Ein explizites FontBackend, pfbpFreeType eingeschlossen, verlangt Renderer = prpSkia, denn PDFium konsultiert das Font-Backend nur für den Skia-Renderer. IsolatePerDocument verlangt, dass V8Isolate nil ist, denn PDFium erzeugt sein Isolate pro Dokument selbst und lässt ein natives CHECK scheitern, wenn man ihm zusätzlich eines reicht. Leere Strings in UserFontPaths werden abgelehnt. Und jeder Aufruf nach dem ersten Ladeversuch scheitert an „PDFium library configuration is already sealed“

Diese letzte Regel hat eine praktische Konsequenz: Sie können die DLL nicht erst sondieren und danach konfigurieren. GetSkiaRenderCapabilities, V8FeaturesAvailable, das Öffnen eines Dokuments und die meisten anderen Einstiegspunkte rufen intern LoadLibrary auf, was die Konfiguration auf der Stelle versiegelt. UnloadLibrary später aufzurufen öffnet sie ebenfalls nicht wieder. Erst konfigurieren, dann laden, dann fragen — genau die Reihenfolge, die eine Diagnose-Routine einhalten sollte:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // eine Kopie, gefahrlos zu inspizieren
  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;
  // Dieselbe Auflösung, die LoadLibrary anwendete, als es FPDF_LIBRARY_CONFIG baute
  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;

Diese Zeile einmal beim Start zu loggen kostet nichts, und sie ist das Erste, was Sie in einem Support-Ticket wollen, das behauptet, „der Text sieht auf dem Server anders aus“. PDFium.Loaded ist aus demselben Grund qualifiziert wie LoadLibrary: Innerhalb einer Formular- oder Komponentenmethode bindet ein nacktes Loaded an TComponent.Loaded

Zwei Arten, an einer versionierten C-Konfigurationsstruktur zu scheitern

Jede versionierte Konfigurationsstruktur, ob FPDF_LIBRARY_CONFIG, ein Win32-cbSize-Record oder ein Plugin-ABI, scheitert auf zwei symmetrischen Arten, und ein Wrapper muss sich gegen beide wappnen. Die erste ist, ein Feld zu füllen, während die Version zu niedrig bleibt; die zweite, die Version zu heben, während ein Feld auf einem Nullwert bleibt, den die Bibliothek als bewusste Entscheidung liest

  1. Feld gesetzt, Version zu niedrig. Schreiben Sie m_BrotliEnabled = 1 in eine Version-2-Struktur, schaut PDFium es nie an. Der Aufruf gelingt, und Brotli-Streams bleiben undekodierbar. Die Verteidigung: die Version aus den tatsächlich benutzten Feldern ableiten, was LoadLibrary tut, statt eine fest zu verdrahten
  2. Version hoch genug, Nullfeld bedeutet etwas. Heben Sie die Version auf 6, und jedes Feld bis Version 6 ist jetzt live. FillChar nullt m_RendererType auf FPDF_RENDERERTYPE_AGG, einen echten Renderer, kein „nicht gesetzt“. Die Verteidigung: jedes Feld, das die gewählte Version abdeckt, mit einem absichtsvollen Wert beschreiben und „Default“ gegen den tatsächlichen Build auflösen, statt ihn vorauszusetzen

Eine dritte Regel folgt für Werte, die den Aufgerufenen zum Absturz bringen können: Sie gegen das prüfen, was die Binärdatei kann, bevor der Aufruf passiert, mit der stärksten verfügbaren Beweislage, und im Code wie in der Doku ehrlich sein, wenn diese Beweislage eine Heuristik ist. Ein exportiertes Symbol ist ein Beweis. Ein Crate-Name in einer String-Tabelle ist eine gute Vermutung

Kurzreferenz: PDFium Component Library-Konfiguration

  • ConfigurePdfLibrary einmal aufrufen, bevor irgendetwas die DLL lädt; jede Capability-Abfrage oder jedes Dokument-Laden versiegelt sie
  • Auf v3.123.0 oder später upgraden, wenn Sie BrotliEnabled oder IsolatePerDocument setzen und von den gebündelten Runtimes Skia-Ausgabe erwarten
  • Renderer auf prpDefault lassen, sofern Sie keinen konkreten Rasterizer brauchen; er löst sich jetzt bei jeder Strukturversion auf den Build-Default auf
  • PdfNativeRendererType mit GetSkiaRenderCapabilities.PageRender benutzen, um zu loggen, welcher Renderer tatsächlich aktiv ist
  • Mit EPdfError rechnen, nicht mit einem Absturz, für prpSkia auf einer reinen AGG-DLL oder pfbpFontations auf einer Nicht-Fontations-DLL in v3.125.0 oder später
  • Nach einer Capability-Ablehnung ist PdfLibraryConfigurationSealed False, und Sie dürfen neu konfigurieren; nach einer gescheiterten DLL-Ladung bleibt es True
  • Die Fontations-Erkennung als Heuristik behandeln und einen FreeType-Fallback vorhalten
  • PDFium.LoadLibrary und PDFium.Loaded mit dem Unit-Namen schreiben, um Win32- und TComponent-Namenskollisionen zu vermeiden

Scheitert die DLL, bevor die Konfiguration überhaupt eine Rolle spielt, starten Sie mit der Diagnose von PDFium-DLL-Ladefehlern in Delphi, und wie die Komponente auf jeder Plattform die richtige Binärdatei findet, siehe die PDFium-nativen Bibliothek auf jedem Ziel laden. Steht der Renderer fest, behandelt Render-Cache- und Smooth-Zoom-Taktiken, wie man das Seiten-Rendering in einem Viewer schnell hält

PDFium Component umhüllt die PDFium-Engine für Delphi und C++Builder mit Konfigurationsprüfungen wie diesen, natives Initialisieren scheitert also als abfangbare Pascal-Exception statt als Prozess-Exit. Produktdetails und Downloads finden Sie auf der PDFium Component for Delphi product page