Techninis straipsnis

HotPDF RenderCacheFolder: diskinis puslapių podėlis Delphi

HotPDF RenderCacheFolder atmintinį HotPDF Delphi komponento atvaizduotų puslapių podėlį paverčia patvariu diskiniu puslapių podėliu: atvaizduoti puslapiai rašomi PNG failais jūsų pasirinktame aplanke, ir kitą kartą atsidarius tą patį PDF šaltinį RenderLoadedPageToBitmapCached juos perskaito vietoj pakartotinio rastravimo. Paieškos tvarka: atmintis, tada diskas, tada atvaizduoklis

Diskinė pakopa API yra nuo v2.416.0, bet iki v2.770.140 ji niekada iš tikrųjų neatidavė puslapio įprastam LoadFromFile ar LoadFromStream kvietimui. Taisymas privertė atsakyti į klausimą, į kurį privalo atsakyti kiekvienas patvarus podėlis: iš ko žinote, kad šiandien atidarytas failas yra vakar atvaizduotas dokumentas, ir kas nutinka podėliuotiems puslapiams, kai jis toks nėra? Žemiau – atsakymai, prie kurių priėjo HotPDF, įskaitant vietas, kur jis tyčia atsisako podėliuoti

Kaip veikia HotPDF diskinis atvaizdavimo podėlis?

HotPDF diskinis atvaizdavimo podėlis – antroji pakopa už atmintinį rastrinį podėlį, ir ji dalyvauja tik tada, kai RenderCacheFolder yra netuščias kelias. Kvietimas RenderLoadedPageToBitmapCached(PageIndex, DPI) pirmiausia peržiūri atminties įrašus, raktus vedančius pagal puslapio indeksą, DPI ir atvaizdavimo nustatymų variantą. Nepataikius klausiama diskinės pakopos; diskas pataikęs iškoduoją PNG, grąžina jį į atmintį ir atiduoda kvietėjui priklausančią kopiją. Tik kai abi pakopos nepataiko, puslapis keliauja pro turinio srauto interpretatorių, aprašytą įkelto PDF puslapio atvaizdavime į TBitmap, o šviežias bitmapas tada irgi rašomas į diską

HotPDF atvaizdavimo podėlio paieškos RenderLoadedPageToBitmapCached diagrama: pirmiausia tikrinama atminties pakopa, rakta pagal puslapį, DPI ir atvaizdavimo variantą, paskui RenderCacheFolder diskinių PNG failų pakopa su atominiu pakeitimu, dar vėliau – turinio srauto interpretatorius, o kiekvienas pataikymas grąžina kvietėjui priklausančią kopiją
HotPDF pirmiausia žiūri atmintyje, paskui diske ir tik tada rastruoja; diskas pataikęs grįžta į atmintį, o kiekvienas kelias atiduoda jums priklausančią kopiją, kurią reikia atlaisvinti

Disko išdėstymas sąmoningai nuobodus. Kiekvienas dokumentas gauna poaplankį, pavadintą iš 16 šešioliktainių simbolių dokumento rakto plius 16 šešioliktainių simbolių atvaizdavimo varianto, kiekvienas puslapis saugomas kaip <page>@<dpi>.png, o index.txt šaknyje laiko dokumentus naujausiai naudotų tvarka už schemos žymos. Schemos nesutapimas pirmu naudojimu išvalo aplanką. Rašymai pirmiausia keliauja į laikinąjį failą ir į vietą perkeliami atominiu pakeitimu, tad avarija rašymo viduryje palieka arba seną puslapį, arba nieko – niekada ne pusę PNG. PNG, kurio nepavyksta iškoduoti, ištrinamas ir skaitomas nepataikymu

Trys ribos apriboja aplanką:

  • RenderCacheMaxDocuments (numatytoji 20) riboja dokumento poaplankių skaičių; seniausiai naudotas aplankas iškeliaujamas pirmas
  • RenderCacheMaxBytes (numatytoji 524288000, tai 500 MB) riboja bendrą visų PNG failų dydį po šaknimi
  • Kiekvienas dokumento aplankas laiko daugiausiai 200 puslapių atvaizdų; ta riba dokumentui fiksuota THotPDF ir nėra skelbiama savybė

RenderCacheCapacity (numatytoji 8) – atskira rankenėlė: ji nustato, kiek atvaizduotų puslapių laiko atminties pakopa, ir su disku užimta vieta niekaip nesusijusi

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Sukonfigūruokite diskinę pakopą prieš pirmąjį podėliuotą atvaizdavimą:
    // aplankas ir abi ribos skaitomos, kai pakopa pirmą kartą naudojama
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // puslapiai atmintyje

    if Pdf.LoadFromFile(FileName) > 0 then
      for I := 0 to Pdf.LoadedPageCount - 1 do
      begin
        Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 96);
        if Bmp <> nil then
        try
          // Čia atiduokite kopiją miniatiūrų juostai
        finally
          Bmp.Free; // podėliuotas kvietimas visada grąžina kvietėjui priklausančią kopiją
        end;
      end;
  finally
    Pdf.Free; // nuo v2.770.140 tai daugiau netrina diskinių įrašų
  end;
end;

Paleiskite tą pačią procedūrą dukart, ir antras paleidimas nė karto nerastruos puslapio, tilpusio į podėlį. Diskinis podėlio objektas kuriamas tingiai pirmojo podėliuoto atvaizdavimo metu ir gyvuoja, kol atlaisvinama THotPDF instancija, tad RenderCacheFolder, RenderCacheMaxDocuments ar RenderCacheMaxBytes keitimas po to negali perkelti ar pakeisti dydžio jau atidarytam podėliui. Puslapiai, per dideli atminties priėmimo politikai (pagal nutylėjimą vienas įrašas negali viršyti 64 MiB 32 bitų pikselių), taip pat neišsaugomi, o diskinė pakopa klausiama tik kol RenderFallbackPolicy laiko savąją numatytąją rfpIgnore, nes atsarginės diagnostikos šalia PNG nesaugoma

Kodėl RenderCacheFolder iki v2.770.140 niekada neveikė?

RenderCacheFolder iki v2.770.140 jokio poveikio neturėjo, nes diskinė pakopa dokumentus raktdavo ženklu iš šaltinio baitų, kurių įprasti įkėlimai niekada nesaugodavo. Dokumento raktas eidavo iš SHA-256 ant vidinės žalių PDF baitų kopijos, bet LoadFromFile ir LoadFromStream šaltinį analizuoja vietoje ir tokios kopijos nesilaiko; laukas buvo užpildomas tik laikinai šifruoto atkūrimo kelyje ir tuoj pat vėl išvalomas. Be baitų raktas visada būdavo tuščias, o tuščias raktas reiškia, kad diskinė pakopa praleidžiama. Jokios klaidos, jokio įspėjimo – tik aplankas, likęs tuščias

Rakto padarymas netuščiu atvėrė antrą klaidą, slėpusią už pirmosios. Senasis InvalidateRenderedPageCache ištrindavo dokumento diskinį aplanką, o InvalidateRenderedPageCache paleidžiamas kiekvieno įkėlimo pradžioje, kiekvieno redagavimo metu ir Free viduje. Taigi vos raktas suveiktų, kiekviena peržiūros sesija išeidama sunaikintų savą podėlį, o kita sesija vis tiek pradėtų šalta. Blogiau – raktas po redagavimo būtų perskaičiuotas iš to paties šaltinio, tad redaguoto dokumento atvaizdavimai būtų saugomi po originalaus failo raktu ir atiduodami kitai sesijai, atsidariusiai nepakeistą PDF. v2.770.140 tapatumą ir nebegaliojimą taiso kartu; vieno jų taisymas būtų išsiuntęs arba negyvą podėlį, arba melagį

Kaip HotPDF atpažįsta PDF neskaitęs viso failo

HotPDF iš vietinio failo įkeltą PDF atpažįsta pagal jo dydžio, last-write laiko ir pirmųjų bei paskutinių 64 KiB piršto atspaudą, o srautą ar atsitiktinės prieigos šaltinį – pagal viso turinio SHA-256. Abu fiksujami kartą, kai įkėlimas pavyksta, ir pirmieji 16 šešioliktainių SHA-256 santraukos simbolių (64 bitai) tampa dokumento raktu

ŠaltinisTapatumasKainaKada fiksujama
LoadFromFileDydis + LastWriteTime + pirmieji ir paskutiniai 64 KiB, hashuojama SHA-256Perskaitoma daugiausiai 128 KiB, nepriklausomai nuo failo dydžioKiekvienas sėkmingas įkėlimas, net jei RenderCacheFolder nustatoma vėliau
LoadFromStreamViso srauto SHA-256Vienas pilnas ėjimas per šaltinįTik jei RenderCacheFolder buvo nustatyta prieš įkėlimą
LoadFromRandomAccessSourceViso šaltinio SHA-256Vienas pilnas ėjimas per šaltinįTik jei aplankas nustatytas pirmiausia ir visas intervalas prieinamas
Bet koks šaltinis su /Encrypt įrašuNėraNėraNiekada; diskinė pakopa praleidžiama
HotPDF šaltinio tapatumo diskiniam atvaizdavimo podėliui žemėlapis: LoadFromFile hashuoja dydį, LastWriteTime ir pirmuosius bei paskutinius 64 KiB, LoadFromStream ir LoadFromRandomAccessSource hashuoja visą turinį tik tada, kai RenderCacheFolder buvo nustatyta pirmiausia, o bet kokio /Encrypt užrakinto dokumento tapatumas nefiksuojamas visai
failai atspaudžiuojami nuo jų galų, nes ten gyvena antraštė, xref ir traileris, srautai už pilną hashą moka tik tada, kai podėlio paprašėte pirmiausia, o šifruoti dokumentai niekada nerašomi į diską

Failo atspaudas – sąmoningas kompromisas. 400 MB skenuoto archyvo pilnas hashavimas kiekvieno atsidarymo metu gali kainuoti daugiau nei dviejų puslapių, kuriuos naudotojas iš tiesų žiūri, atvaizdavimas. Imami gabalai ne atsitiktiniai: antraštė slypi failo pradžioje, o traileris ir paskutinė kryžminių nuorodų sekcija – gale (ISO 32000-1 §7.5). Inkrementinis atnaujinimas prideda naują pagrindą, kryžminių nuorodų sekciją ir trailerį (§7.5.6), tad vienu metu keičia ir dydį, ir uodegą. Pilnas perrašymas bet kokiu normaliu įrankiu keičia last-write laiką. Failams iki 128 KiB abu gabalai dengia kiekvieną baitą, tad maži dokumentai efektyviai hashuojami pilni

Likutinė rizika – to paties dydžio, vietoje atliktas didelio failo vidurio pakeitimas, po kurio rašytojas atstatė originalią laiko žymą. Tam reikia įrankio, kuris redaguodamas turinį sąmoningai išlaiko keitimo laikus – retai, bet ne neįmanoma, ir tada podėlis atiduoda senus puslapius. Kita pusė rami: failo kopijavimas Windows paprastai išlaiko last-write laiką, tad dokumento, jau esančio podėlyje, kopija pataiko į tuos pačius įrašus, kas teisinga, nes baitai identiški

Srautai iš viso neturi keitimo laiko, tad vienintelis sąžiningas tapatumas – turinys. HotPDF už tą pilną SHA-256 ėjimą moka tik tada, kai diskinio podėlio paprašėte iki įkėlimo; kitiems visiems LoadFromStream kvietėjams papildomos kainos nėra. Todėl savybių priskyrimo tvarka tampa lemiamu veiksniu:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Netinkama tvarka srautams: turinio hash skaičiuojamas tik tada, kai
  // aplankas jau nustatytas, tad šis dokumentas praleistų diskinę pakopą
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

  Pdf.RenderCacheFolder := CacheRoot; // nustatykite pirmiausia
  Data.Position := 0;
  if Pdf.LoadFromStream(Data) <= 0 then
    raise Exception.Create('The stream is not a loadable PDF');
end;

Atsitiktinės prieigos šaltinis, kuris vis tebesisiunčia (kai kurių intervalų dar nėra), tapatumo negauna – vietoj dalinio turinio hasho – o jei tapatumo apskaičiavimas dėl kokių priežasčių nepavyksta, įkėlimas vis tiek pavyksta; dokumentas tiesiog atvaizduojamas be diskinės pakopos

Kas panaikina HotPDF diskinio podėlio įrašo galiojimą?

HotPDF diskinio podėlio įrašas niekada nepanaikinamas ištrinant jį redagavimo metu; vietoj to įkelto dokumento redagavimas numeta dokumento tapatumą, tad diskinė pakopa likusiai to įkėlimo daliai praleidžiama, o saugomi puslapiai lieka teisėti nepakeistam šaltiniui. Iš disko įrašai keliauja tik pro LRU ir baitų ribas, sugadintą PNG arba schemos pakeitimą

Raktas aprašo šaltinį diske, o ne objektų grafą atmintyje. Kai tik užspaudžiate puslapį arba keičiate anotaciją, dokumentas daugiau neatitinka to šaltinio, tad nei skaitymas, nei rašymas po jo raktu nebūtų teisingas. Nuo v2.770.140 ir dokumento lygio, ir puslapio lygio nebegaliojimas išvalo tapatumą vietoj aplanko liečimo, o yra ir antra apsauga redagavimams, nekvietusiems InvalidateRenderedPageCache: prieš naudodama diskinę pakopą THotPDF patikrina, ar kuris įkeltas objektas pažymėtas dirty, ir tokį dokumentą traktuoja kaip neturintį tapatumo

Atvaizdavimo nustatymai veikia atvirkščiai. PageRenderBackend perjungimas (arba UseNativeGDIRenderBackend kvietimas), taip pat ConfigureRenderICCWorkflow arba ClearRenderICCWorkflow kvietimai atminties puslapius nuplauna, bet tapatumą palieka, nes dokumentas vis dar atitinka savąjį šaltinį. Tie nustatymai keičia pikselius, nedalyvaudami atminties variante, tad diskinis raktas įsipina backend pavadinimą, juodojo taško kompensacijos vėliavą ir ICC proof bei išvesties profilių SHA-256 santraukas. Pats variantas jau dengia spalvų ketinimą, išvesties ditheringą, overprint peržiūrą, šviesos kaukės režimą, atsarginę politiką ir kiekvienos pasirinktinio turinio grupės matomumą, tad sluoksnio įjungimas atvaizduoja į kitą aplanką, o neperrašo numatytojo vaizdo

HotPDF nebegaliojimo semantika RenderCacheFolder diskiniam podėliui: įkelto dokumento ar bet kurio dirty objekto redagavimas numeta šaltinio tapatumą, tad pakopa praleidžiama; atvaizdavimo backend arba ICC darbo eigos keitimas palieka tapatumą po nauju varianto raktu; o išsaugojimas su pakartotiniu įkėlimu dokumentui duoda naują raktą
redagavimas niekada netrina saugomo aplanko, nustatymų keitimas atvaizduoja po kitokiu raktu, o tik išsaugojimas su pakartotiniu įkėlimu užpelna redaguotam dokumentui šviežią tapatumą

Kad redaguotas dokumentas grįžtų ant diskinės pakopos, duokite jam naują šaltinio tapatumą – išsaugokite ir įkelkite rezultatą:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Po įkelto dokumento redagavimo: atnaujinkite atminties puslapius.
  // Šaltinio tapatumas jau dingo, tad iš originalaus dokumento diskinio
  // aplanko niekas neskaityta ir neįrašyta
  Pdf.InvalidateRenderedPageCache;

  // Išsaugotas failas turi naują dydį ir last-write laiką, vadinasi naują
  // tapatumą; atvaizdavimai po šio įkėlimo podėliuojami nauju raktu
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Originalaus dokumento aplankas lieka ramybėje ir sensta pro RenderCacheMaxDocuments bei RenderCacheMaxBytes kaip ir bet kuris kitas įrašas. Jeigu naudotojas vėl atidaro neredaguotą originalą, jo puslapiai tebėra ten

Saugumo ribos: šifruoti šaltiniai ir susieti aplankai

HotPDF diskinis atvaizdavimo podėlis tyčia atsisako dviejų rūšių įvedimo: niekada nerašo šifruoto PDF puslapių į diską ir niekada neseka dokumento poaplankio, kuris yra jungtis (junction) ar kitas reparse taškas. Abi taisyklės podėlio pataikymus maino į duomenų nesunutekėjimą ir netinkamų failų netrinimą

Šifruoti PDF niekada nepodėliuojami diske

Atvaizduotas puslapis – iššifruotas turinys. Jo užrašymas kaip paprasto PNG į podėlio aplanką paliktų skaitomą slaptažodžiu apsaugoto dokumento kopiją diske, už autoriaus pasirinktos apsaugos ribų (ISO 32000-1 §7.6). Todėl HotPDF jokiam šaltiniui, kurio traileris neša /Encrypt įrašą – įskaitant failus, atidarytus su slaptažodžiu arba su tuščiu naudotojo slaptažodžiu – tapatumo nefiksuoja. Tie dokumentai vis dar naudoja atminties pakopą, kuri žūsta su procesu

Jungčių poaplankiai atmetami nuo v2.770.173

Podėlio šaknis – jūsų pasirinkimas, ir nukreipti ją į jungtį leidžiama. Dokumentų poaplankiai po ja – kitas reikalas: podėlis juos pats kuria, skaito, liečia ir trina – paleidimo atkūrimo metu (kuriuo pašalinami likę laikinieji failai), paieškos metu (kuri atnaujina laiko žymas), saugojimo, nebegaliojimo ir trijų iškėlimo ribų metu. Jeigu kas nors, turintis rašymo prieigą prie podėlio šaknies, dokumento aplanką pakeičia jungtimi į kitą katalogą, visi tie keliai ją sektų, o iškėlimas trintų failus ten, kur podėlis niekada nesavo. Nuo v2.770.173 kiekvienas tų įėjimo taškų tikrina reparse taško atributą ir praleidžia susietą dokumento aplanką: paieška skaičiuoja nepataikymą, saugojimas – rašymo nesėkmę, o iškėlimas jo neliečia

Unicode keliai ir bendros šaknys

Dvi susijusios pataisos svarbios, jei diegiate į naudotojų profilius. Iki v2.770.135 RenderCacheFolder buvo AnsiString, tad aplankas už sistemos koduotės ribų (pvz., kinų naudotojo vardas angliškoje Windows instaliacijoje) būdavo konvertuojamas su nuostoliais, kol podėlis jį pamatydavo; savybė dabar – Unicode string, o atominis pakeitimas naudoja plačiąją Windows API. Nuo v2.770.52 kelios THotPDF instancijos viename procese, rodančios į tą pačią šaknį (po kelio išplėtimo, lyginant neatsižvelgiant į raidžių registrą), dalijasi viena skaičiuojamų nuorodų rodykle ir rakta. Anksčiau kiekviena instancija index.txt perrašydavo savą kopija ir ribas taikydavo savai dalinei peržiūrai, tad aplankas galėdavo išaugti keletą kartų virš savo biudžeto

Tas dalijimasis sustoja ties proceso riba. Du atskiri procesai toje pačioje šaknyje vis dar laiko atskirus atminties indeksus, tad kiekvienai lygiagrečiai besileidžiančiai programai duokite savą podėlio šaknį. Peržiūros, atvaizduojančios darbo gijose, viename procese jaučiasi gerai: PrefetchLoadedPages ir eilė, aprašyta foniniame atvaizdavime su užklausų eile, abu eina ta pačia podėliuota kelia ir ta pačia rakta

Trumpa atmintinė: RenderCacheFolder patikros sąrašas

  • Nustatykite RenderCacheFolder, RenderCacheMaxDocuments ir RenderCacheMaxBytes prieš pirmąjį RenderLoadedPageToBitmapCached kvietimą; srauto ir atsitiktinės prieigos įkėlimams aplanką nustatykite prieš įkeliant
  • Atnaujinkite iki v2.770.140 ar vėlesnės, jeigu remiatės diskine pakopa; ankstesnės versijos savybę priima, bet įprastiems įkėlimams puslapio iš disko niekada neatiduoda
  • Nesitikėkite diskinio podėliavimo šifruotiems PDF, dokumentams, redaguotiems po įkėlimo, arba kol RenderFallbackPolicy nėra rfpIgnore
  • THotPDF instanciją atlaisvinkite įprastai; nuo v2.770.140 nei Free, nei InvalidateRenderedPageCache diskinių įrašų netrina
  • PageRenderBackend arba ICC darbo eigos keitimas dokumentą diskinėje pakopoje palieka po kitokiu raktu
  • Naudokite po vieną podėlio šaknį kiekvienai besileidžiančiai programai; instancijos viename procese nuo v2.770.52 dalijasi rodykle
  • Podėlio šaknį laikykite kiekvienam naudotojui skirtoje vietoje; dokumentų poaplankiai, kurie yra jungtys, nuo v2.770.173 praleidžiami

Patvarus puslapių podėlis atsipirčiausiai ten, kur peržiūra visą dieną vėl atidarinėja tuos pačius dokumentus – būtent tokios formos yra pasirinktinė PDF peržiūros architektūra Delphi, aprašyta kitur šiame tinklaraštyje. RenderCacheFolder, atmintinis rastrinis podėlis ir puslapių atvaizduoklis keliauja su HotPDF Delphi PDF komponentu, skirtu Delphi ir C++Builder