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
| Strukturversion | Feld, das sie hinzufügt | Gesetzt durch |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Immer geschrieben; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform nicht nil |
| 4 | m_RendererType | Renderer anders als prpDefault |
| 5 | m_FontLibraryType | FontBackend anders als pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = 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
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 mitRendereraufprpDefault: HashF75B5EB4728ADE87- Explizites
prpAgg: HashF75B5EB4728ADE87, 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
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,
ConfigurePdfLibraryverweigert 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, dasPdfNativeRendererTypebenutzt. 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
skrifaundread-fonts(auchread_fonts). Die Suche läuft nur, wennpfbpFontationsangefragt 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
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
- 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, wasLoadLibrarytut, statt eine fest zu verdrahten - Version hoch genug, Nullfeld bedeutet etwas. Heben Sie die Version auf 6, und jedes Feld bis Version 6 ist jetzt live.
FillCharnulltm_RendererTypeaufFPDF_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
ConfigurePdfLibraryeinmal 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
BrotliEnabledoderIsolatePerDocumentsetzen und von den gebündelten Runtimes Skia-Ausgabe erwarten RendereraufprpDefaultlassen, sofern Sie keinen konkreten Rasterizer brauchen; er löst sich jetzt bei jeder Strukturversion auf den Build-Default aufPdfNativeRendererTypemitGetSkiaRenderCapabilities.PageRenderbenutzen, um zu loggen, welcher Renderer tatsächlich aktiv ist- Mit
EPdfErrorrechnen, nicht mit einem Absturz, fürprpSkiaauf einer reinen AGG-DLL oderpfbpFontationsauf einer Nicht-Fontations-DLL in v3.125.0 oder später - Nach einer Capability-Ablehnung ist
PdfLibraryConfigurationSealedFalse, 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.LoadLibraryundPDFium.Loadedmit dem Unit-Namen schreiben, um Win32- undTComponent-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