Teknisk artikel

Linjäriserad PDF-utdata i Delphi: HotPDF-hinttabeller

HotPDF skriver linjäriserade PDF-filer, layouten Acrobat kallar Fast Web View, genom egenskapen LinearizeOutputTHotPDF. Att sätta den före BeginDoc får HotPDF att ordna om det färdiga objektgrafen så att en byteintervall-medveten läsare kan visa sida ett efter att bara ha hämtat den inledande delen av filen, i stället för att ladda ner hela dokumentet först. Mekanismen är ISO 32000-1 bilaga F

Anledningen till att detta spelar roll är oglamorös. En vanlig PDF lägger sin korsreferenstabell i slutet, så en läsare måste nå den sista byten innan den vet var något finns. Ge en webbläsare en 200-sidig skannad rapport och användaren stirrar på en snurra under hela överföringen, trots att det enda de ville ha var sida 1. Linjärisering fixar det genom att betala en kostnad vid skrivtillfället. Den här artikeln handlar specifikt om den skrivvägen, partitioneringen, mätningsloopen och de hårda gränserna; för den konceptuella bakgrunden om vad Fast Web View ger dig täcker den tidigare förklaringen av PDF-linjärisering och Fast Web View den marken

Vad den linjäriserade layouten faktiskt garanterar

En linjäriserad fil är en vanlig PDF med en extremt specifik fysisk ordning, och varje garanti den erbjuder kommer från den ordningen snarare än från någon ny objekttyp. HotPDF avger delarna i sekvensen bilaga F föreskriver: linjäriseringsparameterordboken inom de första 1024 byten, en tidig korsreferenstabell, dokumentnivåobjekten, den primära hintströmmen, den första sidan och dess privata objekt, sedan de återstående sidorna, sedan de delade objekten, sedan allt annat, och slutligen huvudkorsreferenstabellen

Partitioneringen är härledd, inte deklarerad. HotPDF vandrar referensgrafen från varje sidobjekt och registrerar, för varje indirekt objekt, hur många sidor når det och vilken sida som nådde det först. Ett objekt använt av exakt en sida blir privat för den sidan. Ett objekt nått av mer än en blir delat. Katalogen, plus vad den refererar under /ViewerPreferences, /OpenAction, /Threads och /AcroForm, plus krypteringsordboken när skydd är aktivt, bildar dokumentnivågruppen som måste föregå allt annat. Sidträdnoder hålls tillbaka avsiktligt så de inte förorenar första-sida-avsnittet

Parameterordboken bär siffrorna en läsare behöver innan den har läst något annat: /L för den totala fillängden, /H för offset och längd på hintströmmen, /O för objektnumret på första sidan, /E för byten där första sidans avsnitt slutar, /N för sidantalet och /T för offseten till huvudkorsreferenstabellens post. Var och en av dessa är en byteoffset in i en fil som inte finns ännu vid den tidpunkt du behöver skriva dem

Varför måste hinttabellens offsetar konvergera?

Eftersom siffrorna i parameterordboken beskriver filen som innehåller dem, och att ändra någon av dem ändrar filen. Det är den centrala svårigheten hos en linjäriserad skrivare, och det är varför HotPDF mäter upprepade gånger i stället för att skriva en gång. Utvidga /T från 6 siffror till 7 och parameterordboken växer med en byte; huvudet växer; varje objekt förskjuts; huvudkorsreferenstabellen flyttas; /T behöver nu ett annat värde. Layouten måste nå en fixpunkt innan en enda byte av verklig utdata committas

HotPDF hanterar detta med en avgränsad iteration. Den serialiserar först varje objekt till en räknande ström som registrerar längd utan att behålla byte, så varje objekt har en känd serialiserad storlek. Den kör sedan ett layoutpass som tilldelar offsetar till dokumentnivågruppen, hintströmmen, första-sida-gruppen, senare sidgrupper, den delade gruppen och resten, och rapporterar var huvudkorsreferenstabellen skulle landa. Det resultatet matas tillbaka som indata till nästa pass. Loopen är begränsad till åtta försök, och icke-konvergens kastar ett undantag i stället för att producera en fil med rimligt utseende fel offsetar

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');

Två detaljer håller loopen från att vackla. Parameterordboken skrivs in i en fast plats på 384 byte, utfylld med mellanslag, så dess egen tillväxt kan aldrig destabilisera layouten; om ordbokstexten någonsin skulle överskrida den reservationen kastar HotPDF i stället för att tyst förskjuta allt. Och efter konvergens kör HotPDF ytterligare ett bekräftande layoutpass och kontrollerar hintströmmens längd igen, eftersom hintströmmen själv kodar offsetar som bara var kända när layouten hade satt sig. Utdelningen av all denna mätning är att HotPDF aldrig buffrar en andra kopia av dokumentet: när offsetarna väl är fasta serialiseras objekt direkt in i destinationsströmmen, med en assertion vid varje avsnittsgräns som kontrollerar att de skrivna byten matchar offseten som utlovades

Att slå på det från Delphi

API-ytan är en enda boolean, och dess enda krav är att du sätter den innan genereringen börjar. LinearizeOutput är som standard False, och layoutpasset körs när dokumentet skrivs, så att tilldela det efter EndDoc åstadkommer 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;

En driftsättningsvarning trumfar allt på kodsidan. Linjärisering lönar sig bara när transporten stödjer HTTP-intervallförfrågningar. Servera samma fil från en slutpunkt som strömmar den hel, eller från en CDN-konfiguration som ignorerar Range, och du har köpt dig en långsammare skrivväg och en större fil utan någon användarsynlig vinst. Kontrollera servern innan du kontrollerar koden

Varför åsidosätter linjärisering UseXRefStream och UseObjectStreams?

Eftersom den linjäriserade skrivaren behöver att varje objekt har sin egen direkt adresserbara byteoffset, och båda de funktionerna tar bort det. HotPDF avger därför traditionella textkorsreferenstabeller och opackade indirekta objekt närhelst LinearizeOutput är aktiverad, även om anroparen också satt UseXRefStream eller UseObjectStreams. Det här är ett avsiktligt åsidosättande, inte en konflikt du måste lösa själv

Resonemanget följer från hinttabellerna. En hinttabell beskriver var ett sidavsnitt börjar och hur långt det är, så en läsare kan begära exakt det intervallet. Ett objekt packat in i en /ObjStm-container har ingen oberoende offset alls; det existerar bara som en skiva inuti en annan komprimerad ström som måste hämtas och packas upp som en enhet. Om du räknade med objektströmmar för filstorlek, förstå att linjärisering och komprimering drar åt motsatta håll här, och läs avvägningen i följdstycket om objektströmmar och inkrementella uppdateringar i HotPDF. Samma spänning formar hybridreferensfiler, som existerar exakt för att hålla äldre läsare fungerande vid sidan av strömbaserade tabeller, som täckt i artikeln om hybrida korsreferensströmmar i Office-genererade PDF-filer

Det finns också ett versionsgolv. Linjärisering kräver PDF 1.2 eller senare. Om den valda versionen är äldre höjer HotPDF den automatiskt, om inte StrictVersionLock är satt, i vilket fall skrivning kastar ett undantag i stället för att tyst befordra ett dokument du fixerat med avsikt

4 GiB-väggen, och varför HotPDF vägrar i stället för att trunkera

Linjäriseringens hinttabeller lagrar offsetar som 32-bitars värden, så en linjäriserad fil kan inte adressera något vid eller bortom 4 GiB, och HotPDF avvisar sådan utdata med ett explicit undantag i stället för att skriva en fil med omvridna offsetar. Gränsen är inte ett HotPDF-implementationsval; det är bredden på fälten bilaga F definierar

Kontrollen tillämpas på tre ställen, och alla tre spelar roll. HotPDF validerar varje objekt när dess serialiserade längd är känd, validerar varje sidavsnittslängd medan hintposterna byggs, och validerar den slutliga fillängden efter att huvudkorsreferenstabellen har storleksatts. Att misslyckas tidigt är hela poängen: en hinttabell med en tyst trunkerad offset producerar en fil som öppnas korrekt i en läsare som laddar ner den hel och misslyckas bara för byteintervall-klienten som linjärisering existerade för att betjäna, vilket är det värsta möjliga felläget eftersom din testläsare aldrig återskapar det. Om du producerar flergigabyte-utdata är linjärisering inte verktyget, och strömningsmetoden som beskrivs i anteckningarna om Direct File API för stora PDF-arbetsflöden är riktningen att titta åt

Att upptäcka linjärisering på en fil du laddat

THotPDF.IsLoadedLinearized rapporterar om det för närvarande laddade dokumentet redan skrevs i linjäriserad form, och den svarar från en ögonblicksbild tagen innan tolkning, inte från den levande strömmen. HotPDF läser de första 1024 byten från position noll i källströmmen, skannar dem efter det första obj-nyckelordet och sedan efter en /Linearized-post med värdet 1, och cachar det booleska 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;

Två begränsningar i den beskrivningen är bärande. Detekteringen kan inte förlita sig på strömpositionen, eftersom vid den tidpunkt applikationskoden ställer frågan har tolken flyttat den, och den kan inte läsa om på begäran eftersom LoadFromFile släpper den interna källströmmen när laddningen slutförs. Därav designen fånga-innan-tolka-och-cacha. Skanningen är också avsiktligt bokstavlig om värdet: bara /Linearized 1 eller en numeriskt likvärdig form med en helt nollställd bråkdel accepteras, eftersom en fil vars parameterordbok säger något annat inte gör bilaga F-löftet

En Delphi-recordfälla värd att stjäla

Lokala poster som innehåller dynamiska arrayer initierar sina hanterade fält och inget annat, och om du håller ett vanligt Count-fält bredvid arrayen måste du rensa det själv. Det här bet linjäriseringspartitioneringen under utveckling, och det är den typen av bugg som kostar en dag exakt eftersom en plattform döljer 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 dynamiska array-fältet är referensräknat, så kompilatorn nollställer det. Count bredvid det är ett vanligt heltal utan någon sådan garanti, och ett oinitierat Count skickar den allra första tilläggningen till ett godtyckligt index. Under Win32 råkade stackplatsen hålla noll, tilläggningen landade på index 0, och varje test klarade sig. Under Win64 skrev samma kod förbi slutet av arrayen. Lärdomen generaliserar långt bortom linjärisering: när en post blandar hanterade och ohanterade fält, tilldela Default(TRecord) och sluta resonera om vilka fält kompilatorn täcker, och behandla aldrig en grön Win32-körning som bevis på att initieringen är korrekt

Medlemmarna LinearizeOutput och IsLoadedLinearized som beskrivs här levereras med standard-HotPDF Component för Delphi och C++Builder; produktsidan bär den fullständiga egenskapsreferensen, inklusive interaktionsreglerna med korsreferensströmmar, objektströmmar och versionslåsning