Teknik Makale

PDFium Library Config: Brotli Skia'yı sessizce değiştirdi

Delphi için PDFium Component'te TPdfLibraryConfiguration içinde BrotliEnabled ya da IsolatePerDocument'i açmak, paketlenmiş Skia derlemesini hatasızca AGG renderer'a geçiriyordu; çünkü iki seçenek de FPDF_LIBRARY_CONFIG'i, PDFium'un m_RendererType'ı harfi harfine okuduğu bir sürüme yükseltiyordu. v3.123.0'dan beri varsayılan renderer, DLL'nin kendi varsayılanı olarak kalır; v3.125.0'dan beri DLL'nin yerine getiremeyeceği bir Skia ya da Fontations isteği, süreci öldürmek yerine yakalanabilir bir EPdfError yükseltir

İki hata da kendini duyurmadı. Birincisi, iyi görünen sayfalar üretti; sadece gönderip test ettiğiniz derlemeden hafifçe farklı anti-aliasing ve metin kenarlarıyla, başka bir rasterizer tarafından render edilmiş. İkincisi kendini duyurdu; gürültüyle, ana süreci yerel başlatmanın içinden indirerek. İkisi de aynı yerden gelir: alanları yalnızca sürüm sayısı dediğinde sayılan, sıfır değerleri "unset" değil gerçek seçim olan sürümlü bir C yapısı

FPDF_LIBRARY_CONFIG, PDFium'un hangi renderer'ı kullanacağına nasıl karar verir?

FPDF_InitLibraryWithConfig, m_RendererType'a yalnızca yapının Version alanı 4 ve üzerindeyken bakar ve o sürümden itibaren değeri yazıldığı gibi kullanır. Sürüm 4'ün altında PDFium alanı yok sayar ve derleme varsayılanını seçer; PDF_USE_SKIA ile derlenmiş derlemelerde Skia, gerisinde AGG

Sonraki her alan aynı örüntüyü izler. Yapı birer yetenekle büyüdü ve her yetenek yeni bir sürüm numarasıyla birlikte geldi. PDFium Component, yerel yapıyı LoadLibrary'de sizin TPdfLibraryConfiguration'ınızdan kurar ve sürümü yalnızca set ettiğiniz seçeneklerin gerektirdiği kadar yükseltir

Yapı sürümüEklediği alanKim set eder
2m_pIsolate, m_v8EmbedderSlotDaima yazılır; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform nil değil
4m_RendererTypeprpDefault dışında bir Renderer
5m_FontLibraryTypepfbpDefault dışında bir FontBackend
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Tuzak son iki satırda. Sürümler kümülatiftir: sürüm 6 bir yapı aynı zamanda sürüm 4 ve sürüm 5 bir yapıdır; dolayısıyla PDFium, yalnızca Brotli istemiş olsanız bile m_RendererType ile m_FontLibraryType'ı okur. O anda o iki alanda ne duruyorsa renderer ile font backend olur; onları seçmek isteseniz de istemeseniz de

PDFium Component FPDF_LIBRARY_CONFIG sürüm merdiveni, sürüm 2'den sürüm 7'ye hangi TPdfLibraryConfiguration seçeneğinin m_RendererType, m_FontLibraryType, m_BrotliEnabled ve m_IsolatePerDocument eklediğini ve kümülatif sürümlerin neden sıfırlanmış bir renderer alanını her derlemede unset değer değil kasıtlı bir AGG seçimi yaptığını gösterir
Her seçenek yapı sürümünü yükseltir ve önceki her alan canlı kalır; dolayısıyla m_RendererType'taki sıfır, PDFium'a açık bir AGG isteği olarak ulaşır

Brotli'yi açmak renderer'ı neden AGG'ye geçirdi?

v3.123.0 öncesinde PDFium Component, prpDefault için m_RendererType'a FPDF_RENDERERTYPE_AGG yazıyordu; dolayısıyla yapıyı sürüm 6'ya ya da 7'ye iten her yapılandırma, bir Skia derlemesinde AGG'yi dayatıyordu. Bileşenle gelen pdfium.dll ile pdfium.v8.dll runtime'ları Skia derlemeleridir; dolayısıyla bu, egzotik bir dağıtımı değil varsayılan dağıtımı vurdu

Eşleme, yazıldığı sırada zararsız görünüyordu. Sürüm 2 ya da 3'te alan hiç okunmaz; dolayısıyla prpDefault gerçekten "DLL ne yaparsa" demekti. BrotliEnabled (sürüm 6) ya da IsolatePerDocument (sürüm 7) resme girdiği anda aynı kod, "tercih yok"u açık bir AGG isteğine çevirdi. Hiçbir şey başarısız olmadı. PDFium normal başlatıldı, her sayfayı render etti ve hiçbir hata kodu döndürmedi; çünkü onun bakış açısından çağıran AGG istemiş ve AGG almıştı

Bir pixel hash, değişimi ekran görüntülerinin göremediği yerde görünür kılar. Aynı örnek belgenin ilk sayfasını üç yapılandırma altında render etmek şunu verdi:

  • Varsayılan yapılandırma: hash 502D77C3711B4ACF
  • BrotliEnabled = True ve Renderer prpDefault'da bırakılmış: hash F75B5EB4728ADE87
  • Açık prpAgg: hash F75B5EB4728ADE87, Brotli koşusuyla aynı

v3.123.0'daki düzeltme, TPdfRendererPreference'ı m_RendererType'a yazılan değere çözen PdfNativeRendererType isimli genel fonksiyondur. prpAgg ile prpSkia bire bir eşlenir. prpDefault artık, yüklenen DLL FPDF_RenderPageSkia'yı export ettiğinde Skia'ya, aksi hâlde AGG'ye eşlenir. O export, Skia varsayılanının kendisiyle aynı PDF_USE_SKIA koşulu altında derlenir; bu da onu, DLL'nin dışından gözlemleyebileceğiniz tek derleme özelliği yapar. Düzeltmeden sonra Brotli yapılandırması, varsayılanla aynı hash'i üretir

PDFium Component pixel hash karşılaştırması: varsayılan Skia render hash'i 502D77C3711B4ACF; v3.123.0 öncesi BrotliEnabled yapılandırmasının, F75B5EB4728ADE87 hash'li açık bir prpAgg koşusuyla eşleşmesi; ve düzeltilmiş wrapper'ın prpDefault'u FPDF_RenderPageSkia export'u üzerinden özgün Skia hash'ine çözmesi
Bir pixel hash, ekran görüntülerinin sakladığını yakalar: Brotli'yi açmak eskiden her sayfayı AGG ile render ederdi ve düzeltilmiş varsayılan artık dokunulmamış yapılandırmayla eşleşir

Font backend hiç aynı sorunu yaşamadı. m_FontLibraryType sürüm 5'ten itibaren okunur ve sıfır değeri olan FPDF_FONTBACKENDTYPE_FREETYPE, alan hiç okunmadığında PDFium'un varsayılanıdır. pfbpDefault için FreeType yazmak bu yüzden yerel varsayılanı aynen üretir. Sıfır değerleri her zaman yanlış değildir; sadece hiçbir zaman otomatik olarak doğru da değildirler

v3.123.0 ve sonrasıyla doğal olarak yazacağınız başlangıç kodu artık dediğini yapar:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Herhangi bir şey yerel kütüphaneyi yüklemeden önce koşmalı
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // FPDF_LIBRARY_CONFIG'i sürüm 6'ya yükseltir
  // Renderer prpDefault kalır: FPDF_RenderPageSkia'yı export eden derlemelerde
  // Skia'ya, yalnızca-AGG derlemelerde AGG'ye çözülür
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

BrotliEnabled'in PDF 2.0 /BrotliDecode stream'lerini yalnızca DLL'nin kendisi PDF_ENABLE_BROTLI ile derlenmişse çözülebilir kıldığını unutmayın. Bayrak bir istektir ve Brotli desteği olmayan bir derlemede etkisizdir. TPdfLibraryConfiguration.Hardened, AllowMachineTime'ın False olması dışında Default ile aynıdır; bu da belge JavaScript'inin gerçek saati okumasını engeller ve güvenilmeyen dosyaların sunucu tarafı işlemesi için makul bir başlangıç noktasıdır

DLL'nin içermediği bir backend istediğinizde ne olur?

PDFium, derlemede eksik bir renderer ya da font backend için hata döndürmez: FPDF_InitLibraryWithConfig yerel bir CHECK'i düşürür; bu, Windows'ta bir breakpoint exception olarak görünür ve çağrının etrafında yapılandırılmış bir exception handler yoksa süreci sonlandırır. Header da aynı şeyi söyler; desteklenmeyen bir değerin "anında çökmeyle benzer biçimde başarısız olacağı" uyarısıyla

İki somut durum: FPDF_RENDERERTYPE_SKIA alan yalnızca-AGG bir derleme ve FPDF_FONTBACKENDTYPE_FONTATIONS alan Fontations'sız bir derleme. Paketlenmiş Skia runtime ikinci gruptadır: Skia ile render eder ama fontlar için FreeType kullanır. Ona karşı prpSkia ile pfbpFontations'ı birlikte istemek, Delphi tarafında External exception 80000003 üretti. Debugger ya da bir exception handler tesadüfen onu yakalasa bile durum hâlâ kurtarılamaz:

  • PDFium yarı başlatılmış kalır
  • Süreç geneli yapılandırma çoktan mühürlenmiştir; dolayısıyla ConfigurePdfLibrary, düzeltilmiş bir yapılandırmayı reddeder
  • Aynı süreçte başka bir yapılandırmayla yeniden denemek artık mümkün değildir

Bu, Brotli hatasının tersi bir başarısızlıktır. Orada alan, kimsenin seçmediği bir değer taşıyordu ve PDFium onu sessizce kabul ediyordu. Burada alan, çağıranın bilerek seçtiği bir değer taşıyor ve PDFium hakkında hiçbir tartışma kabul etmiyor. İkisi de, yerel çağrıdan önce bir wrapper'ın çözmesi gereken sorunlardır; çünkü ondan sonra yakalanacak hiçbir şey kalmaz

PDFium Component Skia ile Fontations'ı nasıl ön-denetler?

v3.125.0'dan beri LoadLibrary, DLL export'larını bağladıktan sonra ve FPDF_InitLibraryWithConfig'i çağırmadan önce yapılandırmayı doğrular ve desteklenmeyen bir renderer ya da font backend'i, suçlu ayarı ile alternatifleri adıyla söyleyen bir EPdfError'a çevirir. DLL boşaltılır ve yapılandırmanın mühürleri açılır; böylece çağıran başka ayarlar seçip yeniden yükleyebilir

Kararın kendisi, yapılandırmayı ve derlemeyi tanımlayan iki Boolean'ı alan, kombinasyon güvenliyken boş string döndüren saf fonksiyon PdfLibraryConfigurationSupportError'da yaşar. Yerel duruma dokunmadığı için kendi testlerinizden herhangi bir yetenek kombinasyonuyla çağırabilirsiniz. LoadLibrary içinde o iki Boolean farklı türden kanıtlardan gelir ve farklı güven düzeylerini hak eder:

  • Skia, FPDF_RenderPageSkia export'unun varlığından saptanır; PdfNativeRendererType'ın kullandığı sinyal aynıdır. Export ile Skia renderer tek koşul altında derlenir; dolayısıyla kontrol kesindir
  • Fontations'ın kendi export'u yoktur. Bıraktığı tek iz, binary'ye çektiği Rust font crate'leridir; dolayısıyla PDFium Component, yüklenen kütüphane dosyasını skrifa ile read-fonts (ve read_fonts) crate adları için tarar. Tarama yalnızca pfbpFontations istendiğinde koşar ve okunamayan bir dosya "Fontations yok" sayılır

Fontations kontrolü bir sezgiseldir ve tek yönde yanılabilir: o string'lerin hepsinden arındırılmış bir Fontations derlemesi, işe yarayabilirdi ama reddedilirdi. O taviz bilinerek yapıldı. Yanlış bir reddin bedeli, yakalayabileceğiniz bir exception ve FreeType'a bir fallback'tir. Yanlış bir kabulün bedeli sürecin kendisidir

Mührü açmak, kontrol kadar önemlidir. LoadLibrary yapılandırmayı yüklemenin en başında mühürler; reset olmadan bir yetenek reddi, ConfigurePdfLibrary'nin her denemeye EPdfError "PDFium library configuration is already sealed" ile karşılık vermesi demek olurdu. Ret yolu önce UnloadLibrary çağırır; onun FPDF_DestroyLibrary çağrısı o noktada güvenlidir, çünkü PDFium henüz başlatılmamıştır ve hemen döner. Eksik bir DLL ya da mimari uyuşmazlığı gibi öteki yükleme başarısızlıkları mührü korur; dolayısıyla bir yeniden deneme döngüsü ikisini ayırt etmek zorundadır:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // unit nitelemeli: Windows.LoadLibrary aynı ada sahiptir
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Yetenek reddi DLL'yi boşaltır ve yapılandırmanın mührünü açar.
      // Hiç yükleyemeyen bir DLL mühürlü kalır: yeniden denemek işe yaramaz
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Açık PDFium.LoadLibrary'ye dikkat. Aynı zamanda Windows ya da Winapi.Windows kullanan bir unit'te nitelemsiz bir LoadLibrary, uses yan tümcesinde en sonda görünen unit'e çözülür; o Win32 fonksiyonu olduğunda parametresiz çağrı, PDFium hakkında hiçbir şey söylemeyen bir argüman sayısı hatasıyla derlenemez

PDFium Component LoadLibrary ön-denetim akışı: ConfigurePdfLibrary yapılandırmayı mühürler; yetenek kontrolü FPDF_RenderPageSkia export'unu ve skrifa string kanıtını test eder; desteklenmeyen bir istek yakalanabilir bir EPdfError yükseltir ve yeniden deneme için mührü açar; hiç yüklenmeyen bir DLL ise PdfLibraryConfigurationSealed'i true tutar
Doğrulama, export'lar bağlandıktan sonra ve başlatmadan önce koşar; dolayısıyla eksik bir backend, süreci öldüren yerel bir CHECK yerine yakalayabileceğiniz bir EPdfError olarak başarısız olur

Daha da erken gerçekleşen doğrulama

ConfigurePdfLibrary, herhangi bir DLL söz konusu olmadan bazı kombinasyonları reddeder; hepsi EPdfError ile. pfbpFreeType dâhil açık bir FontBackend, Renderer = prpSkia ister; çünkü PDFium font backend'ine yalnızca Skia renderer için bakar. IsolatePerDocument, V8Isolate'nin nil olmasını ister; PDFium her belge için kendi isolate'ini kurar ve siz ona bir tane daha uzatırsanız yerel bir CHECK düşürür. UserFontPaths'teki boş string'ler reddedilir. Ve ilk yükleme denemesinden sonraki her çağrı, "PDFium library configuration is already sealed" ile başarısız olur

Son kuralın pratik bir sonucu vardır: önce DLL'yi yoklayıp sonra yapılandıramazsınız. GetSkiaRenderCapabilities, V8FeaturesAvailable, bir belge açmak ve çoğu öteki giriş noktası içeride LoadLibrary çağırır; bu da yapılandırmayı anında mühürler. Sonradan UnloadLibrary çağırmak da onu yeniden açmaz. Önce yapılandırın, sonra yükleyin, sonra sorular sorun; tanısal bir rutinin izlemesi gereken düzen tam olarak budur:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // bir kopya, incelemesi güvenli
  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'nin FPDF_LIBRARY_CONFIG'i kurarken uyguladığı aynı çözüm
  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;

O satırı başlangıçta bir kez loglamak ucuzdur ve "metin sunucuda farklı görünüyor" diyen bir destek kaydında isteyeceğiniz ilk şeydir. PDFium.Loaded, LoadLibrary ile aynı nedenle nitelenmiştir: bir form ya da bileşen metodunun içinde çıplak bir Loaded, TComponent.Loaded'a bağlanır

Sürümlü bir C yapılandırma yapısının yanlış gittiği iki yol

Her sürümlü yapılandırma yapısı — FPDF_LIBRARY_CONFIG olsun, bir Win32 cbSize record'u olsun ya da bir plugin ABI — iki simetrik yoldan başarısız olur ve bir wrapper ikisine karşı da korunmalıdır. Birincisi sürümü fazla düşük bırakıp bir alanı doldurmak; ikincisi bir alanı, kütüphanenin kasıtlı seçim diye okuduğu bir sıfır değerde bırakıp sürümü yükseltmektir

  1. Alan setli, sürüm fazla düşük. Sürüm 2 bir yapıya m_BrotliEnabled = 1 yazın ve PDFium ona hiç bakmaz. Çağrı başarılı olur ve Brotli stream'leri çözülemez kalır. Savunma, sürümü sabit kodlamak yerine fiilen kullanılan alanlardan türetmektir; LoadLibrary'nin yaptığı budur
  2. Sürüm yeterince yüksek, sıfır alan bir şey ifade eder. Sürümü 6'ya yükseltin; sürüm 6'ya kadarki her alan artık canlıdır. FillChar, m_RendererType'ı gerçek bir renderer olan, "unset" olmayan FPDF_RENDERERTYPE_AGG'ye sıfırlar. Savunma, seçilen sürümün kapsadığı her alanı kasıtlı bir değerle yazmak ve "default"u varsaymak yerine gerçek derlemeye karşı çözmektir

Çağrılanı çökertebilecek değerler için üçüncü bir kural gelir: onları, çağrıdan önce kullanılabilir en güçlü kanıtla binary'nin yapabildiklerine karşı doğrulayın ve o kanıt bir sezgiselse kodda da dokümantasyonda da dürüst olun. Export edilmiş bir simge kanıttır. String tablosundaki bir crate adı iyi bir tahmindir

Hızlı başvuru: PDFium Component kütüphane yapılandırması

  • ConfigurePdfLibrary'yi bir kez, herhangi bir şey DLL'yi yüklemeden önce çağırın; her yetenek sorgusu ya da belge yüklemesi onu mühürler
  • BrotliEnabled ya da IsolatePerDocument set edip paketlenmiş runtime'lardan Skia çıktısı bekliyorsanız v3.123.0 ve sonrasına yükseltin
  • Belirli bir rasterizer'a gerek duymuyorsanız Renderer'i prpDefault'da bırakın; artık her yapı sürümünde derleme varsayılanına çözülür
  • Hangi renderer'ın fiilen aktif olduğunu loglamak için PdfNativeRendererType'i GetSkiaRenderCapabilities.PageRender ile kullanın
  • v3.125.0 ve sonrasında yalnızca-AGG bir DLL'de prpSkia ya da Fontations'sız bir DLL'de pfbpFontations için çökme değil EPdfError bekleyin
  • Bir yetenek reddinden sonra PdfLibraryConfigurationSealed False'tur ve yeniden yapılandırabilirsiniz; başarısız bir DLL yüklemesinden sonra True kalır
  • Fontations saptamasını sezgisel sayın ve bir FreeType fallback tutun
  • Win32 ile TComponent ad çakışmalarından kaçınmak için PDFium.LoadLibrary ile PDFium.Loaded'i unit adıyla yazın

DLL, yapılandırma hiç önem kazanmadan başarısız oluyorsa Delphi'de PDFium DLL yükleme başarısızlıklarını teşhis ile başlayın; bileşenin her platformda doğru binary'yi nasıl bulduğu için PDFium yerel kütüphanesini herhangi bir hedefte yükleme yazısına bakın. Renderer oturduktan sonra render cache ve akıcı zoom taktikleri, sayfa render'ını bir görüntüleyicide hızlı tutmayı işler

PDFium Component, PDFium motorunu Delphi ve C++Builder için bu tür yapılandırma kontrolleriyle sarar; böylece yerel başlatma, bir süreç çıkışı yerine işleyebileceğiniz bir Pascal exception olarak başarısız olur. Ürün ayrıntıları ve indirmeler Delphi için PDFium Component ürün sayfasında