Tekninen artikkeli

PDFium Library Config: kun Brotli vaihtaa Skian hiljaisesti

PDFium Component for Delphissä kohteen BrotliEnabled tai IsolatePerDocument kytkeminen päälle kohteessa TPdfLibraryConfiguration vaihtoi mukana toimitetun Skia-koontiversion AGG-renderöijään ilman mitään virhettä, koska molemmat asetukset nostavat kohteen FPDF_LIBRARY_CONFIG versioon, jossa PDFium lukee kohteen m_RendererType kirjaimellisesti. Versiosta v3.123.0 alkaen oletusrenderöijä pysyy DLL:n omana oletuksena, ja versiosta v3.125.0 alkaen Skia- tai Fontations-pyyntö, jota DLL ei voi kunnioittaa, nostaa siepattavan EPdfErrorin prosessin tappamisen sijaan

Kumpikaan vika ei ilmoittanut itsestään. Ensimmäinen tuotti sivuja, jotka näyttivät hyviltä, vain eri rasteroijan renderöiminä, hieman erilaisilla antialiasoinnin ja tekstin reunoilla kuin koontiversiossa, jonka toimitit ja testasit. Toinen ilmoitti itsestään, kovaäänisesti, viemällä isäntäprosessin alas natiivin alustuksen sisältä. Molemmat tulevat samasta paikasta: versioidusta C-rakenteesta, jonka kentät alkavat laskea vasta, kun versionumero sanoo niin, ja jonka nolla-arvot eivät ole "asettamattomia" vaan aitoja valintoja

Miten FPDF_LIBRARY_CONFIG päättää, mitä renderöijää PDFium käyttää?

FPDF_InitLibraryWithConfig kysyy kohteen m_RendererType arvoa vain, kun rakenteen Version-kenttä on 4 tai korkeampi, ja kyseisestä versiosta alkaen se käyttää arvoa täsmälleen kirjoitettuna. Version 4 alapuolella PDFium ohittaa kentän ja poimii koontiversion oletuksen, joka on Skia koontiversioissa, jotka on käännetty PDF_USE_SKIAlla, ja AGG kaikkialla muualla

Jokainen myöhempi kenttä noudattaa samaa kuviota. Rakenne kasvoi yhden ominaisuuden kerrallaan, ja kukin ominaisuus saapui yhdessä uuden versionumeron kanssa. PDFium Component rakentaa natiivin rakenteen funktiossa LoadLibrary kohteestasi TPdfLibraryConfiguration ja nostaa version vain niin pitkälle, kuin asettamasi asetukset vaativat

Rakenteen versioLisäämä kenttäAsettaja
2m_pIsolate, m_v8EmbedderSlotAina kirjoitettu; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform ei nil
4m_RendererTypeRenderer muu kuin prpDefault
5m_FontLibraryTypeFontBackend muu kuin pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Ansa on kahdessa viimeisessä rivissä. Versiot ovat kumulatiivisia: version 6 rakenne on myös version 4 ja version 5 rakenne, joten PDFium lukee kohteet m_RendererType ja m_FontLibraryType, vaikka pyysit vain Brotlia. Se, mikä istuu kyseisissä kentissä sillä hetkellä, muuttuu renderöijäksi ja fonttibackendiksi, halusitko valita ne vai et

PDFium Componentin FPDF_LIBRARY_CONFIG -versiotikapuut versiosta 2 versioon 7 osoittaen, mikä TPdfLibraryConfiguration-asetus lisää kentät m_RendererType, m_FontLibraryType, m_BrotliEnabled ja m_IsolatePerDocument, ja miksi kumulatiiviset versiot tekevät nollatusta renderöijäkentästä tahallisen AGG-valinnan asettamattoman arvon sijaan millä tahansa koontiversiolla
Jokainen asetus nostaa rakenteen version, ja jokainen aiempi kenttä pysyy elossa, joten nolla kohteessa m_RendererType saapuu PDFiumille eksplisiittisenä AGG-pyynnönä

Miksi Brotlin kytkeminen päälle vaihtoi renderöijän AGG:hen?

Ennen versiota v3.123.0 PDFium Component kirjoitti arvon FPDF_RENDERERTYPE_AGG kohteeseen m_RendererType asetukselle prpDefault, joten mikä tahansa konfiguraatio, joka työnsi rakenteen versioon 6 tai 7, pakotti AGG:n Skia-koontiversiolle. Komponentin mukana toimitettavat ajonaikaiset pdfium.dll ja pdfium.v8.dll ovat Skia-koontiversioita, joten tämä osui oletusasennukseen, ei johonkin eksoottiseen

Kuvaus näytti harmittomalta kirjoitushetkellä. Version 2 tai 3 kohdalla kenttää ei koskaan lueta, joten prpDefault oikeasti tarkoitti "sitä, mitä DLL tekee". Heti kun BrotliEnabled (versio 6) tai IsolatePerDocument (versio 7) astui kuvaan, sama koodi muutti "ei preferenssiä" eksplisiittiseksi AGG-pyynnöksi. Mikään ei epäonnistunut. PDFium alustui normaalisti, renderöi jokaisen sivun eikä palauttanut virhekoodia, koska sen näkökulmasta kutsuja oli pyytänyt AGG:tä ja saanut AGG:n

Pikselihashi tekee vaihdoksen näkyväksi siellä, missä kuvakaappaukset eivät. Saman malliasiakirjan ensimmäisen sivun renderöinti kolmella konfiguraatiolla antoi:

  • Oletuskonfiguraatio: hash 502D77C3711B4ACF
  • BrotliEnabled = True, kun Renderer jätettiin arvoon prpDefault: hash F75B5EB4728ADE87
  • Eksplisiittinen prpAgg: hash F75B5EB4728ADE87, identtinen Brotli-ajon kanssa

Korjaus versiossa v3.123.0 on julkinen funktio PdfNativeRendererType, joka ratkaisee kohteen TPdfRendererPreference arvoksi, joka kirjoitetaan kohteeseen m_RendererType. prpAgg ja prpSkia kuvautuvat yksi yhteen. prpDefault kuvautuu nykyään Skiaksi, kun ladattu DLL vie FPDF_RenderPageSkian, ja muuten AGG:hen. Kyseinen vienti käännetään saman PDF_USE_SKIA-ehdon alla kuin Skia-oletus itsekin, mikä tekee siitä ainoan koontiversion ominaisuuden, jonka voi havaita DLL:n ulkopuolelta. Korjauksen jälkeen Brotli-konfiguraatio tuottaa saman hashin kuin oletus

PDFium Componentin pikselihashien vertailu, joka näyttää oletuksen Skia-renderöintihashin 502D77C3711B4ACF, v3.123.0:aa edeltäneen BrotliEnabled-konfiguraation, joka täsmäsi eksplisiittisen prpAgg-ajon kanssa hashilla F75B5EB4728ADE87, ja korjatun kääreen, joka ratkaisee prpDefaultin FPDF_RenderPageSkia-viennin kautta takaisin alkuperäiseen Skia-hashin
Pikselihashi havaitsee sen, mitä kuvakaappaukset piilottavat: Brotlin kytkeminen päälle renderöi aiemmin jokaisen sivun AGG:llä, ja korjattu oletus täsmää nyt koskematonta konfiguraatiota

Fonttibackendillä ei koskaan ollut samaa ongelmaa. m_FontLibraryType luetaan versiosta 5 alkaen, ja sen nolla-arvo, FPDF_FONTBACKENDTYPE_FREETYPE, on myös PDFiumin oletus, kun kenttää ei lueta lainkaan. FreeTypen kirjoittaminen asetukselle pfbpDefault toistaa siksi natiivin oletuksen täsmälleen. Nolla-arvot eivät aina ole vääriä, ne vain eivät ole koskaan automaattisesti oikein

Versiolla v3.123.0 tai uudemmalla luonnollisesti kirjoittamasi käynnistyskoodi tekee nykyään sen, mitä se sanoo:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // On ajettava ennen kuin mikään lataa natiivikirjaston
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // nostaa FPDF_LIBRARY_CONFIGin versioon 6
  // Renderer pysyy arvossa prpDefault: ratkeaa Skiaksi koontiversioissa, jotka vievät
  // FPDF_RenderPageSkian ja AGG:hen vain AGG-koontiversioissa
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Muista, että BrotliEnabled tekee PDF 2.0:n /BrotliDecode-streamit dekoodattaviksi vain, kun itse DLL on käännetty PDF_ENABLE_BROTLIlla. Flagi on pyyntö, ja koontiversiolla ilman Brotli-tukea sillä ei ole vaikutusta. TPdfLibraryConfiguration.Hardened on sama kuin Default, paitsi että AllowMachineTime on False, mikä estää asiakirjan JavaScriptiä lukemasta oikeaa kelloa; se on järkevä lähtökohta luottamattomien tiedostojen palvelinpuolen käsittelyyn

Mitä tapahtuu, kun pyydät backendia, jota DLL ei sisällä?

PDFium ei palauta virhettä renderöijälle tai fonttibackendille, joka puuttuu koontiversiosta: FPDF_InitLibraryWithConfig kaataa natiivin CHECKin, joka Windowsissa ilmenee breakpoint-poikkeuksena ja ilman jäsenneltyä poikkeuskäsittelijää kutsun ympärillä lopettaa prosessin. Otsikkotiedosto sanoo asian niin ikään varoittaen, että ei-tuettu arvo "epäonnistuu vastaavasti välittömällä kaatumisella"

Kaksi konkreettista tapausta ovat vain AGG -koontiversio, joka vastaanottaa arvon FPDF_RENDERERTYPE_SKIA, ja koontiversio ilman Fontationsia, joka vastaanottaa arvon FPDF_FONTBACKENDTYPE_FONTATIONS. Mukana toimitettu Skia-ajonaikainen on toisessa ryhmässä: se renderöi Skialla mutta käyttää FreeTypeä fonteille. prpSkian pyytäminen yhdessä pfbpFontationsin kanssa sitä vastaan tuotti External exception 80000003 -virheen Delphi-puolella. Kun debuggaaja tai poikkeuskäsittelijä sattuu sieppaamaan sen, tilanne on silti palautumaton:

  • PDFium jää puoli-alustuneeksi
  • Prosessinlaajuinen konfiguraatio on jo sinetöity, joten ConfigurePdfLibrary torjuu korjatun konfiguraation
  • Uudelleenyritys eri konfiguraatiolla samassa prosessissa ei ole enää mahdollista

Kyseessä on Brotli-vikan vastakohta. Siellä kenttä kantoi arvoa, jota kukaan ei valinnut, ja PDFium hyväksyi sen hiljaa. Täällä kenttä kantaa arvoa, jonka kutsuja valitsi tahallaan, eikä PDFium hyväksy siitä mitään keskustelua lainkaan. Molemmat ovat ongelmia, jotka kääreen on ratkaistava ennen natiivia kutsua, koska sen jälkeen ei ole enää mitään, mitä siepata

Miten PDFium Component esitarkistaa Skian ja Fontationsin

Versiosta v3.125.0 alkaen LoadLibrary validoi konfiguraation DLL-vientien sidonnan jälkeen ja ennen funktion FPDF_InitLibraryWithConfig kutsumista, ja muuttaa ei-tuetun renderöijän tai fonttibackendin EPdfErroriksi viestillä, joka nimeää syyllisen asetuksen ja vaihtoehdot. DLL puretaan ja konfiguraatio avataan, joten kutsuja voi poimia muita asetuksia ja ladata uudelleen

Itse päätös asuu puhtaassa funktiossa PdfLibraryConfigurationSupportError, joka ottaa konfiguraation plus kaksi totuusarvoa, jotka kuvaavat koontiversiota, ja palauttaa tyhjän merkkijonon, kun yhdistelmä on turvallinen. Koska se ei kosketa natiivia tilaa, voit kutsua sitä omista testeistäsi millä tahansa ominaisuusyhdistelmällä. Funktion LoadLibrary sisällä kaksi totuusarvoa tulevat erilaisista todisteista, ja ne ansaitsevat eri luottamustasot:

  • Skia havaitaan FPDF_RenderPageSkia-viennin läsnäolosta, sama signaali, jota PdfNativeRendererType käyttää. Vienti ja Skia-renderöijä käännetään yhden ehdon alla, joten tarkistus on tarkka
  • Fontationsilla ei ole omaa vientiä. Ainoa jälki, jonka se jättää, on Rustin fontticrate, jotka se vetää binääriin, joten PDFium Component skannaa ladatun kirjastotiedoston cratenimien skrifa ja read-fonts (myös read_fonts) varalta. Skannaus ajaa vain, kun pfbpFontations pyydetään, ja tiedosto, jota ei voi lukea, lasketaan muodossa "ei Fontationsia"

Fontations-tarkistus on heuristiikka, ja se voi olla väärin yhteen suuntaan: Fontations-koontiversio, josta jokainen kyseinen merkkijono on karsittu, torjuttaisiin, vaikka se olisi voinut toimia. Kyseinen kompromissi tehtiin tahallaan. Väärä torjunta maksaa sinulle poikkeuksen, jonka voit siepata, ja paluun FreeTypeen. Väärä hyväksyntä maksaa sinulle prosessin

Sinetin avaaminen merkitsee yhtä paljon kuin tarkistus. LoadLibrary sinetöi konfiguraation latauksen ihan alussa, joten ilman nollausta ominaisuustorjunta jättäisi kohteen ConfigurePdfLibrary vastaamaan jokaista uudelleenyritystä muodossa EPdfError "PDFium library configuration is already sealed". Torjuntapolku kutsuu funktiota UnloadLibrary ensin; sen FPDF_DestroyLibrary-kutsu on turvallinen tuolla hetkellä, koska PDFiumia ei ole vielä alustettu ja se palautuu välittömästi. Muut latausviat, kuten puuttuva DLL tai arkkitehtuurierot, pitävät sinetin, joten uudelleenyrityssilmukan on erotettava kaksi toisistaan:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // yksikkötarkennettu: Windows.LoadLibrarylla on sama nimi
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Ominaisuustorjunta purkaa DLL:n ja avaa konfiguraation sinetin.
      // DLL, joka ei ladannut lainkaan, pysyy sinetoituna: uudelleenyritys ei auta
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Huomaa eksplisiittinen PDFium.LoadLibrary. Yksikössä, joka käyttää myös kohteita Windows tai Winapi.Windows, tarkentamaton LoadLibrary ratkaisee siihen yksikköön, joka ilmestyy viimeisenä uses-lauseessa; kun se on Win32-funktio, parametriton kutsu ei käänny argumenttimäärävirheellä, joka ei sano mitään PDFiumista

PDFium Componentin LoadLibrary-esitarkistusvirtaus, jossa ConfigurePdfLibrary sinetöi konfiguraation, ominaisuustarkistus testaa FPDF_RenderPageSkia-viennin ja skrifa-merkkijonotodisteet, ei-tuettu pyyntö nostaa siepattavan EPdfErrorin ja avaa sinetin uudelleenyritystä varten, kun taas DLL, joka ei koskaan lataudu, pitää PdfLibraryConfigurationSealedin totena
Validointi ajaa vientien sidonnan jälkeen ja ennen alustusta, joten puuttuva backend epäonnistuu siepattavana EPdfErrorina natiivin CHECKin sijaan, joka tappaa prosessin

Validointi, joka tapahtuu vielä aiemmin

ConfigurePdfLibrary torjuu jotkin yhdistelmät ennen kuin mikään DLL on kuvassa, kaikki muodossa EPdfError. Eksplisiittinen FontBackend, mukaan lukien pfbpFreeType, vaatii Rendererin = prpSkian, koska PDFium kysyy fonttibackendia vain Skia-renderöijälle. IsolatePerDocument vaatii kohteen V8Isolate olevan nil, sillä PDFium luo oman isolenssinsä per asiakirja ja kaataa natiivin CHECKin, jos myös sinä ojennat sille sellaisen. Tyhjät merkkijonot kohteessa UserFontPaths torjutaan. Ja mikä tahansa kutsu ensimmäisen latausyrityksen jälkeen epäonnistuu muodossa "PDFium library configuration is already sealed"

Kyseisellä viimeisellä säännöllä on käytännön seuraus: et voi tutkia DLL:ää ensin ja konfiguroida sitä jälkeenpäin. GetSkiaRenderCapabilities, V8FeaturesAvailable, asiakirjan avaaminen ja useimmat muut sisääntulopisteet kutsuvat funktiota LoadLibrary sisäisesti, mikä sinetöi konfiguraation paikallaan. Funktion UnloadLibrary kutsuminen myöhemmin ei avaa sitä uudelleen. Konfiguroi ensin, sitten lataa, sitten kysy kysymyksiä, mikä on täsmälleen se järjestys, jota diagnostiikkarutiinin pitää noudattaa:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // kopio, turvallinen tarkastella
  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;
  // Sama ratkaisu, jonka LoadLibrary sovelsi rakentaessaan FPDF_LIBRARY_CONFIGin
  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;

Kyseisen rivin lokittaminen kerran käynnistyksessä on halpaa, ja se on ensimmäinen asia, jonka haluat tukipyyntöön, joka sanoo "teksti näyttää erilaiselta palvelimella". PDFium.Loaded on tarkennettu samasta syystä kuin LoadLibrary: lomakkeen tai komponentin metodin sisällä paljas Loaded sidotaan kohteeseen TComponent.Loaded

Kaksi tapaa, joilla versioitu C-konfiguraatiorakenne menee pieleen

Jokainen versioitu konfiguraatiorakenne, olkoon se FPDF_LIBRARY_CONFIG, Win32-cbSize-tietue tai pluginin ABI, epäonnistuu kahdella symmetrisellä tavalla, ja kääreen on varjeltava molempia vastaan. Ensimmäinen on kentän täyttäminen jättäen version liian matalaksi; toinen on version nostaminen jättäen kenttä nolla-arvoon, jonka kirjasto lukee tahallisena valintana

  1. Kenttä asetettu, versio liian matala. Kirjoita m_BrotliEnabled = 1 version 2 rakenteeseen, eikä PDFium koskaan katso sitä. Kutsu onnistuu ja Brotli-streamit pysyvät dekoodaamattomina. Puolustus on johtaa version kentistä, jotka oikeasti ovat käytössä, mikä on se, mitä LoadLibrary tekee, kovakoodatun sijaan
  2. Versio tarpeeksi korkea, nollakenttä tarkoittaa jotain. Nosta versio arvoon 6, ja jokainen kenttä versioon 6 asti on nyt elossa. FillChar nollaa kohteen m_RendererType muotoon FPDF_RENDERERTYPE_AGG, joka on aito renderöijä, ei "asettamaton". Puolustus on kirjoittaa jokainen valitun version kattama kenttä tahallisella arvolla ja ratkaista "oletus" todellista koontiversiota vasten olettamisen sijaan

Kolmas sääntö seuraa arvoille, jotka voivat kaataa kutsuttavan: validoi ne sitä vasten, mitä binääri osaa tehdä ennen kutsua, käyttäen vahvinta saatavilla olevaa todistetta, ja ole rehellinen koodissa ja dokumentaatiossa, kun kyseinen todiste on heuristiikka. Vietty symboli on todiste. Cratenimi merkkijonotaulukossa on hyvä arvaus

Pikaopas: PDFium Componentin kirjastokonfiguraatio

  • Kutsu funktiota ConfigurePdfLibrary kerran, ennen kuin mikään lataa DLL:n; mikä tahansa ominaisuuskysely tai asiakirjan lataus sinetöi sen
  • Päivitys versioon v3.123.0 tai uudempaan, jos asetat BrotliEnabledin tai IsolatePerDocumentin ja odotat Skia-tuotosta mukana toimitetuilta ajonaikaisilta
  • Jätä Renderer arvoon prpDefault, ellet tarvitse tiettyä rasteroijaa; se ratkeaa nykyään koontiversion oletukseen jokaisella rakenteen versiolla
  • Käytä funktiota PdfNativeRendererType kohteen GetSkiaRenderCapabilities.PageRender kanssa lokittaaksesi, mikä renderöijä on oikeasti aktiivinen
  • Odota EPdfErroria, ei kaatumista, muodolle prpSkia vain AGG -DLL:llä tai muodolle pfbpFontations ei-Fontations-DLL:llä versiolla v3.125.0 tai uudemmalla
  • Ominaisuustorjunnan jälkeen PdfLibraryConfigurationSealed on False ja voit konfiguroida uudelleen; epäonnistuneen DLL-latauksen jälkeen se pysyy Totena
  • Kohtele Fontationsin tunnistusta heuristiikkana ja pidä FreeType-varapolku
  • Kirjoita PDFium.LoadLibrary ja PDFium.Loaded yksikön nimellä välttääksesi Win32- ja TComponent-nimiristiriidat

Jos DLL epäonnistuu ennen kuin konfiguraatiollakaan on merkitystä, aloita artikkelista PDFium DLL -latausvikojen diagnosointi Delphissä, ja siitä, miten komponentti löytää oikean binäärin kullakin alustalla, kerrotaan artikkelissa PDFium-natiivikirjaston lataaminen millä tahansa kohteella. Kun renderöijä on selvillä, artikkeli render-välimuisti ja sujuvan zoomauksen taktiikat kattaa, miten sivujen renderöinti pidetään nopeana katselimessa

PDFium Component käärii PDFium-moottorin Delphille ja C++Builderille tällaisilla konfiguraatiotarkistuksilla, joten natiivi alustus epäonnistuu Pascal-poikkeuksena, jonka voit käsitellä, prosessin lopetuksen sijaan. Tuoteyksityiskohdat ja lataukset ovat PDFium Component for Delphi -tuotesivulla