Στο PDFium Component για Delphi, το να ανάψεις BrotliEnabled ή IsolatePerDocument στο TPdfLibraryConfiguration άλλαζε την bundled Skia build σε AGG renderer χωρίς κανένα σφάλμα, επειδή και οι δύο επιλογές ανεβάζουν το FPDF_LIBRARY_CONFIG σε version όπου το PDFium διαβάζει το m_RendererType κυριολεκτικά. Από το v3.123.0 ο προεπιλεγμένος renderer μένει στο δικό του default της DLL, και από το v3.125.0 αίτημα Skia ή Fontations που η DLL δεν μπορεί να τιμήσει σηκώνει πιάνσιμο EPdfError αντί να σκοτώσει τη διεργασία
Κανένα από τα δύο bugs δεν ανακοινώθηκε. Το πρώτο παρήγαγε σελίδες που έμοιαζαν εντάξει, απλώς αποδιδόμενες από διαφορετικό rasterizer, με ελαφρώς διαφορετικό anti-aliasing και άκρες κειμένου από το build που στείλατε και τεστάρατε. Το δεύτερο ανακοινώθηκε, δυνατά, κατεβάζοντας τη διεργασία του host από μέσα από native initialization. Και τα δύο έρχονται από το ίδιο μέρος: versioned C δομή της οποίας τα fields μετράνε μόνο όταν το version number το λέει, και της οποίας οι μηδενικές τιμές δεν είναι «μη ορισμένο» αλλά πραγματικές επιλογές
Πώς αποφασίζει το FPDF_LIBRARY_CONFIG ποιον renderer θα χρησιμοποιήσει το PDFium;
Το FPDF_InitLibraryWithConfig συμβουλεύεται το m_RendererType μόνο όταν το πεδίο Version της δομής είναι 4 ή υψηλότερο, και από εκείνη την έκδοση χρησιμοποιεί την τιμή ακριβώς όπως γράφεται. Κάτω από version 4 το PDFium αγνοεί το πεδίο και διαλέγει το default του build, που είναι Skia σε builds compiled με PDF_USE_SKIA και AGG παντού αλλού
Κάθε μεταγενέστερο πεδίο ακολουθεί το ίδιο μοτίβο. Η δομή μεγάλωσε μία δυνατότητα τη φορά, και κάθε δυνατότητα ήρθε μαζί με νέο version number. Το PDFium Component χτίζει τη native δομή στο LoadLibrary από το TPdfLibraryConfiguration σας και ανεβάζει το version μόνο όσο απαιτούν οι επιλογές που ορίσατε
| Version δομής | Πεδίο που προσθέτει | Ορίζεται από |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Γράφεται πάντα· V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform όχι nil |
| 4 | m_RendererType | Renderer άλλο από prpDefault |
| 5 | m_FontLibraryType | FontBackend άλλο από pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
Η παγίδα είναι στις δύο τελευταίες σειρές. Οι versions είναι αθροιστικές: δομή version 6 είναι επίσης δομή version 4 και version 5, οπότε το PDFium διαβάζει m_RendererType και m_FontLibraryType παρόλο που ζητήσατε μόνο Brotli. Ό,τι κάθεται σε εκείνα τα δύο fields εκείνη τη στιγμή γίνεται ο renderer και το font backend, αν το θέλατε ή όχι
Γιατί η ενεργοποίηση Brotli άλλαξε τον renderer σε AGG;
Πριν το v3.123.0, το PDFium Component έγραφε FPDF_RENDERERTYPE_AGG στο m_RendererType για το prpDefault, οπότε κάθε configuration που έσπρωχνε τη δομή σε version 6 ή 7 επέβαλλε AGG σε Skia build. Τα runtimes pdfium.dll και pdfium.v8.dll που στέλνονται με το component είναι Skia builds, οπότε αυτό χτυπούσε το default deployment, όχι κάποιο εξωτικό
Η αντιστοίχιση έμοιαζε αβλαβής όταν γράφτηκε. Σε version 2 ή 3 το πεδίο δεν διαβάζεται ποτέ, οπότε prpDefault σήμαινε πραγματικά «ό,τι κάνει η DLL». Τη στιγμή που το BrotliEnabled (version 6) ή το IsolatePerDocument (version 7) μπήκε στη μέση, ο ίδιος κώδικας έκανε το «καμία προτίμηση» ρητό αίτημα AGG. Τίποτα δεν απέτυχε. Το PDFium αρχικοποιήθηκε κανονικά, απέδωσε κάθε σελίδα, και επέστρεψε κανένα error code, επειδή από τη δική του οπτική ο καλών είχε ζητήσει AGG και έλαβε AGG
Ένα pixel hash κάνει την αλλαγή ορατή εκεί που τα screenshots δεν κάνουν. Η απόδοση της πρώτης σελίδας του ίδιου δείγματος εγγράφου υπό τρεις configurations έδωσε:
- Προεπιλεγμένο configuration: hash
502D77C3711B4ACF BrotliEnabled= True με τοRendererαφήνοντάς το στοprpDefault: hashF75B5EB4728ADE87- Ρητό
prpAgg: hashF75B5EB4728ADE87, πανομοιότυπο με το τρέξιμο Brotli
Η διόρθωση στο v3.123.0 είναι η δημόσια συνάρτηση PdfNativeRendererType, που αναλύει TPdfRendererPreference στην τιμή που γράφεται στο m_RendererType. Τα prpAgg και prpSkia αντιστοιχούνται ένα προς ένα. Το prpDefault τώρα αναλύεται σε Skia όταν η φορτωμένη DLL εξάγει FPDF_RenderPageSkia και σε AGG αλλιώς. Εκείνο το export γίνεται compiled υπό την ίδια συνθήκη PDF_USE_SKIA με το ίδιο το Skia default, που το κάνει τη μία ιδιότητα build που μπορείς να παρατηρήσεις από έξω από τη DLL. Μετά τη διόρθωση το configuration Brotli παράγει το ίδιο hash με το προεπιλεγμένο
Το font backend δεν είχε ποτέ το ίδιο πρόβλημα. Το m_FontLibraryType διαβάζεται από version 5 και μετά, και η μηδενική του τιμή, FPDF_FONTBACKENDTYPE_FREETYPE, είναι επίσης το default του PDFium όταν το πεδίο δεν διαβάζεται καθόλου. Το να γράψεις FreeType για pfbpDefault αναπαράγει άρα το native default ακριβώς. Οι μηδενικές τιμές δεν είναι πάντα λάθος, απλώς δεν είναι ποτέ αυτόματα σωστές
Με v3.123.0 ή νεότερο, ο κώδικας εκκίνησης που θα έγραφες φυσικά τώρα κάνει ό,τι λέει:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// Πρέπει να τρέξει πριν οτιδήποτε φορτώσει τη native βιβλιοθήκη
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // ανεβάζει FPDF_LIBRARY_CONFIG σε version 6
// Renderer μένει prpDefault: αναλύεται σε Skia σε builds που εξάγουν
// FPDF_RenderPageSkia και σε AGG σε builds μόνο AGG
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
Θυμηθείτε ότι το BrotliEnabled κάνει τα streams /BrotliDecode του PDF 2.0 αποκωδικοποίσιμα μόνο όταν η ίδια η DLL χτίστηκε με PDF_ENABLE_BROTLI. Η σημαία είναι αίτημα, και σε build χωρίς υποστήριξη Brotli δεν έχει κανένα αποτέλεσμα. Το TPdfLibraryConfiguration.Hardened είναι ίδιο με Default εκτός ότι το AllowMachineTime είναι False, που μπλοκάρει το document JavaScript να διαβάσει το πραγματικό ρολόι· είναι λογικό σημείο εκκίνησης για server-side επεξεργασία μη αξιόπιστων αρχείων
Τι συμβαίνει όταν ζητάτε backend που η DLL δεν περιέχει;
Το PDFium δεν επιστρέφει σφάλμα για renderer ή font backend που λείπει από το build: το FPDF_InitLibraryWithConfig αποτυγχάνει native CHECK, που στα Windows εκδηλώνεται ως breakpoint exception και, χωρίς structured exception handler γύρω από την κλήση, τερματίζει τη διεργασία. Το header το λέει ξεκάθαρα, προειδοποιώντας ότι μη υποστηριζόμενη τιμή «θα αποτύχει ομοίως με άμεσο crash»
Οι δύο συγκεκριμένες περιπτώσεις είναι build μόνο AGG που λαμβάνει FPDF_RENDERERTYPE_SKIA, και build χωρίς Fontations που λαμβάνει FPDF_FONTBACKENDTYPE_FONTATIONS. Το bundled Skia runtime είναι στη δεύτερη ομάδα: αποδίδει με Skia αλλά χρησιμοποιεί FreeType για γραμματοσειρές. Το να ζητήσετε prpSkia μαζί με pfbpFontations απέναντί του παρήγαγε External exception 80000003 από την πλευρά Delphi. Όταν ο debugger ή ένας exception handler τυχαίνει να το πιάσει, η κατάσταση εξακολουθεί να είναι ανεπανόρθωτη:
- Το PDFium μένει μισοαρχικοποιημένο
- Η configuration όλης της διεργασίας είναι ήδη σφραγισμένη, οπότε το
ConfigurePdfLibraryαρνείται διορθωμένη configuration - Η επανάληψη με διαφορετική configuration στην ίδια διεργασία δεν είναι πια δυνατή
Αυτή είναι η αντίστροφη αστοχία από το bug Brotli. Εκεί το πεδίο κρατούσε τιμή που κανείς δεν διάλεξε και το PDFium τη δέχτηκε σιωπηλά. Εδώ το πεδίο κρατά τιμή που ο καλών διάλεξε σκόπιμα και το PDFium δεν δέχεται καμία συζήτηση γι’ αυτή. Και τα δύο είναι προβλήματα που ένας wrapper πρέπει να λύσει πριν τη native κλήση, γιατί μετά από αυτήν δεν υπάρχει τίποτα απομένου να πιάσεις
Πώς κάνει precheck το PDFium Component σε Skia και Fontations
Από το v3.125.0, το LoadLibrary επαληθεύει τη configuration μετά τη δέσμευση των exports της DLL και πριν καλέσει FPDF_InitLibraryWithConfig, και κάνει μη υποστηριζόμενο renderer ή font backend EPdfError με μήνυμα που ονομάζει την προβληματική ρύθμιση και τις εναλλακτικές. Η DLL ξεφορτώνεται και η configuration ξεσφραγίζεται, οπότε ο καλών μπορεί να διαλέξει άλλες ρυθμίσεις και να φορτώσει ξανά
Η ίδια η απόφαση ζει στην καθαρή συνάρτηση PdfLibraryConfigurationSupportError, που παίρνει τη configuration συν δύο Booleans που περιγράφουν το build και επιστρέφει κενό string όταν ο συνδυασμός είναι ασφαλής. Επειδή δεν αγγίζει κανένα native state, μπορείτε να την καλέσετε από τα δικά σας tests με οποιονδήποτε συνδυασμό δυνατοτήτων. Μέσα στο LoadLibrary τα δύο Booleans έρχονται από διαφορετικά είδη αποδείξεων, και αξίζουν διαφορετικά επίπεδα εμπιστοσύνης:
- Skia ανιχνεύεται από την παρουσία του export
FPDF_RenderPageSkia, το ίδιο σήμα που χρησιμοποιεί τοPdfNativeRendererType. Το export και ο renderer Skia γίνονται compiled υπό μία συνθήκη, οπότε ο έλεγχος είναι ακριβής - Fontations δεν έχει export δικό του. Το μόνο ίχνος που αφήνει είναι τα Rust font crates που τραβάει μέσα στο binary, οπότε το PDFium Component σαρώνει το αρχείο της φορτωμένης βιβλιοθήκης για τα ονόματα crates
skrifaκαιread-fonts(καιread_fonts). Η σάρωση τρέχει μόνο όταν ζητείταιpfbpFontations, και αρχείο που δεν μπορεί να διαβαστεί μετράει ως «όχι Fontations»
Ο έλεγχος Fontations είναι ευρετική, και μπορεί να πέσει έξω προς μία κατεύθυνση: build Fontations απογυμνωμένο από κάθε ένα από εκείνα τα strings θα απορριπτόταν παρόλο που θα μπορούσε να δουλέψει. Εκείνο το trade έγινε επίτηδες. Ψευδής απόρριψη σου κοστίζει ένα exception που μπορείς να πιάσεις και fallback σε FreeType. Ψευδής αποδοχή σου κοστίζει τη διεργασία
Το ξεσφράγισμα μετράει όσο ο έλεγχος. Το LoadLibrary σφραγίζει τη configuration στην πολύ αρχή της φόρτωσης, οπότε χωρίς το reset μια απόρριψη δυνατότητας θα άφηνε το ConfigurePdfLibrary να απαντά κάθε επανάληψη με EPdfError «PDFium library configuration is already sealed». Η διαδρομή απόρριψης καλεί πρώτα UnloadLibrary· η κλήση FPDF_DestroyLibrary της είναι ασφαλής εκείνη τη στιγμή επειδή το PDFium δεν έχει αρχικοποιηθεί ακόμα και επιστρέφει αμέσως. Άλλες αστοχίες φόρτωσης, όπως DLL που λείπει ή ασυμφωνία αρχιτεκτονικής, κρατούν τη σφραγίδα, οπότε βρόχος επανάληψης πρέπει να ξεχωρίζει τα δύο:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // unit-qualified: το Windows.LoadLibrary ονομάζεται ίδια
Result := prpSkia;
except
on E: EPdfError do
begin
// Μια απόρριψη δυνατότητας ξεφορτώνει τη DLL και ξεσφραγίζει τη configuration.
// DLL που απέτυχε να φορτώσει καθόλου μένει σφραγισμένη: η επανάληψη δεν βοηθά
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Προσέξτε το ρητό PDFium.LoadLibrary. Σε μονάδα που χρησιμοποιεί επίσης Windows ή Winapi.Windows, μη προσδιορισμένο LoadLibrary αναλύεται σε όποια μονάδα εμφανίζεται τελευταία στο uses clause· όταν εκείνη είναι η συνάρτηση Win32, η κλήση χωρίς παραμέτρους αποτυγχάνει να γίνει compile με σφάλμα πλήθους ορισμάτων που δεν λέει τίποτα για PDFium
Επαλήθευση που συμβαίνει ακόμα νωρίτερα
Το ConfigurePdfLibrary απορρίπτει κάποιους συνδυασμούς πριν εμπλακεί οποιαδήποτε DLL, όλα με EPdfError. Ρητό FontBackend, συμπεριλαμβανομένου του pfbpFreeType, απαιτεί Renderer = prpSkia, επειδή το PDFium συμβουλεύεται το font backend μόνο για τον renderer Skia. Το IsolatePerDocument απαιτεί V8Isolate να είναι nil, αφού το PDFium δημιουργεί το δικό του isolate ανά έγγραφο και αποτυγχάνει native CHECK αν του δώσετε και εσείς έναν. Κενά strings στο UserFontPaths απορρίπτονται. Και κάθε κλήση μετά την πρώτη προσπάθεια φόρτωσης αποτυγχάνει με «PDFium library configuration is already sealed»
Εκείνος ο τελευταίος κανόνας έχει πρακτική συνέπεια: δεν μπορείς να ρωτήσεις πρώτα τη DLL και να την ρυθμίσεις μετά. Τα GetSkiaRenderCapabilities, V8FeaturesAvailable, το άνοιγμα εγγράφου, και τα περισσότερα άλλα σημεία εισόδου καλούν LoadLibrary εσωτερικά, που σφραγίζει τη configuration επί τόπου. Ούτε το να καλέσεις UnloadLibrary αργότερα την ξανανοίγει. Ρύθμισε πρώτα, μετά φόρτωσε, μετά ρώτα ερωτήσεις, που είναι ακριβώς η σειρά που πρέπει να ακολουθεί διαγνωστική ρουτίνα:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // αντίγραφο, ασφαλές να επιθεωρήσεις
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;
// Ίδια ανάλυση που εφάρμοσε το LoadLibrary όταν έχτισε 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;
Το να καταγράψεις εκείνη τη γραμμή μία φορά στην εκκίνηση είναι φθηνό, και είναι το πρώτο που θέλεις σε support ticket που λέει «το κείμενο μοιάζει διαφορετικό στον server». Το PDFium.Loaded είναι προσδιορισμένο για τον ίδιο λόγο με το LoadLibrary: μέσα σε μέθοδο form ή component, γυμνό Loaded δένει στο TComponent.Loaded
Δύο τρόποι που μια versioned C configuration δομή πάει στραβά
Κάθε versioned configuration δομή, είτε είναι FPDF_LIBRARY_CONFIG, εγγραφή Win32 cbSize, ή plugin ABI, αποτυγχάνει με δύο συμμετρικούς τρόπους, και ένας wrapper πρέπει να φυλάσσεται και για τους δύο. Ο πρώτος είναι να γεμίσεις πεδίο αφήνοντας το version χαμηλό· ο δεύτερος είναι να ανεβάσεις το version αφήνοντας πεδίο σε μηδενική τιμή που η βιβλιοθήκη διαβάζει ως συνειδητή επιλογή
- Πεδίο ορισμένο, version πολύ χαμηλό. Γράφοντας
m_BrotliEnabled= 1 σε δομή version 2, το PDFium δεν κοιτάζει ποτέ εκεί. Η κλήση πετυχαίνει και τα Brotli streams μένουν μη αποκωδικοποίσιμα. Η άμυνα είναι να παράγεις το version από τα fields που χρησιμοποιούνται όντως, που κάνει τοLoadLibrary, αντί να το γράψεις σταθερά - Version αρκετά υψηλό, μηδενικό πεδίο σημαίνει κάτι. Ανέβασε το version στο 6 και κάθε πεδίο έως version 6 είναι πλέον live. Το
FillCharμηδενίζει τοm_RendererTypeσεFPDF_RENDERERTYPE_AGG, που είναι πραγματικός renderer, όχι «μη ορισμένο». Η άμυνα είναι να γράψεις κάθε πεδίο που καλύπτει η διαλεγμένη version με σκόπιμη τιμή, και να αναλύσεις το «default» απέναντι στο πραγματικό build αντί να το υποθέσεις
Τρίτος κανόνας ακολουθεί για τιμές που μπορούν να crashάρουν τον callee: επαλήθευσέ τις απέναντι σε ό,τι μπορεί το binary πριν την κλήση, με την ισχυρότερη απόδειξη διαθέσιμη, και να είστε ειλικρινείς στον κώδικα και την τεκμηρίωση όταν εκείνη η απόδειξη είναι ευρετική. Εξαγόμενο σύμβολο είναι απόδειξη. Όνομα crate σε string table είναι καλή εικασία
Σύντομη αναφορά: library configuration του PDFium Component
- Καλέστε
ConfigurePdfLibraryμία φορά, πριν οτιδήποτε φορτώσει τη DLL· κάθε capability query ή φόρτωση εγγράφου τη σφραγίζει - Αναβαθμίστε σε v3.123.0 ή νεότερο αν ορίζετε
BrotliEnabledήIsolatePerDocumentκαι περιμένετε Skia έξοδο από τα bundled runtimes - Αφήστε το
RendererστοprpDefaultεκτός αν χρειάζεστε συγκεκριμένο rasterizer· τώρα αναλύεται στο default του build σε κάθε version δομής - Χρησιμοποιήστε
PdfNativeRendererTypeμεGetSkiaRenderCapabilities.PageRenderνα καταγράψετε ποιος renderer είναι όντως ενεργός - Περιμένετε
EPdfError, όχι crash, γιαprpSkiaσε DLL μόνο AGG ήpfbpFontationsσε DLL χωρίς Fontations σε v3.125.0 ή νεότερο - Μετά από απόρριψη δυνατότητας, το
PdfLibraryConfigurationSealedείναι False και μπορείτε να ξαναρυθμίσετε· μετά από αποτυχημένη φόρτωση DLL μένει True - Μεταχειριστείτε την ανίχνευση Fontations ως ευρετική και κρατήστε fallback FreeType
- Γράψτε
PDFium.LoadLibraryκαιPDFium.Loadedμε το όνομα μονάδας για να αποφύγετε συγκρούσεις ονομάτων Win32 καιTComponent
Αν η DLL αποτύχει πριν η configuration μετρήσει καν, ξεκινήστε από διάγνωση αστοχιών φόρτωσης PDFium DLL στο Delphi, και για το πώς βρίσκει το component το σωστό binary σε κάθε platform δείτε φόρτωση της native βιβλιοθήκης PDFium σε κάθε στόχο. Μόλις εκκαθαριστεί ο renderer, το render cache και τακτικές ομαλού zoom καλύπτει πώς κρατάς την απόδοση σελίδων γρήγορη σε viewer
Το PDFium Component τυλίγει τη μηχανή PDFium για Delphi και C++Builder με ελέγχους configuration σαν αυτούς, ώστε η native αρχικοποίηση να αποτυγχάνει ως Pascal exception που μπορείς να χειριστείς και όχι ως έξοδος διεργασίας. Λεπτομέρειες προϊόντος και λήψεις στη σελίδα προϊόντος PDFium Component for Delphi