Articol tehnic

Ieșire PDF liniarizată în Delphi: tabelele hint HotPDF

HotPDF scrie fișiere PDF liniarizate, layout-ul pe care Acrobat îl numește Fast Web View, prin proprietatea LinearizeOutput de pe THotPDF. Setarea ei înainte de BeginDoc face ca HotPDF să reordoneze graful de obiecte finalizat, astfel încât un cititor conștient de intervale de octeți să poată afișa pagina unu după ce a preluat doar partea inițială a fișierului, în loc să descarce întregul document mai întâi. Mecanismul este Anexa F din ISO 32000-1

Motivul pentru care contează este lipsit de strălucire. Un PDF normal își pune tabelul cross-reference la sfârșit, așa că un vizualizator trebuie să ajungă la ultimul octet înainte să știe unde se află orice. Dați unui browser un raport scanat de 200 de pagini și utilizatorul se uită la o roată de încărcare pe durata întregului transfer, chiar dacă tot ce a dorit a fost pagina 1. Liniarizarea rezolvă asta plătind un cost la momentul scrierii. Acest articol tratează specific acea cale de scriere, partiționarea, bucla de măsurare și limitele stricte; pentru contextul conceptual despre ce vă aduce Fast Web View, articolul anterior, explicația liniarizării PDF și Fast Web View, acoperă acel teren

Ce garantează de fapt layout-ul liniarizat

Un fișier liniarizat este un PDF obișnuit cu o ordonare fizică extrem de specifică, iar fiecare garanție pe care o oferă vine din acea ordonare, nu dintr-un tip de obiect nou. HotPDF emite părțile în secvența pe care o prescrie Anexa F: dicționarul de parametri de liniarizare în primii 1024 de octeți, un tabel cross-reference timpuriu, obiectele la nivel de document, fluxul hint primar, prima pagină și obiectele ei private, apoi paginile rămase, apoi obiectele partajate, apoi tot restul, și în final tabelul cross-reference principal

Partiționarea este derivată, nu declarată. HotPDF parcurge graful de referințe de la fiecare obiect pagină și înregistrează, pentru fiecare obiect indirect, câte pagini ajung la el și care pagină a ajuns prima. Un obiect folosit de exact o pagină devine privat acelei pagini. Un obiect la care ajung mai multe devine partajat. Catalogul, plus tot ce referențiază sub /ViewerPreferences, /OpenAction, /Threads și /AcroForm, plus dicționarul de criptare când protecția este activă, formează grupul la nivel de document care trebuie să preceadă totul. Nodurile arborelui de pagini sunt ținute deliberat înapoi, ca să nu polueze secțiunea primei pagini

Dicționarul de parametri poartă numerele de care are nevoie un cititor înainte să fi citit orice altceva: /L pentru lungimea totală a fișierului, /H pentru offset-ul și lungimea fluxului hint, /O pentru numărul de obiect al primei pagini, /E pentru octetul la care se termină secțiunea primei pagini, /N pentru numărul de pagini și /T pentru offset-ul intrării tabelului cross-reference principal. Fiecare dintre acestea este un offset de octet într-un fișier care nu există încă în momentul în care trebuie să le scrieți

De ce trebuie să convergă offset-urile tabelului hint?

Pentru că numerele din dicționarul de parametri descriu fișierul care le conține, iar schimbarea oricăruia dintre ele schimbă fișierul. Aceasta este dificultatea centrală a unui writer liniarizat, și de aceea HotPDF măsoară repetat în loc să scrie o singură dată. Lărgiți /T de la 6 cifre la 7 și dicționarul de parametri crește cu un octet; antetul crește; fiecare obiect se deplasează; tabelul cross-reference principal se mută; /T are acum nevoie de o altă valoare. Layout-ul trebuie să ajungă la un punct fix înainte ca un singur octet de ieșire reală să fie angajat

HotPDF gestionează asta printr-o iterație mărginită. Mai întâi serializează fiecare obiect într-un flux de numărare care înregistrează lungimea fără a păstra octeții, astfel încât fiecare obiect are o dimensiune serializată cunoscută. Apoi rulează o trecere de layout care atribuie offset-uri grupului la nivel de document, fluxului hint, grupului primei pagini, grupurilor de pagini ulterioare, grupului partajat și restului, și raportează unde ar aterizca tabelul cross-reference principal. Acel rezultat este introdus înapoi ca intrare pentru trecerea următoare. Bucla este limitată la opt încercări, iar neconvergența ridică o excepție în loc să producă un fișier cu offset-uri greșite dar plauzibile

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

Două detalii împiedică bucla să se zbată. Dicționarul de parametri este scris într-un slot fix de 384 de octeți, umplut cu spații, astfel încât propria sa creștere nu poate niciodată destabiliza layout-ul; dacă textul dicționarului ar depăși vreodată acea rezervare, HotPDF ridică o excepție în loc să deplaseze totul în tăcere. Iar după convergență, HotPDF rulează încă o trecere de layout de confirmare și reverifică lungimea fluxului hint, deoarece fluxul hint în sine codifică offset-uri care erau cunoscute doar odată ce layout-ul s-a stabilizat. Câștigul din toată această măsurare este că HotPDF nu memorează niciodată o a doua copie a documentului: odată ce offset-urile sunt fixate, obiectele sunt serializate direct în fluxul de destinație, cu o aserțiune la fiecare graniță de secțiune că octeții scriși se potrivesc cu offset-ul promis

Activarea din Delphi

Suprafața API este un singur Boolean, iar singura sa cerință este să o setați înainte ca generarea să înceapă. LinearizeOutput este implicit False, iar trecerea de layout rulează când documentul este scris, așa că atribuirea ei după EndDoc nu realizează nimic

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;

O avertizare de implementare depășește orice pe partea de cod. Liniarizarea dă roade doar când transportul suportă cereri de interval HTTP. Serviți același fișier dintr-un endpoint care îl transmite integral, sau dintr-o configurație CDN care ignoră Range, și v-ați cumpărat o cale de scriere mai lentă și un fișier mai mare fără niciun câștig vizibil pentru utilizator. Verificați serverul înainte să verificați codul

De ce liniarizarea suprascrie UseXRefStream și UseObjectStreams?

Pentru că writer-ul liniarizat are nevoie ca fiecare obiect să aibă propriul offset de octet direct adresabil, iar ambele funcționalități iau asta. HotPDF emite deci tabele cross-reference text tradiționale și obiecte indirecte despachetate ori de câte ori LinearizeOutput este activat, chiar dacă apelantul a setat și UseXRefStream sau UseObjectStreams. Aceasta este o suprascriere deliberată, nu un conflict pe care trebuie să îl rezolvați dumneavoastră

Raționamentul rezultă din tabelele hint. Un tabel hint descrie unde începe o secțiune de pagină și cât de lungă este, astfel încât un cititor poate cere exact acel interval. Un obiect ambalat într-un container /ObjStm nu are deloc un offset independent; există doar ca o felie în interiorul altui flux comprimat care trebuie preluat și dezumflat ca o unitate. Dacă v-ați bazat pe fluxurile de obiecte pentru dimensiunea fișierului, înțelegeți că liniarizarea și compresia trag aici în direcții opuse, și citiți compromisul în articolul asociat despre fluxurile de obiecte și actualizările incrementale în HotPDF. Aceeași tensiune modelează fișierele cu referință hibridă, care există exact pentru a menține cititoarele mai vechi funcționale alături de tabelele bazate pe fluxuri, așa cum este tratat în articolul despre fluxurile cross-reference hibride în PDF-urile generate de Office

Există și un prag minim de versiune. Liniarizarea necesită PDF 1.2 sau ulterior. Dacă versiunea selectată este mai veche, HotPDF o ridică automat, cu excepția cazului în care StrictVersionLock este setat, caz în care scrierea ridică o excepție în loc să promoveze tacit un document pe care l-ați fixat intenționat

Zidul de 4 GiB, și de ce HotPDF refuză în loc să trunchieze

Tabelele hint de liniarizare stochează offset-uri ca valori pe 32 de biți, așa că un fișier liniarizat nu poate adresa nimic la sau peste 4 GiB, iar HotPDF respinge o astfel de ieșire cu o excepție explicită în loc să scrie un fișier cu offset-uri înfășurate. Limita nu este o alegere de implementare HotPDF; este lățimea câmpurilor pe care le definește Anexa F

Verificarea este aplicată în trei locuri, și toate trei contează. HotPDF validează fiecare obiect odată ce lungimea lui serializată este cunoscută, validează lungimea fiecărei secțiuni de pagină în timp ce construiește intrările hint și validează lungimea finală a fișierului după ce tabelul cross-reference principal este dimensionat. Eșecul timpuriu este întregul rost: un tabel hint cu un offset trunchiat tacit produce un fișier care se deschide corect într-un vizualizator care îl descarcă integral și eșuează doar pentru clientul cu interval de octeți pe care liniarizarea a existat să îl servească, ceea ce este cel mai rău mod de eșec posibil, deoarece vizualizatorul dumneavoastră de test nu îl reproduce niciodată. Dacă produceți ieșire de mai mulți gigabytes, liniarizarea nu este instrumentul, iar abordarea de streaming descrisă în notele despre API-ul Direct File pentru fluxuri de lucru PDF mari este direcția de urmat

Detectarea liniarizării pe un fișier încărcat

THotPDF.IsLoadedLinearized raportează dacă documentul încărcat curent a fost deja scris în formă liniarizată, și răspunde dintr-un instantaneu preluat înainte de analiză, nu din fluxul viu. HotPDF citește primii 1024 de octeți de la poziția zero a fluxului sursă, îi scanează pentru primul cuvânt cheie obj și apoi pentru o intrare /Linearized cu valoarea 1, și memorează în cache rezultatul boolean

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;

Două constrângeri din acea descriere sunt esențiale. Detecția nu se poate baza pe poziția fluxului, deoarece până când codul aplicației pune întrebarea, parserul a mutat-o, și nu poate reciti la cerere deoarece LoadFromFile eliberează fluxul sursă intern odată ce încărcarea se termină. De aici designul de captare-înainte-de-analiză-și-cache. Scanarea este de asemenea deliberat literală privind valoarea: doar /Linearized 1 sau o formă numeric echivalentă cu o fracție complet zero este acceptată, deoarece un fișier al cărui dicționar de parametri spune altceva nu face promisiunea Anexei F

O capcană de înregistrare Delphi ce merită reținută

Înregistrările locale care conțin array-uri dinamice își inițializează câmpurile gestionate și nimic altceva, iar dacă păstrați un câmp Count simplu alături de array trebuie să îl curățați dumneavoastră înșivă. Aceasta a afectat partiționarea de liniarizare în timpul dezvoltării, și este genul de bug care costă o zi tocmai pentru că o platformă îl ascunde

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

Câmpul array dinamic are numărare de referințe, așa că îl zerofică. Count alături de el este un întreg obișnuit fără o astfel de garanție, iar un Count neinițializat trimite chiar primul append la un index arbitrar. Sub Win32, slotul de stivă s-a întâmplat să conțină zero, append-ul a aterizat la indexul 0, iar fiecare test a trecut. Sub Win64, același cod a scris dincolo de sfârșitul array-ului. Lecția se generalizează mult dincolo de liniarizare: când o înregistrare amestecă câmpuri gestionate și negestionate, atribuiți Default(TRecord) și opriți-vă din a raționa despre care câmpuri le acoperă compilatorul, și nu tratați niciodată o rulare Win32 verde ca dovadă că inițializarea este corectă

Membrii LinearizeOutput și IsLoadedLinearized descriși aici vin cu HotPDF Component standard pentru Delphi și C++Builder; pagina de produs conține referința completă de proprietăți, inclusiv regulile de interacțiune cu fluxurile cross-reference, fluxurile de obiecte și blocarea versiunii