Teknisk artikkel

Linearisert PDF-utdata i Delphi: HotPDF hint-tabeller

HotPDF skriver lineariserte PDF-filer, layouten Acrobat kaller Fast Web View, gjennom egenskapen LinearizeOutputTHotPDF. Å sette den før BeginDoc får HotPDF til å omorganisere den ferdige objektgrafen slik at en byteintervall-bevisst leser kan vise side én etter å ha hentet bare den innledende delen av filen, i stedet for å laste ned hele dokumentet først. Mekanismen er ISO 32000-1 tillegg F

Grunnen til at dette betyr noe er lite glamorøs. En vanlig PDF putter kryssreferansetabellen sin til slutt, så en leser må nå den siste byten før den vet hvor noe er. Gi en nettleser en 200-siders skannet rapport, og brukeren stirrer på en spinner gjennom hele overføringen, selv om det eneste de ville ha var side 1. Linearisering fikser det ved å betale en kostnad ved skrivetidspunktet. Denne artikkelen handler spesifikt om den skrivestien, partisjoneringen, målesløyfen og de harde grensene; for den konseptuelle bakgrunnen om hva Fast Web View gir deg, dekker den tidligere forklaringen på PDF-linearisering og Fast Web View grunnen

Hva den lineariserte layouten faktisk garanterer

En linearisert fil er en vanlig PDF med en ekstremt spesifikk fysisk rekkefølge, og hver garanti den gir kommer fra den rekkefølgen snarere enn fra noen ny objekttype. HotPDF sender ut delene i sekvensen tillegg F foreskriver: lineariseringsparameterordboken innenfor de første 1024 bytene, en tidlig kryssreferansetabell, dokumentnivå-objektene, den primære hint-strømmen, den første siden og dens private objekter, deretter de gjenværende sidene, deretter de delte objektene, deretter alt annet, og til slutt hovedkryssreferansetabellen

Partisjoneringen er utledet, ikke erklært. HotPDF går gjennom referansegrafen fra hvert sideobjekt og registrerer, for hvert indirekte objekt, hvor mange sider som når det og hvilken side som nådde det først. Et objekt brukt av nøyaktig én side blir privat for den siden. Et objekt nådd av mer enn én blir delt. Katalogen, pluss alt den refererer til under /ViewerPreferences, /OpenAction, /Threads og /AcroForm, pluss krypteringsordboken når beskyttelse er aktivt, danner dokumentnivågruppen som må komme foran alt annet. Sidetre-noder holdes bevisst tilbake slik at de ikke forurenser første-side-seksjonen

Parameterordboken bærer tallene en leser trenger før den har lest noe annet: /L for total fillengde, /H for offset og lengde til hint-strømmen, /O for objektnummeret til den første siden, /E for byten hvor første-side-seksjonen slutter, /N for sideantallet og /T for offseten til hovedkryssreferansetabelloppføringen. Hver eneste av disse er en byteoffset inn i en fil som ikke eksisterer ennå på det tidspunktet du trenger å skrive dem

Hvorfor må hint-tabelloffsettene konvergere?

Fordi tallene i parameterordboken beskriver filen som inneholder dem, og å endre noen av dem endrer filen. Det er den sentrale vanskeligheten med en linearisert skriver, og det er grunnen til at HotPDF måler gjentatte ganger i stedet for å skrive én gang. Utvid /T fra 6 siffer til 7, og parameterordboken vokser med én byte; headeren vokser; hvert objekt flytter seg; hovedkryssreferansetabellen flytter seg; /T trenger nå en annen verdi. Layouten må nå et fastpunkt før en eneste byte av reell utdata er forpliktet

HotPDF håndterer dette med en avgrenset iterasjon. Den serialiserer først hvert objekt inn i en tellende strøm som registrerer lengde uten å beholde bytes, slik at hvert objekt har en kjent serialisert størrelse. Den kjører deretter et layoutpass som tildeler offsets til dokumentnivågruppen, hint-strømmen, første-side-gruppen, senere sidegrupper, den delte gruppen og resten, og rapporterer hvor hovedkryssreferansetabellen ville lande. Det resultatet mates tilbake inn som inndata til neste pass. Løkken er begrenset til åtte forsøk, og manglende konvergens kaster et unntak i stedet for å produsere en fil med plausible-utseende feil offsets

CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
  CalculateLayout(CandidateMainOffset, FirstXRefData,
    HintOffset, EndFirstPage, NewMainOffset);
  if NewMainOffset = CandidateMainOffset then
    Break;
  CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
  raise Exception.Create('Linearization layout did not converge');

To detaljer hindrer løkken fra å slingre. Parameterordboken skrives inn i en fast 384-byte slott, fylt ut med mellomrom, slik at dens egen vekst aldri kan destabilisere layouten; hvis ordbokteksten noensinne oversteg den reservasjonen, kaster HotPDF et unntak i stedet for stille å flytte alt. Og etter konvergens kjører HotPDF ett bekreftende layoutpass til og sjekker hint-strømlengden på nytt, fordi hint-strømmen selv koder offsets som først var kjent når layouten hadde satt seg. Gevinsten av all denne målingen er at HotPDF aldri bufrer en andre kopi av dokumentet: når offsettene er fastsatt, serialiseres objekter rett inn i mål-strømmen, med en assert ved hver seksjonsgrense som bekrefter at bytene skrevet stemmer med offseten som ble lovet

Å slå det på fra Delphi

API-flaten er én boolean, og det eneste kravet er at du setter den før generering begynner. LinearizeOutput er som standard False, og layoutpasset kjøres når dokumentet skrives, så å tildele den etter EndDoc oppnår ingenting

var
  PDF: THotPDF;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.FileName := 'fast-view.pdf';
    PDF.Version := pdf17;
    PDF.LinearizeOutput := True;      // must precede BeginDoc
    PDF.BeginDoc;
    PDF.Canvas.TextOut(72, 72, 'First page');
    PDF.EndDoc;
  finally
    PDF.Free;
  end;
end;

Én advarsel om utrulling overgår alt på kodesiden. Linearisering lønner seg bare når transporten støtter HTTP-områdeforespørsler (range requests). Server samme fil fra et endepunkt som strømmer den i sin helhet, eller fra en CDN-konfigurasjon som ignorerer Range, og du har kjøpt deg en tregere skrivesti og en større fil uten synlig gevinst for brukeren. Sjekk serveren før du sjekker koden

Hvorfor overstyrer linearisering UseXRefStream og UseObjectStreams?

Fordi den lineariserte skriveren trenger at hvert objekt har sin egen direkte adresserbare byteoffset, og begge disse funksjonene fjerner det. HotPDF sender derfor ut tradisjonelle tekstkryssreferansetabeller og upakkede indirekte objekter når LinearizeOutput er aktivert, selv om den som kaller også satte UseXRefStream eller UseObjectStreams. Dette er en bevisst overstyring, ikke en konflikt du må løse selv

Resonnementet følger av hint-tabellene. En hint-tabell beskriver hvor en sideseksjon starter og hvor lang den er, slik at en leser kan forespørre nøyaktig det intervallet. Et objekt pakket inn i en /ObjStm-container har ingen uavhengig offset i det hele tatt; det eksisterer bare som en del inne i en annen komprimert strøm som må hentes og pakkes ut som en enhet. Hvis du regnet med objektstrømmer for filstørrelse, forstå at linearisering og komprimering drar i motsatte retninger her, og les avveiningen i følgeartikkelen om objektstrømmer og inkrementelle oppdateringer i HotPDF. Samme spenning former hybride referansefiler, som eksisterer nettopp for å holde eldre lesere fungerende ved siden av strømbaserte tabeller, som dekket i artikkelen om hybride kryssreferansestrømmer i Office-genererte PDF-er

Det finnes også en versjonsgulv. Linearisering krever PDF 1.2 eller nyere. Hvis den valgte versjonen er eldre, hever HotPDF den automatisk, med mindre StrictVersionLock er satt, i så fall kaster skrivingen et unntak i stedet for stille å oppgradere et dokument du festet med hensikt

4 GiB-veggen, og hvorfor HotPDF nekter i stedet for å avkorte

Lineariseringens hint-tabeller lagrer offsets som 32-bit-verdier, så en linearisert fil kan ikke adressere noe ved eller forbi 4 GiB, og HotPDF avviser slik utdata med et eksplisitt unntak i stedet for å skrive en fil med omslåtte offsets. Grensen er ikke et HotPDF-implementasjonsvalg; det er bredden på feltene tillegg F definerer

Sjekken anvendes tre steder, og alle tre betyr noe. HotPDF validerer hvert objekt når dets serialiserte lengde er kjent, validerer hver sideseksjonslengde mens hint-oppføringene bygges, og validerer den endelige fillengden etter at hovedkryssreferansetabellen er dimensjonert. Å feile tidlig er hele poenget: en hint-tabell med en stille avkortet offset produserer en fil som åpner korrekt i en leser som laster den ned i sin helhet, og feiler bare for byteintervall-klienten linearisering fantes for å betjene, som er den verst tenkelige feilmodusen fordi testleseren din aldri reproduserer den. Hvis du produserer flere-gigabyte-utdata, er linearisering ikke verktøyet, og strømmetilnærmingen beskrevet i notatene om Direct File API for store PDF-arbeidsflyter er retningen å se

Å oppdage linearisering på en fil du lastet inn

THotPDF.IsLoadedLinearized rapporterer om det gjeldende innlastede dokumentet allerede ble skrevet i linearisert form, og den svarer fra et øyeblikksbilde tatt før parsing, ikke fra den levende strømmen. HotPDF leser de første 1024 bytene fra posisjon null i kildestrømmen, skanner dem etter det første obj-nøkkelordet og deretter etter en /Linearized-oppføring med verdien 1, og cacher det boolske resultatet

var
  PDF: THotPDF;
  PageCount: Integer;
begin
  PDF := THotPDF.Create(nil);
  try
    PageCount := PDF.LoadFromFile('incoming.pdf');
    if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
      Writeln('Source is not Fast Web View ready');
  finally
    PDF.Free;
  end;
end;

To begrensninger i den beskrivelsen er bærende. Deteksjonen kan ikke stole på strømposisjonen, fordi når applikasjonskoden stiller spørsmålet har parseren allerede flyttet den, og den kan ikke lese på nytt ved behov fordi LoadFromFile frigir den interne kildestrømmen når innlastingen er ferdig. Derav design-mønsteret fang-før-parse-og-cache. Skanningen er også bevisst bokstavelig om verdien: bare /Linearized 1 eller en numerisk ekvivalent form med en helt null brøkdel godtas, fordi en fil hvis parameterordbok sier noe annet ikke gir tillegg F-løftet

En Delphi-record-felle verdt å stjele

Lokale records som inneholder dynamiske arrayer initialiserer sine håndterte felt og ingenting annet, og hvis du holder et vanlig Count-felt ved siden av arrayet, må du nullstille det selv. Dette bet lineariseringspartisjoneringen under utvikling, og det er den typen feil som koster en dag nettopp fordi én plattform skjuler den

type
  THPDFLinearIndexList = record
    Values: THPDFIntegerArray;  // managed field: cleared for you
    Count: Integer;             // plain field: whatever was on the stack
  end;

// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);

Det dynamiske array-feltet er referansetalt, så kompilatoren nullstiller det. Count ved siden av det er et vanlig heltall uten en slik garanti, og en uinitialisert Count sender selve den første tilføyelsen til en vilkårlig indeks. Under Win32 tilfeldigvis inneholdt stakkslotten null, tilføyelsen landet på indeks 0, og hver test bestod. Under Win64 skrev samme kode forbi slutten av arrayet. Lærdommen generaliserer godt utover linearisering: når en record blander håndterte og uhåndterte felt, tildel Default(TRecord) og slutt å resonnere om hvilke felt kompilatoren dekker, og behandle aldri en grønn Win32-kjøring som bevis på at initialisering er korrekt

Medlemmene LinearizeOutput og IsLoadedLinearized beskrevet her leveres med standard HotPDF Component for Delphi og C++Builder; produktsiden bærer den fullstendige egenskapsreferansen, inkludert samspillsreglene med kryssreferansestrømmer, objektstrømmer og versjonslåsing