Τεχνικό Άρθρο

Library Config PDFium: όταν Brotli αλλάζει σιωπηλά το Skia

Στο 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 δομήςΠεδίο που προσθέτειΟρίζεται από
2m_pIsolate, m_v8EmbedderSlotΓράφεται πάντα· V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform όχι nil
4m_RendererTypeRenderer άλλο από prpDefault
5m_FontLibraryTypeFontBackend άλλο από pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Η παγίδα είναι στις δύο τελευταίες σειρές. Οι versions είναι αθροιστικές: δομή version 6 είναι επίσης δομή version 4 και version 5, οπότε το PDFium διαβάζει m_RendererType και m_FontLibraryType παρόλο που ζητήσατε μόνο Brotli. Ό,τι κάθεται σε εκείνα τα δύο fields εκείνη τη στιγμή γίνεται ο renderer και το font backend, αν το θέλατε ή όχι

Σκάλα versions FPDF_LIBRARY_CONFIG PDFium Component από version 2 έως version 7 δείχνει ποια επιλογή TPdfLibraryConfiguration προσθέτει m_RendererType, m_FontLibraryType, m_BrotliEnabled και m_IsolatePerDocument, και γιατί αθροιστικές versions κάνουν μηδενισμένο renderer field συνειδητή επιλογή AGG και όχι μη ορισμένη τιμή σε κάθε build
Κάθε επιλογή ανεβάζει το version της δομής και κάθε προγενέστερο πεδίο μένει live, οπότε το μηδέν στο m_RendererType φτάνει στο PDFium ως ρητό αίτημα AGG

Γιατί η ενεργοποίηση 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: hash F75B5EB4728ADE87
  • Ρητό prpAgg: hash F75B5EB4728ADE87, πανομοιότυπο με το τρέξιμο 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 με το προεπιλεγμένο

Σύγκριση pixel hash PDFium Component δείχνει το default Skia render hash 502D77C3711B4ACF, το configuration BrotliEnabled πριν το v3.123.0 που ταίριαζε ρητό τρέξιμο prpAgg με hash F75B5EB4728ADE87, και τον διορθωμένο wrapper που αναλύει prpDefault μέσω του export FPDF_RenderPageSkia πίσω στο αρχικό Skia hash
Ένα pixel hash πιάνει ό,τι κρύβουν τα screenshots· η ενεργοποίηση Brotli απέδιδε κάθε σελίδα με AGG, και το διορθωμένο default τώρα ταιριάζει την ανέγγιχτη configuration

Το 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

Ροή precheck LoadLibrary PDFium Component όπου ConfigurePdfLibrary σφραγίζει τη configuration, ο έλεγχος δυνατότητας τεστάρει το export FPDF_RenderPageSkia και την απόδειξη string skrifa, μη υποστηριζόμενο αίτημα σηκώνει πιάνσιμο EPdfError και ξεσφραγίζει για επανάληψη, ενώ DLL που δεν φορτώνει ποτέ κρατά PdfLibraryConfigurationSealed αληθές
Η επαλήθευση τρέχει αφού δεθούν τα exports και πριν την αρχικοποίηση, οπότε απόν backend αποτυγχάνει ως EPdfError που μπορείς να πιάσεις αντί για native CHECK που σκοτώνει τη διεργασία

Επαλήθευση που συμβαίνει ακόμα νωρίτερα

Το 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 αφήνοντας πεδίο σε μηδενική τιμή που η βιβλιοθήκη διαβάζει ως συνειδητή επιλογή

  1. Πεδίο ορισμένο, version πολύ χαμηλό. Γράφοντας m_BrotliEnabled = 1 σε δομή version 2, το PDFium δεν κοιτάζει ποτέ εκεί. Η κλήση πετυχαίνει και τα Brotli streams μένουν μη αποκωδικοποίσιμα. Η άμυνα είναι να παράγεις το version από τα fields που χρησιμοποιούνται όντως, που κάνει το LoadLibrary, αντί να το γράψεις σταθερά
  2. 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