Teknisk artikkel

HotPDF RenderCacheFolder: en diskbasert sidebuffer i Delphi

HotPDF RenderCacheFolder gjør den minnebaserte rendret-side-cachen til HotPDF Delphi-komponenten om til en vedvarende diskbasert sidebuffer: rendrete sider skrives som PNG-filer under en mappe du velger, og neste gang samme PDF-kilde åpnes, leser RenderLoadedPageToBitmapCached dem tilbake i stedet for å rasterisere igjen. Oppslagsrekkefølgen er minne, så disk, så rendereren

Disk-laget har vært i API-en siden v2.416.0, men frem til v2.770.140 serverte det aldri faktisk en side for et vanlig LoadFromFile- eller LoadFromStream-kall. Fiksen tvang frem et spørsmål enhver vedvarende buffer må svare på: hvordan vet du at filen du åpnet i dag, er dokumentet du rendret i går, og hva skjer med de cachete sidene når det ikke er det? Nedenfor er svarene HotPDF landet på, inkludert der den med vilje nekter å cache

Hvordan virker HotPDF disk render cache?

HotPDF disk render cache er et andre lag bak minnebasert raster-buffer, og det deltar bare når RenderCacheFolder er en ikke-tom sti. Et kall til RenderLoadedPageToBitmapCached(PageIndex, DPI) skanner først minneoppføringene, nøklet etter sideindeks, DPI og en renderinnstillings-variant. Ved et bom spør den disk-laget; et disk-treff dekoder PNG-en, promoterer den tilbake i minnet og returnerer en kalleid kopi. Først når begge lag bommer går siden gjennom content stream-tolkeren beskrevet i å rendere en lastet PDF-side til en TBitmap, og den ferske bitmapen skrives da til disk også

HotPDF-diagram over render cache-oppslaget for RenderLoadedPageToBitmapCached: minnelaget nøklet etter side, DPI og render-variant sjekkes først, så RenderCacheFolder disk-laget av PNG-filer med atomær erstatning, så content stream-tolkeren, og hvert treff returnerer en kalleid kopi
HotPDF ser i minnet først, så på disk, og rasteriserer først da; et disk-treff promotereres tilbake i minnet, og hver sti gir deg en kopi du eier og må frigjøre

På disk er layouten med vilje kjedelig. Hvert dokument får en undermappe navngitt fra en dokumentnøkkel på 16 heks-tegn pluss en render-variant på 16 heks-tegn, hver side lagres som <page>@<dpi>.png, og en index.txt ved roten holder dokumentene i mest-sist-brukt-rekkefølge bak et schema-merke. Et schema-avvik tømmer mappen ved første bruk. Skrivinger går først til en midlertidig fil og byttes på plass med en atomær erstatning, så et krasj midt i en skriving etterlater enten den gamle siden eller ingenting, aldri en halv PNG. En PNG som feiler å dekode, slettes og telles som et bom

Tre grenser avgrenser mappen:

  • RenderCacheMaxDocuments (standard 20) setter tak for antallet dokumentundermapper; den minst nylig brukte mappen kastes først
  • RenderCacheMaxBytes (standard 524288000, som er 500 MB) setter tak for total størrelse av alle PNG-filer under roten
  • Hver dokumentmappe beholder høyst 200 sidebilder; den per-dokument-grensen er fastsatt av THotPDF og er ikke en publisert egenskap

RenderCacheCapacity (standard 8) er en separat knott: den setter hvor mange rendrete sider minnelaget beholder, og den har ingenting med diskavtrykket å gjøre

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Konfigurer disk-laget før den første cachete renderen:
    // mappen og begge grensene leses når laget brukes første gang
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // sider i minnet

    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
          // Gi kopien til thumbnail-stripen her
        finally
          Bmp.Free; // det cachete kallet returnerer alltid en kalleid kopi
        end;
      end;
  finally
    Pdf.Free; // siden v2.770.140 sletter ikke lenger dette disk-oppføringene
  end;
end;

Kjør samme prosedyre to ganger, og andre kjøring rasteriserer aldri en side som fikk plass i bufferen. Disk-bufferobjektet opprettes dovent ved den første cachete renderingen og lever til THotPDF-instansen frigjøres, så å endre RenderCacheFolder, RenderCacheMaxDocuments eller RenderCacheMaxBytes etter det punktet flytter eller endrer ikke størrelsen på en allerede åpen buffer. Sider for store for minne-admisjonspolicyen (som standard kan ikke en enkelt oppføring overstige 64 MiB av 32-bits piksler) vedvares heller ikke, og disk-laget konsulteres bare mens RenderFallbackPolicy beholder sin standard rfpIgnore, for fallback-diagnostikk lagres ikke sammen med PNG-en

Hvorfor virket aldri RenderCacheFolder før v2.770.140?

RenderCacheFolder hadde ingen effekt før v2.770.140 fordi disk-laget nøklet dokumenter på en hash av kildebyter som vanlige lastinger aldri beholdt. Dokumentnøkkelen kom fra en SHA-256 over en intern kopi av de rå PDF-bytene, men LoadFromFile og LoadFromStream parser kilden in place og beholder ikke en slik kopi; feltet ble bare fylt midlertidig på en kryptert-gjenopprettingssti og tømt igjen rett etter. Uten bytes var nøkkelen alltid tom, og en tom nøkkel betyr at disk-laget omgås. Ingen feil, ingen advarsel, bare en mappe som forble tom

Å gjøre nøkkelen ikke-tom avdekket en andre bug som hadde gjemt seg bak den første. Den gamle InvalidateRenderedPageCache slettet dokumentets disk-mappe, og InvalidateRenderedPageCache kjører ved starten av hver lasting, ved hver redigering og inne i Free. Så i det øyeblikket nøkkelen virket, ville hver viewer-økt ha ødelagt sin egen buffer ved avslutning, og neste økt ville ha startet kald uansett. Verre, nøkkelen ble beregnet på nytt fra samme kilde etter en redigering, så renderinger av det redigerte dokumentet ville ha blitt lagret under nøkkelen til originalfilen og servert til neste økt som åpnet den umodifiserte PDF-en. v2.770.140 fikser identiteten og ugjøringen sammen; å fikse bare én av dem ville ha skipet enten en død buffer eller en som løy

Hvordan HotPDF identifiserer en PDF uten å lese hele filen

HotPDF identifiserer en PDF lastet fra en lokal fil med et fingeravtrykk av størrelsen, dens last-write-tid og dens første og siste 64 KiB, og identifiserer en strøm- eller random access-kilde med en SHA-256 av hele innholdet. Begge fanges én gang, når en lasting lykkes, og de første 16 heks-tegnene i SHA-256-digesten (64 bit) blir dokumentnøkkelen

KildeIdentitetKostnadFanget når
LoadFromFileStørrelse + LastWriteTime + ledende og hale 64 KiB, hasjet med SHA-256Høyst 128 KiB lest, uavhengig av filstørrelseHver vellykket lasting, selv om RenderCacheFolder settes senere
LoadFromStreamSHA-256 av hele strømmenEtt fullt pass over kildenBare hvis RenderCacheFolder var satt før lasting
LoadFromRandomAccessSourceSHA-256 av hele kildenEtt fullt pass over kildenBare hvis mappen ble satt først og hele området er tilgjengelig
Enhver kilde med en /Encrypt-oppføringIngenIngenAldri; disk-laget omgås
HotPDF kildeidentitetskart for disk render cache: LoadFromFile hasher størrelse, LastWriteTime og første og siste 64 KiB, LoadFromStream og LoadFromRandomAccessSource hasher hele innholdet bare når RenderCacheFolder ble satt først, og enhver /Encrypt-trailer fanger ingen identitet i det hele tatt
Filer fingeravtrykkes fra endene sine fordi header, xref og trailer bor der, strømmer betaler bare for en full hash når du ba om bufferen først, og krypterte dokumenter skrives aldri til disk

Filfingeravtrykket er en bevisst avveining. Å hasje et 400 MB skannet arkiv i sin helhet ved hver åpning kan koste mer enn å rendere de to sidene en bruker faktisk ser på. De samplede regionene er ikke vilkårlige: headeren ligger ved filens start, og traileren og siste kryssreferanseseksjon ligger ved slutten (ISO 32000-1 §7.5). En inkrementell oppdatering legger til en ny kropp, kryssreferanseseksjon og trailer (§7.5.6), så den endrer størrelsen og halen på én gang. En full omskriving av ethvert normalt verktøy endrer last-write-tiden. For filer opptil 128 KiB dekker de to samplene hver byte, så små dokumenter hasjes i praksis i sin helhet

Restrisikoen er en like stor, in place-endring midt i en stor fil hvis skriver så gjenoppretter originaltidsstempelet. Det krever et verktøy som bevisst bevarer modifikasjonstider mens det redigerer innhold, noe som er sjeldent men ikke umulig, og i så fall serverer bufferen foreldede sider. Den andre siden er godartet: å kopiere en fil på Windows bevarer normalt last-write-tiden, så en kopi av et dokument allerede i bufferen treffer de samme oppføringene, noe som er korrekt fordi bytene er identiske

Strømmer har ingen modifikasjonstid i det hele tatt, så den eneste ærlige identiteten er innholdet. HotPDF betaler bare for det fulle SHA-256-passet når du har bedt om en disk-buffer før lasting; enhver annen kaller av LoadFromStream ser ingen ekstra kostnad. Det gjør egenskapstilordningsrekkefølgen bærende:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Feil rekkefølge for strømmer: innholdshasjen beregnes bare når
  // mappen allerede er satt, så dette dokumentet ville omgått disk-laget
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

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

En random access-kilde som fortsatt laster ned (noen områder ikke tilgjengelige ennå) får ingen identitet i stedet for en hash av delvis innhold, og hvis beregning av identiteten feiler av hvilken som helst grunn, lykkes lasting likevel; dokumentet rendrer bare uten disk-laget

Hva ugjør en HotPDF disk cache-oppføring?

En HotPDF disk cache-oppføring ugjøres aldri ved å slettes ved redigering; i stedet slipper redigering av det lastede dokumentet dokumentidentiteten, så disk-laget omgås for resten av den lastingen, og de lagrede sidene forblir gyldige for den umodifiserte kilden. Oppføringer forlater disken bare gjennom LRU- og byte-grensene, en korrupt PNG, eller et schema-endring

Nøkkelen beskriver en kilde på disk, ikke objektgrafen i minnet. Så snart du stampler en side eller endrer en annotering, matcher dokumentet ikke lenger den kilden, så verken lesing eller skriving under dens nøkkel ville vært korrekt. Siden v2.770.140 tømmer både dokumentnivå- og sidenivå-ugjøring identiteten i stedet for å røre mappen, og det finnes en andre vakt for redigeringer som ikke kalte InvalidateRenderedPageCache: før bruk av disk-laget sjekker THotPDF om noe lastet objekt er skittent og behandler et skittent dokument som uten identitet

Renderinnstillinger virker motsatt vei. Å bytte PageRenderBackend (eller kalle UseNativeGDIRenderBackend), og å kalle ConfigureRenderICCWorkflow eller ClearRenderICCWorkflow, tømmer minnesidene men beholder identiteten, for dokumentet matcher fortsatt kilden sin. De innstillingene endrer pikslene uten å være del av minnevarianten, så disknøkkelen foldes inn med backend-navnet, black-point compensation-flagget og SHA-256-digester av ICC-proof- og output-profilene. Varianten selv dekker allerede fargeintensjonen, output-dithering, overprint-preview, luminosity mask-modus, fallback-policy og synligheten til hver valgfri innholdsgruppe, så å slå et lag av og på rendrer inn i en annen mappe i stedet for å overskrive standardvisningen

HotPDF ugjøringssemantikk for RenderCacheFolder disk cache: å redigere det lastede dokumentet eller et skittent objekt slipper kildeidentiteten slik at laget omgås, å endre render-backend eller ICC-arbeidsflyt beholder identiteten under en ny variantnøkkel, og å lagre pluss laste på nytt re-nøkler dokumentet
En redigering sletter aldri den lagrede mappen, en innstillingsendring rendrer under en annen nøkkel, og bare lagring pluss pånyttlasting tjener det redigerte dokumentet en fersk identitet

For å få et redigert dokument tilbake på disk-laget, gi det en ny kildeidentitet ved å lagre det og laste resultatet:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Etter redigering av det lastede dokumentet: oppfrisk minnesidene.
  // Kildeidentiteten er allerede borte, så ingenting leses fra eller
  // skrives til originaldokumentets disk-mappe
  Pdf.InvalidateRenderedPageCache;

  // En lagret fil har ny størrelse og last-write-tid, og dermed en ny
  // identitet; renderinger etter denne lasting caches under den nye nøkkelen
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Originaldokumentets mappe blir latt i fred og eldes ut gjennom RenderCacheMaxDocuments og RenderCacheMaxBytes som enhver annen oppføring. Åpner brukeren den uredigerte originalen på nytt, er sidene der fortsatt

Sikkerhetsgrenser: krypterte kilder og koblede mapper

HotPDF disk render cache nekter to typer input med vilje: den skriver aldri sider av en kryptert PDF til disk, og den følger aldri en dokumentundermappe som er en junction eller et annet reparse-punkt. Begge regler bytter cache-treff mot ikke å lekke data eller slette feil filer

Krypterte PDF-er caches aldri på disk

En rendret side er dekryptert innhold. Å skrive den som en vanlig PNG inn i en cache-mappe ville etterlate en lesbar kopi av et passordbeskyttet dokument på disk, utenfor beskyttelsen forfatteren valgte (ISO 32000-1 §7.6). HotPDF fanger derfor ingen identitet for noen kilde hvis trailer bærer en /Encrypt-oppføring, inkludert filer åpnet med et passord eller med et tomt brukerpassord. De dokumentene bruker fortsatt minnelaget, som dør med prosessen

Junction-undermapper avvises siden v2.770.173

Bufferroten er ditt valg, og å peke den mot en junction er tillatt. Dokumentundermappene under den er en annen sak: bufferen oppretter, leser, berører og sletter dem på egen hånd, under oppstarts-gjenoppretting (som fjerner igjenblitt midlertidige filer), oppslag (som oppdaterer tidsstempler), lagring, ugjøring og de tre eviction-grensene. Ersetter noen med skrivetilgang til bufferroten en dokumentmappe med en junction til en annen katalog, ville hver av de stiene fulgt den, og eviction ville slettet filer et sted bufferen aldri eide. Siden v2.770.173 sjekker hver av de inngangspunktene reparse-punkt-attributten og hopper over en koblet dokumentmappe: et oppslag teller et bom, en lagring teller en skrivefeil, og eviction lar den være i fred

Unicode-stier og delte røtter

To relaterte fikser betyr noe hvis du deployerer til brukerprofiler. Før v2.770.135 var RenderCacheFolder en AnsiString, så en mappe utenfor systemets kodepage (et kinesisk brukernavn på en engelsk Windows-installasjon, for eksempel) ble konvertert med tap før bufferen så den; egenskapen er nå en Unicode string, og den atomære erstatningen bruker det vide Windows API. Siden v2.770.52 deler flere THotPDF-instanser i én prosess som peker på samme rot (etter stiutvidelse, sammenlignet case-insensitivt) en enkelt referansetalt indeks og lås. Tidligere overskrev hver instans index.txt med sin egen kopi og håndhevet grensene mot sitt delvise syn, så mappen kunne vokse flere ganger forbi budsjettet sitt

Den delingen stopper ved prosessgrensen. To separate prosesser på samme rot holder fortsatt separate minneindekser, så gi hver samtidig kjørende applikasjon sin egen bufferrot. Viewere som rendrer på worker-tråder har det fint innenfor én prosess: PrefetchLoadedPages og køen dekket i bakgrunnsrendering med en request-kø går begge gjennom samme cachete sti og samme lås

Hurtigreferanse: RenderCacheFolder-sjekkliste

  • Sett RenderCacheFolder, RenderCacheMaxDocuments og RenderCacheMaxBytes før det første kallet til RenderLoadedPageToBitmapCached; for strøm- og random access-lastinger, sett mappen før lasting
  • Oppgrader til v2.770.140 eller senere hvis du er avhengig av disk-laget; tidligere versjoner godtar egenskapen men serverer aldri en side fra disk for vanlige lastinger
  • Forvent ingen disk-caching for krypterte PDF-er, for dokumenter redigert etter lasting, eller mens RenderFallbackPolicy ikke er rfpIgnore
  • Frigjør THotPDF-instansen normalt; siden v2.770.140 sletter verken Free eller InvalidateRenderedPageCache disk-oppføringer
  • Å endre PageRenderBackend eller ICC-arbeidsflyten beholder dokumentet på disk-laget under en annen nøkkel
  • Bruk én bufferrot per kjørende applikasjon; instanser innenfor én prosess deler indeksen siden v2.770.52
  • Hold bufferroten på et per-bruker-sted; dokumentundermapper som er junctions hoppes over siden v2.770.173

En vedvarende sidebuffer betaler seg best i en viewer som åpner de samme dokumentene på nytt hele dagen, noe som er nøyaktig formen på arkitekturen for custom PDF viewer i Delphi beskrevet andre steder på denne bloggen. RenderCacheFolder, minnebasert raster-buffer og siderendereren følger med HotPDF Delphi PDF-komponenten for Delphi og C++Builder