Teknisk artikel

Lineariseret PDF-output i Delphi: HotPDF hint-tabeller

HotPDF skriver lineariserede PDF-filer, det layout Acrobat kalder Fast Web View, gennem egenskaben LinearizeOutputTHotPDF. At sætte den før BeginDoc får HotPDF til at omordne den færdige objektgraf, så en byte-range-bevidst læser kan vise side ét efter kun at have hentet den forreste del af filen, i stedet for at skulle downloade hele dokumentet først. Mekanismen er ISO 32000-1 Annex F

Grunden til, at dette betyder noget, er ganske ugla­morøs. En normal PDF placerer sin cross-reference-tabel til sidst, så en læser skal nå den sidste byte, før den ved, hvor noget er. Giv en browser en scannet rapport på 200 sider, og brugeren stirrer på en spinner under hele overførslen, selvom det eneste, de ville have, var side 1. Linearisering løser det ved at betale en pris ved skrivetidspunktet. Denne artikel handler specifikt om den skrivesti, opdelingen, målingsløkken og de hårde grænser; for den begrebsmæssige baggrund for, hvad Fast Web View giver, dækker den tidligere forklaring af PDF-linearisering og Fast Web View det terræn

Hvad det lineariserede layout egentlig garanterer

En lineariseret fil er en almindelig PDF med en ekstremt specifik fysisk ordning, og enhver garanti, den tilbyder, kommer fra den ordning frem for fra nogen ny objekttype. HotPDF udsender delene i rækkefølgen, Annex F foreskriver: linearization parameter-dictionary'et inden for de første 1024 bytes, en tidlig cross-reference-tabel, dokumentniveau-objekterne, den primære hint-strøm, den første side og dens private objekter, dernæst de resterende sider, dernæst de delte objekter, og til sidst alt andet, og til allersidst hoved-cross-reference-tabellen

Opdelingen er udledt, ikke erklæret. HotPDF gennemgår referencegrafen fra hvert sideobjekt og registrerer for hvert indirekte objekt, hvor mange sider der når det, og hvilken side der nåede det først. Et objekt, brugt af nøjagtig én side, bliver privat for den side. Et objekt, nået af mere end én, bliver delt. Kataloget, plus hvad det refererer under /ViewerPreferences, /OpenAction, /Threads og /AcroForm, plus krypteringsdictionary'et når beskyttelse er aktiv, danner dokumentniveau-gruppen, der skal komme forud for alt andet. Sidetræ-noder holdes bevidst tilbage, så de ikke forurener første-side-sektionen

Parameter-dictionary'et bærer de tal, en læser behøver, før den har læst noget som helst: /L for den samlede fillængde, /H for offset og længde af hint-strømmen, /O for objektnummeret på første side, /E for den byte, hvor første-side-sektionen slutter, /N for sideantal, og /T for offsetten af hoved-cross-reference-tabellens post. Hver eneste af dem er en byte-offset ind i en fil, der ikke findes endnu på det tidspunkt, man behøver dem

Hvorfor skal hint-tabel-offsetterne konvergere?

Fordi tallene i parameter-dictionary'et beskriver filen, der indeholder dem, og at ændre nogen af dem ændrer filen. Det er den centrale vanskelighed ved en lineariseret writer, og det er grunden til, at HotPDF måler gentagne gange frem for at skrive én gang. Udvid /T fra 6 cifre til 7, og parameter-dictionary'et vokser med en byte; hovedet vokser; hvert objekt forskydes; hoved-cross-reference-tabellen flytter sig; /T behøver nu en anden værdi. Layoutet skal nå et fikspunkt, før en eneste byte reelt output er forpligtet

HotPDF håndterer dette med en afgrænset iteration. Den serialiserer først hvert objekt ind i en tælende strøm, der registrerer længde uden at beholde bytes, så hvert objekt har en kendt serialiseret størrelse. Den kører så et layoutgennemløb, der tildeler offsets til dokumentniveau-gruppen, hint-strømmen, første-side-gruppen, de senere sidegrupper, den delte gruppe og resten, og rapporterer, hvor hoved-cross-reference-tabellen ville lande. Det resultat fodres tilbage som input til næste gennemløb. Løkken er begrænset til otte forsøg, og manglende konvergens rejser en undtagelse frem for at producere en fil med plausibelt udseende forkerte 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 holder løkken fra at hakke rundt. Parameter-dictionary'et skrives ind i en fast 384-byte plads, polstret med mellemrum, så dets egen vækst aldrig kan destabilisere layoutet; skulle dictionary-teksten nogensinde overskride den reservation, rejser HotPDF en undtagelse frem for stiltiende at forskyde alt. Og efter konvergens kører HotPDF ét ekstra bekræftende layoutgennemløb og gentjekker hint-strømmens længde, fordi hint-strømmen selv koder offsets, der kun var kendt, når layoutet var slået fast. Gevinsten ved al denne måling er, at HotPDF aldrig bufrer en anden kopi af dokumentet: når offsetterne er fastlåst, serialiseres objekter direkte ind i destinationsstrømmen, med en assertion ved hver sektionsgrænse, om at de skrevne bytes matcher den offset, der blev lovet

Sådan slås det til fra Delphi

API-fladen er én boolsk værdi, og dens eneste krav er, at man sætter den, før generering begynder. LinearizeOutput er som standard False, og layoutgennemløbet kører, når dokumentet skrives, så at tildele den efter EndDoc udretter intet

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;

Ét deployment-forbehold overtrumfer alt på kodesiden. Linearisering betaler sig kun, når transportlaget understøtter HTTP range-forespørgsler. Server samme fil fra et endpoint, der streamer den hel, eller fra en CDN-konfiguration, der ignorerer Range, og man har købt sig en langsommere skrivesti og en større fil uden brugersynlig gevinst. Tjek serveren, før man tjekker koden

Hvorfor overstyrer linearisering UseXRefStream og UseObjectStreams?

Fordi den lineariserede writer behøver, at hvert objekt har sin egen direkte adresserbare byte-offset, og begge de funktioner tager det fra den. HotPDF udsender derfor traditionelle tekst-cross-reference-tabeller og upakkede indirekte objekter, når LinearizeOutput er aktiveret, selv hvis den kaldende også har sat UseXRefStream eller UseObjectStreams. Det er en bevidst overstyring, ikke en konflikt, man selv skal løse

Begrundelsen følger af hint-tabellerne. En hint-tabel beskriver, hvor en sidesektion starter, og hvor lang den er, så en læser kan anmode om netop det interval. Et objekt pakket ind i en /ObjStm-container har ingen selvstændig offset overhovedet; det eksisterer kun som en skive inde i en anden komprimeret strøm, der skal hentes og udpakkes som en enhed. Regnede man med objektstrømme for filstørrelse, forstå at linearisering og komprimering trækker i modsatte retninger her, og læs afvejningen i søsterstykket om objektstrømme og inkrementelle opdateringer i HotPDF. Samme spænding former hybrid-reference-filer, som eksisterer netop for at holde ældre læsere kørende ved siden af strømbaserede tabeller, som dækket i artiklen om hybride cross-reference-streams i Office-genererede PDF'er

Der er også en versionsgrænse. Linearisering kræver PDF 1.2 eller nyere. Er den valgte version ældre, hæver HotPDF den automatisk, medmindre StrictVersionLock er sat, i hvilket tilfælde skrivning rejser en undtagelse frem for stiltiende at forfremme et dokument, man bevidst fastlåste

4 GiB-muren, og hvorfor HotPDF nægter frem for at afkorte

Linearization hint-tabeller lagrer offsets som 32-bit-værdier, så en lineariseret fil ikke kan adressere noget ved eller ud over 4 GiB, og HotPDF afviser sådant output med en eksplicit undtagelse frem for at skrive en fil med ombukkede offsets. Grænsen er ikke et HotPDF-implementeringsvalg; det er bredden på de felter, Annex F definerer

Tjekket anvendes tre steder, og alle tre betyder noget. HotPDF validerer hvert objekt, når dets serialiserede længde er kendt, validerer hver sidesektions længde under opbygning af hint-posterne, og validerer den endelige fillængde, efter hoved-cross-reference-tabellen er dimensioneret. At fejle tidligt er hele pointen: en hint-tabel med en stiltiende afkortet offset producerer en fil, der åbner korrekt i en læser, der downloader den hel, og kun fejler for den byte-range-klient, linearisering fandtes for at betjene, hvilket er den værst tænkelige fejltilstand, fordi ens testlæser aldrig reproducerer den. Producerer man output i flergigabyte-størrelse, er linearisering ikke værktøjet, og streaming-tilgangen beskrevet i noterne om Direct File API til store PDF-workflows er retningen at kigge i

Registrering af linearisering på en indlæst fil

THotPDF.IsLoadedLinearized rapporterer, om det aktuelt indlæste dokument allerede var skrevet i lineariseret form, og den svarer ud fra et snapshot taget før parsing, ikke ud fra den levende strøm. HotPDF læser de første 1024 bytes fra position nul i kildestrømmen, scanner dem for det første obj-nøgleord og derefter for en /Linearized-post med værdien 1, og cacher det boolske resultat

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 begrænsninger i den beskrivelse er bærende. Registreringen kan ikke stole på strøm-positionen, fordi parseren, når applikationskoden spørger, allerede har flyttet den, og den kan ikke genlæse på anmodning, fordi LoadFromFile frigiver den interne kildestrøm, når indlæsning er færdig. Deraf designet med capture-før-parse-og-cache. Skanningen er også bevidst bogstavelig omkring værdien: kun /Linearized 1 eller en numerisk ækvivalent form med en helt nul brøkdel accepteres, fordi en fil, hvis parameter-dictionary siger noget andet, ikke afgiver Annex F-løftet

En Delphi-record-fælde værd at stjæle

Lokale records, der indeholder dynamiske arrays, initialiserer deres managede felter og intet andet, og har man et almindeligt Count-felt ved siden af arrayet, må man selv rydde det. Dette bed linearization-opdelingen under udvikling, og det er den slags fejl, der koster en dag netop fordi én platform skjuler det

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-felt er reference-tællet, så kompilatoren nulstiller det. Count ved siden af er et almindeligt heltal uden en sådan garanti, og et uinitialiseret Count sender selve den første append til et vilkårligt indeks. Under Win32 tilfældigvis holdt stack-pladsen nul, appenden landede ved indeks 0, og hver test bestod. Under Win64 skrev samme kode forbi enden af arrayet. Lektien generaliserer langt ud over linearisering: når en record blander managede og ikke-managede felter, tildel Default(TRecord) og stop med at ræsonnere om, hvilke felter kompilatoren dækker, og betragt aldrig en grøn Win32-kørsel som bevis for, at initialisering er korrekt

LinearizeOutput- og IsLoadedLinearized-medlemmerne beskrevet her leveres med den standard HotPDF Component til Delphi og C++Builder; produktsiden bærer den fulde egenskabsreference, inklusive interaktionsreglerne med cross-reference-streams, objektstrømme og versionslåsning