Techninis straipsnis

PDFium Library Config: kai Brotli tyliai sukeičia Skia

PDFium Component for Delphi įjungus BrotliEnabled arba IsolatePerDocument TPdfLibraryConfiguration viduje, pridėtoji Skia varianta anksčiau pereidavo į AGG atvaizduotoją be jokios klaidos, nes abi parinktys pakelia FPDF_LIBRARY_CONFIG iki versijos, kurioje PDFium m_RendererType skaito pažodžiui. Nuo v3.123.0 numatytasis atvaizduotojas lieka DLL sava numatytasis, o nuo v3.125.0 Skia arba Fontations prašymas, kurio DLL negali įvykdyti, kelia pagaunamą EPdfError vietoj proceso žūties

Nė viena klaida nesipaskelbė pati. Pirmoji duodavo puslapius, atrodančius gerai – tiesiog atvaizduotus kitu rasterizatoriumi, su kiek kita glotninimo ir teksto kraštine negu variante, kurį išleidote ir išbandėte. Antroji paskelbėsi – garsiai, nukeldama hosto procesą iš savosios inicijacijos vidaus. Abi ateina iš tos pačios vietos: versijuotos C struktūros, kurios laukai skaitosi tik tada, kai versijos numeris to nori, ir kurios nulinės reikšmės nėra „nenustatyta“, o tikri pasirinkimai

Kaip FPDF_LIBRARY_CONFIG nusprendžia, kurį atvaizduotoją naudos PDFium?

FPDF_InitLibraryWithConfig į m_RendererType žiūri tik tada, kai struktūros Version laukas 4 ar aukštesnis, ir nuo tos versijos reikšmę naudoja tiksliai taip, kaip parašyta. Žemiau versijos 4 PDFium lauką ignoruoja ir ima varianto numatytąjį – Skia variantuose, sukompiliuotuose su PDF_USE_SKIA, ir AGG visur kitur

Kiekvienas vėlesnis laukas seka tą pačią struktūrą. Struktūra augo po vieną gebėjimą, ir kiekvienas gebėjimas atkeldavo kartu su nauju versijos numeriu. PDFium Component savąją struktūrą LoadLibrary viduje stato iš jūsų TPdfLibraryConfiguration ir versiją kelia tik tiek, kiek reikalauja jūsų nustatytos parinktys

Struktūros versijaPridedamas laukasNustato
2m_pIsolate, m_v8EmbedderSlotVisada rašoma; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform ne nil
4m_RendererTypeRenderer kitoks nei prpDefault
5m_FontLibraryTypeFontBackend kitoks nei pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Spąstai – paskutinėse dviejose eilutėse. Versijos kaupiamosios: versijos 6 struktūra yra ir versijos 4, ir versijos 5 struktūra, tad PDFium skaito m_RendererType ir m_FontLibraryType, nors prašėte vien Brotli. Kas tuomet tupi tuose dviejuose laukuose, tas tampa atvaizduotoju ir šriftų backendu – norėjote jų rinktis ar ne

PDFium Component FPDF_LIBRARY_CONFIG versijų laiptai nuo versijos 2 iki versijos 7, rodantys kuri TPdfLibraryConfiguration parinktis prideda m_RendererType, m_FontLibraryType, m_BrotliEnabled ir m_IsolatePerDocument, ir kodėl kaupiamosios versijos sunulintą atvaizduotojo lauką padaro sąmoningu AGG pasirinkimu, o ne nenustatyta reikšme bet kuriame variante
Kiekviena parinktis pakelia struktūros versiją, o kiekvienas ankstesnis laukas lieka gyvas, tad nulis m_RendererType atkeliauja į PDFium kaip aiškus AGG prašymas

Kodėl Brotli įjungimas persijungė atvaizduotoją į AGG?

Iki v3.123.0 PDFium Component prpDefault atveju į m_RendererType rašydavo FPDF_RENDERERTYPE_AGG, tad bet kokia struktūrą iki versijos 6 ar 7 pakėlusi konfigūracija Skia variante priverstinai dėdavo AGG. pdfium.dll ir pdfium.v8.dll vykdymo aplinkos, keliaujančios su komponentu, yra Skia variantai, tad tai ištikdavo numatytąjį išdėstymą, o ne kokį egzotiškąjį

Susiejimas, kai buvo parašytas, atrodė nekaltas. Ant versijos 2 ar 3 laukas niekada neskaitomas, tad prpDefault iš tiesų reiškė „ką DLL bedarytų“. Kai tik BrotliEnabled (versija 6) arba IsolatePerDocument (versija 7) įėjo į žaidimą, tas pats kodas „be nuostatų“ pavertė aiškiu AGG prašymu. Nieko nesugriuvo. PDFium inicijavosi normaliai, atvaizdavo kiekvieną puslapį ir negrąžino jokio klaidos kodo, nes jo akimis kvientėjas buvo paprašęs AGG ir AGG gavo

Pikselių maišos kodas permainą padaro matomą ten, kur ekrano kopijos ne. Atvaizdavus to paties pavyzdinio dokumento pirmąjį puslapį po trijomis konfigūracijomis gauta:

  • Numatytoji konfigūracija: maišos kodas 502D77C3711B4ACF
  • BrotliEnabled = True su Renderer, paliktu ant prpDefault: maišos kodas F75B5EB4728ADE87
  • Aiškus prpAgg: maišos kodas F75B5EB4728ADE87 – identiškas Brotli paleidimui

v3.123.0 pataisa – viešoji funkcija PdfNativeRendererType, kuri TPdfRendererPreference išsprendžia į reikšmę, rašomą į m_RendererType. prpAgg ir prpSkia susieja vienas su vienu. prpDefault dabar susieja su Skia, kai pakrautoji DLL eksportuoja FPDF_RenderPageSkia, ir su AGG kitu atveju. Tas eksportas kompiliuojamas toje pačioje PDF_USE_SKIA sąlygoje kaip ir pats Skia numatytasis, tad tai vienintelė varianto savybė, kurią galima stebėti iš DLL išorės. Po pataisymo Brotli konfigūracija duoda tą patį maišos kodą kaip numatytoji

PDFium Component pikselių maišos kodų palyginimas, rodantis numatytosios Skia atvaizduotės maišos kodą 502D77C3711B4ACF, iki v3.123.0 buvusią BrotliEnabled konfigūraciją, sutampančią su aiškiu prpAgg paleidimu su maišos kodu F75B5EB4728ADE87, ir sutvarkytą aplinką, išsprendžiančią prpDefault per FPDF_RenderPageSkia eksportą atgal į originalųjį Skia maišos kodą
Pikselių maišos kodas sugauna tai, ką ekrano kopijos slepia: Brotli įjungimas anksčiau atvaizduodavo kiekvieną puslapį su AGG, o sutvarkytasis numatytasis dabar sutampa su neliečiamąja konfigūracija

Šriftų backendas tos pačios problemos niekada neturėjo. m_FontLibraryType skaitomas nuo versijos 5, o jo nulinė reikšmė, FPDF_FONTBACKENDTYPE_FREETYPE, yra ir PDFium numatytasis, kai laukas apskritai neskaitomas. FreeType rašymas pfbpDefault atveju todėl tiksliai atkuria savąjį numatytąjį. Nulinės reikšmės ne visada blogos – jos tiesiog niekada nėra automatiškai teisingos

Su v3.123.0 ar naujesne starto kodas, kurį rašytumėte natūraliai, dabar daro tai, ką sako:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Turi vykti prieš bet ką pakraunant savąją biblioteką
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // pakelia FPDF_LIBRARY_CONFIG iki versijos 6
  // Renderer lieka prpDefault: išsprendžiama į Skia variantuose, eksportuojančiuose
  // FPDF_RenderPageSkia, ir į AGG vien AGG variantuose
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Atsiminkite, jog BrotliEnabled PDF 2.0 /BrotliDecode srautus padaro išskaitomus tik tada, kai pati DLL sukonstruota su PDF_ENABLE_BROTLI. Vėliavėlė – prašymas, ir variante be Brotli palaikymo ji nieko nedaro. TPdfLibraryConfiguration.Hardened sutampa su Default, išskyrus AllowMachineTime = False, kas draudžia dokumento JavaScript skaityti tikrąjį laikrodį; protinga pradžia serveriniam nepatikimų failų apdorojimui

Kas nutinka, kai prašote backendą, kurio DLL neturi?

PDFium klaidos negrąžina atvaizduotojui ar šriftų backendui, kurių variante nėra: FPDF_InitLibraryWithConfig žlugdo savąjį CHECK, kuris Windows aplinkoje pasirodo kaip stabdžio taško išimtis ir, be struktūrinio išimčių doroklio aplink kvietimą, baigia procesą. Antraštė sako tą patį, įspėdama, kad nepalaikoma reikšmė „panašiai žlugs su neišvengiamu strigtimu“

Du konkretūs atvejai – vien AGG variantas, gaunantis FPDF_RENDERERTYPE_SKIA, ir variantas be Fontations, gaunantis FPDF_FONTBACKENDTYPE_FONTATIONS. Pridėtoji Skia vykdymo aplinka priklauso antrajai grupei: ji atvaizduoja su Skia, bet šriftams naudoja FreeType. prpSkia prašymas kartu su pfbpFontations ant jos Delphi pusėje duodavo External exception 80000003. Kai derintojas ar išimčių doroklė atsitiktinai tai sugauna, padėtis vis tiek neištaisoma:

  • PDFium lieka pusiau inicijuota
  • Viso proceso konfigūracija jau užantspauduota, tad ConfigurePdfLibrary atmeta pataisytąją konfigūraciją
  • Bandymas iš naujo su kita konfigūracija tame pačiame procese nebepavyks

Tai – atvirkščias Brotli klaidos nesėkmės atvejis. Ten lauke tupėjo reikšmė, kurios niekas nepasirinko, ir PDFium ją tyliai priėmė. Čia lauke tupi reikšmė, kurią kvientėjas pasirinko sąmoningai, ir PDFium apie jos niekų nenori girdėti. Abu dalykai – problemos, kurias aplinkas turi išspręsti prieš savąjį kvietimą, nes po jo lieka nebegautino

Kaip PDFium Component pirmiau patikrina Skia ir Fontations

Nuo v3.125.0 LoadLibrary konfigūraciją patikrina po DLL eksportų susiejimo ir prieš kviesdamas FPDF_InitLibraryWithConfig, o nepalaikomą atvaizduotoją ar šriftų backendą paverčia EPdfError su žinute, įvardijančia kaltąjį nustatymą ir alternatyvas. DLL iškraunama, konfigūracija atspaudžiama, tad kvientėjas gali rinktis kitus nustatymus ir krauti dar kartą

Pats sprendimas gyvena grynojoje funkcijoje PdfLibraryConfigurationSupportError, imančioje konfigūraciją plius du Boolean, aprašančius variantą, ir grąžinančioje tuščią eilutę, kai derinys saugus. Kadangi ji savosios būsenos neliečia, ją galite kviesti iš savųjų testų su bet kuriuo gebėjimų deriniu. LoadLibrary viduje du Boolean ateina iš skirtingų rūšių įrodymų, ir jie nusipelno skirtingo pasitikėjimo:

  • Skia aptinkama iš FPDF_RenderPageSkia eksporto buvimo – to paties ženklo, kuriuo naudojasi PdfNativeRendererType. Eksportas ir Skia atvaizduotojas kompiliuojami vienoje sąlygoje, tad patikra tiksli
  • Fontations savo eksporto neturi. Vienintelis paliktas pėdsakas – Rust šriftų crate'ai, kuriuos jis suvilioja į dvejetainį failą, tad PDFium Component pakrautąją bibliotekos failą išskenuoja ieškodamas crate pavadinimų skrifa ir read-fonts (taip pat read_fonts). Skenuojama tik kai prašoma pfbpFontations, o neperskaitomas failas skaitosi kaip „be Fontations“

Fontations patikra – heuristika, ir ji gali klysti viena kryptimi: Fontations variantas, nukirptas nuo kiekvienos tų eilučių, būtų atmetamas, nors galėjo veikti. Tas kompromisas padarytas tyčia. Klaidingas atmetimas kainuoja išimtį, kurią sugaunate, ir atsitraukimą į FreeType. Klaidingas priėmimas kainuoja procesą

Atspaudimas svarbus ne mažiau už patikrą. LoadLibrary konfigūraciją užspaudžia pačioje krovimo pradžioje, tad be atstatymo gebėjimų atmetimas paliktų ConfigurePdfLibrary atsakant į kiekvieną pakartojimą su EPdfError „PDFium library configuration is already sealed“. Atmetimo kelias pirmiau kviečia UnloadLibrary; jo FPDF_DestroyLibrary kvietimas tą akimirką saugus, nes PDFium dar neinicijuota ir grąžina atsakymą nedelsdama. Kiti krovimo nesėkmės, tokios kaip dingusi DLL ar architektūros nesutapimas, antspaudą palieka, tad pakartojimų kilpa turi abi atskirti:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // su modulio kvalifikatoriumi: Windows.LoadLibrary turi tą patį vardą
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Gebėjimų atmetimas iškrauna DLL ir atspaudžia konfigūraciją.
      // DLL, visai nepakrauta, lieka užspausta: pakartojimas nepadės
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Atkreipkite dėmesį į aiškųjį PDFium.LoadLibrary. Modulyje, naudojančiame ir Windows, arba Winapi.Windows, nekvalifikuotasis LoadLibrary išsiresolvina į tai, kuri module paskutinė uses sąraše; kai tai Win32 funkcija, kvietimas be argumentų nesukompiliuos – argumentų skaičiaus klaida, apie PDFium nesakančią nieko

PDFium Component LoadLibrary ikipatikros eiga, kur ConfigurePdfLibrary užspaudžia konfigūraciją, gebėjimų patikra išbando FPDF_RenderPageSkia eksportą ir skrifa eilučių įrodymą, nepalaikomas prašymas kelia pagaunamą EPdfError ir atspaudžia pakartojimui, o DLL, niekada nepakrauta, palieka PdfLibraryConfigurationSealed true
Patikra lekia po eksportų susiejimo ir prieš inicijaciją, tad dingęs backendas žlunga kaip pagaunama EPdfError, o ne kaip savasis CHECK, žudantis procesą

Patikra, vykstanti dar anksčiau

ConfigurePdfLibrary kai kuriuos derinius atmeta dar prieš bet kokią DLL, visus su EPdfError. Aiškus FontBackend, įskaitant pfbpFreeType, reikalauja Renderer = prpSkia, nes PDFium šriftų backendą svarsto tik Skia atvaizduotojui. IsolatePerDocument reikalauja, kad V8Isolate būtų nil, nes PDFium kiekvienam dokumentui sukuria savąjį izoliatą ir žlugdo savąjį CHECK, jei jam dar vieną įduodate. Tuščios eilutės UserFontPaths viduje atmetamos. Ir bet koks kvietimas po pirmojo krovimo bandymo žlunga su „PDFium library configuration is already sealed“

Tas paskutinis variantas turi praktinę išvadą: DLL pirmiau iščiuopti ir tik paskui sukonfigūruoti negalima. GetSkiaRenderCapabilities, V8FeaturesAvailable, dokumento atvėrimas ir dauguma kitų įėjimo taškų viduje kviečia LoadLibrary, kuri konfigūraciją užspaudžia vietoje. Vėliau kviečiama UnloadLibrary jos irgi neatveria. Pirmiau sukonfigūruokite, paskui pakraukite, paskui klauskite – būtent ta tvarka, kurios turėtų laikytis diagnostinė procedūra:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // kopija, saugi apžiūrėti
  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;
  // Tas pats sprendimas, kurį LoadLibrary taikė statydamas 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;

Tos eilutės įrašymas į žurnalą kartą starto metu pigus, ir tai pirmasis dalykas, kurio norėsite remonto talone su „tekstas serveryje atrodo kitaip“. PDFium.Loaded kvalifikuotas ta pačia priežastimi kaip LoadLibrary: formos ar komponento metodo viduje plikasis Loaded prisiriša prie TComponent.Loaded

Du būdai, kaip versijuotoji C konfigūracijos struktūra suyra

Kiekviena versijuotoji konfigūracijos struktūra – ar tai FPDF_LIBRARY_CONFIG, Win32 cbSize įrašas, ar papildinio ABI – žlunga dviem simetriškomis kryptimis, ir aplinkas turi saugotis abiejų. Pirmoji – užpildyti lauką, paliekant versiją per žemą; antroji – pakelti versiją, paliekant lauką nulinėje reikšmėje, kurią biblioteka skaito kaip sąmoningą pasirinkimą

  1. Laukas nustatytas, versija per žema. Įrašykite m_BrotliEnabled = 1 į versijos 2 struktūrą, ir PDFium į jį niekada nepažiūrės. Kvietimas pavyks, o Brotli srautai lieka neišskaitomi. Gynyba – išvesti versiją iš realiai naudojamų laukų, ką daro LoadLibrary, o ne įkalti vieną į kodą
  2. Versija pakankama, nulinis laukas reiškia kažką. Pakelkite versiją iki 6, ir kiekvienas laukas iki versijos 6 dabar gyvas. FillChar sunulina m_RendererType į FPDF_RENDERERTYPE_AGG – tikrą atvaizduotoją, o ne „nenustatyta“. Gynyba – kiekvieną lauką, kurį dengia pasirinktoji versija, rašyti su sąmoninga reikšme, o „numatytąjį“ spręsti prieš tikrąjį variantą, o ne manyti

Trečia taisyklė seka reikšmėms, gebančioms sugriauti kviečiamąjį: patikrinkite jas prieš tai, ką dvejetainis failas geba, dar prieš kvietimą, imdami stipriausią prieinamą įrodymą, ir būkite sąžiningi kode ir dokumentacijoje, kai tas įrodymas – heuristika. Eksportuotas simbolis – įrodymas. Crate pavadinimas eilučių lentelėje – geras spėjimas

Trumpa atmintinė: PDFium Component bibliotekos konfigūracija

  • Kvieskite ConfigurePdfLibrary kartą, prieš bet ką pakraunant DLL; bet kokia gebėjimų užklausa ar dokumento atvėrimas ją užspaudžia
  • Atnaujinkite iki v3.123.0 ar naujesnės, jei nustatote BrotliEnabled arba IsolatePerDocument ir iš pridėtųjų vykdymo aplinkų tikitės Skia išvesties
  • Renderer palikite ant prpDefault, nebent reikia konkretaus rasterizatoriaus; jis dabar kiekvienoje struktūros versijoje išsprendžiamas į varianto numatytąjį
  • Naudokite PdfNativeRendererType su GetSkiaRenderCapabilities.PageRender, kad užregistruotumėte, kuris atvaizduotojas iš tikrųjų aktyvus
  • Tikėkitės EPdfError, o ne strigimo, dėl prpSkia ant vien AGG DLL ar pfbpFontations ant be Fontations DLL v3.125.0 ar naujesnėje
  • Po gebėjimų atmetimo PdfLibraryConfigurationSealed yra False, ir galite konfigūruoti iš naujo; po nepavykusio DLL krovimo lieka True
  • Fontations aptikimą laikykite heuristika ir turėkite FreeType atsarginį kelią
  • Rašykite PDFium.LoadLibrary ir PDFium.Loaded su modulio vardu, kad išvengtumėte Win32 ir TComponent vardų susidūrimų

Jei DLL žlunga dar prieš konfigūracijai tampant svarbia, pradėkite nuo PDFium DLL krovimo nesėkmių diagnostikos Delphi, o kaip komponentas kiekvienoje platformoje randa tinkamąjį dvejetainį failą – žiūrėkite PDFium savosios bibliotekos krovimas bet kuriame tiksle. Kai atvaizduotojas jau apsispręsta, atvaizdavimo podėlis ir sklandaus mastelio taktikos dengia, kaip peržiūrovoje išlaikyti greitą puslapių atvaizdavimą

PDFium Component apgaubia PDFium variklį Delphi ir C++Builder aplinkai su tokiais konfigūracijos patikrinimais, tad savoji inicijacija žlunga kaip Pascal išimtis, kurią galite apdoroti, o ne kaip proceso išėjimas. Produkto detalės ir atsiuntimai yra PDFium Component for Delphi produkto puslapyje